Skip to content

Commit

Permalink
Minor fixes and extend coverage of XPath2Parser
Browse files Browse the repository at this point in the history
  • Loading branch information
brunato committed Jan 29, 2021
1 parent 1c9b073 commit dfd2a29
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 39 deletions.
48 changes: 15 additions & 33 deletions elementpath/xpath2/xpath2_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions tests/test_schema_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class XMLSchemaProxyTest(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.schema1 = xmlschema.XMLSchema(dedent('''
cls.schema1 = xmlschema.XMLSchema(dedent('''\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://xpath.test/ns" targetNamespace="http://xpath.test/ns">
<xs:element name="a">
Expand All @@ -42,9 +42,9 @@ def setUpClass(cls):
<xs:element name="b3" type="xs:float"/>
</xs:schema>'''))

cls.schema2 = xmlschema.XMLSchema(dedent('''
cls.schema2 = xmlschema.XMLSchema(dedent('''\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root"/>
<xs:element name="root" type="xs:string"/>
</xs:schema>'''))

def test_name_token(self):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_schema_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))')
Expand All @@ -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)",
Expand Down
109 changes: 108 additions & 1 deletion tests/test_xpath2_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -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('<root/>')
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'])
Expand Down Expand Up @@ -382,15 +389,24 @@ 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()
self.check_value("0.05 eq 0.05", True)
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)
Expand All @@ -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('<value>false</value>'))
Expand Down Expand Up @@ -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)
Expand All @@ -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('''\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root" type="xs:string"/>
</xs:schema>'''))

root = self.etree.XML('<root>hello</root>')
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('<A a="10" b="20">text<B/>tail<B/></A>')
context = XPathContext(root)
Expand Down Expand Up @@ -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('''
<transactions>
<purchase><parcel>28-451</parcel></purchase>
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:simpleType name="floatType">
<xs:restriction base="xs:double"/>
</xs:simpleType>
</xs:schema>"""))

with self.schema_bound_parser(schema.xpath_proxy):
root = self.etree.XML('<root/>')
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("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root">
Expand Down

0 comments on commit dfd2a29

Please sign in to comment.