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 +22 -0
- otit/_path.py +15 -0
- otit/_resolve.py +187 -0
- otit/exceptions.py +23 -0
- otit/py.typed +0 -0
- otit/traversal.py +516 -0
- otit-0.1.0.dist-info/METADATA +437 -0
- otit-0.1.0.dist-info/RECORD +10 -0
- otit-0.1.0.dist-info/WHEEL +4 -0
- otit-0.1.0.dist-info/licenses/LICENSE +674 -0
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
|