From 190cc4e4ece3401fa3fb03f037ed797fa7c8e884 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Jan 2026 16:27:30 +0100 Subject: [PATCH 1/3] fix .// xpath queries Previously, when iter() encountered a non-Element child (like a text string), it would yield it unconditionally, even if the caller was searching for a specific tag (e.g., .//c). ``` def test_Element_findall_dotslashslash(): c1 = Element('c') c2 = Element('c') text = "text" b1 = Element('b', children=(c1, text, c2)) b2 = Element('b') a1 = Element('a', children=(b1, b2, )) result = list(a1.findall('.//c')) > assert len(result) == 2 E AssertionError: assert 3 == 2 E + where 3 = len([, 'text', ]) src/emeraldtree/tests/test_tree.py:208: AssertionError ``` The fix ensures that non-Element children are only yielded if no tag filter is specified (tag is None). --- src/emeraldtree/tests/test_tree.py | 2 -- src/emeraldtree/tree.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/emeraldtree/tests/test_tree.py b/src/emeraldtree/tests/test_tree.py index 5aff12e..3d523b7 100644 --- a/src/emeraldtree/tests/test_tree.py +++ b/src/emeraldtree/tests/test_tree.py @@ -184,7 +184,6 @@ def test_Element_findall_dotdot(): assert result[1] is c2 def test_Element_findall_slashslash(): - pytest.skip('broken') c1 = Element('c') c2 = Element('c') text = "text" @@ -199,7 +198,6 @@ def test_Element_findall_slashslash(): assert result[1] is c2 def test_Element_findall_dotslashslash(): - pytest.skip('broken') c1 = Element('c') c2 = Element('c') text = "text" diff --git a/src/emeraldtree/tree.py b/src/emeraldtree/tree.py index b2ebff7..82783c5 100644 --- a/src/emeraldtree/tree.py +++ b/src/emeraldtree/tree.py @@ -355,7 +355,8 @@ def iter(self, tag=None): for e in e.iter(tag): yield e else: - yield e + if tag is None: + yield e ## # Creates a text iterator. The iterator loops over this element From 3b5e955183230b1adf82e9a2dc55b10779a38579 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 26 Jan 2026 23:51:06 +0100 Subject: [PATCH 2/3] ElementPath: support [position] predicates - Implemented 1-based indexing for predicates (e.g. tag[1]). - Raise SyntaxError for invalid indices (< 1) to match xml.etree behavior. - Enabled test_Element_findall_position and added test_Element_findall_position_invalid. --- src/emeraldtree/ElementPath.py | 19 +++++++++++++++---- src/emeraldtree/tests/test_tree.py | 8 +++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/emeraldtree/ElementPath.py b/src/emeraldtree/ElementPath.py index 55b9952..20cc9ab 100644 --- a/src/emeraldtree/ElementPath.py +++ b/src/emeraldtree/ElementPath.py @@ -146,10 +146,21 @@ def select(context, result): token = next() if token[0] != "]": raise SyntaxError("invalid node predicate") - def select(context, result): - for elem in result: - if elem.find(tag) is not None: - yield elem + try: + index = int(tag) + except ValueError: + def select(context, result): + for elem in result: + if elem.find(tag) is not None: + yield elem + else: + if index < 1: + raise SyntaxError("XPath position >= 1 expected") + def select(context, result): + for i, elem in enumerate(result): + if i + 1 == index: + yield elem + break else: raise SyntaxError("invalid predicate") return select diff --git a/src/emeraldtree/tests/test_tree.py b/src/emeraldtree/tests/test_tree.py index 3d523b7..73a5942 100644 --- a/src/emeraldtree/tests/test_tree.py +++ b/src/emeraldtree/tests/test_tree.py @@ -233,7 +233,6 @@ def test_Element_findall_attribute(): assert len(result) == 0 def test_Element_findall_position(): - pytest.skip('not supported') c1 = Element('c') c2 = Element('c') text = "text" @@ -249,6 +248,13 @@ def test_Element_findall_position(): assert len(result) == 1 assert result[0] is c2 +def test_Element_findall_position_invalid(): + b1 = Element('b') + with pytest.raises(SyntaxError): + list(b1.findall('c[0]')) + with pytest.raises(SyntaxError): + list(b1.findall('c[-1]')) + def test_Element_findtext_default(): elem = Element('a') default_text = 'defaulttext' From fb251b5080956740170334fd35712b56fbd254be Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 28 Feb 2026 15:51:24 +0100 Subject: [PATCH 3/3] fix xpath .. (parent) selector, fix tests Build a temporary parent_map dynamically in prepare_dot_dot by iterating from context.root, matching stdlib ElementTree's approach. New: skip non-iterable children (text nodes) when building the map. Like stdlib, ../tag returns empty when called from a leaf element that has no ancestor context. Paths like b/../c work correctly when called from a higher element that contains the full subtree. --- src/emeraldtree/ElementPath.py | 4 ++++ src/emeraldtree/tests/test_tree.py | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/emeraldtree/ElementPath.py b/src/emeraldtree/ElementPath.py index 20cc9ab..72166ed 100644 --- a/src/emeraldtree/ElementPath.py +++ b/src/emeraldtree/ElementPath.py @@ -106,6 +106,10 @@ def select(context, result): if parent_map is None: context.parent_map = parent_map = {} for p in context.root.iter(): + try: + iter(p) + except TypeError: + continue for e in p: parent_map[e] = p for elem in result: diff --git a/src/emeraldtree/tests/test_tree.py b/src/emeraldtree/tests/test_tree.py index 73a5942..22b52a5 100644 --- a/src/emeraldtree/tests/test_tree.py +++ b/src/emeraldtree/tests/test_tree.py @@ -170,18 +170,23 @@ def test_Element_findall_bracketed_tag(): assert result[0] is b1 # b1 has 'c' childs def test_Element_findall_dotdot(): - pytest.skip('broken') - c1 = Element('c') - c2 = Element('c') + d1 = Element('d') + d2 = Element('d') text = "text" - b1 = Element('b', children=(c1, text, c2)) - b2 = Element('b') - a1 = Element('a', children=(b1, b2, )) - + c1 = Element('c', children=(d1, text, d2)) + b1 = Element('b') + a1 = Element('a', children=(b1, c1, )) + + # this is something we can not support. + # we do not have parent pointers and also we only have a context starting from c1 here. + # we give an empty result, similar to stdlib elementree, which has the same limitation. result = list(c1.findall('../c')) - assert len(result) == 2 + assert len(result) == 0 + + # this is something we can support as we start at a higher element. + result = list(a1.findall('b/../c')) + assert len(result) == 1 assert result[0] is c1 - assert result[1] is c2 def test_Element_findall_slashslash(): c1 = Element('c')