Coverage for fpdf2_textindex/interface/node.py: 86.52%
71 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-01 14:22 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-01 14:22 +0000
1"""Node."""
3from __future__ import annotations
5import bisect
6from collections.abc import Iterator
7import dataclasses
8from typing import ClassVar
10from typing_extensions import Self
12from fpdf2_textindex.constants import LOGGER
13from fpdf2_textindex.interface.abc import _LabelPathABC
14from fpdf2_textindex.interface.label_path import LabelPath
17@dataclasses.dataclass(kw_only=True, repr=False, slots=True)
18class Node(_LabelPathABC):
19 """Node."""
21 _next_id: ClassVar[int] = 0
23 id: int = dataclasses.field(init=False)
24 """The id."""
26 label: str
27 """The label."""
29 parent: Self | None = None
30 """The parent."""
32 _children: list[Self] = dataclasses.field(default_factory=list, init=False)
33 """The children."""
35 def __post_init__(self) -> None:
36 self.id = type(self)._next_id
37 type(self)._next_id += 1
39 if self.parent is not None:
40 self.parent.add_child(self) # type: ignore[arg-type]
42 def __bool__(self) -> bool:
43 return True
45 def __iter__(self) -> Iterator[Self]:
46 yield self
47 yield from self.iter_children()
49 def __hash__(self) -> int:
50 return hash((self.id, self.label))
52 def __repr__(self) -> str:
53 kw: dict[str, int | str] = {}
54 kw["id"] = self.id
55 kw["label"] = repr(self.label)
56 kw["depth"] = self.depth
57 kw["label_path"] = repr(self.joined_label_path)
58 n_children = len(self._children)
59 kw["children"] = (
60 f"[{n_children:d} child{'ren' if n_children > 1 else '':s}]"
61 )
62 kw_str = ", ".join(f"{k:s}: {v!s:s}" for k, v in kw.items())
63 return f"{type(self).__name__:s}({kw_str:s})"
65 def __str__(self) -> str:
66 return self.label or ""
68 @property
69 def children(self) -> list[Self]:
70 """The sorted children."""
71 return self._children.copy()
73 @property
74 def depth(self) -> int:
75 """The depth.
77 Possible values are:
78 - (invisible) root: 0,
79 - entries: 1,
80 - subentries: 2,
81 - sub-subentries: 3.
83 Deeper entries are not recommended.
84 """
85 return sum(1 for _ in self.iter_parents()) + 1
87 @property
88 def label_path(self) -> LabelPath:
89 """The label path."""
90 return LabelPath(
91 reversed([self.label, *(p.label for p in self.iter_parents())])
92 )
94 def add_child(self, child: Self) -> None:
95 """Adds a child.
97 Args:
98 child: The child to add.
100 Raises:
101 ValueError: If there is already a child with the same label or the
102 child's parent differs from this node.
103 """
104 if self.depth >= 3: 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true
105 LOGGER.warning(
106 "Entries below sub-subentries (`depth >= 3`) are not "
107 "recommended"
108 )
109 if self.get_child(child.label) is not None: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 msg = "cannot add second child with same label"
111 raise ValueError(msg)
112 if child.parent is None: 112 ↛ 113line 112 didn't jump to line 113 because the condition on line 112 was never true
113 child.parent = self
114 elif child.parent is not self: 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 msg = "cannot add child to second parent"
116 raise ValueError(msg)
117 child.parent = self
118 bisect.insort(self._children, child, key=lambda c: c.label)
120 def get_child(self, label: str) -> Self | None:
121 """Returns a child by its label or `None` if not existing.
123 Args:
124 label: The label to search by.
126 Returns:
127 The child with the label or `None` if not existing.
128 """
129 idx = bisect.bisect_left(self._children, label, key=lambda c: c.label)
130 if idx < len(self._children) and self._children[idx].label == label:
131 return self._children[idx]
132 return None
134 def iter_children(self) -> Iterator[Self]:
135 """Iterates over the children (going down).
137 Yields:
138 The first child, its grandchildren, great-grandchildren, ..., then
139 the second child, its grandchildren, great-grandchildren, ..., and
140 so forth.
141 """
142 for child in self._children:
143 yield from iter(child) # type: ignore[misc]
145 def iter_parents(self) -> Iterator[Self]:
146 """Iterates over the parents without the root (going up).
148 Yields:
149 The parent, grandparent, great-grandparent, ..., and so forth,
150 stopping before root.
151 """
152 # Do not yield root
153 if self.parent is None:
154 return
155 par = self.parent
156 while par.parent is not None:
157 yield par
158 par = par.parent