Skip to content

Commit baaccc9

Browse files
authored
Make sorting in climb() and prune() configurable (#271)
Allow users to disable sorting by node names and preserve the order in which child nodes where inserted into the tree.
1 parent db640a0 commit baaccc9

4 files changed

Lines changed: 106 additions & 8 deletions

File tree

docs/modules.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,29 @@
33
Modules
44
===============
55

6+
.. _sort:
7+
8+
Sort
9+
----
10+
11+
By default, when exploring test metadata in the tree, child nodes
12+
are sorted alphabetically by node name. This applies to command
13+
line usage such as ``fmf ls`` or ``fmf show`` as well as for the
14+
:py:meth:`fmf.Tree.climb()` and :py:meth:`fmf.Tree.prune()`
15+
methods.
16+
17+
If the tree content is not created from files on disk but created
18+
manually using the :py:meth:`fmf.Tree.child()` method, the child
19+
order can be preserved by providing the ``sort=False`` parameter
20+
to the :py:meth:`fmf.Tree.climb()` and :py:meth:`fmf.Tree.prune()`
21+
methods.
22+
23+
.. versionadded:: 1.6
24+
25+
26+
fmf
27+
---
28+
629
.. automodule:: fmf
730
:members:
831
:undoc-members:

docs/releases.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ In order to search :ref:`context` dimension values using regular
1212
expressions, it is now possible to use operator ``~`` for matching
1313
patterns and operator ``!~`` for non matching patterns.
1414

15+
When exploring trees using the :py:meth:`fmf.Tree.climb()` and
16+
:py:meth:`fmf.Tree.prune()` methods, optional parameter ``sort``
17+
can be used to preserve the original order in which child nodes
18+
where inserted into the tree. See the :ref:`sort` section for more
19+
details.
20+
1521

1622
fmf-1.5.0
1723
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

fmf/base.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -707,12 +707,33 @@ def grow(self, path):
707707
del self.children[name]
708708
log.debug("Empty tree '{0}' removed.".format(child.name))
709709

710-
def climb(self, whole=False):
711-
""" Climb through the tree (iterate leaf/all nodes) """
710+
def climb(self, whole: bool = False, sort: bool = True):
711+
"""
712+
Climb through the tree (iterate over nodes)
713+
714+
:param whole: By default only leaf nodes are considered. When
715+
set to ``True`` all nodes are iterated, including parent
716+
branches.
717+
718+
:param sort: When iterating, child nodes are sorted by name by
719+
default. Set to ``False`` if you prefer to keep the order in
720+
which the child nodes were inserted into the tree.
721+
"""
722+
723+
# Include branches when `whole` is enabled or the `select`
724+
# directive has been used to pick this node.
712725
if whole or self.select:
713726
yield self
714-
for name, child in sorted(self.children.items()):
715-
for node in child.climb(whole):
727+
728+
# Sort child nodes by name only if requested
729+
if sort:
730+
children = [child for _, child in sorted(self.children.items())]
731+
else:
732+
children = self.children.values()
733+
734+
# Iterate through each child node
735+
for child in children:
736+
for node in child.climb(whole=whole, sort=sort):
716737
yield node
717738

718739
@property
@@ -730,9 +751,36 @@ def find(self, name):
730751
return node
731752
return None
732753

733-
def prune(self, whole=False, keys=None, names=None, filters=None,
734-
conditions=None, sources=None):
735-
""" Filter tree nodes based on given criteria """
754+
def prune(
755+
self,
756+
whole: bool = False,
757+
keys: Optional[list[str]] = None,
758+
names: Optional[list[str]] = None,
759+
filters: Optional[list[str]] = None,
760+
conditions: Optional[list[str]] = None,
761+
sources: Optional[list[str]] = None,
762+
sort: bool = True):
763+
"""
764+
Filter tree nodes based on given criteria
765+
766+
:param whole: By default only leaf nodes are considered. When
767+
set to ``True`` all nodes are iterated, including parent
768+
branches.
769+
770+
:param keys: Include only nodes containing given keys.
771+
772+
:param names: Include only nodes matching provided names.
773+
774+
:param filters: Include only nodes matching given filters.
775+
776+
:param conditions: Include only nodes satisfying the conditions.
777+
778+
:param sources: Filter by source fmf file names on disk.
779+
780+
:param sort: When iterating, child nodes are sorted by name by
781+
default. Set to ``False`` if you prefer to keep the order in
782+
which the child nodes were inserted into the tree.
783+
"""
736784
keys = keys or []
737785
names = names or []
738786
filters = filters or []
@@ -742,7 +790,7 @@ def prune(self, whole=False, keys=None, names=None, filters=None,
742790
if sources:
743791
sources = {os.path.abspath(src) for src in sources}
744792

745-
for node in self.climb(whole):
793+
for node in self.climb(whole, sort=sort):
746794
# Select only nodes with key content
747795
if not all([key in node.data for key in keys]):
748796
continue

tests/unit/test_base.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,27 @@ def test_subtrees(self):
8080
child = Tree(EXAMPLES + "child")
8181
assert child.find("/nobody") is None
8282

83+
def test_insert_child(self):
84+
""" Manual child creation """
85+
86+
# Prepare a simple tree by manually inserting child nodes
87+
tree = Tree(data={"key": "value"})
88+
tree.child(name="child2", data={"key": "value"})
89+
tree.child(name="child3", data={"key": "value"})
90+
tree.child(name="child1", data={"key": "value"})
91+
92+
# By default, node names should be sorted when climbing & prunning
93+
expected = ['/child1', '/child2', '/child3']
94+
assert [node.name for node in tree.climb()] == expected
95+
assert [node.name for node in tree.prune()] == expected
96+
assert [node.name for node in tree.climb(sort=True)] == expected
97+
assert [node.name for node in tree.prune(sort=True)] == expected
98+
99+
# Original order should be kept if requested
100+
expected = ['/child2', '/child3', '/child1']
101+
assert [node.name for node in tree.climb(sort=False)] == expected
102+
assert [node.name for node in tree.prune(sort=False)] == expected
103+
83104
def test_prune_sources(self):
84105
""" Pruning by sources """
85106
original_directory = os.getcwd()

0 commit comments

Comments
 (0)