Skip to content

Commit

Permalink
Make sorting in climb() and prune() configurable (#271)
Browse files Browse the repository at this point in the history
Allow users to disable sorting by node names and preserve the
order in which child nodes where inserted into the tree.
  • Loading branch information
psss authored Jan 7, 2025
1 parent db640a0 commit baaccc9
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 8 deletions.
23 changes: 23 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
Modules
===============

.. _sort:

Sort
----

By default, when exploring test metadata in the tree, child nodes
are sorted alphabetically by node name. This applies to command
line usage such as ``fmf ls`` or ``fmf show`` as well as for the
:py:meth:`fmf.Tree.climb()` and :py:meth:`fmf.Tree.prune()`
methods.

If the tree content is not created from files on disk but created
manually using the :py:meth:`fmf.Tree.child()` method, the child
order can be preserved by providing the ``sort=False`` parameter
to the :py:meth:`fmf.Tree.climb()` and :py:meth:`fmf.Tree.prune()`
methods.

.. versionadded:: 1.6


fmf
---

.. automodule:: fmf
:members:
:undoc-members:
Expand Down
6 changes: 6 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ In order to search :ref:`context` dimension values using regular
expressions, it is now possible to use operator ``~`` for matching
patterns and operator ``!~`` for non matching patterns.

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


fmf-1.5.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
64 changes: 56 additions & 8 deletions fmf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,12 +707,33 @@ def grow(self, path):
del self.children[name]
log.debug("Empty tree '{0}' removed.".format(child.name))

def climb(self, whole=False):
""" Climb through the tree (iterate leaf/all nodes) """
def climb(self, whole: bool = False, sort: bool = True):
"""
Climb through the tree (iterate over nodes)
:param whole: By default only leaf nodes are considered. When
set to ``True`` all nodes are iterated, including parent
branches.
:param sort: When iterating, child nodes are sorted by name by
default. Set to ``False`` if you prefer to keep the order in
which the child nodes were inserted into the tree.
"""

# Include branches when `whole` is enabled or the `select`
# directive has been used to pick this node.
if whole or self.select:
yield self
for name, child in sorted(self.children.items()):
for node in child.climb(whole):

# Sort child nodes by name only if requested
if sort:
children = [child for _, child in sorted(self.children.items())]
else:
children = self.children.values()

# Iterate through each child node
for child in children:
for node in child.climb(whole=whole, sort=sort):
yield node

@property
Expand All @@ -730,9 +751,36 @@ def find(self, name):
return node
return None

def prune(self, whole=False, keys=None, names=None, filters=None,
conditions=None, sources=None):
""" Filter tree nodes based on given criteria """
def prune(
self,
whole: bool = False,
keys: Optional[list[str]] = None,
names: Optional[list[str]] = None,
filters: Optional[list[str]] = None,
conditions: Optional[list[str]] = None,
sources: Optional[list[str]] = None,
sort: bool = True):
"""
Filter tree nodes based on given criteria
:param whole: By default only leaf nodes are considered. When
set to ``True`` all nodes are iterated, including parent
branches.
:param keys: Include only nodes containing given keys.
:param names: Include only nodes matching provided names.
:param filters: Include only nodes matching given filters.
:param conditions: Include only nodes satisfying the conditions.
:param sources: Filter by source fmf file names on disk.
:param sort: When iterating, child nodes are sorted by name by
default. Set to ``False`` if you prefer to keep the order in
which the child nodes were inserted into the tree.
"""
keys = keys or []
names = names or []
filters = filters or []
Expand All @@ -742,7 +790,7 @@ def prune(self, whole=False, keys=None, names=None, filters=None,
if sources:
sources = {os.path.abspath(src) for src in sources}

for node in self.climb(whole):
for node in self.climb(whole, sort=sort):
# Select only nodes with key content
if not all([key in node.data for key in keys]):
continue
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ def test_subtrees(self):
child = Tree(EXAMPLES + "child")
assert child.find("/nobody") is None

def test_insert_child(self):
""" Manual child creation """

# Prepare a simple tree by manually inserting child nodes
tree = Tree(data={"key": "value"})
tree.child(name="child2", data={"key": "value"})
tree.child(name="child3", data={"key": "value"})
tree.child(name="child1", data={"key": "value"})

# By default, node names should be sorted when climbing & prunning
expected = ['/child1', '/child2', '/child3']
assert [node.name for node in tree.climb()] == expected
assert [node.name for node in tree.prune()] == expected
assert [node.name for node in tree.climb(sort=True)] == expected
assert [node.name for node in tree.prune(sort=True)] == expected

# Original order should be kept if requested
expected = ['/child2', '/child3', '/child1']
assert [node.name for node in tree.climb(sort=False)] == expected
assert [node.name for node in tree.prune(sort=False)] == expected

def test_prune_sources(self):
""" Pruning by sources """
original_directory = os.getcwd()
Expand Down

0 comments on commit baaccc9

Please sign in to comment.