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 f4085502c495d8dfbf1580acd71a4c3e2678bf86 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Jan 2026 16:26:42 +0100 Subject: [PATCH 2/3] fix ../ xpath queries This commit implements robust support for the .. (parent) XPath selector. Tree Structure Changes: It adds a _parent attribute to the Element class. The init method and all list-modification methods are updated to automatically maintain this parent pointer when children are modified. Path Resolution: It updates ElementPath.py to use these _parent pointers when resolving. If a parent pointer is missing (e.g., for objects not attached to the current context), it falls back to building a temporary parent map by traversing from the root. --- src/emeraldtree/ElementPath.py | 23 +++++++---- src/emeraldtree/tests/test_tree.py | 1 - src/emeraldtree/tree.py | 61 +++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/emeraldtree/ElementPath.py b/src/emeraldtree/ElementPath.py index 55b9952..e5e5810 100644 --- a/src/emeraldtree/ElementPath.py +++ b/src/emeraldtree/ElementPath.py @@ -102,15 +102,22 @@ def select(context, result): def prepare_dot_dot(next, token): def select(context, result): - parent_map = context.parent_map - if parent_map is None: - context.parent_map = parent_map = {} - for p in context.root.iter(): - for e in p: - parent_map[e] = p for elem in result: - if elem in parent_map: - yield parent_map[elem] + parent = getattr(elem, '_parent', None) + if parent is not None: + yield parent + else: + if context.parent_map is None: + context.parent_map = {} + for p in context.root.iter(): + try: + iter(p) + except TypeError: + continue + for e in p: + context.parent_map[e] = p + if elem in context.parent_map: + yield context.parent_map[elem] return select def prepare_predicate(next, token): diff --git a/src/emeraldtree/tests/test_tree.py b/src/emeraldtree/tests/test_tree.py index 3d523b7..01d436e 100644 --- a/src/emeraldtree/tests/test_tree.py +++ b/src/emeraldtree/tests/test_tree.py @@ -170,7 +170,6 @@ 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') text = "text" diff --git a/src/emeraldtree/tree.py b/src/emeraldtree/tree.py index 82783c5..2ac9faa 100644 --- a/src/emeraldtree/tree.py +++ b/src/emeraldtree/tree.py @@ -121,6 +121,9 @@ class Element(Node): attrib = None + # Parent pointer (internal). None for root or detached elements. + _parent = None + ## # (Attribute) Text before first subelement. This is either a # string or the value None, if there was no text. @@ -151,6 +154,10 @@ def __init__(self, tag, attrib=None, children=(), **extra): self.tag = tag self.attrib = attrib self._children = list(children) + # set parent pointers for element children + for ch in self._children: + if isinstance(ch, Element): + ch._parent = self def __repr__(self): return "".format(repr(self.tag), id(self)) @@ -186,7 +193,34 @@ def __getitem__(self, index): # @exception AssertionError If element is not a valid object. def __setitem__(self, index, element): - self._children.__setitem__(index, element) + # clear parent of replaced children and set parent of new ones + if isinstance(index, slice): + # clear parents for removed elements + old_items = self._children[index] + for old in old_items: + if isinstance(old, Element): + old._parent = None + # assign + self._children[index] = element + # set parents for new elements + try: + iterator = iter(element) + except TypeError: + iterator = None + if iterator is not None: + for new in element: + if isinstance(new, Element): + new._parent = self + else: + try: + old = self._children[index] + except Exception: + old = None + if isinstance(old, Element): + old._parent = None + self._children[index] = element + if isinstance(element, Element): + element._parent = self ## # Deletes the given subelement. @@ -195,6 +229,19 @@ def __setitem__(self, index, element): # @exception IndexError If the given element does not exist. def __delitem__(self, index): + # clear parent pointer for removed element(s) + if isinstance(index, slice): + old_items = self._children[index] + for old in old_items: + if isinstance(old, Element): + old._parent = None + else: + try: + old = self._children[index] + except Exception: + old = None + if isinstance(old, Element): + old._parent = None self._children.__delitem__(index) ## @@ -205,6 +252,8 @@ def __delitem__(self, index): def append(self, element): self._children.append(element) + if isinstance(element, Element): + element._parent = self ## # Appends subelements from a sequence. @@ -215,6 +264,9 @@ def append(self, element): def extend(self, elements): self._children.extend(elements) + for e in elements: + if isinstance(e, Element): + e._parent = self ## # Inserts a subelement at the given position in this element. @@ -224,6 +276,8 @@ def extend(self, elements): def insert(self, index, element): self._children.insert(index, element) + if isinstance(element, Element): + element._parent = self ## # Removes a matching subelement. Unlike the find methods, @@ -236,11 +290,16 @@ def insert(self, index, element): def remove(self, element): self._children.remove(element) + if isinstance(element, Element): + element._parent = None ## # Removes all subelements. def remove_all(self): + for ch in self._children: + if isinstance(ch, Element): + ch._parent = None self._children = [] ## From 2cc436dcfac6437417c6297026ddc10bd9d9bcb4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 26 Jan 2026 23:51:06 +0100 Subject: [PATCH 3/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 e5e5810..8392ebb 100644 --- a/src/emeraldtree/ElementPath.py +++ b/src/emeraldtree/ElementPath.py @@ -153,10 +153,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 01d436e..7758d48 100644 --- a/src/emeraldtree/tests/test_tree.py +++ b/src/emeraldtree/tests/test_tree.py @@ -232,7 +232,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" @@ -248,6 +247,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'