Coverage for phml\utilities\locate\index.py: 100%
30 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-05 15:06 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-05 15:06 -0500
1from typing import Any, Callable, overload
3from phml.nodes import MISSING, Element, Parent
4from phml.utilities.validate.check import Test
7class Index:
8 """Uses the given key or key generator and creates a mutable dict of key value pairs
9 that can be easily indexed.
11 Nodes that don't match the condition or don't have a valid key are not indexed.
12 """
14 indexed_tree: dict[Any, list[Element]]
15 """The indexed collection of elements"""
17 def __init__(
18 self,
19 start: Parent,
20 key: str | Callable[[Element], str],
21 condition: Test | None = None,
22 ) -> None:
23 """
24 Args:
25 `key` (str | Callable): Str represents the attribute to use as an index. Callable
26 represents a function to call on each element to generate a key. The returned key
27 must be able to be converted to a string. If none then element is skipped.
28 `start` (Parent): The root or node to start at while indexing
29 `test` (Test): The test to apply to each node. Only valid/passing nodes
30 will be indexed
31 """
32 from phml.utilities import ( # pylint: disable=import-outside-toplevel
33 check,
34 walk,
35 )
37 self.indexed_tree = {}
38 self.key = key
40 for node in walk(start):
41 if isinstance(node, Element):
42 if condition is not None:
43 if check(node, condition):
44 self.add(node)
45 else:
46 self.add(node)
48 def __iter__(self):
49 return iter(self.indexed_tree)
51 def __contains__(self, _key: str) -> bool:
52 return _key in self.indexed_tree
54 def __str__(self):
55 return str(self.indexed_tree)
57 def items(self): # pragma: no cover
58 """Get the key value pairs of all indexes."""
59 return self.indexed_tree.items()
61 def values(self): # pragma: no cover
62 """Get all the values in the collection."""
63 return self.indexed_tree.values()
65 def keys(self): # pragma: no cover
66 """Get all the keys in the collection."""
67 return self.indexed_tree.keys()
69 def add(self, node: Element):
70 """Adds element to indexed collection if not already there."""
72 key = node.get(self.key, "") if isinstance(self.key, str) else self.key(node)
73 if key not in self.indexed_tree:
74 self.indexed_tree[key] = [node]
76 if node not in self.indexed_tree[key]:
77 self.indexed_tree[key].append(node)
79 def remove(self, node: Element):
80 """Removes element from indexed collection if there."""
82 key = self.key if isinstance(self.key, str) else self.key(node)
83 if key in self.indexed_tree and node in self.indexed_tree[key]:
84 self.indexed_tree[key].remove(node)
85 if len(self.indexed_tree[key]) == 0:
86 self.indexed_tree.pop(key, None)
88 def __getitem__(self, key: Any) -> list[Element]:
89 return self.indexed_tree[key]
91 @overload
92 def get(self, _key: str, _default: Any = MISSING) -> list[Element] | Any:
93 ...
95 @overload
96 def get(self, _key: str) -> list[Element] | None:
97 ...
99 def get(
100 self, _key: str, _default: Any = MISSING
101 ) -> list[Element] | None: # pragma: no cover
102 """Get a specific index from the indexed tree."""
103 if _default != MISSING:
104 return self.indexed_tree.get(_key, _default)
105 return self.indexed_tree.get(_key, None)
107 # Built in key functions
109 @staticmethod
110 def key_by_tag(node: Element) -> str:
111 """Builds the key from an elements tag. If the node is not an element
112 then the node's type is returned."""
114 return node.tag