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

1"""Node.""" 

2 

3from __future__ import annotations 

4 

5import bisect 

6from collections.abc import Iterator 

7import dataclasses 

8from typing import ClassVar 

9 

10from typing_extensions import Self 

11 

12from fpdf2_textindex.constants import LOGGER 

13from fpdf2_textindex.interface.abc import _LabelPathABC 

14from fpdf2_textindex.interface.label_path import LabelPath 

15 

16 

17@dataclasses.dataclass(kw_only=True, repr=False, slots=True) 

18class Node(_LabelPathABC): 

19 """Node.""" 

20 

21 _next_id: ClassVar[int] = 0 

22 

23 id: int = dataclasses.field(init=False) 

24 """The id.""" 

25 

26 label: str 

27 """The label.""" 

28 

29 parent: Self | None = None 

30 """The parent.""" 

31 

32 _children: list[Self] = dataclasses.field(default_factory=list, init=False) 

33 """The children.""" 

34 

35 def __post_init__(self) -> None: 

36 self.id = type(self)._next_id 

37 type(self)._next_id += 1 

38 

39 if self.parent is not None: 

40 self.parent.add_child(self) # type: ignore[arg-type] 

41 

42 def __bool__(self) -> bool: 

43 return True 

44 

45 def __iter__(self) -> Iterator[Self]: 

46 yield self 

47 yield from self.iter_children() 

48 

49 def __hash__(self) -> int: 

50 return hash((self.id, self.label)) 

51 

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})" 

64 

65 def __str__(self) -> str: 

66 return self.label or "" 

67 

68 @property 

69 def children(self) -> list[Self]: 

70 """The sorted children.""" 

71 return self._children.copy() 

72 

73 @property 

74 def depth(self) -> int: 

75 """The depth. 

76 

77 Possible values are: 

78 - (invisible) root: 0, 

79 - entries: 1, 

80 - subentries: 2, 

81 - sub-subentries: 3. 

82 

83 Deeper entries are not recommended. 

84 """ 

85 return sum(1 for _ in self.iter_parents()) + 1 

86 

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 ) 

93 

94 def add_child(self, child: Self) -> None: 

95 """Adds a child. 

96 

97 Args: 

98 child: The child to add. 

99 

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) 

119 

120 def get_child(self, label: str) -> Self | None: 

121 """Returns a child by its label or `None` if not existing. 

122 

123 Args: 

124 label: The label to search by. 

125 

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 

133 

134 def iter_children(self) -> Iterator[Self]: 

135 """Iterates over the children (going down). 

136 

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] 

144 

145 def iter_parents(self) -> Iterator[Self]: 

146 """Iterates over the parents without the root (going up). 

147 

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