otit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
otit/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Object Traversal & Inspection Toolkit."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .exceptions import InvalidPath, OtitError, PathNotFound
6
+ from .traversal import delete, find, get, has, leaves, omit, paths, pick, set, walk
7
+
8
+ __all__ = [
9
+ "OtitError",
10
+ "PathNotFound",
11
+ "InvalidPath",
12
+ "delete",
13
+ "find",
14
+ "get",
15
+ "has",
16
+ "leaves",
17
+ "omit",
18
+ "paths",
19
+ "pick",
20
+ "set",
21
+ "walk",
22
+ ]
otit/_path.py ADDED
@@ -0,0 +1,15 @@
1
+ from collections.abc import Sequence
2
+ from typing import TypeAlias
3
+
4
+ PathSegment: TypeAlias = str | int
5
+ Path: TypeAlias = str | Sequence[PathSegment]
6
+
7
+
8
+ def parse_path(path: Path) -> tuple[PathSegment, ...]:
9
+ if isinstance(path, str):
10
+ if not path:
11
+ return ()
12
+
13
+ return tuple(path.split("."))
14
+
15
+ return tuple(path)
otit/_resolve.py ADDED
@@ -0,0 +1,187 @@
1
+ import inspect
2
+ from collections.abc import Iterator, Mapping, MutableMapping, MutableSequence, Sequence
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from ._path import PathSegment
7
+
8
+
9
+ class _ResolutionError(Exception):
10
+ """Internal error raised when a path segment cannot be resolved."""
11
+
12
+ class _SegmentKind(Enum):
13
+ MAPPING = "mapping"
14
+ SEQUENCE = "sequence"
15
+ ATTRIBUTE = "attribute"
16
+
17
+
18
+ def _sequence_index(segment: PathSegment) -> int:
19
+ """Convert a path segment to a sequence index."""
20
+ if isinstance(segment, bool):
21
+ raise _ResolutionError from None
22
+
23
+ try:
24
+ return int(segment)
25
+ except (TypeError, ValueError):
26
+ raise _ResolutionError from None
27
+
28
+ def resolved_segment(
29
+ obj: Any,
30
+ segment: PathSegment,
31
+ ) -> tuple[PathSegment, _SegmentKind]:
32
+ """Return the canonical segment and its traversal kind."""
33
+ if isinstance(obj, Mapping):
34
+ return segment, _SegmentKind.MAPPING
35
+
36
+ if (
37
+ isinstance(obj, Sequence)
38
+ and not isinstance(obj, (str, bytes, bytearray))
39
+ ):
40
+ return _sequence_index(segment), _SegmentKind.SEQUENCE
41
+
42
+ if not isinstance(segment, str):
43
+ raise _ResolutionError from None
44
+
45
+ return segment, _SegmentKind.ATTRIBUTE
46
+
47
+ def resolve(obj: Any, segment: PathSegment) -> Any:
48
+ """Resolve one path segment against an object."""
49
+ if isinstance(obj, Mapping):
50
+ try:
51
+ return obj[segment]
52
+ except KeyError:
53
+ raise _ResolutionError from None
54
+
55
+ if (
56
+ isinstance(obj, Sequence)
57
+ and not isinstance(obj, (str, bytes, bytearray))
58
+ ):
59
+ index = _sequence_index(segment)
60
+
61
+ try:
62
+ return obj[index]
63
+ except IndexError:
64
+ raise _ResolutionError from None
65
+
66
+ if not isinstance(segment, str):
67
+ raise _ResolutionError from None
68
+
69
+ try:
70
+ inspect.getattr_static(obj, segment)
71
+ except AttributeError:
72
+ raise _ResolutionError from None
73
+
74
+ return getattr(obj, segment)
75
+
76
+
77
+ def assign(
78
+ obj: Any,
79
+ segment: PathSegment,
80
+ value: Any,
81
+ *,
82
+ create: bool=False,
83
+ ) -> None:
84
+ """Assign a value to one path segment."""
85
+ if isinstance(obj, MutableMapping):
86
+ if not create and segment not in obj:
87
+ raise _ResolutionError
88
+ obj[segment] = value
89
+ return
90
+
91
+ if (
92
+ isinstance(obj, MutableSequence)
93
+ and not isinstance(obj, (str, bytes, bytearray))
94
+ ):
95
+ index = _sequence_index(segment)
96
+
97
+ try:
98
+ obj[index] = value
99
+ except IndexError:
100
+ raise _ResolutionError from None
101
+
102
+ return
103
+
104
+ if not isinstance(segment, str):
105
+ raise _ResolutionError from None
106
+
107
+ if not create:
108
+ try:
109
+ inspect.getattr_static(obj, segment)
110
+ except AttributeError:
111
+ raise _ResolutionError from None
112
+
113
+ setattr(obj, segment, value)
114
+
115
+
116
+ def remove(obj: Any, segment: PathSegment) -> None:
117
+ """Remove the value at one path segment."""
118
+ if isinstance(obj, MutableMapping):
119
+ try:
120
+ del obj[segment]
121
+ except KeyError:
122
+ raise _ResolutionError from None
123
+
124
+ return
125
+
126
+ if (
127
+ isinstance(obj, MutableSequence)
128
+ and not isinstance(obj, (str, bytes, bytearray))
129
+ ):
130
+ index = _sequence_index(segment)
131
+
132
+ try:
133
+ del obj[index]
134
+ except IndexError:
135
+ raise _ResolutionError from None
136
+
137
+ return
138
+
139
+ if not isinstance(segment, str):
140
+ raise _ResolutionError from None
141
+
142
+ try:
143
+ inspect.getattr_static(obj, segment)
144
+ except AttributeError:
145
+ raise _ResolutionError from None
146
+
147
+ delattr(obj, segment)
148
+
149
+ def iter_children(
150
+ obj: Any,
151
+ ) -> Iterator[tuple[PathSegment, Any]]:
152
+ """Yield directly traversable children of an object."""
153
+ if isinstance(obj, Mapping):
154
+ yield from obj.items()
155
+ return
156
+
157
+ if (
158
+ isinstance(obj, Sequence)
159
+ and not isinstance(obj, (str, bytes, bytearray))
160
+ ):
161
+ yield from enumerate(obj)
162
+ return
163
+
164
+ try:
165
+ attributes = vars(obj)
166
+ except TypeError:
167
+ return
168
+
169
+ yield from attributes.items()
170
+
171
+ def _is_leaf(obj: Any) -> bool:
172
+ """Return whether an Object is a terminal traversal value"""
173
+ if isinstance(obj, Mapping):
174
+ return False
175
+
176
+ if (
177
+ isinstance(obj, Sequence)
178
+ and not isinstance(obj, (str, bytes, bytearray))
179
+ ):
180
+ return False
181
+
182
+ try:
183
+ vars(obj)
184
+ except TypeError:
185
+ return True
186
+
187
+ return False
otit/exceptions.py ADDED
@@ -0,0 +1,23 @@
1
+ class OtitError(Exception):
2
+ """Base exception for OTIT."""
3
+
4
+ class InvalidPath(OtitError):
5
+ """Raised when a path is invalid for the requested operation."""
6
+
7
+ class PathNotFound(OtitError):
8
+ """Raised when a path cannot be resolved."""
9
+
10
+ def __init__(
11
+ self,
12
+ path: object,
13
+ segment: object,
14
+ position: int,
15
+ ) -> None:
16
+ self.path = path
17
+ self.segment = segment
18
+ self.position = position
19
+
20
+ super().__init__(
21
+ f"Could not resolve segment {segment!r} "
22
+ f"at position {position} in path {path!r}"
23
+ )
otit/py.typed ADDED
File without changes