Skip to content

Commit 608e18f

Browse files
committed
usage of generic Node in Tree definition, dataclass Node
1 parent 3004019 commit 608e18f

File tree

4 files changed

+92
-80
lines changed

4 files changed

+92
-80
lines changed

lighttree/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
from .interactive import TreeBasedObj
2-
from .tree import Tree, Node, Key, KeyedNode, KeyedTree
2+
from .tree import Tree, Key, KeyedNode, KeyedTree
3+
from .node import Node, AutoIdNode
34

4-
__all__ = ["Tree", "Node", "TreeBasedObj", "Key", "KeyedNode", "KeyedTree"]
5+
__all__ = [
6+
"Tree",
7+
"Node",
8+
"AutoIdNode",
9+
"TreeBasedObj",
10+
"Key",
11+
"KeyedNode",
12+
"KeyedTree",
13+
]

lighttree/implementations/json_tree.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from typing import Dict, Optional, Any, Union, List
2-
from lighttree import Node, Tree, Key
2+
from lighttree import Tree, Key, AutoIdNode
33
from lighttree.node import NodeId
44
from lighttree.interactive import TreeBasedObj
55

@@ -32,26 +32,26 @@ def _fill(self, data: Any, key: Key, strict: bool, path: str = "") -> None:
3232
else:
3333
pid = self.get_node_id_by_path(path=path)
3434
if isinstance(data, list) or not strict and isinstance(data, tuple):
35-
k = self.insert_node(Node(keyed=False), parent_id=pid, key=key)
35+
k = self.insert_node(AutoIdNode(keyed=False), parent_id=pid, key=key)
3636
path = self._concat(path, k)
3737
for el in data:
3838
self._fill(el, strict=strict, path=path, key=None)
3939
return
4040
if isinstance(data, dict):
41-
k = self.insert_node(Node(keyed=True), key=key, parent_id=pid)
41+
k = self.insert_node(AutoIdNode(keyed=True), key=key, parent_id=pid)
4242
path = self._concat(path, k)
4343
for sk, el in data.items():
4444
self._fill(el, strict=strict, path=path, key=sk)
4545
return
4646
if isinstance(data, (str, int, float)):
4747
self.insert_node(
48-
Node(accept_children=False, repr_=str(data), data=data),
48+
AutoIdNode(accept_children=False, repr_=str(data), data=data),
4949
parent_id=pid,
5050
key=key,
5151
)
5252
return
5353
if data is None:
54-
self.insert_node(Node(accept_children=False), parent_id=pid)
54+
self.insert_node(AutoIdNode(accept_children=False), parent_id=pid)
5555
return
5656
raise TypeError("Unsupported type %s" % type(data))
5757

lighttree/node.py

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,63 @@
11
import uuid
22
from typing import Optional, Any, Tuple
3+
from dataclasses import dataclass
34

45
NodeId = str
56

67

7-
class Node(object):
8-
def __init__(
9-
self,
10-
identifier: Optional[NodeId] = None,
11-
auto_uuid: bool = True,
12-
keyed: bool = True,
13-
accept_children: bool = True,
14-
repr_: Optional[str] = None,
15-
data: Any = None,
16-
) -> None:
17-
"""
18-
:param identifier: node identifier, must be unique per tree
19-
"""
20-
if identifier is None:
21-
if not auto_uuid:
22-
raise ValueError("Required identifier")
23-
identifier = str(uuid.uuid4())
24-
self.identifier = identifier
25-
self.keyed = keyed
26-
self.accept_children = accept_children
27-
self.repr = repr_
28-
self.data = data
8+
@dataclass
9+
class Node:
10+
11+
identifier: NodeId
12+
keyed: bool = True
13+
accept_children: bool = True
14+
repr_: Optional[str] = None
15+
data: Any = None
2916

3017
def line_repr(self, depth: int, **kwargs: Any) -> Tuple[str, str]:
3118
"""Control how node is displayed in tree representation.
32-
_
33-
├── one end
34-
│ └── two myEnd
35-
└── three
19+
First returned string is how node is represented on left, second string is how node is represented on right.
20+
21+
MyTree
22+
├── one OneEnd
23+
│ └── two twoEnd
24+
└── three threeEnd
3625
"""
37-
if self.repr is not None:
38-
return self.repr, ""
26+
if self.repr_ is not None:
27+
return self.repr_, ""
3928
if not self.accept_children:
40-
return str(self.data), ""
29+
if hasattr(self.data, "__str__"):
30+
return str(self.data), ""
31+
return "", ""
4132
if self.keyed:
4233
return "{}", ""
4334
return "[]", ""
4435

45-
def __eq__(self, other: Any) -> bool:
46-
if not isinstance(other, self.__class__):
47-
return False
48-
return self.identifier == other.identifier
4936

50-
def __str__(self) -> str:
51-
return "%s, id=%s" % (self.__class__.__name__, self.identifier)
37+
class AutoIdNode(Node):
38+
def __init__(
39+
self,
40+
identifier: Optional[NodeId] = None,
41+
keyed: bool = True,
42+
accept_children: bool = True,
43+
repr_: Optional[str] = None,
44+
data: Any = None,
45+
):
46+
47+
self._auto_generated_id: bool
48+
identifier_: NodeId
49+
50+
if identifier is None:
51+
identifier_ = str(uuid.uuid4())
52+
self._auto_generated_id = True
53+
else:
54+
identifier_ = identifier
55+
self._auto_generated_id = False
5256

53-
def __repr__(self) -> str:
54-
return self.__str__()
57+
super(AutoIdNode, self).__init__(
58+
identifier=identifier_,
59+
keyed=keyed,
60+
accept_children=accept_children,
61+
repr_=repr_,
62+
data=data,
63+
)

lighttree/tree.py

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
cast,
1212
Dict,
1313
Any,
14+
Generic,
15+
TypeVar,
16+
Iterator,
1417
)
1518
from collections import defaultdict
1619
from operator import itemgetter
@@ -22,12 +25,14 @@
2225

2326
# root has no key (None), keyed node has children with str keys, unkeyed node has children with int keys
2427
Key = Union[None, str, int]
25-
KeyedNode = Tuple[Key, Node]
2628
KeyedTree = Tuple[Key, "Tree"]
2729
Path = str
2830

31+
GenericNode = TypeVar("GenericNode", bound=Node)
32+
KeyedNode = Tuple[Key, GenericNode]
2933

30-
class Tree(object):
34+
35+
class Tree(Generic[GenericNode]):
3136

3237
"""Principles:
3338
- each node is identified by an id
@@ -49,7 +54,7 @@ def __init__(self, path_separator: str = ".") -> None:
4954
# nodes references and hierarchy in tree
5055
self.root: Optional[NodeId] = None
5156
# node identifier -> node
52-
self._nodes_map: Dict[NodeId, Node] = {}
57+
self._nodes_map: Dict[NodeId, GenericNode] = {}
5358
# node identifier -> parent node identifier
5459
self._nodes_parent: Dict[NodeId, Optional[NodeId]] = defaultdict(lambda: None)
5560
# "map" node identifier -> map of children nodes identifier -> key
@@ -61,10 +66,6 @@ def __contains__(self, identifier: NodeId) -> bool:
6166
return identifier in self._nodes_map
6267

6368
def get(self, nid: NodeId) -> KeyedNode:
64-
"""Get a node by its id.
65-
:param nid: str, identifier of node to fetch
66-
:rtype: lighttree.node.Node
67-
"""
6869
self._ensure_present(nid)
6970
return self.get_key(nid), self._nodes_map[nid]
7071

@@ -128,13 +129,12 @@ def list(
128129
self,
129130
id_in: Optional[Sequence[NodeId]] = None,
130131
depth_in: Optional[Sequence[int]] = None,
131-
filter_: Optional[Callable[[Node], bool]] = None,
132+
filter_: Optional[Callable[[GenericNode], bool]] = None,
132133
) -> List[KeyedNode]:
133134
"""List nodes.
134135
:param id_in: list of str, optional, filter nodes among provided identifiers
135136
:param depth_in: list of int, optional, filter nodes whose depth in tree is among provided values
136137
:param filter\_: function, optional, filtering function to apply to each node
137-
:rtype: list of lighttree.node.Node
138138
"""
139139
return [
140140
(self.get_key(nid), node)
@@ -167,7 +167,7 @@ def _ensure_present(
167167
raise NotFoundNodeError("Node id <%s> doesn't exist in tree" % nid)
168168
return nid
169169

170-
def _validate_node_insertion(self, node: Node) -> None:
170+
def _validate_node_insertion(self, node: GenericNode) -> None:
171171
if node.identifier in self._nodes_map.keys():
172172
raise DuplicatedNodeError(
173173
"Can't create node with id '%s'" % node.identifier
@@ -330,24 +330,12 @@ def leaves_ids(self, nid: Optional[NodeId] = None) -> List[NodeId]:
330330

331331
def insert(
332332
self,
333-
item: Union[Node, "Tree"],
333+
item: Union[GenericNode, "Tree"],
334334
parent_id: Optional[NodeId] = None,
335335
child_id: Optional[NodeId] = None,
336336
child_id_below: Optional[NodeId] = None,
337337
key: Key = None,
338338
) -> "Tree":
339-
if isinstance(item, Node):
340-
if child_id_below is not None:
341-
raise ValueError(
342-
'"child_id_below" parameter is reserved to Tree insertion.'
343-
)
344-
self.insert_node(
345-
node=item,
346-
parent_id=parent_id,
347-
child_id=child_id,
348-
key=key,
349-
)
350-
return self
351339
if isinstance(item, Tree):
352340
self.insert_tree(
353341
new_tree=item,
@@ -357,13 +345,22 @@ def insert(
357345
key=key,
358346
)
359347
return self
360-
raise ValueError(
361-
'"item" parameter must either be a Node, or a Tree, got <%s>.' % type(item)
348+
# item is GenericNode
349+
if child_id_below is not None:
350+
raise ValueError(
351+
'"child_id_below" parameter is reserved to Tree insertion.'
352+
)
353+
self.insert_node(
354+
node=item,
355+
parent_id=parent_id,
356+
child_id=child_id,
357+
key=key,
362358
)
359+
return self
363360

364361
def insert_node(
365362
self,
366-
node: Node,
363+
node: GenericNode,
367364
parent_id: Optional[NodeId] = None,
368365
child_id: Optional[NodeId] = None,
369366
key: Key = None,
@@ -386,7 +383,7 @@ def insert_node(
386383

387384
def _insert_node_below(
388385
self,
389-
node: Node,
386+
node: GenericNode,
390387
parent_id: Optional[NodeId],
391388
key: Key,
392389
) -> None:
@@ -433,7 +430,7 @@ def _insert_node_below(
433430
self._nodes_map[node_id] = node
434431
self._nodes_parent[node_id] = parent_id
435432

436-
def _insert_node_above(self, node: Node, child_id: NodeId, key: Key) -> None:
433+
def _insert_node_above(self, node: GenericNode, child_id: NodeId, key: Key) -> None:
437434
self._ensure_present(child_id)
438435
# get parent_id before dropping subtree
439436
try:
@@ -601,10 +598,10 @@ def expand_tree(
601598
self,
602599
nid: Optional[NodeId] = None,
603600
mode: str = "depth",
604-
filter_: Optional[Callable[[Union[None, str, int], Node], bool]] = None,
601+
filter_: Optional[Callable[[Union[None, str, int], GenericNode], bool]] = None,
605602
filter_through: bool = False,
606603
reverse: bool = False,
607-
) -> Iterable[KeyedNode]:
604+
) -> Iterator[KeyedNode]:
608605
"""Python generator traversing the tree (or a subtree) with optional node filtering.
609606
610607
Inspired by treelib implementation https://github.com/caesar0301/treelib/blob/master/treelib/tree.py#L374
@@ -615,7 +612,6 @@ def expand_tree(
615612
:param filter_through: if True, excluded nodes don't exclude their children.
616613
:param reverse: the ``reverse`` param for sorting :class:`Node` objects in the same level
617614
:return: node ids that satisfy the conditions if ``id_only`` is True, else nodes.
618-
:rtype: generator
619615
"""
620616
if mode not in ("depth", "width"):
621617
raise NotImplementedError("Traversal mode '%s' is not supported" % mode)
@@ -656,7 +652,7 @@ def expand_tree(
656652
def show(
657653
self,
658654
nid: Optional[NodeId] = None,
659-
filter_: Optional[Callable[[Node], bool]] = None,
655+
filter_: Optional[Callable[[GenericNode], bool]] = None,
660656
display_key: bool = True,
661657
reverse: bool = False,
662658
line_type: str = "ascii-ex",
@@ -676,8 +672,6 @@ def show(
676672
:param limit: int, truncate tree display to this number of lines
677673
:param kwargs: kwargs params passed to node ``line_repr`` method
678674
:param line_max_length
679-
:rtype: unicode in python2, str in python3
680-
681675
"""
682676
output = ""
683677

@@ -714,10 +708,10 @@ def show(
714708
def _iter_nodes_with_location(
715709
self,
716710
nid: Optional[NodeId],
717-
filter_: Optional[Callable[[Node], bool]],
711+
filter_: Optional[Callable[[GenericNode], bool]],
718712
reverse: bool,
719713
is_last_list: Optional[List[bool]] = None,
720-
) -> Iterable[Tuple[Tuple[bool, ...], Key, Node]]:
714+
) -> Iterable[Tuple[Tuple[bool, ...], Key, GenericNode]]:
721715
"""Yield nodes with information on how they are placed.
722716
:param nid: starting node identifier
723717
:param filter_: filter function applied on nodes

0 commit comments

Comments
 (0)