Skip to content

Commit ff11c6b

Browse files
committed
Improve traversal docs and improve typing.
1 parent 11385f0 commit ff11c6b

3 files changed

Lines changed: 51 additions & 15 deletions

File tree

docs/ref/traversal.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,20 @@ Traversal package provides tools for iterating and navigating a resource tree.
1010
TraversalPath
1111
=============
1212

13-
*Todo*: In progress...
13+
A method of defining a location within a data structure, which can then be applied to
14+
the datastructure to extract the value.
15+
16+
A ``TraversalPath`` can be expressed as a string using ``.`` as a separator::
17+
18+
field1.field2
19+
20+
Both lists and dicts can be included using ``[]`` and ``{}`` syntax::
21+
22+
field[1].field2
23+
24+
or::
25+
26+
field{key=value}.field2
1427

1528

1629
ResourceTraversalIterator
@@ -23,3 +36,6 @@ This class has hooks that can be used by subclasses to customise the behaviour o
2336

2437
- *on_enter* - Called after entering a new resource.
2538
- *on_exit* - Called after exiting a resource.
39+
40+
.. autoclass:: odin.traversal.ResourceTraversalIterator
41+
:members:

src/odin/traversal.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Union
1+
"""Traversal of a datastructure."""
2+
from typing import Union, Sequence, Iterable, Optional, Tuple, Type
23

34
from odin.utils import getmeta
45

@@ -10,7 +11,13 @@ class NotSupplied:
1011
pass
1112

1213

13-
def _split_atom(atom):
14+
NotSuppliedType = Type[NotSupplied]
15+
OptionalStr = Union[str, NotSuppliedType]
16+
PathAtom = Tuple[OptionalStr, OptionalStr, str]
17+
18+
19+
def _split_atom(atom: str) -> PathAtom:
20+
"""Split a section of a path into lookups that can be used to navigate a path."""
1421
if "[" in atom:
1522
field, _, idx = atom.rstrip("]").partition("[")
1623
return idx, NotSupplied, field
@@ -26,19 +33,23 @@ class TraversalPath:
2633
"""A path through a resource structure."""
2734

2835
@classmethod
29-
def parse(cls, path: Union["TraversalPath", str]):
36+
def parse(cls, path: Union["TraversalPath", str]) -> Optional["TraversalPath"]:
37+
"""Parse a traversal path string."""
3038
if isinstance(path, TraversalPath):
3139
return path
3240
if isinstance(path, str):
3341
return cls(*[_split_atom(a) for a in path.split(".")])
3442

35-
def __init__(self, *path):
43+
__slots__ = ("_path",)
44+
45+
def __init__(self, *path: PathAtom):
46+
"""Initialise traversal path"""
3647
self._path = path
3748

3849
def __repr__(self):
3950
return f"<TraversalPath: {self}>"
4051

41-
def __str__(self):
52+
def __str__(self) -> str:
4253
atoms = []
4354
for value, key, field in self._path:
4455
if value is NotSupplied:
@@ -49,15 +60,18 @@ def __str__(self):
4960
atoms.append(f"{field}{{{key}={value}}}")
5061
return ".".join(atoms)
5162

52-
def __hash__(self):
63+
def __hash__(self) -> int:
64+
"""Hash of the path."""
5365
return hash(self._path)
5466

55-
def __eq__(self, other):
67+
def __eq__(self, other) -> bool:
68+
"""Compare to another path."""
5669
if isinstance(other, TraversalPath):
5770
return hash(self) == hash(other)
5871
return NotImplemented
5972

60-
def __add__(self, other):
73+
def __add__(self, other) -> "TraversalPath":
74+
"""Join paths together."""
6175
if isinstance(other, TraversalPath):
6276
return TraversalPath(*(self._path + other._path))
6377

@@ -69,7 +83,8 @@ def __add__(self, other):
6983

7084
raise TypeError(f"Cannot add '{other}' to a path.")
7185

72-
def __iter__(self):
86+
def __iter__(self) -> Iterable[PathAtom]:
87+
"""Iterate a path returning each element on the path."""
7388
return iter(self._path)
7489

7590
def get_value(self, root_resource: ResourceBase):
@@ -126,7 +141,10 @@ class ResourceTraversalIterator:
126141
127142
"""
128143

129-
def __init__(self, resource):
144+
__slots__ = ("_resource_iters", "_field_iters", "_path", "_resource_stack")
145+
146+
def __init__(self, resource: Union[ResourceBase, Sequence[ResourceBase]]):
147+
"""Initialise instance with the initial resource or sequence of resources."""
130148
if isinstance(resource, (list, tuple)):
131149
# Stack of resource iterators (starts initially with entries from the list)
132150
self._resource_iters = [iter([(i, r) for i, r in enumerate(resource)])]
@@ -139,10 +157,12 @@ def __init__(self, resource):
139157
self._path = [(NotSupplied, NotSupplied, NotSupplied)]
140158
self._resource_stack = [None]
141159

142-
def __iter__(self):
160+
def __iter__(self) -> Iterable[ResourceBase]:
161+
"""Obtain an iterable instance."""
143162
return self
144163

145-
def __next__(self):
164+
def __next__(self) -> ResourceBase:
165+
"""Get next resource instance."""
146166
if self._resource_iters:
147167
if self._field_iters:
148168
# Check if the last entry in the field stack has any unprocessed fields.
@@ -211,7 +231,7 @@ def depth(self) -> int:
211231
return len(self._path) - 1
212232

213233
@property
214-
def current_resource(self):
234+
def current_resource(self) -> Optional[ResourceBase]:
215235
"""The current resource being traversed."""
216236
if self._resource_stack:
217237
return self._resource_stack[-1]

tests/test_traversal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class Meta:
6767

6868
class ResourceTraversalIteratorTest(traversal.ResourceTraversalIterator):
6969
def __init__(self, resource):
70-
super(ResourceTraversalIteratorTest, self).__init__(resource)
70+
super().__init__(resource)
7171
self.events = []
7272

7373
def on_pre_enter(self):

0 commit comments

Comments
 (0)