diff --git a/elementpath/xpath2/xpath2_parser.py b/elementpath/xpath2/xpath2_parser.py
index 98836d76..20deee99 100644
--- a/elementpath/xpath2/xpath2_parser.py
+++ b/elementpath/xpath2/xpath2_parser.py
@@ -24,8 +24,8 @@
from ..namespaces import XSD_NAMESPACE, XML_NAMESPACE, XLINK_NAMESPACE, \
XPATH_FUNCTIONS_NAMESPACE, XQT_ERRORS_NAMESPACE, XSD_NOTATION, \
XSD_ANY_ATOMIC_TYPE, get_namespace, get_prefixed_name, get_expanded_name
-from ..datatypes import UntypedAtomic, QName, AnyURI, Duration
-from ..xpath_nodes import TypedAttribute, is_xpath_node, \
+from ..datatypes import UntypedAtomic, QName, AnyURI, Duration, Integer
+from ..xpath_nodes import TypedElement, is_xpath_node, \
match_attribute_node, is_element_node, is_document_node
from ..xpath_token import UNICODE_CODEPOINT_COLLATION
from ..xpath1 import XPath1Parser
@@ -751,6 +751,7 @@ def evaluate(self, context=None):
@method('cast', bp=63)
def led(self, left):
self.parser.advance('as')
+ self.parser.expected_name('(name)', ':')
self[:] = left, self.parser.expression(rbp=self.rbp)
if self.parser.next_token.symbol == '?':
self[2:] = self.parser.symbol_table['?'](self.parser), # Add nullary token
@@ -791,7 +792,7 @@ def evaluate(self, context=None):
arg = self.data_value(result[0])
try:
if namespace != XSD_NAMESPACE:
- value = self.parser.schema.cast_as(arg, atomic_type)
+ value = self.parser.schema.cast_as(self.string_value(arg), atomic_type)
else:
local_name = atomic_type.split('}')[1]
token_class = self.parser.symbol_table.get(local_name)
@@ -801,28 +802,16 @@ def evaluate(self, context=None):
token = token_class(self.parser)
value = token.cast(arg)
+
except ElementPathError:
if self.symbol != 'cast':
return False
raise
- except KeyError:
- msg = "atomic type %r not found in the in-scope schema types"
- self.unknown_atomic_type(msg % self[1].source)
- except TypeError as err:
- if self.symbol != 'cast':
- return False
- elif isinstance(arg, UntypedAtomic):
- raise self.error('FORG0001', err)
- elif self[0].symbol == ':' and self[0][1].symbol == 'string':
- raise self.error('FORG0001', err) from None
-
- raise self.error('XPTY0004', err) from None
- except ValueError as err:
+ except (TypeError, ValueError) as err:
if self.symbol != 'cast':
return False
- elif self[0].symbol == ':' and self[0][1].symbol == 'string':
+ elif isinstance(arg, (UntypedAtomic, str)):
raise self.error('FORG0001', err) from None
-
raise self.error('XPTY0004', err) from None
else:
return value if self.symbol == 'cast' else True
@@ -921,8 +910,6 @@ def evaluate(self, context=None):
operands[0] = float(operands[0])
elif all(isinstance(x, Duration) for x in operands) and self.symbol in ('eq', 'ne'):
pass
- elif (issubclass(cls0, cls1) or issubclass(cls1, cls0)) and not issubclass(cls0, Duration):
- pass
else:
msg = "cannot apply {} between {!r} and {!r}".format(self, *operands)
raise self.error('XPTY0004', msg)
@@ -973,7 +960,7 @@ def evaluate(self, context=None):
else:
if left[0] is right[0]:
return False
- for item in context.root.iter():
+ for item in context.root.iter(): # pragma: no cover
if left[0] is item:
return True if symbol == '<<' else False
elif right[0] is item:
@@ -994,13 +981,11 @@ def led(self, left):
@method('to')
def evaluate(self, context=None):
- start, stop = self.get_operands(context, cls=int)
+ start, stop = self.get_operands(context, cls=Integer)
try:
return [x for x in range(start, stop + 1)]
- except TypeError as err:
- if context is None or start is None or stop is None:
- return []
- raise self.error('FORG0006', err) from None
+ except TypeError:
+ return []
@method('to')
@@ -1080,10 +1065,10 @@ def select(self, context=None):
for item in self[0].select(context):
if len(self) == 1:
yield item
- elif self.xsd_types:
- type_annotation = self[1].evaluate(context)
- if self.xsd_types.is_matching(type_annotation, self.parser.default_namespace):
- yield context.item
+ elif isinstance(item, TypedElement):
+ for type_annotation in self[1].select():
+ if type_annotation == item.xsd_type.name:
+ yield item
@method('element')
@@ -1209,9 +1194,6 @@ def select(self, context=None):
self.add_xsd_type(attribute)
elif not type_name:
yield attribute.value
- elif isinstance(attribute, TypedAttribute):
- if attribute.xsd_type.name == type_name:
- yield attribute.value
else:
xsd_type = self.get_xsd_type(attribute)
if xsd_type is not None and xsd_type.name == type_name:
diff --git a/tests/test_schema_context.py b/tests/test_schema_context.py
index e1ba902b..339b21e3 100644
--- a/tests/test_schema_context.py
+++ b/tests/test_schema_context.py
@@ -27,7 +27,7 @@ class XMLSchemaProxyTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
- cls.schema1 = xmlschema.XMLSchema(dedent('''
+ cls.schema1 = xmlschema.XMLSchema(dedent('''\
@@ -42,9 +42,9 @@ def setUpClass(cls):
'''))
- cls.schema2 = xmlschema.XMLSchema(dedent('''
+ cls.schema2 = xmlschema.XMLSchema(dedent('''\
-
+
'''))
def test_name_token(self):
diff --git a/tests/test_schema_proxy.py b/tests/test_schema_proxy.py
index 9e34ceaf..4a1e784b 100644
--- a/tests/test_schema_proxy.py
+++ b/tests/test_schema_proxy.py
@@ -102,7 +102,7 @@ def test_xmlschema_proxy(self):
self.wrong_syntax("schema-element(*)")
self.wrong_name("schema-element(nil)")
self.wrong_name("schema-element(xs:string)")
- self.check_value("self::schema-element(xs:complexType)", MissingContextError)
+ self.check_value("schema-element(xs:complexType)", MissingContextError)
self.check_value("self::schema-element(xs:complexType)", NameError, context)
self.check_value("self::schema-element(xs:schema)", [context.item], context)
self.check_tree("schema-element(xs:group)", '(schema-element (: (xs) (group)))')
@@ -111,7 +111,7 @@ def test_xmlschema_proxy(self):
self.wrong_syntax("schema-attribute(*)")
self.wrong_name("schema-attribute(nil)")
self.wrong_name("schema-attribute(xs:string)")
- self.check_value("self::schema-attribute(xml:lang)", MissingContextError)
+ self.check_value("schema-attribute(xml:lang)", MissingContextError)
self.check_value("schema-attribute(xml:lang)", NameError, context)
self.check_value("self::schema-attribute(xml:lang)", [context.item], context)
self.check_tree("schema-attribute(xsi:schemaLocation)",
diff --git a/tests/test_xpath2_parser.py b/tests/test_xpath2_parser.py
index 7989c6d4..65cf3111 100644
--- a/tests/test_xpath2_parser.py
+++ b/tests/test_xpath2_parser.py
@@ -232,6 +232,8 @@ def test_xpath_comments(self):
def test_comma_operator(self):
self.check_value("1, 2", [1, 2])
self.check_value("(1, 2)", [1, 2])
+ self.check_value("(1, 2, ())", [1, 2])
+ self.check_value("(1, fn:round-half-to-even(()), 7)", [1, 7])
self.check_value("(-9, 28, 10)", [-9, 28, 10])
self.check_value("(1, 2)", [1, 2])
@@ -248,6 +250,11 @@ def test_range_expressions(self):
self.check_value("10 to 10", [10])
self.check_value("15 to 10", [])
self.check_value("fn:reverse(10 to 15)", [15, 14, 13, 12, 11, 10])
+ self.wrong_syntax("1 to 10 to 20", 'XPST0003')
+
+ root = self.etree.XML('')
+ self.wrong_type("'1' to '10'", 'XPTY0004', context=XPathContext(root))
+ self.wrong_type("true() to 10", 'XPTY0004')
def test_parenthesized_expressions(self):
self.check_value("(1, 2, '10')", [1, 2, '10'])
@@ -382,8 +389,12 @@ def test_idiv_operator(self):
self.check_value("5 idiv 2", 2)
self.check_value("-3.5 idiv -2", 1)
self.check_value("-3.5 idiv 2", -1)
+ self.check_value('xs:float("-3.5") idiv xs:float("3")', -1)
self.check_value("-3.5 idiv 0", ZeroDivisionError)
self.check_value("xs:float('INF') idiv 2", OverflowError)
+ self.wrong_value("-3.5 idiv ()", 'XPST0005')
+ self.check_raise('xs:float("NaN") idiv 1', OverflowError, 'FOAR0002')
+ self.wrong_type("5 idiv '2'", 'XPTY0004')
def test_comparison_operators(self):
super(XPath2ParserTest, self).test_comparison_operators()
@@ -391,6 +402,11 @@ def test_comparison_operators(self):
self.check_value("19.03 ne 19.02999", True)
self.check_value("-1.0 eq 1.0", False)
self.check_value("1 le 2", True)
+ self.check_value("1e0 eq 1e2", False)
+ self.check_value("xs:float('1e0') eq 1e2", False)
+ self.check_value("1.0 lt 1e2", True)
+ self.check_value("1e2 lt 1000", True)
+
self.check_value("3 le 2", False)
self.check_value("5 ge 9", False)
self.check_value("5 gt 3", True)
@@ -401,9 +417,16 @@ def test_comparison_operators(self):
self.check_value("() * 7")
self.check_value("() * ()")
+ self.check_value('xs:string("http://xpath.test") eq xs:anyURI("http://xpath.test")', True)
+
self.check_value("() le 4")
self.check_value("4 gt ()")
self.check_value("() eq ()") # Equality of empty sequences is also an empty sequence
+ self.wrong_syntax('true() eq true() eq true()', 'XPST0003')
+
+ # From W3C XQuery/XPath tests
+ self.check_value('xs:duration("P31D") ne xs:yearMonthDuration("P1M")', True)
+ self.wrong_type('QName("", "ncname") le QName("", "ncname")', 'XPTY0004')
def test_comparison_in_expression(self):
context = XPathContext(self.etree.XML('false'))
@@ -692,6 +715,11 @@ def test_document_node_accessor(self):
self.check_selector("self::document-node(element(A))", document, [document])
self.check_selector("self::document-node(element(B))", document, [])
+ context = XPathContext(root=document.getroot())
+ self.check_select("document-node()", [], context)
+ self.check_select("self::document-node()", [], context)
+ self.check_select("self::document-node(element(A))", [], context)
+
def test_element_accessor(self):
element = self.etree.Element('schema')
context = XPathContext(root=element)
@@ -708,6 +736,19 @@ def test_element_accessor(self):
self.check_select("element(B)", root[:], context)
self.check_select("element(A)", [], context)
+ if xmlschema is not None:
+ schema = xmlschema.XMLSchema(dedent('''\
+
+
+ '''))
+
+ root = self.etree.XML('hello')
+ context = XPathContext(root)
+ with self.schema_bound_parser(schema.elements['root'].xpath_proxy):
+ typed_element = TypedElement(root, schema.elements['root'], 'hello')
+ self.check_select("self::element(*, xs:string)", [typed_element], context)
+ self.check_select("self::element(*, xs:int)", [], context)
+
def test_attribute_accessor(self):
root = self.etree.XML('texttail')
context = XPathContext(root)
@@ -802,6 +843,18 @@ def test_node_comparison_operators(self):
self.check_selector('/books/book[isbn="not a code"] is /books/book[call="QA76.9 C3847"]',
root, [])
+ context = XPathContext(root)
+ self.check_value('/books/book[isbn="1558604820"] is ()', context=context)
+ self.wrong_type('/books/book[isbn="1558604820"] is (1, 2)', 'XPTY0004', context=context)
+
+ self.check_value('/books/book[isbn="1558604820"] << /books/book[isbn="1558604820"]',
+ False, context=context)
+
+ context = XPathContext(root, variables={'a': self.etree.Element('a'),
+ 'b': self.etree.Element('b')})
+ self.wrong_value('$a << $b', 'FOCA0002', 'operands are not nodes of the XML tree',
+ context=context)
+
root = self.etree.XML('''
28-451
@@ -826,6 +879,10 @@ def test_node_comparison_operators(self):
root, TypeError
)
+ self.wrong_type('is ()', 'XPST0017')
+ self.wrong_syntax('is B', 'XPST0003')
+ self.wrong_syntax('A is B is C', 'XPST0003')
+
def test_empty_sequence_type(self):
self.check_value("() treat as empty-sequence()", [])
self.check_value("6 treat as empty-sequence()", TypeError)
@@ -913,12 +970,23 @@ def test_treat_as_expression(self):
self.check_value("5 treat as empty-sequence()", ElementPathTypeError)
self.check_value("() treat as empty-sequence()", [])
+ self.check_value("() treat as xs:integer?", [])
+ self.wrong_type("() treat as xs:integer", 'XPDY0050')
+
+ # Test dynamic evaluation error on prefixed name
+ parser = XPath2Parser()
+ token = parser.parse('5 treat as xs:decimal')
+ parser.namespaces.pop('xs')
+ with self.assertRaises(NameError) as ctx:
+ token.evaluate()
+ self.assertIn('XPST0081', str(ctx.exception))
# From W3C XQuery/XPath tests
self.check_value("3 treat as item()+", [3], context)
self.wrong_type("3 treat as node()+", 'XPDY0050', context=context)
self.check_value("(1, 2, 3) treat as item()+", [1, 2, 3], context)
self.wrong_type("(1, 2, 3) treat as item()", 'XPDY0050', context=context)
+ self.wrong_name("3 treat as xs:doesNotExist")
def test_castable_expression(self):
self.check_value("5 castable as xs:integer", True)
@@ -928,11 +996,18 @@ def test_castable_expression(self):
self.check_value("() castable as xs:integer", False)
self.check_value("() castable as xs:integer?", True)
+ self.wrong_syntax("5 castable as empty-sequence()", 'XPST0003')
+ self.wrong_name("5 castable as void", 'XPST0051')
+ self.check_value("5 castable as xs:void", False)
+
self.check_value("'NaN' castable as xs:double", True)
self.check_value("'None' castable as xs:double", False)
self.check_value("'NaN' castable as xs:float", True)
self.check_value("'NaN' castable as xs:integer", False)
+ # From W3C XQuery/XPath tests
+ self.check_value("(1E3) castable as xs:double?", True)
+
def test_cast_expression(self):
self.check_value("5 cast as xs:integer", 5)
self.check_value("'5' cast as xs:integer", 5)
@@ -943,10 +1018,42 @@ def test_cast_expression(self):
self.check_value('"1" cast as xs:boolean', True)
self.check_value('"0" cast as xs:boolean', False)
+ self.check_value("xs:untypedAtomic('1E3') cast as xs:double", 1E3)
+ self.wrong_value("xs:untypedAtomic('x') cast as xs:double", 'FORG0001')
+
+ # Test dynamic evaluation error on prefixed name
+ parser = XPath2Parser()
+ token = parser.parse("() cast as xs:string?")
+ parser.namespaces.pop('xs')
+ with self.assertRaises(NameError) as ctx:
+ token.evaluate()
+ self.assertIn('XPST0081', str(ctx.exception))
+
+ @unittest.skipIf(xmlschema is None, "xmlschema library is not installed!")
+ def test_cast_or_castable_with_derived_type(self):
+ schema = xmlschema.XMLSchema(dedent("""\n
+
+
+
+
+ """))
+
+ with self.schema_bound_parser(schema.xpath_proxy):
+ root = self.etree.XML('')
+ context = XPathContext(root)
+
+ self.check_value("'1E3' castable as floatType", True, context)
+ self.check_value("(1E3) castable as floatType", True, context)
+ self.check_value("xs:untypedAtomic('1E3') cast as floatType", 1E3)
+ self.check_value("xs:untypedAtomic('x') castable as floatType", False)
+ self.wrong_value("xs:untypedAtomic('x') cast as floatType", 'FORG0001')
+ self.wrong_value("'x' cast as floatType", 'FORG0001')
+ self.wrong_type("xs:anyURI('http://xpath.test') cast as floatType", 'XPTY0004')
+
def test_logical_expressions_(self):
super(XPath2ParserTest, self).test_logical_expressions()
- if xmlschema is not None and xmlschema.__version__ >= '1.2.3':
+ if xmlschema is not None:
schema = xmlschema.XMLSchema("""