Skip to content

Commit ef1725c

Browse files
committed
Path now an iterator of Keys
1 parent fb4b9b6 commit ef1725c

File tree

4 files changed

+61
-74
lines changed

4 files changed

+61
-74
lines changed

lighttree/implementations/json_tree.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,34 @@
55

66

77
class JsonTree(Tree):
8-
def __init__(
9-
self, d: Optional[Dict] = None, strict: bool = True, path_separator: str = "."
10-
) -> None:
8+
def __init__(self, d: Optional[Dict] = None, strict: bool = True) -> None:
119
"""
1210
:param d:
1311
:param strict: if False, will convert tuples into arrays, else raise error
14-
:param path_separator: separator used to build path
1512
"""
16-
super(JsonTree, self).__init__(path_separator=path_separator)
13+
super(JsonTree, self).__init__()
1714
if d is not None:
1815
self._fill(d, strict=strict, key=None)
1916

20-
@staticmethod
21-
def _concat(a: Any, b: Any) -> str:
22-
if not a and not b:
23-
return ""
24-
if not a:
25-
return str(b)
26-
return ".".join([str(a), str(b)])
27-
28-
def _fill(self, data: Any, key: Key, strict: bool, path: str = "") -> None:
17+
def _fill(
18+
self, data: Any, key: Optional[Key], strict: bool, path: Optional[List] = None
19+
) -> None:
2920
pid: Optional[NodeId]
21+
path_: List = path or []
3022
if self.is_empty():
3123
pid = None
3224
else:
33-
pid = self.get_node_id_by_path(path=path)
25+
pid = self.get_node_id_by_path(path=path_)
26+
3427
if isinstance(data, list) or not strict and isinstance(data, tuple):
3528
k = self.insert_node(AutoIdNode(keyed=False), parent_id=pid, key=key)
36-
path = self._concat(path, k)
3729
for el in data:
38-
self._fill(el, strict=strict, path=path, key=None)
30+
self._fill(el, strict=strict, path=path_ + ([k] if k else []), key=None)
3931
return
4032
if isinstance(data, dict):
4133
k = self.insert_node(AutoIdNode(keyed=True), key=key, parent_id=pid)
42-
path = self._concat(path, k)
4334
for sk, el in data.items():
44-
self._fill(el, strict=strict, path=path, key=sk)
35+
self._fill(el, strict=strict, path=path_ + ([k] if k else []), key=sk)
4536
return
4637
if isinstance(data, (str, int, float)):
4738
self.insert_node(

lighttree/tree.py

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@
2323
from .utils import STYLES
2424

2525

26-
# root has no key (None), keyed node has children with str keys, unkeyed node has children with int keys
27-
Key = Union[None, str, int]
28-
KeyedTree = Tuple[Key, "Tree"]
29-
Path = str
26+
# keyed node has children with str keys, unkeyed node has children with int keys
27+
# note: root has no key (None value)
28+
Key = Union[str, int]
29+
KeyedTree = Tuple[Optional[Key], "Tree"]
30+
Path = Iterable[Key]
3031

3132
GenericNode = TypeVar("GenericNode", bound=Node)
32-
KeyedNode = Tuple[Key, GenericNode]
33+
KeyedNode = Tuple[Optional[Key], GenericNode]
3334

3435

3536
class Tree(Generic[GenericNode]):
@@ -48,9 +49,7 @@ class Tree(Generic[GenericNode]):
4849
4950
"""
5051

51-
def __init__(self, path_separator: str = ".") -> None:
52-
self.path_separator = path_separator
53-
52+
def __init__(self) -> None:
5453
# nodes references and hierarchy in tree
5554
self.root: Optional[NodeId] = None
5655
# node identifier -> node
@@ -79,37 +78,40 @@ def child_id(self, nid: NodeId, key: Key) -> NodeId:
7978
if child_id is None:
8079
raise ValueError("No child of key %s below %s" % (key, nid))
8180
return child_id
82-
if not isinstance(key, (str, int)):
83-
raise ValueError("Expected integer 'castable' key, got %s" % key)
84-
return self._nodes_children_list[nid][int(key)]
81+
try:
82+
return self._nodes_children_list[nid][int(key)]
83+
except (KeyError, ValueError, TypeError):
84+
raise ValueError("No child of key %s below %s" % (key, nid))
8585

8686
def child(self, nid: NodeId, key: Key) -> KeyedNode:
8787
return self.get(self.child_id(nid, key))
8888

89-
def get_node_id_by_path(self, path: Path) -> NodeId:
89+
def get_node_id_by_path(self, path: Path, strict: bool = True) -> NodeId:
9090
nid = self.root
9191
if nid is None:
9292
raise ValueError("Empty tree")
9393
if path == "":
9494
return nid
95-
keys = str(path).split(self.path_separator)
96-
for k in keys:
97-
nid = self.child_id(nid, k)
95+
for k in path:
96+
try:
97+
nid = self.child_id(nid, k)
98+
except ValueError:
99+
if strict or not isinstance(k, str) or not k.isdigit():
100+
raise
101+
nid = self.child_id(nid, int(k))
98102
if nid is None:
99-
raise ValueError("Empty tree")
103+
raise ValueError("Not found")
100104
return nid
101105

102106
def get_path(self, nid: NodeId) -> Path:
103-
return self.path_separator.join(
104-
[
105-
str(k)
106-
for k, _ in self.ancestors(nid, from_root=True, include_current=True)[
107-
1:
108-
]
109-
]
110-
)
107+
return [
108+
# ignore typing warning of potential None value, since None only applies at root node which is excluded
109+
# [1:] -> exclude root node key
110+
k # type: ignore
111+
for k, _ in self.ancestors(nid, from_root=True, include_current=True)[1:]
112+
]
111113

112-
def get_key(self, nid: NodeId) -> Key:
114+
def get_key(self, nid: NodeId) -> Optional[Key]:
113115
"""Get a node's key.
114116
:param nid: str, identifier of node
115117
@@ -334,7 +336,7 @@ def insert(
334336
parent_id: Optional[NodeId] = None,
335337
child_id: Optional[NodeId] = None,
336338
child_id_below: Optional[NodeId] = None,
337-
key: Key = None,
339+
key: Optional[Key] = None,
338340
) -> "Tree":
339341
if isinstance(item, Tree):
340342
self.insert_tree(
@@ -363,15 +365,8 @@ def insert_node(
363365
node: GenericNode,
364366
parent_id: Optional[NodeId] = None,
365367
child_id: Optional[NodeId] = None,
366-
key: Key = None,
367-
) -> Key:
368-
"""Insert node, return key
369-
:param node:
370-
:param parent_id:
371-
:param child_id:
372-
:param key:
373-
:return:
374-
"""
368+
key: Optional[Key] = None,
369+
) -> Optional[Key]:
375370
self._validate_node_insertion(node)
376371
if parent_id is not None and child_id is not None:
377372
raise ValueError('Can declare at most "parent_id" or "child_id"')
@@ -385,7 +380,7 @@ def _insert_node_below(
385380
self,
386381
node: GenericNode,
387382
parent_id: Optional[NodeId],
388-
key: Key,
383+
key: Optional[Key],
389384
) -> None:
390385
# insertion at root
391386
if parent_id is None:
@@ -430,7 +425,9 @@ def _insert_node_below(
430425
self._nodes_map[node_id] = node
431426
self._nodes_parent[node_id] = parent_id
432427

433-
def _insert_node_above(self, node: GenericNode, child_id: NodeId, key: Key) -> None:
428+
def _insert_node_above(
429+
self, node: GenericNode, child_id: NodeId, key: Optional[Key]
430+
) -> None:
434431
self._ensure_present(child_id)
435432
# get parent_id before dropping subtree
436433
try:
@@ -454,9 +451,8 @@ def insert_tree(
454451
parent_id: Optional[NodeId] = None,
455452
child_id: Optional[NodeId] = None,
456453
child_id_below: Optional[NodeId] = None,
457-
key: Key = None,
458-
) -> Key:
459-
"""Return new key"""
454+
key: Optional[Key] = None,
455+
) -> Optional[Key]:
460456
self._validate_tree_insertion(new_tree)
461457
if new_tree.root is None:
462458
raise ValueError("Empty inserted tree")
@@ -480,7 +476,7 @@ def _insert_tree_below(
480476
self,
481477
new_tree: "Tree",
482478
parent_id: Optional[NodeId],
483-
key: Key,
479+
key: Optional[Key],
484480
) -> "Tree":
485481
if parent_id is None:
486482
# insertion at root requires tree to be empty
@@ -506,7 +502,7 @@ def _insert_tree_above(
506502
new_tree: "Tree",
507503
child_id: NodeId,
508504
child_id_below: Optional[NodeId],
509-
key: Key,
505+
key: Optional[Key],
510506
) -> None:
511507
# make all checks before modifying tree
512508
self._ensure_present(child_id)
@@ -711,7 +707,7 @@ def _iter_nodes_with_location(
711707
filter_: Optional[Callable[[GenericNode], bool]],
712708
reverse: bool,
713709
is_last_list: Optional[List[bool]] = None,
714-
) -> Iterable[Tuple[Tuple[bool, ...], Key, GenericNode]]:
710+
) -> Iterable[Tuple[Tuple[bool, ...], Optional[Key], GenericNode]]:
715711
"""Yield nodes with information on how they are placed.
716712
:param nid: starting node identifier
717713
:param filter_: filter function applied on nodes

tests/test_tree.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -883,16 +883,16 @@ def test_drop_subtree(self):
883883

884884
def test_get_node_id_by_path(self):
885885
t = get_sample_tree()
886-
self.assertEqual(t.get_node_id_by_path("a"), "a")
887-
self.assertEqual(t.get_node_id_by_path("a.b"), "ab")
888-
self.assertEqual(t.get_node_id_by_path("a.a.1"), "aa1")
889-
self.assertEqual(t.get_node_id_by_path("c.1"), "c1")
886+
self.assertEqual(t.get_node_id_by_path(["a"]), "a")
887+
self.assertEqual(t.get_node_id_by_path(["a", "b"]), "ab")
888+
self.assertEqual(t.get_node_id_by_path(["a", "a", 1]), "aa1")
889+
self.assertEqual(t.get_node_id_by_path(["c", 1]), "c1")
890890

891891
def test_subtree(self):
892892
t = get_sample_tree()
893893

894894
# by id
895-
nid = t.get_node_id_by_path("a.a")
895+
nid = t.get_node_id_by_path(["a", "a"])
896896
k, st = t.subtree(nid=nid)
897897
self.assertEqual(k, "a")
898898
self.assertEqual(
@@ -905,11 +905,11 @@ def test_subtree(self):
905905

906906
def test_path(self):
907907
t = get_sample_tree()
908-
for p in ("a.a", "a.b", "a", "", "a.a.1"):
908+
for p in [["a", "a"], ["a", "b"], ["a"], [], ["a", "a", 1]]:
909909
nid = t.get_node_id_by_path(p)
910910
self.assertEqual(t.get_path(nid), p)
911911

912-
t = get_sample_tree(path_separator="|")
913-
for p in ("a|a", "a|b", "a", "", "a|a|1"):
914-
nid = t.get_node_id_by_path(p)
915-
self.assertEqual(t.get_path(nid), p)
912+
# strict = False -> coerce "1" -> int
913+
t = get_sample_tree()
914+
nid = t.get_node_id_by_path(["a", "a", "1"])
915+
self.assertEqual(t.get_path(nid), ["a", "a", 1])

tests/testing_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def tree_sanity_check(tree):
4242
# testing samples
4343

4444

45-
def get_sample_tree(path_separator="."):
45+
def get_sample_tree():
4646
"""
4747
root {}
4848
├── a {}
@@ -54,7 +54,7 @@ def get_sample_tree(path_separator="."):
5454
├── c0
5555
└── c1
5656
"""
57-
t = Tree(path_separator=path_separator)
57+
t = Tree()
5858
t.insert_node(Node(identifier="root"))
5959
t.insert_node(Node(identifier="a"), parent_id="root", key="a")
6060
t.insert_node(Node(identifier="aa", keyed=False), parent_id="a", key="a")

0 commit comments

Comments
 (0)