341 lines
17 KiB
Python
341 lines
17 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
|
|
# All rights reserved.
|
|
# This file is distributed under the terms of the MIT License.
|
|
# See the file 'LICENSE' in the root directory of the present
|
|
# distribution, or http://opensource.org/licenses/MIT.
|
|
#
|
|
# @author Davide Brunato <brunato@sissa.it>
|
|
#
|
|
from __future__ import unicode_literals
|
|
import unittest
|
|
import xml.etree.ElementTree as ElementTree
|
|
import io
|
|
try:
|
|
import lxml.etree as lxml_etree
|
|
except ImportError:
|
|
lxml_etree = None
|
|
|
|
from elementpath import AttributeNode, XPathContext, XPath2Parser, ElementPathTypeError
|
|
from elementpath.compat import PY3
|
|
from elementpath.namespaces import XML_LANG, XSD_NAMESPACE
|
|
|
|
try:
|
|
# noinspection PyPackageRequirements
|
|
import xmlschema
|
|
except (ImportError, AttributeError):
|
|
xmlschema = None
|
|
else:
|
|
try:
|
|
from xmlschema.xpath import XMLSchemaProxy # it works if xmlschema~=1.0.14
|
|
except ImportError:
|
|
from elementpath.schema_proxy import XMLSchemaProxy
|
|
|
|
try:
|
|
from tests import test_xpath2_parser
|
|
except ImportError:
|
|
# Python2 fallback
|
|
import test_xpath2_parser
|
|
|
|
|
|
@unittest.skipIf(xmlschema is None, "xmlschema library required.")
|
|
class XPath2ParserXMLSchemaTest(test_xpath2_parser.XPath2ParserTest):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.schema = xmlschema.XMLSchema('''
|
|
<!-- Dummy schema for testing proxy API -->
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://xpath.test/ns">
|
|
<xs:element name="test_element" type="xs:string"/>
|
|
<xs:attribute name="test_attribute" type="xs:string"/>
|
|
</xs:schema>''')
|
|
|
|
def setUp(self):
|
|
self.schema_proxy = XMLSchemaProxy(self.schema)
|
|
self.parser = XPath2Parser(namespaces=self.namespaces, schema=self.schema_proxy,
|
|
variables=self.variables)
|
|
|
|
def test_schema_proxy_init(self):
|
|
schema_src = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
<xs:element name="test_element" type="xs:string"/>
|
|
</xs:schema>"""
|
|
schema_tree = ElementTree.parse(io.StringIO(schema_src))
|
|
|
|
self.assertIsInstance(XMLSchemaProxy(), XMLSchemaProxy)
|
|
self.assertIsInstance(XMLSchemaProxy(xmlschema.XMLSchema(schema_src)), XMLSchemaProxy)
|
|
with self.assertRaises(TypeError):
|
|
XMLSchemaProxy(schema=schema_tree)
|
|
with self.assertRaises(TypeError):
|
|
XMLSchemaProxy(schema=xmlschema.XMLSchema(schema_src),
|
|
base_element=schema_tree)
|
|
with self.assertRaises(TypeError):
|
|
XMLSchemaProxy(schema=xmlschema.XMLSchema(schema_src),
|
|
base_element=schema_tree.getroot())
|
|
|
|
schema = xmlschema.XMLSchema(schema_src)
|
|
with self.assertRaises(ValueError):
|
|
XMLSchemaProxy(base_element=schema.elements['test_element'])
|
|
|
|
def test_xmlschema_proxy(self):
|
|
context = XPathContext(
|
|
root=self.etree.XML('<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"/>')
|
|
)
|
|
|
|
self.wrong_name("schema-element(nil)")
|
|
self.wrong_name("schema-element(xs:string)")
|
|
self.check_value("schema-element(xs:complexType)", None)
|
|
self.check_value("schema-element(xs:schema)", context.item, context)
|
|
self.check_tree("schema-element(xs:group)", '(schema-element (: (xs) (group)))')
|
|
|
|
context.item = AttributeNode(XML_LANG, 'en')
|
|
self.wrong_name("schema-attribute(nil)")
|
|
self.wrong_name("schema-attribute(xs:string)")
|
|
self.check_value("schema-attribute(xml:lang)", None)
|
|
self.check_value("schema-attribute(xml:lang)", context.item, context)
|
|
self.check_tree("schema-attribute(xsi:schemaLocation)",
|
|
'(schema-attribute (: (xsi) (schemaLocation)))')
|
|
|
|
def test_get_type_api(self):
|
|
schema_proxy = XMLSchemaProxy()
|
|
self.assertIsNone(schema_proxy.get_type('unknown'))
|
|
self.assertEqual(schema_proxy.get_type('{%s}string' % XSD_NAMESPACE),
|
|
xmlschema.XMLSchema.builtin_types()['string'])
|
|
|
|
def test_get_primitive_type_api(self):
|
|
schema_proxy = XMLSchemaProxy()
|
|
short_type = schema_proxy.get_type('{%s}short' % XSD_NAMESPACE)
|
|
decimal_type = schema_proxy.get_type('{%s}decimal' % XSD_NAMESPACE)
|
|
self.assertEqual(schema_proxy.get_primitive_type(short_type), decimal_type)
|
|
|
|
ntokens_type = schema_proxy.get_type('{%s}NMTOKENS' % XSD_NAMESPACE)
|
|
string_type = schema_proxy.get_type('{%s}string' % XSD_NAMESPACE)
|
|
self.assertEqual(schema_proxy.get_primitive_type(ntokens_type), string_type)
|
|
|
|
facet_type = schema_proxy.get_type('{%s}facet' % XSD_NAMESPACE)
|
|
any_type = schema_proxy.get_type('{%s}anyType' % XSD_NAMESPACE)
|
|
self.assertEqual(schema_proxy.get_primitive_type(facet_type), any_type)
|
|
self.assertEqual(schema_proxy.get_primitive_type(any_type), any_type)
|
|
|
|
any_simple_type = schema_proxy.get_type('{%s}anySimpleType' % XSD_NAMESPACE)
|
|
self.assertEqual(schema_proxy.get_primitive_type(any_simple_type), any_simple_type)
|
|
|
|
def test_find_api(self):
|
|
schema_src = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
<xs:element name="test_element" type="xs:string"/>
|
|
</xs:schema>"""
|
|
schema = xmlschema.XMLSchema(schema_src)
|
|
schema_proxy = XMLSchemaProxy(schema=schema)
|
|
if xmlschema.__version__ == '1.0.14':
|
|
self.assertIsNone(schema_proxy.find('/test_element')) # Not implemented!
|
|
else:
|
|
self.assertEqual(schema_proxy.find('/test_element'), schema.elements['test_element'])
|
|
|
|
def test_is_instance_api(self):
|
|
self.assertFalse(self.schema_proxy.is_instance(True, '{%s}integer' % XSD_NAMESPACE))
|
|
self.assertTrue(self.schema_proxy.is_instance(5, '{%s}integer' % XSD_NAMESPACE))
|
|
self.assertFalse(self.schema_proxy.is_instance('alpha', '{%s}integer' % XSD_NAMESPACE))
|
|
self.assertTrue(self.schema_proxy.is_instance('alpha', '{%s}string' % XSD_NAMESPACE))
|
|
self.assertTrue(self.schema_proxy.is_instance('alpha beta', '{%s}token' % XSD_NAMESPACE))
|
|
self.assertTrue(self.schema_proxy.is_instance('alpha', '{%s}Name' % XSD_NAMESPACE))
|
|
self.assertFalse(self.schema_proxy.is_instance('alpha beta', '{%s}Name' % XSD_NAMESPACE))
|
|
self.assertFalse(self.schema_proxy.is_instance('1alpha', '{%s}Name' % XSD_NAMESPACE))
|
|
self.assertTrue(self.schema_proxy.is_instance('alpha', '{%s}NCName' % XSD_NAMESPACE))
|
|
self.assertFalse(self.schema_proxy.is_instance('eg:alpha', '{%s}NCName' % XSD_NAMESPACE))
|
|
|
|
def test_cast_as_api(self):
|
|
schema_proxy = XMLSchemaProxy()
|
|
self.assertEqual(schema_proxy.cast_as('19', '{%s}short' % XSD_NAMESPACE), 19)
|
|
|
|
def test_attributes_type(self):
|
|
parser = XPath2Parser(namespaces=self.namespaces)
|
|
token = parser.parse("@min le @max")
|
|
self.assertTrue(token.evaluate(context=XPathContext(self.etree.XML('<root min="10" max="20" />'))))
|
|
self.assertTrue(token.evaluate(context=XPathContext(self.etree.XML('<root min="10" max="2" />'))))
|
|
|
|
schema = xmlschema.XMLSchema('''
|
|
<xs:schema xmlns="http://xpath.test/ns" xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
targetNamespace="http://xpath.test/ns">
|
|
<xs:element name="range" type="intRange"/>
|
|
<xs:complexType name="intRange">
|
|
<xs:attribute name="min" type="xs:int"/>
|
|
<xs:attribute name="max" type="xs:int"/>
|
|
</xs:complexType>
|
|
</xs:schema>''')
|
|
parser = XPath2Parser(namespaces=self.namespaces,
|
|
schema=XMLSchemaProxy(schema, schema.elements['range']))
|
|
token = parser.parse("@min le @max")
|
|
self.assertTrue(token.evaluate(context=XPathContext(self.etree.XML('<root min="10" max="20" />'))))
|
|
self.assertFalse(token.evaluate(context=XPathContext(self.etree.XML('<root min="10" max="2" />'))))
|
|
|
|
schema = xmlschema.XMLSchema('''
|
|
<xs:schema xmlns="http://xpath.test/ns" xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
targetNamespace="http://xpath.test/ns">
|
|
<xs:element name="range" type="intRange"/>
|
|
<xs:complexType name="intRange">
|
|
<xs:attribute name="min" type="xs:int"/>
|
|
<xs:attribute name="max" type="xs:string"/>
|
|
</xs:complexType>
|
|
</xs:schema>''')
|
|
parser = XPath2Parser(namespaces=self.namespaces,
|
|
schema=XMLSchemaProxy(schema, schema.elements['range']))
|
|
if PY3:
|
|
self.assertRaises(TypeError, parser.parse, '@min le @max')
|
|
else:
|
|
# In Python 2 strings and numbers are comparable and strings are 'greater than' numbers.
|
|
token = parser.parse("@min le @max")
|
|
self.assertTrue(token.evaluate(context=XPathContext(self.etree.XML('<root min="10" max="20" />'))))
|
|
self.assertTrue(token.evaluate(context=XPathContext(self.etree.XML('<root min="10" max="2" />'))))
|
|
|
|
def test_elements_type(self):
|
|
schema = xmlschema.XMLSchema('''
|
|
<xs:schema xmlns="http://xpath.test/ns" xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
targetNamespace="http://xpath.test/ns">
|
|
<xs:element name="values">
|
|
<xs:complexType>
|
|
<xs:sequence>
|
|
<xs:element name="a" type="xs:string"/>
|
|
<xs:element name="b" type="xs:integer"/>
|
|
<xs:element name="c" type="xs:boolean"/>
|
|
<xs:element name="d" type="xs:float"/>
|
|
</xs:sequence>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
</xs:schema>''')
|
|
parser = XPath2Parser(namespaces={'': "http://xpath.test/ns", 'xs': XSD_NAMESPACE},
|
|
schema=XMLSchemaProxy(schema))
|
|
token = parser.parse("//a")
|
|
self.assertEqual(token[0].xsd_type, schema.maps.types['{%s}string' % XSD_NAMESPACE])
|
|
token = parser.parse("//b")
|
|
self.assertEqual(token[0].xsd_type, schema.maps.types['{%s}integer' % XSD_NAMESPACE])
|
|
token = parser.parse("//values/c")
|
|
self.assertEqual(token[0][0].xsd_type, schema.elements['values'].type)
|
|
self.assertEqual(token[1].xsd_type, schema.maps.types['{%s}boolean' % XSD_NAMESPACE])
|
|
token = parser.parse("values/c")
|
|
self.assertEqual(token[0].xsd_type, schema.elements['values'].type)
|
|
self.assertEqual(token[1].xsd_type, schema.maps.types['{%s}boolean' % XSD_NAMESPACE])
|
|
|
|
schema = xmlschema.XMLSchema('''
|
|
<xs:schema xmlns="http://xpath.test/ns" xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
targetNamespace="http://xpath.test/ns">
|
|
<xs:element name="values">
|
|
<xs:complexType>
|
|
<xs:sequence>
|
|
<xs:element name="a" type="xs:string"/>
|
|
<xs:element name="b" type="rangeType"/>
|
|
<xs:element name="c" type="xs:boolean"/>
|
|
<xs:element name="d" type="xs:float"/>
|
|
</xs:sequence>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
<xs:complexType name="rangeType">
|
|
<xs:simpleContent>
|
|
<xs:extension base="xs:integer">
|
|
<xs:attribute name="min" type="xs:integer"/>
|
|
<xs:attribute name="max" type="xs:integer"/>
|
|
</xs:extension>
|
|
</xs:simpleContent>
|
|
</xs:complexType>
|
|
</xs:schema>''')
|
|
parser = XPath2Parser(namespaces={'': "http://xpath.test/ns", 'xs': XSD_NAMESPACE},
|
|
schema=XMLSchemaProxy(schema))
|
|
token = parser.parse("//a")
|
|
self.assertEqual(token[0].xsd_type, schema.maps.types['{%s}string' % XSD_NAMESPACE])
|
|
token = parser.parse("//b")
|
|
self.assertEqual(token[0].xsd_type, schema.types['rangeType'])
|
|
token = parser.parse("values/c")
|
|
self.assertEqual(token[0].xsd_type, schema.elements['values'].type)
|
|
self.assertEqual(token[1].xsd_type, schema.maps.types['{%s}boolean' % XSD_NAMESPACE])
|
|
token = parser.parse("//b/@min")
|
|
self.assertEqual(token[0][0].xsd_type, schema.types['rangeType'])
|
|
self.assertEqual(token[1][0].xsd_type, schema.maps.types['{%s}integer' % XSD_NAMESPACE])
|
|
token = parser.parse("values/b/@min")
|
|
self.assertEqual(token[0][0].xsd_type, schema.elements['values'].type)
|
|
self.assertEqual(token[0][1].xsd_type, schema.types['rangeType'])
|
|
self.assertEqual(token[1][0].xsd_type, schema.maps.types['{%s}integer' % XSD_NAMESPACE])
|
|
|
|
token = parser.parse("//b/@min lt //b/@max")
|
|
self.assertEqual(token[0][0][0].xsd_type, schema.types['rangeType'])
|
|
self.assertEqual(token[0][1][0].xsd_type, schema.maps.types['{%s}integer' % XSD_NAMESPACE])
|
|
self.assertEqual(token[1][0][0].xsd_type, schema.types['rangeType'])
|
|
self.assertEqual(token[1][1][0].xsd_type, schema.maps.types['{%s}integer' % XSD_NAMESPACE])
|
|
|
|
root = self.etree.XML('<values xmlns="http://xpath.test/ns"><b min="19"/></values>')
|
|
with self.assertRaises(TypeError):
|
|
token.evaluate(context=XPathContext(root))
|
|
|
|
root = self.etree.XML('<values xmlns="http://xpath.test/ns"><b min="19">30</b></values>')
|
|
self.assertIsNone(token.evaluate(context=XPathContext(root)))
|
|
|
|
root = self.etree.XML('<values xmlns="http://xpath.test/ns"><b min="19" max="40">30</b></values>')
|
|
context = XPathContext(root)
|
|
self.assertTrue(token.evaluate(context))
|
|
|
|
root = self.etree.XML('<values xmlns="http://xpath.test/ns"><b min="19" max="10">30</b></values>')
|
|
context = XPathContext(root)
|
|
self.assertFalse(token.evaluate(context))
|
|
|
|
def test_instance_of_expression(self):
|
|
element = self.etree.Element('schema')
|
|
context = XPathContext(element)
|
|
|
|
# Test cases from https://www.w3.org/TR/xpath20/#id-instance-of
|
|
self.check_value("5 instance of xs:integer", True)
|
|
self.check_value("5 instance of xs:decimal", True)
|
|
self.check_value("9.0 instance of xs:integer",
|
|
False if [int(n) for n in xmlschema.__version__.split('.')] >= [1, 0, 8] else True)
|
|
self.check_value("(5, 6) instance of xs:integer+", True)
|
|
self.check_value(". instance of element()", True, context)
|
|
|
|
self.check_value("(5, 6) instance of xs:integer", False)
|
|
self.check_value("(5, 6) instance of xs:integer*", True)
|
|
self.check_value("(5, 6) instance of xs:integer?", False)
|
|
|
|
self.check_value("5 instance of empty-sequence()", False)
|
|
self.check_value("() instance of empty-sequence()", True)
|
|
|
|
def test_treat_as_expression(self):
|
|
element = self.etree.Element('schema')
|
|
context = XPathContext(element)
|
|
|
|
self.check_value("5 treat as xs:integer", [5])
|
|
# self.check_value("5 treat as xs:string", ElementPathTypeError) # FIXME: a bug of xmlschema!
|
|
self.check_value("5 treat as xs:decimal", [5])
|
|
self.check_value("(5, 6) treat as xs:integer+", [5, 6])
|
|
self.check_value(". treat as element()", [element], context)
|
|
|
|
self.check_value("(5, 6) treat as xs:integer", ElementPathTypeError)
|
|
self.check_value("(5, 6) treat as xs:integer*", [5, 6])
|
|
self.check_value("(5, 6) treat as xs:integer?", ElementPathTypeError)
|
|
|
|
self.check_value("5 treat as empty-sequence()", ElementPathTypeError)
|
|
self.check_value("() treat as empty-sequence()", [])
|
|
|
|
def test_castable_expression(self):
|
|
self.check_value("5 castable as xs:integer", True)
|
|
self.check_value("'5' castable as xs:integer", True)
|
|
self.check_value("'hello' castable as xs:integer", False)
|
|
self.check_value("('5', '6') castable as xs:integer", False)
|
|
self.check_value("() castable as xs:integer", False)
|
|
self.check_value("() castable as xs:integer?", True)
|
|
|
|
def test_cast_expression(self):
|
|
self.check_value("5 cast as xs:integer", 5)
|
|
self.check_value("'5' cast as xs:integer", 5)
|
|
self.check_value("'hello' cast as xs:integer", ValueError)
|
|
self.check_value("('5', '6') cast as xs:integer", TypeError)
|
|
self.check_value("() cast as xs:integer", TypeError)
|
|
self.check_value("() cast as xs:integer?", [])
|
|
self.check_value('"1" cast as xs:boolean', True)
|
|
self.check_value('"0" cast as xs:boolean', False)
|
|
|
|
|
|
@unittest.skipIf(xmlschema is None or lxml_etree is None, "both xmlschema and lxml required")
|
|
class LxmlXPath2ParserXMLSchemaTest(XPath2ParserXMLSchemaTest):
|
|
etree = lxml_etree
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|