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("""