Skip to content

Commit

Permalink
kind parameter is now mandatory for add() and related methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
mar10 committed Nov 5, 2024
1 parent b405323 commit 7b08963
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 84 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## 0.10.0 (unreleased)

- BREAKING:
- `kind` parameter is now mandatory for `add()` and related methods.
`kind=None` is still allowed to use the default ('child').
- Rename `shadow_attrs` argument to `forward_attrs`.
- Enforce that the same object instance is not added multiple times to one parent.
- Rename `GenericNodeData` to `DictWrapper` and remove support for attribut access.
Expand Down
26 changes: 24 additions & 2 deletions nutree/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,30 @@ def add_child(
node_id=node_id,
)

#: Alias for :meth:`add_child`
add = add_child
# NOTE: mypy cannot handle this alias correctly, so we have to write the
# method signature again:
# #: Alias for :meth:`add_child`
# add = add_child
def add(
self,
child: TNode | Self | TData,
*,
before: TNode | bool | int | None = None,
deep: bool | None = None,
data_id: DataIdType | None = None,
node_id=None,
) -> TNode:
"""Add a toplevel node (same as shortcut :meth:`add`).
See Node's :meth:`~nutree.node.Node.add_child` method for details.
"""
return self.system_root.add_child(
child,
before=before,
deep=deep,
data_id=data_id,
node_id=node_id,
)

def copy(
self,
Expand Down
91 changes: 79 additions & 12 deletions nutree/typed_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
DeserializeMapperType,
KeyMapType,
MapperCallbackType,
PredicateCallbackType,
SerializeMapperType,
UniqueConstraintError,
ValueMapType,
Expand Down Expand Up @@ -215,23 +216,50 @@ def is_last_sibling(self, *, any_kind=False) -> bool:
return self is self._parent.children[-1]
return self is self.last_sibling(any_kind=False)

def _add_from(
self, other: Self, *, predicate: PredicateCallbackType | None = None
) -> None:
"""Append copies of all source descendants to self.
See also :ref:`iteration-callbacks`.
"""
if predicate:
return self._add_filtered(other, predicate)

assert not self._children
for child in other.children:
new_child = self.add_child(child.data, kind=None, data_id=child._data_id)
if child.children:
new_child._add_from(child, predicate=None)
return

def add_child(
self,
child: Self | TypedTree | TData,
*,
kind: str | None = None,
kind: str | None,
before: Self | bool | int | None = None,
deep: bool | None = None,
data_id: DataIdType | None = None,
node_id: int | None = None,
) -> Self:
"""See ..."""
"""See ...
Args:
kind: the type of the new child node. Pass None to use the same
type as `child` (if that is a node) or default to `"child"`.
See also :meth:`~nutree.node.Node.add_child` method for details.
"""
# assert not isinstance(child, TypedNode) or child.kind == self.kind
# TODO: kind is optional if child is a TypedNode
# TODO: Check if target and child types match
# TODO: share more code from overloaded method
if kind is None:
kind = cast(TypedTree, self._tree).DEFAULT_CHILD_TYPE
if isinstance(child, TypedNode):
kind = child.kind
else:
kind = cast(TypedTree, self._tree).DEFAULT_CHILD_TYPE

if isinstance(child, (Node, Tree)) and not isinstance(
child, (TypedNode, TypedTree)
Expand Down Expand Up @@ -320,17 +348,17 @@ def add_child(
# method signature again:
# #: Alias for :meth:`add_child`
# add = add_child

def add(
self,
child: Self | TypedTree | TData,
*,
kind: str | None = None,
kind: str | None,
before: Self | bool | int | None = None,
deep: bool | None = None,
data_id: DataIdType | None = None,
node_id: int | None = None,
) -> Self:
"""Alias for :meth:`add_child`)."""
return self.add_child(
child,
kind=kind,
Expand All @@ -344,7 +372,7 @@ def append_child(
self,
child: Self | TypedTree | TData,
*,
kind: str | None = None,
kind: str | None,
deep: bool | None = None,
data_id: DataIdType | None = None,
node_id: int | None = None,
Expand All @@ -366,7 +394,7 @@ def prepend_child(
self,
child: Self | TypedTree | TData,
*,
kind: str | None = None,
kind: str | None,
deep: bool | None = None,
data_id: DataIdType | None = None,
node_id: int | None = None,
Expand All @@ -388,7 +416,7 @@ def prepend_sibling(
self,
child: Self | TypedTree | TData,
*,
kind: str | None = None,
kind: str | None,
deep=None,
data_id=None,
node_id=None,
Expand All @@ -405,7 +433,7 @@ def append_sibling(
self,
child: Self | TypedTree | TData,
*,
kind: str | None = None,
kind: str | None,
deep: bool | None = None,
data_id: DataIdType | None = None,
node_id: int | None = None,
Expand All @@ -424,6 +452,21 @@ def append_sibling(
node_id=node_id,
)

def copy(
self, *, add_self=True, predicate: PredicateCallbackType | None = None
) -> TypedTree[TData]:
"""Return a new :class:`~nutree.tree.Tree` instance from this branch.
See also :ref:`iteration-callbacks`.
"""
new_tree = cast("TypedTree[TData]", self._tree.__class__())
if add_self:
root = new_tree.add(self, kind=self.kind)
else:
root = new_tree.system_root
root._add_from(self, predicate=predicate)
return new_tree

@classmethod
def _make_list_entry(cls, node: Self) -> dict[str, Any]:
node_data = node._data
Expand Down Expand Up @@ -531,7 +574,7 @@ def add_child(
self,
child: TypedNode[TData] | Self | TData,
*,
kind: str | None = None,
kind: str | None,
before: TypedNode[TData] | bool | int | None = None,
deep: bool | None = None,
data_id: DataIdType | None = None,
Expand All @@ -550,8 +593,32 @@ def add_child(
node_id=node_id,
)

#: Alias for :meth:`add_child`
add = add_child # Must re-bind here
# NOTE: mypy cannot handle this alias correctly, so we have to write the
# method signature again:
# #: Alias for :meth:`add_child`
# add = add_child
def add(
self,
child: TypedNode[TData] | Self | TData,
*,
kind: str | None,
before: TypedNode[TData] | bool | int | None = None,
deep: bool | None = None,
data_id: DataIdType | None = None,
node_id: int | None = None,
) -> TypedNode[TData]:
"""Alias for shortcut :meth:`add_child`).
See Node's :meth:`~nutree.typed_tree.TypedNode.add_child` method for details.
"""
return self.system_root.add_child(
child,
kind=kind,
before=before,
deep=deep,
data_id=data_id,
node_id=node_id,
)

def first_child(self, kind: str | Type[ANY_KIND]) -> TypedNode[TData] | None:
"""Return the first toplevel node."""
Expand Down
112 changes: 62 additions & 50 deletions tests/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,70 +134,82 @@ def create_tree(
return tree


def create_typed_tree(
def create_typed_tree_objects(
*,
name="fixture",
clones=False,
tree: TypedTree[OrgaUnit] | None = None,
print=True,
) -> TypedTree[OrgaUnit]:
"""
TypedTree<'*'>
├── org_unit → Department<Development>
│ ├── manager → Person<Alice, 23>
│ ├── member → Person<Bob, 32>
│ ╰── member → Person<Charleen, 43>
╰── department → Department<Marketing>
├── member → Person<Charleen, 43>
╰── manager → Person<Dave, 54>
"""
if tree is None:
tree = TypedTree[OrgaUnit](name)
assert len(tree) == 0

dev = tree.add(Department("Development", guid="{012-345}"), kind="org_unit")
dev.add(Person("Alice", age=23, guid="{123-456}"), kind="manager")
dev.add(Person("Bob", age=32, guid="{234-456}"), kind="member")

markt = tree.add(Department("Marketing", guid="{345-456}"), kind="org_unit")
charleen = markt.add(Person("Charleen", age=43, guid="{345-456}"), kind="member")
markt.add(Person("Dave", age=54, guid="{456-456}"), kind="manager")

if clones:
dev.add(charleen, kind="member")

# Since the output is only displayed when a test fails, it may be handy to
# see (unless caller modifies afterwards and then prints):
if print:
tree.print(repr="{node}")
return tree


def create_typed_tree_simple(
*,
style="simple",
name="fixture",
clones=False,
tree: TypedTree[Any] | None = None,
print=True,
) -> TypedTree:
"""
TypedTree<*>
+- function → func1
| +- failure → fail1
| | +- cause → cause1
| | +- cause → cause2
| | +- effect → eff1
| | `- effect → eff2
| `- failure → fail2
`- function → func2
"""
if tree is not None:
assert not tree, "must be empty"
assert isinstance(tree, TypedTree)
else:
tree = TypedTree[Any](name)

if style == "simple":
"""
TypedTree<*>
+- function → func1
| +- failure → fail1
| | +- cause → cause1
| | +- cause → cause2
| | +- effect → eff1
| | `- effect → eff2
| `- failure → fail2
`- function → func2
"""
func = tree.add("func1", kind="function")
fail1 = func.add("fail1", kind="failure")
fail1.add("cause1", kind="cause")
fail1.add("cause2", kind="cause")
fail1.add("eff1", kind="effect")
fail1.add("eff2", kind="effect")
func.add("fail2", kind="failure")
func2 = tree.add("func2", kind="function")
func = tree.add("func1", kind="function")
fail1 = func.add("fail1", kind="failure")
fail1.add("cause1", kind="cause")
fail1.add("cause2", kind="cause")
fail1.add("eff1", kind="effect")
fail1.add("eff2", kind="effect")
func.add("fail2", kind="failure")
func2 = tree.add("func2", kind="function")

if clones:
func2.add(fail1)
if clones:
func2.add(fail1, kind=None)
# func2.add(fail1, kind="failure")

elif style == "objects":
"""
TypedTree<'*'>
├── org_unit → Department<Development>
│ ├── manager → Person<Alice, 23>
│ ├── member → Person<Bob, 32>
│ ╰── member → Person<Charleen, 43>
╰── department → Department<Marketing>
├── member → Person<Charleen, 43>
╰── manager → Person<Dave, 54>
"""
dev = tree.add(Department("Development", guid="{012-345}"), kind="org_unit")
dev.add(Person("Alice", age=23, guid="{123-456}"), kind="manager")
dev.add(Person("Bob", age=32, guid="{234-456}"), kind="member")

markt = tree.add(Department("Marketing", guid="{345-456}"), kind="org_unit")
charleen = markt.add(
Person("Charleen", age=43, guid="{345-456}"), kind="member"
)
markt.add(Person("Dave", age=54, guid="{456-456}"), kind="manager")

if clones:
dev.add(charleen, kind="member")

else:
raise NotImplementedError(style)
# Since the output is only displayed when a test fails, it may be handy to
# see (unless caller modifies afterwards and then prints):
if print:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_clones.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def test_clones_typed(self):
│ ╰── failure → fail2
╰── function → func2
"""
tree = fixture.create_typed_tree()
tree = fixture.create_typed_tree_simple()

assert tree.count == 8
assert tree.count_unique == 8
Expand All @@ -96,7 +96,7 @@ def test_clones_typed(self):
# Not allowed to add two clones to same parent
with pytest.raises(UniqueConstraintError):
fail1.add("cause1", kind="cause")
fail1.add("cause1")
fail1.add("cause1", kind="other")
tree.print()
assert tree.count == 9
assert tree.count_unique == 8
Expand Down
4 changes: 2 additions & 2 deletions tests/test_mermaid.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def test_serialize_mermaid_mappers(self):
def test_serialize_mermaid_typed(self):
"""Save/load as object tree with clones."""
KEEP_FILES = not fixture.is_running_on_ci() and False
tree = fixture.create_typed_tree(style="simple", clones=True, name="Root")
tree = fixture.create_typed_tree_simple(clones=True, name="Root")

with fixture.WritableTempFile("w", suffix=".md") as temp_file:
tree.to_mermaid_flowchart(
Expand Down Expand Up @@ -113,7 +113,7 @@ def test_serialize_mermaid_png(self):
KEEP_FILES = not fixture.is_running_on_ci() and False
FORMAT = "png"

tree = fixture.create_typed_tree(style="simple", clones=True, name="Root")
tree = fixture.create_typed_tree_simple(clones=True, name="Root")

with fixture.WritableTempFile("w", suffix=f".{FORMAT}") as temp_file:
tree.to_mermaid_flowchart(
Expand Down
Loading

0 comments on commit 7b08963

Please sign in to comment.