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