Skip to content

Commit

Permalink
Add tests for axes and XPath 1.0 functions
Browse files Browse the repository at this point in the history
  • Loading branch information
brunato committed Jan 6, 2021
1 parent 396554c commit 797f84f
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 17 deletions.
1 change: 0 additions & 1 deletion elementpath/xpath1/xpath1_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from ..xpath_nodes import NamespaceNode, is_element_node
from .xpath1_functions import XPath1Parser


method = XPath1Parser.method
axis = XPath1Parser.axis

Expand Down
29 changes: 19 additions & 10 deletions elementpath/xpath1/xpath1_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#
# @author Davide Brunato <brunato@sissa.it>
#
import sys
import math
import decimal
from ..datatypes import Duration, DayTimeDuration, YearMonthDuration, StringProxy, AnyURI
Expand Down Expand Up @@ -44,7 +45,12 @@ def select(self, context=None):
yield context.item
else:
arg = self.get_argument(context, cls=str)
if context.item.tag == ' '.join(arg.strip().split()):
if hasattr(context.item, 'target'):
target = context.item.target
else:
target = context.item.text.split()[0] if context.item.text else ''

if target == ' '.join(arg.strip().split()):
yield context.item


Expand Down Expand Up @@ -136,7 +142,7 @@ def evaluate(self, context=None):
return name if symbol == 'local-name' else ''
elif symbol == 'local-name':
return name.split('}')[1]
elif symbol == 'namespace-uri':
else:
return name.split('}')[0][1:]


Expand Down Expand Up @@ -227,7 +233,7 @@ def evaluate(self, context=None):
start = int(round(start)) - 1

if len(self) == 2:
return '' if item is None else item[max(start, 0):]
return item[max(start, 0):]
else:
length = self.get_argument(context, index=2)
try:
Expand All @@ -236,22 +242,18 @@ def evaluate(self, context=None):
except TypeError:
raise self.wrong_type("the third argument must be xs:numeric") from None

if item is None:
return ''
elif math.isinf(length):
if math.isinf(length):
return item[max(start, 0):]
else:
stop = start + int(round(length))
return '' if item is None else item[slice(max(start, 0), max(stop, 0))]
return item[slice(max(start, 0), max(stop, 0))]


@method(function('substring-before', nargs=2))
@method(function('substring-after', nargs=2))
def evaluate(self, context=None):
arg1 = self.get_argument(context, default='', cls=str)
arg2 = self.get_argument(context, index=1, default='', cls=str)
if arg1 is None:
return ''

index = arg1.find(arg2)
if index < 0:
Expand Down Expand Up @@ -331,7 +333,12 @@ def evaluate(self, context=None):
return sum(values)
elif all(isinstance(x, DayTimeDuration) for x in values) or \
all(isinstance(x, YearMonthDuration) for x in values):
return sum(values[1:], start=values[0])
if sys.version_info >= (3, 8):
return sum(values[1:], start=values[0])
result = values[0]
for val in values[1:]:
result += val
return result
elif any(isinstance(x, Duration) for x in values):
raise self.error('FORG0006', 'invalid sum of duration values')
elif any(isinstance(x, (StringProxy, AnyURI)) for x in values):
Expand Down Expand Up @@ -390,6 +397,8 @@ def evaluate(self, context=None):
except TypeError as err:
raise self.error('FORG0006', err) from None
except decimal.InvalidOperation:
if isinstance(arg, str):
raise self.error('XPTY0004') from None
return round(arg)
except decimal.DecimalException as err:
raise self.error('FOCA0002', err) from None
1 change: 1 addition & 0 deletions elementpath/xpath2/xpath2_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ def check_variables(self, values):
unregister('id')
unregister('substring-before')
unregister('substring-after')
unregister('starts-with')

###
# Symbols
Expand Down
107 changes: 101 additions & 6 deletions tests/test_xpath1_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,21 +321,33 @@ def test_node_types(self):
attribute = AttributeNode('id', '0212349350')
namespace = NamespaceNode('xs', 'http://www.w3.org/2001/XMLSchema')
comment = self.etree.Comment('nothing important')
pi = self.etree.ProcessingInstruction('action', 'nothing to do')
pi = self.etree.ProcessingInstruction('action')
text = TextNode('aldebaran')

context = XPathContext(element)
self.check_select("node()", [document.getroot()], context=XPathContext(document))
self.check_selector("node()", element, [])

context.item = attribute
self.check_select("self::node()", [attribute], context)

context.item = namespace
self.check_select("self::node()", [namespace], context)

self.check_value("comment()", [], context=context)
context.item = comment
self.check_select("self::node()", [comment], context)
self.check_select("self::comment()", [comment], context)
self.check_value("comment()", MissingContextError)

self.check_value("processing-instruction()", [], context=context)
context.item = pi
self.check_select("self::node()", [pi], context)
self.check_select("self::processing-instruction()", [pi], context)
self.check_select("self::processing-instruction('action')", [pi], context)
self.check_select("self::processing-instruction('other')", [], context)
self.check_value("processing-instruction()", MissingContextError)

context.item = text
self.check_select("self::node()", [text], context)
self.check_select("text()", [], context) # Selects the children
Expand All @@ -348,13 +360,33 @@ def test_node_types(self):
# self.check_value("//self::node()", [document, root, 'Dickens'], context=context)
# Skip lxml test because lxml's XPath doesn't include document root
self.check_selector("//self::node()", document, [document, root, 'Dickens'])
self.check_selector("/self::node()", document, [document])
self.check_selector("/self::node()", root, [root])

self.check_selector("//self::text()", root, ['Dickens'])

context = XPathContext(root)
context.item = None
self.check_value("/self::node()", expected=[], context=context)
context.item = 1
self.check_value("self::node()", expected=[], context=context)

def test_node_set_id_function(self):
# XPath 1.0 id() function: https://www.w3.org/TR/1999/REC-xpath-19991116/#function-id
root = self.etree.XML('<A><B1 xml:id="foo"/><B2/><B3 xml:id="bar"/><B4 xml:id="baz"/></A>')
self.check_selector('id("foo")', root, [root[0]])

context = XPathContext(root)
self.check_value('./B/@xml:id[id("bar")]', expected=[], context=context)

context.item = None
self.check_value('id("none")', expected=[], context=context)
self.check_value('id("foo")', expected=[root[0]], context=context)
self.check_value('id("bar")', expected=[root[2]], context=context)

context.item = self.etree.Comment('a comment')
self.check_value('id("foo")', expected=[], context=context)

def test_node_set_functions(self):
root = self.etree.XML('<A><B1><C1/><C2/></B1><B2/><B3><C3/><C4/><C5/></B3></A>')
context = XPathContext(root, item=root[1], size=3, position=3)
Expand All @@ -370,6 +402,7 @@ def test_node_set_functions(self):

self.check_selector("name(.)", root, 'A')
self.check_selector("name(A)", root, '')
self.check_selector("name(1.0)", root, TypeError)
self.check_selector("local-name(A)", root, '')
self.check_selector("namespace-uri(A)", root, '')
self.check_selector("name(B2)", root, 'B2')
Expand All @@ -378,6 +411,9 @@ def test_node_set_functions(self):
if self.parser.version <= '1.0':
self.check_selector("name(*)", root, 'B1')

context = XPathContext(root, item=self.etree.Comment('a comment'))
self.check_value("name()", '', context=context)

root = self.etree.XML('<tst:A xmlns:tst="http://xpath.test/ns"><tst:B1/></tst:A>')
self.check_selector("name(.)", root, 'tst:A', namespaces={'tst': "http://xpath.test/ns"})
self.check_selector("local-name(.)", root, 'A')
Expand All @@ -388,12 +424,16 @@ def test_node_set_functions(self):
namespaces={'tst': "http://xpath.test/ns", '': ''})

def test_string_function(self):
self.check_value("string()", MissingContextError)
self.check_value("string(10.0)", '10')
if self.parser.version == '1.0':
self.wrong_syntax("string(())")
else:
self.check_value("string(())", '')

root = self.etree.XML('<root>foo</root>')
self.check_value("string()", 'foo', context=XPathContext(root))

def test_string_length_function(self):
root = self.etree.XML(XML_GENERIC_TEST)

Expand All @@ -420,6 +460,9 @@ def test_string_length_function(self):
self.check_value("string-length(12345)", 5)
self.parser.compatibility_mode = False

root = self.etree.XML('<root>foo</root>')
self.check_value("string-length()", 3, context=XPathContext(root))

def test_normalize_space_function(self):
root = self.etree.XML(XML_GENERIC_TEST)

Expand All @@ -446,6 +489,7 @@ def test_translate_function(self):
self.check_value("translate('hello world!', 'hw', 'HW')", 'Hello World!')
self.check_value("translate('hello world!', 'hwx', 'HW')", 'Hello World!')
self.check_value("translate('hello world!', 'hw!', 'HW')", 'Hello World')
self.check_value("translate('hello world!', 'hw', 'HW!')", 'Hello World!')
self.check_selector("a[translate(@id, 'id', 'no') = 'a_no']", root, [root[0]])
self.check_selector("a[translate(@id, 'id', 'na') = 'a_no']", root, [])
self.check_selector(
Expand All @@ -459,6 +503,10 @@ def test_translate_function(self):

if self.parser.version > '1.0':
self.check_value("translate((), 'hw', 'HW')", '')
self.wrong_type("translate((), (), 'HW')", 'XPTY0004',
'2nd argument', 'empty sequence')
self.wrong_type("translate((), 'hw', ())", 'XPTY0004',
'3rd argument', 'empty sequence')

def test_variable_substitution(self):
root = self.etree.XML('<ups-units>'
Expand Down Expand Up @@ -730,11 +778,38 @@ def test_nonempty_elements(self):

def test_lang_function(self):
# From https://www.w3.org/TR/1999/REC-xpath-19991116/#section-Boolean-Functions
self.check_selector('lang("en")', self.etree.XML('<para xml:lang="en"/>'), True)
self.check_selector('lang("en")', self.etree.XML('<div xml:lang="en"><para/></div>'), True)
root = self.etree.XML('<para xml:lang="en"/>')
self.check_selector('lang("en")', root, True)

root = self.etree.XML('<div xml:lang="en"><para/></div>')
document = self.etree.ElementTree(root)
self.check_selector('lang("en")', root, True)
if self.parser.version > '1.0':
self.check_selector('para/lang("en")', root, True)
else:
context = XPathContext(document, item=root[0])
self.check_value('lang("en")', True, context=context)
self.check_value('lang("it")', False, context=context)

root = self.etree.XML('<a><b><c/></b></a>')
self.check_selector('lang("en")', root, False)
if self.parser.version > '1.0':
self.check_selector('b/c/lang("en")', root, False)
else:
context = XPathContext(root, item=root[0][0])
self.check_value('lang("en")', False, context=context)

self.check_selector('lang("en")', self.etree.XML('<para xml:lang="EN"/>'), True)
self.check_selector('lang("en")', self.etree.XML('<para xml:lang="en-us"/>'), True)
self.check_selector('lang("en")', self.etree.XML('<para xml:lang="it"/>'), False)
self.check_selector('lang("en")', self.etree.XML('<div/>'), False)

document = self.etree.ElementTree(root)
context = XPathContext(root=document)
if self.parser.version > '1.0':
self.check_value('lang("en")', expected=TypeError, context=context)
else:
self.check_value('lang("en")', expected=False, context=context)

def test_logical_and_operator(self):
self.check_value("false() and true()", False)
Expand Down Expand Up @@ -918,13 +993,19 @@ def test_sum_function(self):
root = self.etree.XML(XML_DATA_TEST)
context = XPathContext(root, variables=self.variables)
self.check_value("sum($values)", 35, context)
self.check_selector("sum(/values/a)", root, 13.299999999999999)
self.check_selector("sum(/values/*)", root, math.isnan)

if self.parser.version == '1.0':
self.wrong_syntax("sum(())")
else:
self.check_value("sum(())", 0)
self.check_value("sum((), ())", [])
self.check_selector("sum(/values/a)", root, 13.299999999999999)
self.check_selector("sum(/values/*)", root, float('nan'))
self.check_value('sum((xs:yearMonthDuration("P2Y"), xs:yearMonthDuration("P1Y")))',
datatypes.YearMonthDuration(months=36))
self.wrong_type('sum((xs:duration("P2Y"), xs:duration("P1Y")))', 'FORG0006')
self.wrong_type('sum(("P2Y", "P1Y"))', 'FORG0006')
self.check_value("sum((1.0, xs:float('NaN')))", math.isnan)

def test_ceiling_function(self):
root = self.etree.XML(XML_DATA_TEST)
Expand All @@ -937,6 +1018,7 @@ def test_ceiling_function(self):
else:
self.check_value("ceiling(())", [])
self.check_value("ceiling((10.5))", 11)
self.check_value("ceiling((xs:float('NaN')))", math.isnan)
self.wrong_type("ceiling((10.5, 17.3))")

def test_floor_function(self):
Expand All @@ -960,10 +1042,14 @@ def test_round_function(self):
self.check_value("round(-2.5)", -2)
if self.parser.version == '1.0':
self.wrong_syntax("round(())")
self.check_value("round('foo')", math.isnan)
else:
self.check_value("round(())", [])
self.check_value("round((10.5))", 11)
self.wrong_type("round((2.5, 12.2))")
self.check_value("round(xs:double('NaN'))", math.isnan)
self.wrong_type("round('foo')", 'XPTY0004')
self.check_value('fn:round(xs:double("1E300"))', 1E300)

def test_context_variables(self):
root = self.etree.XML('<A><B1><C/></B1><B2/><B3><C1/><C2/></B3></A>')
Expand Down Expand Up @@ -1129,6 +1215,7 @@ def test_following_axis(self):
root[1], root[2], root[2][0], root[2][1], root[3], root[3][0], root[3][0][0]
])
self.check_selector('/A/B1/following::C1', root, [root[2][0], root[3][0]])
self.check_value('following::*', MissingContextError)

def test_following_sibling_axis(self):
root = self.etree.XML('<A><B1><C1 a="1"/><C2/><C3/></B1><B2><C1/><C2/><C3/><C4/></B2></A>')
Expand All @@ -1140,6 +1227,7 @@ def test_following_sibling_axis(self):

self.check_selector("/A/B1/C1/1/following-sibling::*", root, TypeError)
self.check_selector("/A/B1/C1/@a/following-sibling::*", root, [])
self.check_value('following-sibling::*', MissingContextError)

def test_attribute_abbreviation_and_axis(self):
root = self.etree.XML('<A id="1" a="alpha">'
Expand All @@ -1158,12 +1246,13 @@ def test_attribute_abbreviation_and_axis(self):
self.check_value('@1', SyntaxError, context=XPathContext(root))

def test_namespace_axis(self):
root = self.etree.XML('<A xmlns:tst="http://xpath.test/ns"><tst:B1/></A>')
root = self.etree.XML('<A xmlns:tst="http://xpath.test/ns">10<tst:B1/></A>')
namespaces = list(self.parser.DEFAULT_NAMESPACES.items()) \
+ [('tst', 'http://xpath.test/ns')]
self.check_selector('/A/namespace::*', root, expected=set(namespaces),
namespaces=namespaces[-1:])
self.check_value('namespace::*', MissingContextError)
self.check_value('./text()/namespace::*', [], context=XPathContext(root))

def test_parent_shortcut_and_axis(self):
root = self.etree.XML(
Expand All @@ -1175,6 +1264,7 @@ def test_parent_shortcut_and_axis(self):
self.check_selector('/A/*/*/parent::node()', root, [root[0], root[2], root[3]])
self.check_selector('//C2/parent::node()', root, [root[2]])
self.check_value('..', MissingContextError)
self.check_value('parent::*', MissingContextError)

def test_ancestor_axes(self):
root = self.etree.XML(
Expand All @@ -1187,6 +1277,7 @@ def test_ancestor_axes(self):
self.check_selector('/A/*/C1/ancestor-or-self::*', root, [
root, root[0], root[0][0], root[1], root[1][0], root[2], root[2][0]
])
self.check_value('ancestor-or-self::*', MissingContextError)

def test_preceding_axis(self):
root = self.etree.XML('<A><B1><C1/><C2/><C3/></B1><B2><C1/><C2/><C3/><C4/></B2></A>')
Expand All @@ -1198,6 +1289,10 @@ def test_preceding_axis(self):

self.check_tree("/root/e/preceding::b", '(/ (/ (/ (root)) (e)) (preceding (b)))')
self.check_selector('/root/e[2]/preceding::b', root, [root[0][0][0], root[0][1][0]])
self.check_value('preceding::*', MissingContextError)

root = self.etree.XML('<A>value</A>')
self.check_selector('./text()/preceding::*', root, [])

def test_preceding_sibling_axis(self):
root = self.etree.XML('<A><B1><C1/><C2/><C3/></B1><B2><C1/><C2/><C3/><C4/></B2></A>')
Expand Down

0 comments on commit 797f84f

Please sign in to comment.