1#!/usr/bin/env python
2#
3# Copyright (c), 2018-2020, SISSA (International School for Advanced Studies).
4# All rights reserved.
5# This file is distributed under the terms of the MIT License.
6# See the file 'LICENSE' in the root directory of the present
7# distribution, or http://opensource.org/licenses/MIT.
8#
9# @author Davide Brunato <brunato@sissa.it>
10#
11import unittest
12import xml.etree.ElementTree as ElementTree
13import io
14from textwrap import dedent
15try:
16    import lxml.etree as lxml_etree
17except ImportError:
18    lxml_etree = None
19
20from elementpath import AttributeNode, XPathContext, XPath2Parser, MissingContextError
21from elementpath.namespaces import XML_LANG, XSD_NAMESPACE, XSD_ANY_ATOMIC_TYPE, XSD_NOTATION
22
23try:
24    # noinspection PyPackageRequirements
25    import xmlschema
26    from xmlschema.xpath import XMLSchemaProxy
27except (ImportError, AttributeError):
28    xmlschema = None
29
30try:
31    from tests import xpath_test_class
32except ImportError:
33    import xpath_test_class
34
35
36@unittest.skipIf(xmlschema is None, "xmlschema library required.")
37class XMLSchemaProxyTest(xpath_test_class.XPathTestCase):
38
39    @classmethod
40    def setUpClass(cls):
41        cls.schema = xmlschema.XMLSchema('''
42        <!-- Dummy schema for testing proxy API -->
43        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
44              targetNamespace="http://xpath.test/ns">
45          <xs:element name="test_element" type="xs:string"/>
46          <xs:attribute name="test_attribute" type="xs:string"/>
47          <xs:element name="A">
48            <xs:complexType>
49              <xs:sequence>
50                <xs:element name="B1"/>
51                <xs:element name="B2"/>
52                <xs:element name="B3"/>
53              </xs:sequence>
54            </xs:complexType>
55          </xs:element>
56        </xs:schema>''')
57
58    def setUp(self):
59        self.schema_proxy = XMLSchemaProxy(self.schema)
60        self.parser = XPath2Parser(namespaces=self.namespaces, schema=self.schema_proxy)
61
62    def test_abstract_xsd_schema(self):
63        class GlobalMaps:
64            types = {}
65            attributes = {}
66            elements = {}
67            substitution_groups = {}
68
69        class XsdSchema:
70            tag = '{%s}schema' % XSD_NAMESPACE
71            xsd_version = '1.1'
72            maps = GlobalMaps()
73            text = None
74
75            @property
76            def attrib(self):
77                return {}
78
79            def __iter__(self):
80                return iter(())
81
82            def find(self, path, namespaces=None):
83                return
84
85        schema = XsdSchema()
86        self.assertEqual(schema.tag, '{http://www.w3.org/2001/XMLSchema}schema')
87        self.assertIsNone(schema.text)
88
89    def test_schema_proxy_init(self):
90        schema_src = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
91                            <xs:element name="test_element" type="xs:string"/>
92                        </xs:schema>"""
93        schema_tree = ElementTree.parse(io.StringIO(schema_src))
94
95        self.assertIsInstance(XMLSchemaProxy(), XMLSchemaProxy)
96        self.assertIsInstance(XMLSchemaProxy(xmlschema.XMLSchema(schema_src)), XMLSchemaProxy)
97        with self.assertRaises(TypeError):
98            XMLSchemaProxy(schema=schema_tree)
99        with self.assertRaises(TypeError):
100            XMLSchemaProxy(schema=xmlschema.XMLSchema(schema_src),
101                           base_element=schema_tree)
102        with self.assertRaises(TypeError):
103            XMLSchemaProxy(schema=xmlschema.XMLSchema(schema_src),
104                           base_element=schema_tree.getroot())
105
106        schema = xmlschema.XMLSchema(schema_src)
107        with self.assertRaises(ValueError):
108            XMLSchemaProxy(base_element=schema.elements['test_element'])
109
110    def test_xmlschema_proxy(self):
111        context = XPathContext(
112            root=self.etree.XML('<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"/>')
113        )
114
115        self.wrong_syntax("schema-element(*)")
116        self.wrong_name("schema-element(nil)")
117        self.wrong_name("schema-element(xs:string)")
118        self.check_value("schema-element(xs:complexType)", MissingContextError)
119        self.check_value("self::schema-element(xs:complexType)", NameError, context)
120        self.check_value("self::schema-element(xs:schema)", [context.item], context)
121        self.check_tree("schema-element(xs:group)", '(schema-element (: (xs) (group)))')
122
123        context.item = AttributeNode(XML_LANG, 'en')
124        self.wrong_syntax("schema-attribute(*)")
125        self.wrong_name("schema-attribute(nil)")
126        self.wrong_name("schema-attribute(xs:string)")
127        self.check_value("schema-attribute(xml:lang)", MissingContextError)
128        self.check_value("schema-attribute(xml:lang)", NameError, context)
129        self.check_value("self::schema-attribute(xml:lang)", [context.item], context)
130        self.check_tree("schema-attribute(xsi:schemaLocation)",
131                        '(schema-attribute (: (xsi) (schemaLocation)))')
132
133        token = self.parser.parse("self::schema-attribute(xml:lang)")
134        context.item = AttributeNode(XML_LANG, 'en')
135        context.axis = 'attribute'
136        self.assertEqual(list(token.select(context)), [context.item])
137
138    def test_bind_parser_method(self):
139        schema_src = dedent("""
140            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
141                <xs:simpleType name="stringType">
142                    <xs:restriction base="xs:string"/>
143                </xs:simpleType>
144            </xs:schema>""")
145        schema = xmlschema.XMLSchema(schema_src)
146
147        schema_proxy = XMLSchemaProxy(schema=schema)
148        parser = XPath2Parser(namespaces=self.namespaces)
149        self.assertFalse(parser.is_schema_bound())
150
151        schema_proxy.bind_parser(parser)
152        self.assertTrue(parser.is_schema_bound())
153        self.assertIs(schema_proxy, parser.schema)
154
155        # To test AbstractSchemaProxy.bind_parser()
156        parser = XPath2Parser(namespaces=self.namespaces)
157        super(XMLSchemaProxy, schema_proxy).bind_parser(parser)
158        self.assertIs(schema_proxy, parser.schema)
159        super(XMLSchemaProxy, schema_proxy).bind_parser(parser)
160        self.assertIs(schema_proxy, parser.schema)
161
162    def test_schema_constructors(self):
163        schema_src = dedent("""
164            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
165                <xs:simpleType name="stringType">
166                    <xs:restriction base="xs:string"/>
167                </xs:simpleType>
168                <xs:simpleType name="intType">
169                    <xs:restriction base="xs:int"/>
170                </xs:simpleType>
171            </xs:schema>""")
172        schema = xmlschema.XMLSchema(schema_src)
173        schema_proxy = XMLSchemaProxy(schema=schema)
174        parser = XPath2Parser(namespaces=self.namespaces, schema=schema_proxy)
175
176        with self.assertRaises(NameError) as ctx:
177            parser.schema_constructor(XSD_ANY_ATOMIC_TYPE)
178        self.assertIn('XPST0080', str(ctx.exception))
179
180        with self.assertRaises(NameError) as ctx:
181            parser.schema_constructor(XSD_NOTATION)
182        self.assertIn('XPST0080', str(ctx.exception))
183
184        token = parser.parse('stringType("apple")')
185        self.assertEqual(token.symbol, 'stringType')
186        self.assertEqual(token.label, 'constructor function')
187        self.assertEqual(token.evaluate(), 'apple')
188
189        token = parser.parse('stringType(())')
190        self.assertEqual(token.symbol, 'stringType')
191        self.assertEqual(token.label, 'constructor function')
192        self.assertEqual(token.evaluate(), [])
193
194        token = parser.parse('stringType(10)')
195        self.assertEqual(token.symbol, 'stringType')
196        self.assertEqual(token.label, 'constructor function')
197        self.assertEqual(token.evaluate(), '10')
198
199        token = parser.parse('stringType(.)')
200        self.assertEqual(token.symbol, 'stringType')
201        self.assertEqual(token.label, 'constructor function')
202
203        token = parser.parse('intType(10)')
204        self.assertEqual(token.symbol, 'intType')
205        self.assertEqual(token.label, 'constructor function')
206        self.assertEqual(token.evaluate(), 10)
207
208        with self.assertRaises(ValueError) as ctx:
209            parser.parse('intType(true())')
210        self.assertIn('FORG0001', str(ctx.exception))
211
212    def test_get_context_method(self):
213        schema_proxy = XMLSchemaProxy()
214        self.assertIsInstance(schema_proxy.get_context(), XPathContext)
215        self.assertIsInstance(super(XMLSchemaProxy, schema_proxy).get_context(), XPathContext)
216
217    def test_get_type_api(self):
218        schema_proxy = XMLSchemaProxy()
219        self.assertIsNone(schema_proxy.get_type('unknown'))
220        self.assertEqual(schema_proxy.get_type('{%s}string' % XSD_NAMESPACE),
221                         xmlschema.XMLSchema.builtin_types()['string'])
222
223    def test_get_primitive_type_api(self):
224        schema_proxy = XMLSchemaProxy()
225        short_type = schema_proxy.get_type('{%s}short' % XSD_NAMESPACE)
226        decimal_type = schema_proxy.get_type('{%s}decimal' % XSD_NAMESPACE)
227        self.assertEqual(schema_proxy.get_primitive_type(short_type), decimal_type)
228
229        ntokens_type = schema_proxy.get_type('{%s}NMTOKENS' % XSD_NAMESPACE)
230        string_type = schema_proxy.get_type('{%s}string' % XSD_NAMESPACE)
231        self.assertEqual(schema_proxy.get_primitive_type(ntokens_type), string_type)
232
233        facet_type = schema_proxy.get_type('{%s}facet' % XSD_NAMESPACE)
234        any_type = schema_proxy.get_type('{%s}anyType' % XSD_NAMESPACE)
235        self.assertEqual(schema_proxy.get_primitive_type(facet_type), any_type)
236        self.assertEqual(schema_proxy.get_primitive_type(any_type), any_type)
237
238        any_simple_type = schema_proxy.get_type('{%s}anySimpleType' % XSD_NAMESPACE)
239        self.assertEqual(schema_proxy.get_primitive_type(any_simple_type), any_simple_type)
240
241    def test_xsd_version_api(self):
242        self.assertEqual(self.schema_proxy.xsd_version, '1.0')
243
244    def test_find_api(self):
245        schema_src = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
246                            <xs:element name="test_element" type="xs:string"/>
247                        </xs:schema>"""
248        schema = xmlschema.XMLSchema(schema_src)
249        schema_proxy = XMLSchemaProxy(schema=schema)
250        self.assertEqual(schema_proxy.find('/test_element'), schema.elements['test_element'])
251
252    def test_get_attribute_api(self):
253        self.assertIs(
254            self.schema_proxy.get_attribute("{http://xpath.test/ns}test_attribute"),
255            self.schema_proxy._schema.maps.attributes["{http://xpath.test/ns}test_attribute"]
256        )
257
258    def test_get_element_api(self):
259        self.assertIs(
260            self.schema_proxy.get_element("{http://xpath.test/ns}test_element"),
261            self.schema_proxy._schema.maps.elements["{http://xpath.test/ns}test_element"]
262        )
263
264    def test_get_substitution_group_api(self):
265        self.assertIsNone(self.schema_proxy.get_substitution_group('x'))
266
267    def test_is_instance_api(self):
268        self.assertFalse(self.schema_proxy.is_instance(True, '{%s}integer' % XSD_NAMESPACE))
269        self.assertTrue(self.schema_proxy.is_instance(5, '{%s}integer' % XSD_NAMESPACE))
270        self.assertFalse(self.schema_proxy.is_instance('alpha', '{%s}integer' % XSD_NAMESPACE))
271        self.assertTrue(self.schema_proxy.is_instance('alpha', '{%s}string' % XSD_NAMESPACE))
272        self.assertTrue(self.schema_proxy.is_instance('alpha beta', '{%s}token' % XSD_NAMESPACE))
273        self.assertTrue(self.schema_proxy.is_instance('alpha', '{%s}Name' % XSD_NAMESPACE))
274        self.assertFalse(self.schema_proxy.is_instance('alpha beta', '{%s}Name' % XSD_NAMESPACE))
275        self.assertFalse(self.schema_proxy.is_instance('1alpha', '{%s}Name' % XSD_NAMESPACE))
276        self.assertTrue(self.schema_proxy.is_instance('alpha', '{%s}NCName' % XSD_NAMESPACE))
277        self.assertFalse(self.schema_proxy.is_instance('eg:alpha', '{%s}NCName' % XSD_NAMESPACE))
278
279    def test_cast_as_api(self):
280        schema_proxy = XMLSchemaProxy()
281        self.assertEqual(schema_proxy.cast_as('19', '{%s}short' % XSD_NAMESPACE), 19)
282
283    def test_attributes_type(self):
284        parser = XPath2Parser(namespaces=self.namespaces)
285        token = parser.parse("@min le @max")
286
287        context = XPathContext(self.etree.XML('<root min="10" max="20" />'))
288        self.assertTrue(token.evaluate(context))
289
290        context = XPathContext(self.etree.XML('<root min="10" max="2" />'))
291        self.assertTrue(token.evaluate(context))
292
293        schema = xmlschema.XMLSchema('''
294            <xs:schema xmlns="http://xpath.test/ns" xmlns:xs="http://www.w3.org/2001/XMLSchema"
295                targetNamespace="http://xpath.test/ns">
296              <xs:element name="range" type="intRange"/>
297              <xs:complexType name="intRange">
298                <xs:attribute name="min" type="xs:int"/>
299                <xs:attribute name="max" type="xs:int"/>
300              </xs:complexType>
301            </xs:schema>''')
302        parser = XPath2Parser(namespaces=self.namespaces,
303                              schema=XMLSchemaProxy(schema, schema.elements['range']))
304        token = parser.parse("@min le @max")
305
306        context = XPathContext(self.etree.XML('<root min="10" max="20" />'))
307        self.assertTrue(token.evaluate(context))
308        context = XPathContext(self.etree.XML('<root min="10" max="2" />'))
309        self.assertFalse(token.evaluate(context))
310
311        schema = xmlschema.XMLSchema('''
312            <xs:schema xmlns="http://xpath.test/ns" xmlns:xs="http://www.w3.org/2001/XMLSchema"
313                targetNamespace="http://xpath.test/ns">
314              <xs:element name="range" type="intRange"/>
315              <xs:complexType name="intRange">
316                <xs:attribute name="min" type="xs:int"/>
317                <xs:attribute name="max" type="xs:string"/>
318              </xs:complexType>
319            </xs:schema>''')
320        parser = XPath2Parser(namespaces=self.namespaces,
321                              schema=XMLSchemaProxy(schema, schema.elements['range']))
322        self.assertRaises(TypeError, parser.parse, '@min le @max')
323
324    def test_elements_type(self):
325        schema = xmlschema.XMLSchema('''
326            <xs:schema xmlns="http://xpath.test/ns" xmlns:xs="http://www.w3.org/2001/XMLSchema"
327                    targetNamespace="http://xpath.test/ns">
328                <xs:element name="values">
329                    <xs:complexType>
330                        <xs:sequence>
331                            <xs:element name="a" type="xs:string"/>
332                            <xs:element name="b" type="xs:integer"/>
333                            <xs:element name="c" type="xs:boolean"/>
334                            <xs:element name="d" type="xs:float"/>
335                        </xs:sequence>
336                    </xs:complexType>
337                </xs:element>
338            </xs:schema>''')
339        parser = XPath2Parser(namespaces={'': "http://xpath.test/ns", 'xs': XSD_NAMESPACE},
340                              schema=XMLSchemaProxy(schema))
341
342        token = parser.parse("//a")
343        self.assertEqual(token[0].xsd_types['a'], schema.maps.types['{%s}string' % XSD_NAMESPACE])
344        token = parser.parse("//b")
345        self.assertEqual(token[0].xsd_types['b'], schema.maps.types['{%s}integer' % XSD_NAMESPACE])
346        token = parser.parse("//values/c")
347
348        self.assertEqual(token[0][0].xsd_types["{http://xpath.test/ns}values"],
349                         schema.elements['values'].type)
350        self.assertEqual(token[1].xsd_types['c'], schema.maps.types['{%s}boolean' % XSD_NAMESPACE])
351
352        token = parser.parse("values/c")
353        self.assertEqual(token[0].xsd_types['{http://xpath.test/ns}values'],
354                         schema.elements['values'].type)
355        self.assertEqual(token[1].xsd_types['c'], schema.maps.types['{%s}boolean' % XSD_NAMESPACE])
356
357        token = parser.parse("values/*")
358        self.assertEqual(token[1].xsd_types, {
359            'a': schema.maps.types['{%s}string' % XSD_NAMESPACE],
360            'b': schema.maps.types['{%s}integer' % XSD_NAMESPACE],
361            'c': schema.maps.types['{%s}boolean' % XSD_NAMESPACE],
362            'd': schema.maps.types['{%s}float' % XSD_NAMESPACE],
363        })
364
365    def test_elements_and_attributes_type(self):
366        schema = xmlschema.XMLSchema('''
367            <xs:schema xmlns="http://xpath.test/ns" xmlns:xs="http://www.w3.org/2001/XMLSchema"
368                    targetNamespace="http://xpath.test/ns">
369                <xs:element name="values">
370                    <xs:complexType>
371                        <xs:sequence>
372                            <xs:element name="a" type="xs:string"/>
373                            <xs:element name="b" type="rangeType"/>
374                            <xs:element name="c" type="xs:boolean"/>
375                            <xs:element name="d" type="xs:float"/>
376                        </xs:sequence>
377                    </xs:complexType>
378                </xs:element>
379                <xs:complexType name="rangeType">
380                    <xs:simpleContent>
381                        <xs:extension base="xs:integer">
382                            <xs:attribute name="min" type="xs:integer"/>
383                            <xs:attribute name="max" type="xs:integer"/>
384                        </xs:extension>
385                    </xs:simpleContent>
386                </xs:complexType>
387            </xs:schema>''')
388        parser = XPath2Parser(namespaces={'': "http://xpath.test/ns", 'xs': XSD_NAMESPACE},
389                              schema=XMLSchemaProxy(schema))
390        token = parser.parse("//a")
391        self.assertEqual(token[0].xsd_types['a'], schema.maps.types['{%s}string' % XSD_NAMESPACE])
392        token = parser.parse("//b")
393        self.assertEqual(token[0].xsd_types['b'], schema.types['rangeType'])
394        token = parser.parse("values/c")
395
396        self.assertEqual(token[0].xsd_types['{http://xpath.test/ns}values'],
397                         schema.elements['values'].type)
398        self.assertEqual(token[1].xsd_types['c'], schema.maps.types['{%s}boolean' % XSD_NAMESPACE])
399        token = parser.parse("//b/@min")
400        self.assertEqual(token[0][0].xsd_types['b'], schema.types['rangeType'])
401
402        self.assertEqual(token[1][0].xsd_types['min'],
403                         schema.maps.types['{%s}integer' % XSD_NAMESPACE])
404
405        token = parser.parse("values/b/@min")
406        self.assertEqual(token[0][0].xsd_types['{http://xpath.test/ns}values'],
407                         schema.elements['values'].type)
408        self.assertEqual(token[0][1].xsd_types['b'], schema.types['rangeType'])
409        self.assertEqual(token[1][0].xsd_types['min'],
410                         schema.maps.types['{%s}integer' % XSD_NAMESPACE])
411
412        token = parser.parse("//b/@min lt //b/@max")
413        self.assertEqual(token[0][0][0].xsd_types['b'], schema.types['rangeType'])
414        self.assertEqual(token[0][1][0].xsd_types['min'],
415                         schema.maps.types['{%s}integer' % XSD_NAMESPACE])
416        self.assertEqual(token[1][0][0].xsd_types['b'], schema.types['rangeType'])
417        self.assertEqual(token[1][1][0].xsd_types['max'],
418                         schema.maps.types['{%s}integer' % XSD_NAMESPACE])
419
420        root = self.etree.XML('<values xmlns="http://xpath.test/ns"><b min="19"/></values>')
421        self.assertIsNone(token.evaluate(context=XPathContext(root)))
422
423        root = self.etree.XML('<values xmlns="http://xpath.test/ns"><b min="19">30</b></values>')
424        self.assertIsNone(token.evaluate(context=XPathContext(root)))
425
426        root = self.etree.XML(
427            '<values xmlns="http://xpath.test/ns"><b min="19" max="40">30</b></values>')
428        context = XPathContext(root)
429        self.assertTrue(token.evaluate(context))
430
431        root = self.etree.XML(
432            '<values xmlns="http://xpath.test/ns"><b min="19" max="10">30</b></values>')
433        context = XPathContext(root)
434        self.assertFalse(token.evaluate(context))
435
436    def test_issue_10(self):
437        schema = xmlschema.XMLSchema('''
438            <xs:schema xmlns="http://xpath.test/ns#" xmlns:xs="http://www.w3.org/2001/XMLSchema"
439                    targetNamespace="http://xpath.test/ns#">
440                <xs:element name="root" type="rootType" />
441                <xs:simpleType name="rootType">
442                    <xs:restriction base="xs:string"/>
443                </xs:simpleType>
444            </xs:schema>''')
445
446        # TODO: test fail with xmlschema-1.0.17+, added namespaces as temporary fix for test.
447        #  A fix for xmlschema.xpath.ElementPathMixin._get_xpath_namespaces() is required.
448        root = schema.find('root', namespaces={'': 'http://xpath.test/ns#'})
449        self.assertEqual(getattr(root, 'tag', None), '{http://xpath.test/ns#}root')
450
451
452@unittest.skipIf(xmlschema is None or lxml_etree is None, "both xmlschema and lxml required")
453class LxmlXMLSchemaProxyTest(XMLSchemaProxyTest):
454    etree = lxml_etree
455
456
457if __name__ == '__main__':
458    unittest.main()
459