diff --git a/elementpath/xpath1/xpath1_axes.py b/elementpath/xpath1/xpath1_axes.py index 4e7a0275..8b4547fa 100644 --- a/elementpath/xpath1/xpath1_axes.py +++ b/elementpath/xpath1/xpath1_axes.py @@ -10,7 +10,6 @@ from ..xpath_nodes import NamespaceNode, is_element_node from .xpath1_functions import XPath1Parser - method = XPath1Parser.method axis = XPath1Parser.axis diff --git a/elementpath/xpath1/xpath1_functions.py b/elementpath/xpath1/xpath1_functions.py index 61fa9ba5..e9c9c539 100644 --- a/elementpath/xpath1/xpath1_functions.py +++ b/elementpath/xpath1/xpath1_functions.py @@ -7,6 +7,7 @@ # # @author Davide Brunato # +import sys import math import decimal from ..datatypes import Duration, DayTimeDuration, YearMonthDuration, StringProxy, AnyURI @@ -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 @@ -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:] @@ -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: @@ -236,13 +242,11 @@ 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)) @@ -250,8 +254,6 @@ def evaluate(self, context=None): 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: @@ -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): @@ -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 diff --git a/elementpath/xpath2/xpath2_parser.py b/elementpath/xpath2/xpath2_parser.py index b06deeb4..6e35771c 100644 --- a/elementpath/xpath2/xpath2_parser.py +++ b/elementpath/xpath2/xpath2_parser.py @@ -448,6 +448,7 @@ def check_variables(self, values): unregister('id') unregister('substring-before') unregister('substring-after') +unregister('starts-with') ### # Symbols diff --git a/tests/test_xpath1_parser.py b/tests/test_xpath1_parser.py index 9afcb846..6498f003 100644 --- a/tests/test_xpath1_parser.py +++ b/tests/test_xpath1_parser.py @@ -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 @@ -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('') 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('') context = XPathContext(root, item=root[1], size=3, position=3) @@ -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') @@ -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('') self.check_selector("name(.)", root, 'tst:A', namespaces={'tst': "http://xpath.test/ns"}) self.check_selector("local-name(.)", root, 'A') @@ -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('foo') + self.check_value("string()", 'foo', context=XPathContext(root)) + def test_string_length_function(self): root = self.etree.XML(XML_GENERIC_TEST) @@ -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('foo') + self.check_value("string-length()", 3, context=XPathContext(root)) + def test_normalize_space_function(self): root = self.etree.XML(XML_GENERIC_TEST) @@ -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( @@ -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('' @@ -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(''), True) - self.check_selector('lang("en")', self.etree.XML('
'), True) + root = self.etree.XML('') + self.check_selector('lang("en")', root, True) + + root = self.etree.XML('
') + 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('') + 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(''), True) self.check_selector('lang("en")', self.etree.XML(''), True) self.check_selector('lang("en")', self.etree.XML(''), False) + self.check_selector('lang("en")', self.etree.XML('
'), 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) @@ -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) @@ -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): @@ -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('') @@ -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('') @@ -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('' @@ -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('') + root = self.etree.XML('10') 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( @@ -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( @@ -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('') @@ -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('value') + self.check_selector('./text()/preceding::*', root, []) def test_preceding_sibling_axis(self): root = self.etree.XML('')