Skip to content

Commit 312b0de

Browse files
committed
always use a _PathPart object for all split paths
1 parent f4561ca commit 312b0de

6 files changed

Lines changed: 121 additions & 158 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ _In development_: public API is not stable, support available solely at develope
2727
* An `InvalidIterationError` will be raised if these iterable path parts are passed to functions other than
2828
`iterate()`.
2929
* A `ValidationError` will be raised if the data structure does not match types of all path parts:
30-
* Strings keys outside of square brackets MUST correspond to an object that subclasses `dict`
31-
* Numerical indicies within square brackets MUST correspond to an object that subclasses `list`
30+
* Strings keys (outside of square brackets) MUST correspond to an object that subclasses `dict`
31+
* Numerical indicies (within square brackets) MUST correspond to an object that subclasses `list`

datapath/_base.py

Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
NO_DEFAULT,
1616
DatapathError,
1717
ValidationError,
18-
TypeValidationError,
19-
TypeMismatchValidationError,
2018
InvalidIterationError,
2119
PathLookupError,
22-
_IterationPoint,
20+
_PathPart,
21+
_IndexPart,
22+
_KeyPart,
2323
_ListIterationPoint,
2424
_StarIterationPoint,
2525
_RangeIterationPoint,
@@ -58,7 +58,7 @@ def is_path(path: str, iterable: bool = True) -> bool:
5858
if iterable:
5959
return True
6060
try:
61-
_split_match(match, iterable=False)
61+
_split_path_from_match(match, iterable=False)
6262
return True
6363
except ValidationError:
6464
return False
@@ -74,33 +74,42 @@ def validate_path(path: str, iterable: bool = True) -> None:
7474
return
7575
match = _match_validate(path)
7676
if not iterable:
77-
_split_match(match, iterable=False)
77+
_split_path_from_match(match, iterable=False)
7878

7979

8080
def split(path: str, iterable: bool = False) -> SplitPath:
8181
"""inverse of join() -- split the path string to it's component keys/indexes in order"""
8282
if path == '':
8383
return ()
8484
match = _match_validate(path)
85-
return _split_match(match, iterable)
85+
return _split_path_from_match(match, iterable)
8686

8787

88-
def _split_match(match: re.Match, iterable: bool) -> SplitPath:
88+
def _split_path_from_match(match: re.Match, iterable: bool) -> SplitPath:
89+
return _split_path_from_iterable(match.captures('part'), iterable)
90+
91+
92+
def _split_path_from_iterable(parts: Iterable[Key|_PathPart], iterable: bool) -> SplitPath:
8993
split_path: list[Key] = []
90-
for part in match.captures('part'):
91-
if part[0] == '[' and part[-1] == ']':
92-
index = part[1:-1]
93-
if ':' in index:
94+
for part in parts:
95+
if isinstance(part, _PathPart):
96+
path_part = part
97+
elif isinstance(part, int):
98+
path_part = _IndexPart(f'[{part}]')
99+
elif not isinstance(part, str):
100+
raise ValidationError('path parts must be str or int')
101+
elif part[0] == '[' and part[-1] == ']':
102+
if ':' in part:
94103
path_part = _RangeIterationPoint(part)
95-
elif index:
96-
path_part = int(index)
97-
else:
104+
elif part == '[]':
98105
path_part = _ListIterationPoint(part)
106+
else:
107+
path_part = _IndexPart(part)
99108
elif '*' in part:
100109
path_part = _StarIterationPoint(part)
101110
else:
102-
path_part = part
103-
if not iterable and isinstance(path_part, _IterationPoint):
111+
path_part = _KeyPart(part)
112+
if not iterable and path_part.iterable:
104113
raise InvalidIterationError(f'iterable {path_part.name} {path_part} not allowed here')
105114
split_path.append(path_part)
106115
return tuple(split_path)
@@ -117,54 +126,22 @@ def join(split_path: Iterable[Key]) -> str:
117126
```
118127
"""
119128
path = ''
120-
for i, part in enumerate(split_path):
121-
if isinstance(part, str):
122-
if path:
123-
path = f'{path}.{part}'
124-
else:
125-
path = part
126-
elif isinstance(part, int):
127-
if path:
128-
path = f'{path}[{part}]'
129-
else:
130-
path = f'[{part}]'
131-
elif isinstance(part, _IterationPoint):
132-
path = part.append_path(path)
133-
else:
134-
raise ValidationError(f'index {i} is invalid, must be str/int or iteration point, '
135-
f'got {type(part).__name__}')
129+
for i, part in enumerate(_split_path_from_iterable(split_path, True)):
130+
path = part.append_path(path)
136131
return path
137132

138133

139-
def _validate_key_collection_type(obj: Collection, key: Key) -> None:
140-
"""
141-
validate a collection object and key are valid and corresponding types
142-
raise a ValidationError if they are not
143-
"""
144-
if isinstance(key, _IterationPoint):
145-
raise TypeError('bug: iteration not supported here')
146-
if not isinstance(obj, _collection_types):
147-
raise TypeValidationError('object must be list/dict')
148-
if not isinstance(key, _key_types):
149-
raise TypeValidationError('path parts must all be str or int')
150-
if isinstance(key, int) and not isinstance(obj, list):
151-
raise TypeMismatchValidationError(f'int key requires list, got {type(obj).__name__}')
152-
if isinstance(key, str) and not isinstance(obj, dict):
153-
raise TypeMismatchValidationError(f'str key requires dict, got {type(obj).__name__}')
154-
155-
156-
def _contextual_validate_key_collection_type(at_path: list[Key],
157-
obj: Collection,
158-
key: Key) -> None:
134+
def _check_collection_for_path_part(at_path: list[_PathPart], path_part: _PathPart, obj: Collection) -> None:
159135
"""
160136
validate_key_collection_type(), except the path where the error occurred is prepended
161137
to the exception message
162138
"""
163-
at_path.append(key)
139+
if path_part.iterable:
140+
raise RuntimeError('bug: iteration not supported here')
164141
try:
165-
_validate_key_collection_type(obj, key)
142+
path_part.check(obj)
166143
except ValidationError as e:
167-
raise type(e)(f'{join(at_path)}: {e}') from None
144+
raise ValidationError(f'{join(at_path)}: {e}') from None
168145

169146

170147
def leaf(obj: Collection, path: str) -> CollectionKey:
@@ -174,16 +151,17 @@ def leaf(obj: Collection, path: str) -> CollectionKey:
174151

175152
def _leaf(obj: Collection, split_path: SplitPath) -> CollectionKey:
176153
"""leaf() on an already-split path"""
177-
at_path: list[Key] = []
178-
for key in split_path[:-1]:
179-
_contextual_validate_key_collection_type(at_path, obj, key)
154+
at_path: list[_PathPart] = []
155+
for path_part in split_path[:-1]:
156+
_check_collection_for_path_part(at_path, path_part, obj)
180157
try:
181-
obj = obj[key]
158+
obj = obj[path_part.key]
159+
at_path.append(path_part)
182160
except LookupError:
183-
raise PathLookupError(f'{join(at_path[:-1])}: could not find key/index {key!r}') from None
184-
leaf_key = split_path[-1]
185-
_contextual_validate_key_collection_type(at_path, obj, leaf_key)
186-
return obj, leaf_key
161+
raise PathLookupError(f'{join(at_path)}: could not find key/index {path_part.key!r}') from None
162+
leaf_path_part = split_path[-1]
163+
_check_collection_for_path_part(at_path, leaf_path_part, obj)
164+
return obj, leaf_path_part.key
187165

188166

189167
def get(obj: Collection, path: str, default: Any = NO_DEFAULT) -> Any:
@@ -196,7 +174,7 @@ def get(obj: Collection, path: str, default: Any = NO_DEFAULT) -> Any:
196174
return _get(obj, split(path), default)
197175

198176

199-
def _get(obj: Collection, split_path: str, default: Any = NO_DEFAULT) -> Any:
177+
def _get(obj: Collection, split_path: SplitPath, default: Any = NO_DEFAULT) -> Any:
200178
"""get() on an already-split path"""
201179
if not split_path:
202180
return obj
@@ -253,7 +231,7 @@ def _iterate(obj: Collection,
253231
iter_index = None
254232
iter_point = None
255233
for index, part in enumerate(split_path):
256-
if isinstance(part, _IterationPoint):
234+
if part.iterable:
257235
iter_index = index
258236
iter_point = part
259237
break

datapath/collection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __getitem__(self, key: Key) -> Any:
5353
return self.get(key)
5454
if isinstance(key, int) and isinstance(self.root, list):
5555
return self.root[key]
56-
raise TypeError('unsupported key type')
56+
raise ValueError('unsupported key type')
5757

5858
def iterate(self, path: str, default: Any = NO_DEFAULT, wrap: bool = NO_DEFAULT) -> Generator[tuple[str, Any], None, None]:
5959
"""identical to iterate() for the wrapped root object

datapath/folding.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
PathDict,
1515
RootPathDict,
1616
ValidationError,
17+
_IndexPart,
18+
_KeyPart,
19+
InvalidIterationError,
1720
)
1821

1922

@@ -93,22 +96,24 @@ def unfold_path_dict(paths: PathDict,
9396
continue
9497
did_any = True
9598
parent = join(split_path[:-1])
96-
key = split_path[-1]
97-
if isinstance(key, int):
99+
path_part = split_path[-1]
100+
if isinstance(path_part, _IndexPart):
98101
default_collection: PartialList = []
99-
elif isinstance(key, str):
102+
elif isinstance(path_part, _KeyPart):
100103
default_collection: Map = {}
104+
elif path_part.iterable:
105+
raise InvalidIterationError('iteration not supported here')
101106
else:
102107
raise TypeError('bug: unsupported path part type')
103108
collection: PartialCollection = path_dict.setdefault(parent, default_collection)
104109
value = path_dict.pop(path)
105-
if isinstance(key, int) and isinstance(collection, list):
106-
collection.append((key, value))
107-
elif isinstance(key, str) and isinstance(collection, dict):
108-
collection[key] = value
110+
if isinstance(default_collection, list) and isinstance(collection, list):
111+
collection.append((path_part.key, value))
112+
elif isinstance(default_collection, dict) and isinstance(collection, dict):
113+
collection[path_part.key] = value
109114
else:
110115
raise ValidationError(f'unsupported/inconsistent types: parent {parent!r} '
111-
f'has type {type(collection).__name__} key/index {key!r}')
116+
f'has type {type(collection).__name__} for key/index {path_part.key!r}')
112117
if not did_any:
113118
path_length -= 1
114119
if complete_intermediates:

0 commit comments

Comments
 (0)