diff --git a/README.md b/README.md
index 31fe974..7f74bc8 100644
--- a/README.md
+++ b/README.md
@@ -15,16 +15,16 @@
Handle multiple references of single objects ('clones')
Search by name pattern, id, or object reference
-Unobtrusive handling of arbitrary objects
Compare two trees and calculate patches
+Unobtrusive handling of arbitrary objects
Save as DOT file and graphwiz diagram
Nodes can be plain strings or objects
(De)Serialize to (compressed) JSON
Save as Mermaid flow diagram
Different traversal methods
Generate random trees
-Fully type annotated
Convert to RDF graph
+Fully type annotated
Typed child nodes
Pretty print
Navigation
diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst
index 055a4b6..4ad65ee 100644
--- a/docs/sphinx/index.rst
+++ b/docs/sphinx/index.rst
@@ -32,66 +32,14 @@ nutree
**Note:** Run ``pip install "nutree[graph]"`` or ``pip install "nutree[all]"``
instead, in order to install additional graph support.
-.. ::
-
-.. from nutree import Tree, Node
-
-.. tree = Tree("Store")
-
-.. n = tree.add("Records")
-
-.. n.add("Let It Be")
-.. n.add("Get Yer Ya-Ya's Out!")
-
-.. n = tree.add("Books")
-.. n.add("The Little Prince")
-
-.. tree.print()
-
-.. ::
-
-.. Tree<'Store'>
-.. ├─── 'Records'
-.. │ ├─── 'Let It Be'
-.. │ ╰─── "Get Yer Ya-Ya's Out!"
-.. ╰─── 'Books'
-.. ╰─── 'The Little Prince'
-
-
-.. Tree nodes wrap the data and also expose methods for navigation, searching,
-.. iteration, ... ::
-
-.. records_node = tree["Records"]
-
-.. assert isinstance(records_node, Node)
-.. assert records_node.name == "Records"
-
-.. print(records_node.first_child())
-
-.. ::
-
-.. Node<'Let It Be', data_id=510268653885439170>
-
-.. Nodes may be strings or arbitrary objects::
-
-.. alice = Person("Alice", age=23, guid="{123-456}")
-.. tree.add(alice)
-
-.. # Lookup nodes by object, data_id, name pattern, ...
-.. alice_node = tree[alice]
-.. assert isinstance(alice_node.data, Person)
-.. assert alice_node.data is alice
-
-.. del tree[alice]
-
Nutree Facts
============
* :ref:`Handle multiple references of single objects ('clones') `
* :ref:`Search by name pattern, id, or object reference `
- * :ref:`Unobtrusive handling of arbitrary objects `
* :ref:`Compare two trees and calculate patches `
+ * :ref:`Unobtrusive handling of arbitrary objects `
* :ref:`Save as DOT file and graphwiz diagram `
* :ref:`Nodes can be plain strings or objects `
* :ref:`(De)Serialize to (compressed) JSON `
diff --git a/nutree/tree_generator.py b/nutree/tree_generator.py
index eaf3bef..5f9c643 100644
--- a/nutree/tree_generator.py
+++ b/nutree/tree_generator.py
@@ -20,7 +20,7 @@
from fabulist import Fabulist
fab = Fabulist()
-except ImportError:
+except ImportError: # pragma: no cover
# We run without fabulist (with reduced functionality in this case)
Fabulist = None
fab = None
@@ -149,7 +149,6 @@ def __init__(
def generate(self) -> Union[date, float, None]:
# print(self.min, self.max, self.delta_days, self.probability)
if self._skip_value():
- # print("SKIP")
return
res = self.min + timedelta(days=random.randrange(self.delta_days))
# print(res)
@@ -205,7 +204,7 @@ def __init__(
super().__init__(probability=probability)
self.sample_list = sample_list
# TODO: remove this when support for Python 3.8 is removed
- if sys.version_info < (3, 9) and counts:
+ if sys.version_info < (3, 9) and counts: # pragma: no cover
raise RuntimeError("counts argument requires Python 3.9 or later.")
self.counts = counts
@@ -214,7 +213,7 @@ def generate(self) -> Any:
if self._skip_value():
return
# TODO: remove this when support for Python 3.8 is removed
- if sys.version_info < (3, 9) and not self.counts:
+ if sys.version_info < (3, 9) and not self.counts: # pragma: no cover
return random.sample(self.sample_list, 1)[0]
return random.sample(self.sample_list, 1, counts=self.counts)[0]
@@ -242,7 +241,7 @@ class TextRandomizer(Randomizer):
def __init__(self, template: str | list[str], *, probability: float = 1.0) -> None:
super().__init__(probability=probability)
- if not fab:
+ if not fab: # pragma: no cover
raise RuntimeError("Need fabulist installed to generate random text.")
self.template = template
@@ -283,7 +282,7 @@ def __init__(
probability: float = 1.0,
) -> None:
super().__init__(probability=probability)
- if not fab:
+ if not fab: # pragma: no cover
raise RuntimeError("Need fabulist installed to generate random text.")
self.sentence_count = sentence_count
diff --git a/nutree/typed_tree.py b/nutree/typed_tree.py
index 4c97baa..260eebc 100644
--- a/nutree/typed_tree.py
+++ b/nutree/typed_tree.py
@@ -411,39 +411,6 @@ def append_sibling(
node_id=node_id,
)
- # def move_to(
- # self,
- # new_parent: TypedNode | TypedTree,
- # *,
- # before: TypedNode | bool | int | None = None,
- # ):
- # """Move this node before or after `new_parent`."""
- # raise NotImplementedError
-
- # def copy(self, *, add_self=True, predicate=None) -> TypedTree:
- # """Return a new :class:`~nutree.typed_tree.TypedTree` instance from this
- # branch.
-
- # See also :meth:`_add_from` and :ref:`iteration-callbacks`.
- # """
- # return super().copy(add_self=add_self, predicate=predicate)
-
- # def filtered(self, predicate: PredicateCallbackType) -> TypedTree:
- # """Return a filtered copy of this node and descendants as tree.
-
- # See also :ref:`iteration-callbacks`.
- # """
- # return super().filtered(predicate=predicate)
-
- # def iterator(
- # self, method: IterMethod = IterMethod.PRE_ORDER, *, add_self=False
- # ) -> Iterator[Node]:
- # """Generator that walks the hierarchy."""
- # return super().iterator(method=method, add_self=add_self)
-
- # #: Implement ``for subnode in node: ...`` syntax to iterate descendant nodes.
- # __iter__ = iterator
-
@classmethod
def _make_list_entry(cls, node: Self) -> dict[str, Any]:
node_data = node._data
diff --git a/tests/test_core.py b/tests/test_core.py
index d385f0a..88045f6 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -180,6 +180,8 @@ def test_basic(self):
assert records.tree is records._tree
+ assert len(tree.get_toplevel_nodes()) == 2
+
assert tree.find(data="Records") is records
# TODO: hashes are salted in Py3, so we can't assume stable keys in tests
# assert tree.find(data_id="1862529381406879915") is records
diff --git a/tests/test_tree_generator.py b/tests/test_tree_generator.py
index cb28321..3f24e89 100644
--- a/tests/test_tree_generator.py
+++ b/tests/test_tree_generator.py
@@ -24,6 +24,13 @@
class TestBase:
def test_simple(self):
+ _cb_count = 0
+
+ def _calback(data):
+ nonlocal _cb_count
+ assert data["title"].startswith("Failure ")
+ _cb_count += 1
+
structure_def = {
"name": "fmea",
#: Types define the default properties of the nodes
@@ -41,28 +48,29 @@ def test_simple(self):
"relations": {
"__root__": {
"function": {
- ":count": 3,
+ ":count": 30,
"title": "Function {hier_idx}",
"date": DateRangeRandomizer(
datetime.date(2020, 1, 1), datetime.date(2020, 12, 31)
),
"date2": DateRangeRandomizer(
- datetime.date(2020, 1, 1), 365, probability=0.99
+ datetime.date(2020, 1, 1), 365, probability=0.5
),
"value": ValueRandomizer("foo", probability=0.5),
"expanded": SparseBoolRandomizer(probability=0.5),
- "state": SampleRandomizer(["open", "closed"], probability=0.99),
+ "state": SampleRandomizer(["open", "closed"], probability=0.5),
},
},
"function": {
"failure": {
":count": RangeRandomizer(1, 3),
+ ":callback": _calback,
"title": "Failure {hier_idx}",
},
},
"failure": {
"cause": {
- ":count": RangeRandomizer(1, 3, probability=0.99),
+ ":count": RangeRandomizer(1, 3, probability=0.5),
"title": "Cause {hier_idx}",
},
"effect": {
@@ -76,6 +84,7 @@ def test_simple(self):
tree.print()
assert type(tree) is Tree
assert tree.calc_height() == 3
+ assert _cb_count >= 3
tree2 = TypedTree.build_random_tree(structure_def)
tree2.print()
diff --git a/tests/test_typed_tree.py b/tests/test_typed_tree.py
index 95f7a27..e3fa159 100644
--- a/tests/test_typed_tree.py
+++ b/tests/test_typed_tree.py
@@ -5,6 +5,7 @@
# pyright: reportOptionalMemberAccess=false
import re
+from pathlib import Path
from nutree.typed_tree import ANY_KIND, TypedNode, TypedTree, _SystemRootTypedNode
@@ -12,6 +13,7 @@
class TestTypedTree:
+ # def met
def test_add_child(self):
tree = TypedTree("fixture")
@@ -58,6 +60,7 @@ def test_add_child(self):
`- function → func2
""",
)
+ assert tree.last_child(ANY_KIND).name == "func2"
assert len(fail1.children) == 4
assert fail1.get_children(kind=ANY_KIND) == fail1.children
@@ -139,6 +142,36 @@ def test_add_child(self):
subtree = func2.copy()
assert isinstance(subtree, TypedTree)
+ def test_add_child_2(self):
+ tree = TypedTree("fixture")
+
+ a = tree.add("A")
+ a.append_child("a1")
+ a.prepend_child("a0")
+ a.append_sibling("A2")
+ a.prepend_sibling("A0")
+
+ b = tree.add("B", kind="letter")
+ tree_2 = TypedTree("fixture2").add("X").add("x1").up(2).add("Y").add("y1").tree
+ b.append_child(tree_2)
+ tree.print()
+ assert fixture.check_content(
+ tree,
+ """
+ TypedTree<*>
+ +- child → A0
+ +- child → A
+ | +- child → a0
+ | `- child → a1
+ +- child → A2
+ `- letter → B
+ +- child → X
+ | `- child → x1
+ `- child → Y
+ `- child → y1
+ """,
+ )
+
def test_graph_product(self):
tree = TypedTree("Pencil")
@@ -172,16 +205,13 @@ def test_graph_product(self):
# tree.print()
# raise
- # with fixture.WritableTempFile("w", suffix=".png") as temp_file:
-
- # tree.to_dotfile(
- # # temp_file.name,
- # "/Users/martin/Downloads/tree_graph_pencil.png",
- # format="png",
- # graph_attrs={"rankdir": "LR"},
- # # add_root=False,
- # # node_mapper=node_mapper,
- # # edge_mapper=edge_mapper,
- # # unique_nodes=False,
- # )
- # assert False
+ def test_graph_product2(self):
+ tree = fixture.create_typed_tree()
+ tree.print()
+ with fixture.WritableTempFile("w", suffix=".gv") as temp_file:
+ tree.to_dotfile(temp_file.name)
+
+ buffer = Path(temp_file.name).read_text()
+
+ print(buffer)
+ assert '[label="func2"]' in buffer