From 8cc023d077fe54b6ed27d412b4cd28aa2c6dd15a Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Tue, 24 Sep 2019 16:12:12 +0200 Subject: [PATCH] Add iter_preceding() to XPathContext --- elementpath/xpath1_parser.py | 20 ++++++-------------- elementpath/xpath_context.py | 29 +++++++++++++++++++++++++++++ tests/test_xpath1_parser.py | 22 +++++++++++++++++----- tests/test_xpath2_parser.py | 13 +++++++++++-- 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/elementpath/xpath1_parser.py b/elementpath/xpath1_parser.py index 62dea67..3f05cf8 100644 --- a/elementpath/xpath1_parser.py +++ b/elementpath/xpath1_parser.py @@ -754,13 +754,10 @@ def led(self, left): @method('[') def select(self, context=None): if context is not None: - left_results = list(self[0].select(context)) - context.size = len(left_results) - for context.position, context.item in enumerate(left_results): + for position, _ in enumerate(self[0].select(context), start=1): predicate = list(self[1].select(context.copy())) - if len(predicate) == 1 and not isinstance(predicate[0], bool) and \ - isinstance(predicate[0], (int, float)): - if context.position == predicate[0] - 1: + if len(predicate) == 1 and isinstance(predicate[0], NumericTypeProxy): + if position == predicate[0]: yield context.item elif self.boolean_value(predicate): yield context.item @@ -939,14 +936,9 @@ def select(self, context=None): @method(axis('preceding')) def select(self, context=None): if context is not None and is_element_node(context.item): - elem = context.item - ancestors = set(context.iter_ancestors(axis=self.symbol)) - for e in context.root.iter(): - if e is elem: - break - if e not in ancestors: - context.item = e - yield e + for _ in context.iter_preceding(): + for result in self[0].select(context): + yield result ### diff --git a/elementpath/xpath_context.py b/elementpath/xpath_context.py index 37d4874..44f47a2 100644 --- a/elementpath/xpath_context.py +++ b/elementpath/xpath_context.py @@ -82,6 +82,7 @@ class XPathContext(object): @property def parent_map(self): + # TODO: try to implement a dynamic parent map to save memory ... if self._parent_map is None: self._parent_map = {child: elem for elem in self._root.iter() for child in elem} return self._parent_map @@ -138,6 +139,34 @@ class XPathContext(object): self.item, self.size, self.position, self.axis = status + def iter_preceding(self): + status = self.item, self.size, self.position, self.axis + + elem = self.item + if not is_etree_element(elem): + return + self.axis = 'preceding' + ancestors = [] + + while True: + try: + parent = self.parent_map[elem] + except KeyError: + break + else: + ancestors.append(parent) + elem = parent + + elem = self.item + for e in self.root.iter(): + if e is elem: + break + if e not in ancestors: + self.item = e + yield e + + self.item, self.size, self.position, self.axis = status + def iter_parent(self, axis=None): status = self.item, self.size, self.position, self.axis self.axis = axis diff --git a/tests/test_xpath1_parser.py b/tests/test_xpath1_parser.py index 78402b3..422f560 100644 --- a/tests/test_xpath1_parser.py +++ b/tests/test_xpath1_parser.py @@ -1079,12 +1079,19 @@ class XPath1ParserTest(unittest.TestCase): root, root[0], root[0][0], root[1], root[1][0], root[2], root[2][0] ]) - def test_preceding_axes(self): + def test_preceding_axis(self): root = self.etree.XML('') self.check_selector('/A/B1/C2/preceding::*', root, [root[0][0]]) self.check_selector('/A/B2/C4/preceding::*', root, [ root[0], root[0][0], root[0][1], root[0][2], root[1][0], root[1][1], root[1][2] ]) + root = self.etree.XML("") + + 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]]) + + def test_preceding_sibling_axis(self): + root = self.etree.XML('') self.check_selector('/A/B1/C2/preceding-sibling::*', root, [root[0][0]]) self.check_selector('/A/B2/C4/preceding-sibling::*', root, [root[1][0], root[1][1], root[1][2]]) self.check_selector('/A/B1/C2/preceding-sibling::C3', root, []) @@ -1132,10 +1139,15 @@ class XPath1ParserTest(unittest.TestCase): self.check_selector("child::a[b][c]", root, [root[1]]) root = self.etree.XML("") - self.check_selector("e/a[not(b)]", root, [root[1][0]]) - self.check_selector("preceding::e/a[not(b)]", root, []) - self.check_selector("e/a[preceding::e/a[not(b)]]", root, []) - self.check_selector("not(e/a[preceding::e/a[not(b)]])", root, True) + self.check_tree("a[not(b)]", '([ (a) (not (b)))') + + self.check_value("a[not(b)]", [], context=XPathContext(root, item=root[0])) + self.check_value("a[not(b)]", [root[1][0]], context=XPathContext(root, item=root[1])) + + self.check_tree("preceding::a[not(b)]", '([ (preceding (a)) (not (b)))') + + self.check_value("a[preceding::a[not(b)]]", [], context=XPathContext(root, item=root[0])) + self.check_value("a[preceding::a[not(b)]]", [], context=XPathContext(root, item=root[1])) def test_union(self): root = self.etree.XML('') diff --git a/tests/test_xpath2_parser.py b/tests/test_xpath2_parser.py index 5bede88..41bed69 100644 --- a/tests/test_xpath2_parser.py +++ b/tests/test_xpath2_parser.py @@ -185,7 +185,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest): """) # Test step-by-step, testing also other basic features. - self.check_selector("author[1]", root[0], [root[0][1]]) + self.check_selector("book/author[1]", root, [root[0][1], root[1][1], root[2][1]]) self.check_selector("book/author[. = $a]", root, [root[0][1], root[1][1]], variables={'a': 'Stevens'}) self.check_tree("book/author[. = $a][1]", '(/ (book) ([ ([ (author) (= (.) ($ (a)))) (1)))') self.check_selector("book/author[. = $a][1]", root, [root[0][1], root[1][1]], variables={'a': 'Stevens'}) @@ -219,7 +219,8 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest): self.wrong_value("-3.5 idiv 0") self.wrong_value("xs:float('INF') idiv 2") - def test_comparison_operators2(self): + 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) @@ -264,6 +265,14 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest): self.check_value('@min le @max', False, context=XPathContext(root=root)) self.check_value('@min le @maximum', None, context=XPathContext(root=root)) + root = self.etree.XML('1103050') + self.check_selector("a = (1 to 30)", root, True) + self.check_selector("a = (2)", root, False) + self.check_selector("a[1] = (1 to 10, 30)", root, True) + self.check_selector("a[2] = (1 to 10, 30)", root, True) + self.check_selector("a[3] = (1 to 10, 30)", root, True) + self.check_selector("a[4] = (1 to 10, 30)", root, False) + def test_number_functions2(self): # Test cases taken from https://www.w3.org/TR/xquery-operators/#numeric-value-functions self.check_value("abs(10.5)", 10.5)