nested-mapping 0.10.2__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.
- nested_mapping/__init__.py +2 -0
- nested_mapping/class_wrapper.py +53 -0
- nested_mapping/ipython.py +3 -0
- nested_mapping/nested_mapping.py +611 -0
- nested_mapping/nested_mapping_access.py +47 -0
- nested_mapping/tools/__init__.py +4 -0
- nested_mapping/tools/filter.py +34 -0
- nested_mapping/tools/map.py +166 -0
- nested_mapping/tools/match.py +128 -0
- nested_mapping/tools/reorder_key.py +9 -0
- nested_mapping/typing.py +41 -0
- nested_mapping/visitor.py +65 -0
- nested_mapping-0.10.2.dist-info/METADATA +43 -0
- nested_mapping-0.10.2.dist-info/RECORD +17 -0
- nested_mapping-0.10.2.dist-info/WHEEL +5 -0
- nested_mapping-0.10.2.dist-info/licenses/LICENSE +21 -0
- nested_mapping-0.10.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ClassWrapper:
|
|
5
|
+
__slots__ = ("_object", "_types", "_wrapper_class")
|
|
6
|
+
_object: Any
|
|
7
|
+
_types: Any
|
|
8
|
+
_wrapper_class: Any
|
|
9
|
+
|
|
10
|
+
def __init__(self, obj, *, types=None):
|
|
11
|
+
self._object = obj
|
|
12
|
+
self._types = type(obj) if types is None else types
|
|
13
|
+
self._wrapper_class = type(self)
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def object(self) -> Any:
|
|
17
|
+
return self._object
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
return str(self._object)
|
|
21
|
+
|
|
22
|
+
def __repr__(self) -> str:
|
|
23
|
+
return repr(self._object)
|
|
24
|
+
|
|
25
|
+
def __dir__(self) -> list[str]:
|
|
26
|
+
return dir(self._object)
|
|
27
|
+
|
|
28
|
+
def __len__(self) -> int:
|
|
29
|
+
return len(self._object)
|
|
30
|
+
|
|
31
|
+
def __bool__(self) -> bool:
|
|
32
|
+
return bool(self._object)
|
|
33
|
+
|
|
34
|
+
def __contains__(self, v) -> bool:
|
|
35
|
+
return v in self._object
|
|
36
|
+
|
|
37
|
+
def __eq__(self, other) -> bool:
|
|
38
|
+
if isinstance(other, ClassWrapper):
|
|
39
|
+
return self._object == other._object
|
|
40
|
+
|
|
41
|
+
return self._object == other
|
|
42
|
+
|
|
43
|
+
def _wrap(self, obj, **kwargs) -> Any:
|
|
44
|
+
if isinstance(obj, ClassWrapper):
|
|
45
|
+
return obj
|
|
46
|
+
|
|
47
|
+
if isinstance(obj, self._types):
|
|
48
|
+
return self._wrapper_class(obj, **kwargs)
|
|
49
|
+
|
|
50
|
+
return obj
|
|
51
|
+
|
|
52
|
+
def _wrap_(self, obj, **kwargs) -> Any:
|
|
53
|
+
return self._wrapper_class(obj, **kwargs)
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Any, Self
|
|
9
|
+
from collections.abc import Generator, Iterable
|
|
10
|
+
|
|
11
|
+
from .class_wrapper import ClassWrapper
|
|
12
|
+
from .ipython import repr_pretty
|
|
13
|
+
from .nested_mapping_access import NestedMappingAccess
|
|
14
|
+
from .typing import KeyLike
|
|
15
|
+
from .visitor import MakeNestedMappingVisitor, NestedMappingVisitor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NestedMapping(ClassWrapper):
|
|
19
|
+
"""Dictionary wrapper managing nested dictionaries.
|
|
20
|
+
|
|
21
|
+
The following functionality is implemented:
|
|
22
|
+
- Tuple keys are treated to access nested dictionaries ('key1', 'key2', 'key3')
|
|
23
|
+
- Optionally sep symbol may be set to automatically split string keys into tuple keys:
|
|
24
|
+
'key1.key2.key3' will be treated as a nested key if '.' is set for the sep symbol
|
|
25
|
+
- self._ may be used to access nested dictionaries via attributes: dw.key1.key2.key3
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__slots__ = ("_sep", "_parent", "_types", "_not_recursive_to_others")
|
|
29
|
+
_sep: str | None
|
|
30
|
+
_parent: Any
|
|
31
|
+
_not_recursive_to_others: bool
|
|
32
|
+
|
|
33
|
+
def __new__(cls, dic: MutableMapping = {}, *args, **kwargs):
|
|
34
|
+
if not isinstance(dic, (MutableMapping, NestedMapping)):
|
|
35
|
+
return dic
|
|
36
|
+
return ClassWrapper.__new__(cls)
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
dic: MutableMapping | None = None,
|
|
41
|
+
*,
|
|
42
|
+
sep: str | None = None,
|
|
43
|
+
parent: Any | None = None,
|
|
44
|
+
recursive_to_others: bool = False,
|
|
45
|
+
):
|
|
46
|
+
if dic is None:
|
|
47
|
+
dic = {}
|
|
48
|
+
if isinstance(dic, NestedMapping):
|
|
49
|
+
if sep is None:
|
|
50
|
+
sep = dic._sep
|
|
51
|
+
recursive_to_others = not dic._not_recursive_to_others
|
|
52
|
+
dic = dic._object
|
|
53
|
+
super().__init__(dic, types=type(dic))
|
|
54
|
+
|
|
55
|
+
self._sep = sep
|
|
56
|
+
self._not_recursive_to_others = not recursive_to_others
|
|
57
|
+
self._parent = parent
|
|
58
|
+
if parent:
|
|
59
|
+
if sep and sep != parent._sep:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Inconsistent separators: {sep} (self) and {parent._sep} (parent)"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self._sep = parent._sep
|
|
65
|
+
self._types = parent._types
|
|
66
|
+
self._not_recursive_to_others = parent._not_recursive_to_others
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
return f"NestedMapping({list(self.keys())})"
|
|
70
|
+
|
|
71
|
+
def __repr__(self):
|
|
72
|
+
return object.__repr__(self)
|
|
73
|
+
|
|
74
|
+
_repr_pretty_ = repr_pretty
|
|
75
|
+
|
|
76
|
+
def _ipython_key_completions_(self) -> list[str]:
|
|
77
|
+
return list(self.walkjoinedkeys(include_dicts=True))
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_flatdict(
|
|
81
|
+
cls,
|
|
82
|
+
dct: Mapping[KeyLike, Any] | Iterable[tuple[KeyLike, Any]],
|
|
83
|
+
*args,
|
|
84
|
+
**kwargs,
|
|
85
|
+
) -> Self:
|
|
86
|
+
"""Make a nested dictionary from a flat dictionary."""
|
|
87
|
+
ret = cls({}, *args, **kwargs)
|
|
88
|
+
iterable = dct.items() if isinstance(dct, Mapping) else dct
|
|
89
|
+
for key, value in iterable:
|
|
90
|
+
ret[key] = value
|
|
91
|
+
return ret
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def _(self):
|
|
95
|
+
return NestedMappingAccess(self)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def parent(self) -> Self | None:
|
|
99
|
+
return self._parent
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def parent_key(self):
|
|
103
|
+
if not self._parent:
|
|
104
|
+
return None
|
|
105
|
+
for key, value in self._parent.items():
|
|
106
|
+
if isinstance(value, NestedMapping) and value.object is self.object:
|
|
107
|
+
return key
|
|
108
|
+
|
|
109
|
+
raise RuntimeError("Parent key not identified")
|
|
110
|
+
|
|
111
|
+
def get_parent(self, level: int = 1) -> Self | None:
|
|
112
|
+
if level == 0:
|
|
113
|
+
return self
|
|
114
|
+
elif level < 0:
|
|
115
|
+
raise ValueError("get_parent: level should be >=0")
|
|
116
|
+
|
|
117
|
+
current = self
|
|
118
|
+
for _ in range(level):
|
|
119
|
+
newcurrent = current.parent
|
|
120
|
+
if newcurrent is None:
|
|
121
|
+
return current
|
|
122
|
+
current = newcurrent
|
|
123
|
+
|
|
124
|
+
return current
|
|
125
|
+
|
|
126
|
+
def create_child(self, key, *args, type=None, **kwargs) -> Self:
|
|
127
|
+
try:
|
|
128
|
+
ret = self(key)
|
|
129
|
+
except KeyError:
|
|
130
|
+
if type is None:
|
|
131
|
+
type = self._types
|
|
132
|
+
self[key] = (ret := type(*args, **kwargs))
|
|
133
|
+
return self._wrap(ret, parent=self)
|
|
134
|
+
|
|
135
|
+
if not isinstance(ret, self._wrapper_class):
|
|
136
|
+
raise KeyError(f"Child {key!s} is not NestedMapping")
|
|
137
|
+
|
|
138
|
+
return ret
|
|
139
|
+
|
|
140
|
+
def get_dict(self, key, *, unwrap: bool = False) -> Self:
|
|
141
|
+
if key == ():
|
|
142
|
+
return self
|
|
143
|
+
head, rest = self.splitkey(key)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
sub = self._object[head]
|
|
147
|
+
except KeyError as e:
|
|
148
|
+
raise KeyError(key) from e
|
|
149
|
+
|
|
150
|
+
if rest:
|
|
151
|
+
sub = self._wrap(sub, parent=self)
|
|
152
|
+
if self._not_recursive_to_others and not isinstance(sub, NestedMapping):
|
|
153
|
+
raise TypeError(
|
|
154
|
+
f"Expect nested dictionary as value for {key}, got {type(sub).__name__}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
return sub.get_dict(rest, unwrap=unwrap)
|
|
159
|
+
except KeyError as e:
|
|
160
|
+
raise KeyError(key) from e
|
|
161
|
+
|
|
162
|
+
if not isinstance(sub, (ClassWrapper, self._types)):
|
|
163
|
+
raise TypeError(
|
|
164
|
+
f"Invalid value type {type(sub)} for key ({key}). Expect non-mapping. Perhaps, one should use [{key}] or .get_any({key})..."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if unwrap:
|
|
168
|
+
return sub
|
|
169
|
+
|
|
170
|
+
return self._wrap_(sub, parent=self)
|
|
171
|
+
|
|
172
|
+
__call__ = get_dict
|
|
173
|
+
|
|
174
|
+
def keys(self):
|
|
175
|
+
return self._object.keys()
|
|
176
|
+
|
|
177
|
+
def iterkey(self, key):
|
|
178
|
+
if isinstance(key, str):
|
|
179
|
+
if self._sep:
|
|
180
|
+
yield from key.split(self._sep)
|
|
181
|
+
else:
|
|
182
|
+
yield key
|
|
183
|
+
elif isinstance(key, Sequence):
|
|
184
|
+
for sk in key:
|
|
185
|
+
yield from self.iterkey(sk)
|
|
186
|
+
else:
|
|
187
|
+
yield key
|
|
188
|
+
|
|
189
|
+
def splitkey(self, key) -> Any:
|
|
190
|
+
it = self.iterkey(key)
|
|
191
|
+
try:
|
|
192
|
+
return next(it), tuple(it)
|
|
193
|
+
except StopIteration:
|
|
194
|
+
return None, None
|
|
195
|
+
|
|
196
|
+
def joinkey(self, key: KeyLike) -> str:
|
|
197
|
+
if isinstance(key, str):
|
|
198
|
+
return key
|
|
199
|
+
elif isinstance(key, Sequence):
|
|
200
|
+
if self._sep is None:
|
|
201
|
+
raise ValueError("sep is None")
|
|
202
|
+
return self._sep.join(key)
|
|
203
|
+
|
|
204
|
+
raise ValueError(f"Invalid key: {key}")
|
|
205
|
+
|
|
206
|
+
def get(self, key, default=None):
|
|
207
|
+
if key == ():
|
|
208
|
+
raise TypeError("May not return self")
|
|
209
|
+
|
|
210
|
+
head, rest = self.splitkey(key)
|
|
211
|
+
|
|
212
|
+
if head not in self._object:
|
|
213
|
+
if rest:
|
|
214
|
+
raise KeyError(key)
|
|
215
|
+
else:
|
|
216
|
+
return default
|
|
217
|
+
|
|
218
|
+
sub = self._object.get(head)
|
|
219
|
+
if rest:
|
|
220
|
+
sub = self._wrap(sub, parent=self)
|
|
221
|
+
if self._not_recursive_to_others and not isinstance(sub, NestedMapping):
|
|
222
|
+
raise TypeError(
|
|
223
|
+
f"Expect non-mapping as value for {key}, got {type(sub).__name__}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return sub.get(rest, default)
|
|
227
|
+
|
|
228
|
+
if isinstance(sub, (ClassWrapper, self._types)):
|
|
229
|
+
raise TypeError(
|
|
230
|
+
f"Invalid value type {type(sub)} for key [{key}]. Expect non-mapping. Perhaps, one should use ({key}) or .get_any({key})..."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return sub
|
|
234
|
+
|
|
235
|
+
def get_value(self, key) -> Any:
|
|
236
|
+
if key == ():
|
|
237
|
+
raise TypeError("May not return self")
|
|
238
|
+
|
|
239
|
+
head, rest = self.splitkey(key)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
sub = self._object.__getitem__(head)
|
|
243
|
+
except KeyError as e:
|
|
244
|
+
raise KeyError(f"{key}: {head}") from e
|
|
245
|
+
|
|
246
|
+
if rest:
|
|
247
|
+
sub = self._wrap(sub, parent=self)
|
|
248
|
+
if self._not_recursive_to_others and not isinstance(sub, NestedMapping):
|
|
249
|
+
raise TypeError(
|
|
250
|
+
f"Expect non-mapping as value for {key}, got {type(sub).__name__}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
return sub.get_value(rest)
|
|
255
|
+
except KeyError as e:
|
|
256
|
+
failed_rest = getattr(e, "rest", rest)
|
|
257
|
+
error = KeyError(f"{key}: {failed_rest}")
|
|
258
|
+
with suppress(RuntimeError):
|
|
259
|
+
error.rest = ( # pyright: ignore [reportAttributeAccessIssue]
|
|
260
|
+
failed_rest
|
|
261
|
+
)
|
|
262
|
+
raise error from e
|
|
263
|
+
|
|
264
|
+
if isinstance(sub, (ClassWrapper, self._types)):
|
|
265
|
+
raise TypeError(
|
|
266
|
+
f"Invalid value type {type(sub)} for key {key}. Expect non-mapping."
|
|
267
|
+
)
|
|
268
|
+
return sub
|
|
269
|
+
|
|
270
|
+
def get_any(self, key, *, unwrap: bool = False) -> Any:
|
|
271
|
+
if key == ():
|
|
272
|
+
return self
|
|
273
|
+
head, rest = self.splitkey(key)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
sub = self._object.__getitem__(head)
|
|
277
|
+
except KeyError as e:
|
|
278
|
+
raise KeyError(f"No nested key '{key}'") from e
|
|
279
|
+
|
|
280
|
+
if not rest:
|
|
281
|
+
if unwrap:
|
|
282
|
+
return sub
|
|
283
|
+
sub = self._wrap(sub, parent=self)
|
|
284
|
+
return sub
|
|
285
|
+
|
|
286
|
+
sub = self._wrap(sub, parent=self)
|
|
287
|
+
if self._not_recursive_to_others and not isinstance(sub, NestedMapping):
|
|
288
|
+
raise TypeError(f"Nested value for {key} has wrong type")
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
return sub.get_any(rest, unwrap=unwrap) if unwrap else sub[rest]
|
|
292
|
+
except KeyError as e:
|
|
293
|
+
raise KeyError(key) from e
|
|
294
|
+
|
|
295
|
+
__getitem__ = get_any
|
|
296
|
+
|
|
297
|
+
def pop(self, key, *, delete_parents: bool = False):
|
|
298
|
+
if key == ():
|
|
299
|
+
raise ValueError("May not delete itself")
|
|
300
|
+
key, rest = self.splitkey(key)
|
|
301
|
+
|
|
302
|
+
sub = self._wrap(self._object.__getitem__(key), parent=self)
|
|
303
|
+
if not rest:
|
|
304
|
+
return self._object.pop(key)
|
|
305
|
+
|
|
306
|
+
if self._not_recursive_to_others and not isinstance(sub, NestedMapping):
|
|
307
|
+
raise TypeError(f"Nested value for {key} has wrong type")
|
|
308
|
+
|
|
309
|
+
ret = sub.pop(rest, delete_parents=delete_parents)
|
|
310
|
+
if delete_parents and not sub:
|
|
311
|
+
del self._object[key]
|
|
312
|
+
return ret
|
|
313
|
+
|
|
314
|
+
def delete_with_parents(self, key):
|
|
315
|
+
self.pop(key, delete_parents=True)
|
|
316
|
+
|
|
317
|
+
def __delitem__(self, key):
|
|
318
|
+
if key == ():
|
|
319
|
+
raise ValueError("May not delete itself")
|
|
320
|
+
key, rest = self.splitkey(key)
|
|
321
|
+
|
|
322
|
+
if not rest:
|
|
323
|
+
del self._object[key]
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
if key not in self:
|
|
327
|
+
raise KeyError(key)
|
|
328
|
+
|
|
329
|
+
sub = self._wrap(self._object.__getitem__(key), parent=self)
|
|
330
|
+
if isinstance(sub, NestedMapping):
|
|
331
|
+
sub.__delitem__(rest)
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
if self._not_recursive_to_others:
|
|
335
|
+
raise TypeError(f"Nested value for {key} (sub: {rest}) has wrong type")
|
|
336
|
+
|
|
337
|
+
sub.__delitem__(rest)
|
|
338
|
+
|
|
339
|
+
def setdefault(self, key, value) -> Any:
|
|
340
|
+
key, rest = self.splitkey(key)
|
|
341
|
+
|
|
342
|
+
if not rest:
|
|
343
|
+
ret = self._object.setdefault(key, value)
|
|
344
|
+
return self._wrap(ret, parent=self)
|
|
345
|
+
|
|
346
|
+
if key in self:
|
|
347
|
+
sub = self._wrap(self._object.get(key), parent=self)
|
|
348
|
+
else:
|
|
349
|
+
sub = self._object[key] = self._types()
|
|
350
|
+
sub = self._wrap(sub, parent=self)
|
|
351
|
+
# # cfg._set_parent( self )
|
|
352
|
+
|
|
353
|
+
if self._not_recursive_to_others and not isinstance(sub, NestedMapping):
|
|
354
|
+
raise TypeError(f"Nested value for {key} has wrong type")
|
|
355
|
+
|
|
356
|
+
return sub.setdefault(rest, value)
|
|
357
|
+
|
|
358
|
+
def _set(self, key, value):
|
|
359
|
+
key, rest = self.splitkey(key)
|
|
360
|
+
|
|
361
|
+
if not rest:
|
|
362
|
+
self._object[key] = value
|
|
363
|
+
return value
|
|
364
|
+
|
|
365
|
+
if key in self:
|
|
366
|
+
sub = self._wrap(self._object.get(key), parent=self)
|
|
367
|
+
else:
|
|
368
|
+
sub = self._object[key] = self._types()
|
|
369
|
+
sub = self._wrap(sub, parent=self)
|
|
370
|
+
# # cfg._set_parent( self )
|
|
371
|
+
|
|
372
|
+
if isinstance(sub, NestedMapping):
|
|
373
|
+
return sub._set(rest, value)
|
|
374
|
+
|
|
375
|
+
if self._not_recursive_to_others:
|
|
376
|
+
raise TypeError(f"Nested value for {key} (sub: {rest}) has wrong type")
|
|
377
|
+
|
|
378
|
+
return sub.__setitem__(rest, value)
|
|
379
|
+
|
|
380
|
+
def set(self, key, value):
|
|
381
|
+
return self._set(key, value)
|
|
382
|
+
|
|
383
|
+
__setitem__ = set
|
|
384
|
+
|
|
385
|
+
def __contains__(self, key):
|
|
386
|
+
if key == ():
|
|
387
|
+
return True
|
|
388
|
+
key, rest = self.splitkey(key)
|
|
389
|
+
|
|
390
|
+
if key not in self._object:
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
if rest:
|
|
394
|
+
sub = self._wrap(self._object.get(key), parent=self)
|
|
395
|
+
|
|
396
|
+
if self._not_recursive_to_others and not isinstance(sub, NestedMapping):
|
|
397
|
+
raise TypeError(f"Nested value for {key} is not a nested dictionary")
|
|
398
|
+
|
|
399
|
+
return rest in sub
|
|
400
|
+
|
|
401
|
+
return True
|
|
402
|
+
|
|
403
|
+
def items(self):
|
|
404
|
+
for k, v in self._object.items():
|
|
405
|
+
yield k, self._wrap(v, parent=self)
|
|
406
|
+
|
|
407
|
+
def values(self):
|
|
408
|
+
for v in self._object.values():
|
|
409
|
+
yield self._wrap(v, parent=self)
|
|
410
|
+
|
|
411
|
+
def copy(self) -> Self:
|
|
412
|
+
cls = type(self)
|
|
413
|
+
return cls(
|
|
414
|
+
self.object.copy(),
|
|
415
|
+
parent=self._parent,
|
|
416
|
+
sep=self._sep,
|
|
417
|
+
recursive_to_others=not self._not_recursive_to_others,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def deepcopy(self) -> Self:
|
|
421
|
+
cls = type(self)
|
|
422
|
+
new = cls(
|
|
423
|
+
self._types(),
|
|
424
|
+
parent=self._parent,
|
|
425
|
+
sep=self._sep,
|
|
426
|
+
recursive_to_others=not self._not_recursive_to_others,
|
|
427
|
+
)
|
|
428
|
+
for k, v in self.items():
|
|
429
|
+
k = (k,)
|
|
430
|
+
if isinstance(v, self._wrapper_class):
|
|
431
|
+
new[k] = v.deepcopy()._object
|
|
432
|
+
else:
|
|
433
|
+
new[k] = v
|
|
434
|
+
|
|
435
|
+
new._sep = self._sep
|
|
436
|
+
|
|
437
|
+
return new
|
|
438
|
+
|
|
439
|
+
def flatten(self, sep: str | None = None) -> dict[str, Any]:
|
|
440
|
+
return dict(self.walkjoineditems(sep=sep))
|
|
441
|
+
|
|
442
|
+
def walkitems(
|
|
443
|
+
self,
|
|
444
|
+
startfromkey=(),
|
|
445
|
+
*,
|
|
446
|
+
include_dicts: bool = False,
|
|
447
|
+
appendstartkey: bool = False,
|
|
448
|
+
maxdepth: int | None = None,
|
|
449
|
+
):
|
|
450
|
+
v0 = self.get_any(startfromkey)
|
|
451
|
+
k0 = tuple(self.iterkey(startfromkey))
|
|
452
|
+
|
|
453
|
+
if maxdepth is None:
|
|
454
|
+
nextdepth = None
|
|
455
|
+
else:
|
|
456
|
+
nextdepth = max(maxdepth - len(k0) - 1, 0)
|
|
457
|
+
|
|
458
|
+
if maxdepth == 0 or not isinstance(v0, self._wrapper_class):
|
|
459
|
+
if appendstartkey:
|
|
460
|
+
yield k0, v0
|
|
461
|
+
else:
|
|
462
|
+
yield (), v0
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
if not appendstartkey:
|
|
466
|
+
k0 = ()
|
|
467
|
+
|
|
468
|
+
for k, v in v0.items():
|
|
469
|
+
k = k0 + (k,)
|
|
470
|
+
if isinstance(v, self._wrapper_class):
|
|
471
|
+
if include_dicts:
|
|
472
|
+
yield k, v
|
|
473
|
+
for k1, v1 in v.walkitems(
|
|
474
|
+
include_dicts=include_dicts, maxdepth=nextdepth
|
|
475
|
+
):
|
|
476
|
+
yield k + k1, v1
|
|
477
|
+
elif not self._not_recursive_to_others and isinstance(v, Mapping):
|
|
478
|
+
if include_dicts:
|
|
479
|
+
yield k, v
|
|
480
|
+
for k1, v1 in v.items():
|
|
481
|
+
if isinstance(k1, tuple):
|
|
482
|
+
yield k + k1, v1
|
|
483
|
+
else:
|
|
484
|
+
yield k + (k1,), v1
|
|
485
|
+
else:
|
|
486
|
+
yield k, v
|
|
487
|
+
|
|
488
|
+
def walkdicts(self, *, yieldself=False, ignorekeys: Sequence = ()):
|
|
489
|
+
for k, v in self.items():
|
|
490
|
+
if k in ignorekeys:
|
|
491
|
+
continue
|
|
492
|
+
k = (k,)
|
|
493
|
+
if isinstance(v, self._wrapper_class):
|
|
494
|
+
yieldself = False
|
|
495
|
+
for k1, v1 in v.walkdicts(yieldself=True, ignorekeys=ignorekeys):
|
|
496
|
+
yield k + k1, v1
|
|
497
|
+
if yieldself:
|
|
498
|
+
yield (), self
|
|
499
|
+
|
|
500
|
+
def keysmap(self) -> NestedMapping:
|
|
501
|
+
"""Return a nested dictionary instance with similar structure, but
|
|
502
|
+
dictionaries are replaced with tuples of their keys."""
|
|
503
|
+
return NestedMapping.from_flatdict(
|
|
504
|
+
{k: tuple(dct.keys()) for k, dct in self.walkdicts()}
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def unique_key_parts(self) -> set[str]:
|
|
508
|
+
"""Return a set with all the unique set parts."""
|
|
509
|
+
ret = set()
|
|
510
|
+
for key in self.walkkeys():
|
|
511
|
+
ret.update(key)
|
|
512
|
+
return ret
|
|
513
|
+
|
|
514
|
+
def walkkeys(self, *args, **kwargs):
|
|
515
|
+
for k, _ in self.walkitems(*args, **kwargs):
|
|
516
|
+
yield k
|
|
517
|
+
|
|
518
|
+
def walkjoinedkeys(
|
|
519
|
+
self, *args, sep: str | None = None, **kwargs
|
|
520
|
+
) -> Generator[str, None, None]:
|
|
521
|
+
if sep is None:
|
|
522
|
+
sep = self._sep
|
|
523
|
+
if sep is None:
|
|
524
|
+
sep = "."
|
|
525
|
+
for k, _ in self.walkitems(*args, **kwargs):
|
|
526
|
+
yield sep.join(k)
|
|
527
|
+
|
|
528
|
+
def walkjoineditems(
|
|
529
|
+
self, *args, sep: str | None = None, **kwargs
|
|
530
|
+
) -> Generator[tuple[str, Any], None, None]:
|
|
531
|
+
if sep is None:
|
|
532
|
+
sep = self._sep
|
|
533
|
+
if sep is None:
|
|
534
|
+
sep = "."
|
|
535
|
+
for k, v in self.walkitems(*args, **kwargs):
|
|
536
|
+
yield sep.join(k), v
|
|
537
|
+
|
|
538
|
+
def walkvalues(self, *args, **kwargs):
|
|
539
|
+
for _, v in self.walkitems(*args, **kwargs):
|
|
540
|
+
yield v
|
|
541
|
+
|
|
542
|
+
def len_recursive(self, *args, **kwargs) -> int:
|
|
543
|
+
return sum(1 for _ in self.walkitems(*args, **kwargs))
|
|
544
|
+
|
|
545
|
+
def depth(self):
|
|
546
|
+
return max(len(k) for k, _ in self.walkitems())
|
|
547
|
+
|
|
548
|
+
def visit(self, visitor, parentkey=()) -> NestedMappingVisitor:
|
|
549
|
+
visitor = MakeNestedMappingVisitor(visitor)
|
|
550
|
+
|
|
551
|
+
if not parentkey:
|
|
552
|
+
visitor.start(self)
|
|
553
|
+
|
|
554
|
+
visitor.enterdict(parentkey, self)
|
|
555
|
+
for k, v in self.items():
|
|
556
|
+
key = parentkey + (k,)
|
|
557
|
+
if isinstance(v, self._wrapper_class):
|
|
558
|
+
v.visit(visitor, parentkey=key)
|
|
559
|
+
else:
|
|
560
|
+
visitor.visit(key, v)
|
|
561
|
+
|
|
562
|
+
visitor.exitdict(parentkey, self)
|
|
563
|
+
|
|
564
|
+
if not parentkey:
|
|
565
|
+
visitor.stop(self)
|
|
566
|
+
|
|
567
|
+
return visitor
|
|
568
|
+
|
|
569
|
+
def update(self, other) -> Self:
|
|
570
|
+
other = self._wrap(other)
|
|
571
|
+
for k, v in other.walkitems():
|
|
572
|
+
self[k] = v
|
|
573
|
+
return self
|
|
574
|
+
|
|
575
|
+
__ior__ = update
|
|
576
|
+
|
|
577
|
+
def update_missing(self, other) -> Self:
|
|
578
|
+
other = self._wrap(other)
|
|
579
|
+
for k, v in other.walkitems():
|
|
580
|
+
try:
|
|
581
|
+
key_already_present = k in self
|
|
582
|
+
except TypeError as e:
|
|
583
|
+
raise TypeError(f"Value for part({k}) is non nestable") from e
|
|
584
|
+
else:
|
|
585
|
+
if key_already_present:
|
|
586
|
+
raise TypeError(f"Key {k} already present")
|
|
587
|
+
self[k] = v
|
|
588
|
+
return self
|
|
589
|
+
|
|
590
|
+
__ixor__ = update_missing
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def walkitems(obj: NestedMapping | Any, *args, **kwargs):
|
|
594
|
+
if isinstance(obj, NestedMapping):
|
|
595
|
+
yield from obj.walkitems(*args, **kwargs)
|
|
596
|
+
else:
|
|
597
|
+
yield (), obj
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def walkvalues(obj: NestedMapping | Any, *args, **kwargs):
|
|
601
|
+
if isinstance(obj, NestedMapping):
|
|
602
|
+
yield from obj.walkvalues(*args, **kwargs)
|
|
603
|
+
else:
|
|
604
|
+
yield obj
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def walkkeys(obj: NestedMapping | Any, *args, **kwargs):
|
|
608
|
+
if isinstance(obj, NestedMapping):
|
|
609
|
+
yield from obj.walkkeys(*args, **kwargs)
|
|
610
|
+
else:
|
|
611
|
+
yield ()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .ipython import repr_pretty
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .nested_mapping import NestedMapping
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NestedMappingAccess:
|
|
12
|
+
"""NestedMapping wrapper. Enables attribute based access to nested dictionaries"""
|
|
13
|
+
|
|
14
|
+
__slots__ = ("_",)
|
|
15
|
+
_: NestedMapping
|
|
16
|
+
|
|
17
|
+
def __init__(self, dct: NestedMapping):
|
|
18
|
+
object.__setattr__(self, "_", dct)
|
|
19
|
+
|
|
20
|
+
def __str__(self):
|
|
21
|
+
return str(self._)
|
|
22
|
+
|
|
23
|
+
_repr_pretty_ = repr_pretty
|
|
24
|
+
|
|
25
|
+
def __call__(self, key):
|
|
26
|
+
return self._.create_child(key)._
|
|
27
|
+
|
|
28
|
+
def __getattr__(self, key):
|
|
29
|
+
if key.startswith("__") and key.endswith("__"):
|
|
30
|
+
# needed for proper operation of __dir__() and ipython completion
|
|
31
|
+
return object.__getattr__(self, key)
|
|
32
|
+
|
|
33
|
+
ret = self._[key]
|
|
34
|
+
|
|
35
|
+
if isinstance(ret, self._._wrapper_class):
|
|
36
|
+
return ret._
|
|
37
|
+
|
|
38
|
+
return ret
|
|
39
|
+
|
|
40
|
+
def __setattr__(self, key, value):
|
|
41
|
+
self._[key] = value
|
|
42
|
+
|
|
43
|
+
def __delattr__(self, key):
|
|
44
|
+
del self._[key]
|
|
45
|
+
|
|
46
|
+
def __dir__(self):
|
|
47
|
+
return list(self._.keys())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ..nested_mapping import NestedMapping
|
|
6
|
+
|
|
7
|
+
from ..typing import Key
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable, Sequence
|
|
11
|
+
from typing import Any, Generator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def filter_items(
|
|
15
|
+
items: Iterable[tuple[Key, Any]], exclude: Sequence[Sequence[str] | str]
|
|
16
|
+
) -> Generator[tuple[Key, Any]]:
|
|
17
|
+
_exclude = [{key} if isinstance(key, str) else set(key) for key in exclude]
|
|
18
|
+
for key, value in items:
|
|
19
|
+
if any(mask.issubset(key) for mask in _exclude):
|
|
20
|
+
continue
|
|
21
|
+
|
|
22
|
+
yield key, value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def mkfilter_items(
|
|
26
|
+
mkdict: NestedMapping, exclude: Sequence[Sequence[str] | str]
|
|
27
|
+
) -> NestedMapping:
|
|
28
|
+
ret = mkdict.__class__({}, sep=mkdict._sep)
|
|
29
|
+
|
|
30
|
+
for key, value in filter_items(mkdict.walkitems(), exclude):
|
|
31
|
+
ret[key] = value
|
|
32
|
+
|
|
33
|
+
return ret
|
|
34
|
+
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ..nested_mapping import NestedMapping
|
|
6
|
+
from ..typing import setkey
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..typing import Key, KeyLike
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mkmap(
|
|
16
|
+
fcn: Callable,
|
|
17
|
+
arg0: NestedMapping,
|
|
18
|
+
*args: NestedMapping,
|
|
19
|
+
sep: int | str | bool | None = None,
|
|
20
|
+
) -> NestedMapping:
|
|
21
|
+
match sep:
|
|
22
|
+
case False:
|
|
23
|
+
# False matches case 0, but 0 does not match case False
|
|
24
|
+
sep = None
|
|
25
|
+
case 0 | True:
|
|
26
|
+
sep = arg0._sep
|
|
27
|
+
case int():
|
|
28
|
+
if sep > 0:
|
|
29
|
+
sep = args[sep - 1]._sep
|
|
30
|
+
elif sep >= -len(args):
|
|
31
|
+
sep = args[sep]._sep
|
|
32
|
+
else:
|
|
33
|
+
raise IndexError()
|
|
34
|
+
case str() | None:
|
|
35
|
+
pass
|
|
36
|
+
case _:
|
|
37
|
+
raise TypeError(f"Invalid sep: {sep}")
|
|
38
|
+
ret = NestedMapping({}, sep=sep)
|
|
39
|
+
for key, value0 in arg0.walkitems():
|
|
40
|
+
values = (arg[key] for arg in args)
|
|
41
|
+
ret[key] = fcn(value0, *values)
|
|
42
|
+
return ret
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def remap_items(
|
|
46
|
+
source: NestedMapping,
|
|
47
|
+
target: NestedMapping | None = None,
|
|
48
|
+
*,
|
|
49
|
+
rename_indices: Mapping[str, Sequence[str]] | None = None,
|
|
50
|
+
reorder_indices: (
|
|
51
|
+
tuple[int]
|
|
52
|
+
| tuple[tuple[str, ...], tuple[str, ...]]
|
|
53
|
+
| list[int]
|
|
54
|
+
| list[list[str]]
|
|
55
|
+
| Mapping[str, list[str] | tuple[str]]
|
|
56
|
+
| None
|
|
57
|
+
) = None,
|
|
58
|
+
skip_indices_source: Sequence[KeyLike | set] | None = None,
|
|
59
|
+
skip_indices_target: Sequence[KeyLike | set] | None = None,
|
|
60
|
+
fcn: Callable[[Any], Any] | None = None,
|
|
61
|
+
verbose: bool = False,
|
|
62
|
+
) -> NestedMapping:
|
|
63
|
+
from itertools import product
|
|
64
|
+
|
|
65
|
+
if target is None:
|
|
66
|
+
target = NestedMapping()
|
|
67
|
+
|
|
68
|
+
skip_source = _make_skip_fcn(skip_indices_source)
|
|
69
|
+
skip_target = _make_skip_fcn(skip_indices_target)
|
|
70
|
+
reorder = make_reorder_function(reorder_indices)
|
|
71
|
+
|
|
72
|
+
if fcn is None:
|
|
73
|
+
fcn = lambda o: o
|
|
74
|
+
has_fcn = False
|
|
75
|
+
else:
|
|
76
|
+
has_fcn = True
|
|
77
|
+
|
|
78
|
+
if rename_indices is not None:
|
|
79
|
+
for key, value in source.walkitems():
|
|
80
|
+
if skip_source(key):
|
|
81
|
+
continue
|
|
82
|
+
for newkey in product(
|
|
83
|
+
*tuple(rename_indices.get(skey, (skey,)) for skey in key)
|
|
84
|
+
):
|
|
85
|
+
newkey_ordered = reorder(newkey)
|
|
86
|
+
if skip_target(newkey_ordered):
|
|
87
|
+
if verbose:
|
|
88
|
+
print(
|
|
89
|
+
f"remap: skip {'.'.join(key)} → {'.'.join(newkey_ordered)}"
|
|
90
|
+
)
|
|
91
|
+
continue
|
|
92
|
+
if verbose:
|
|
93
|
+
print(
|
|
94
|
+
f"remap {'(fcn) ' if has_fcn else ''}{'.'.join(key)} → {'.'.join(newkey_ordered)}"
|
|
95
|
+
)
|
|
96
|
+
target[newkey_ordered] = fcn(value)
|
|
97
|
+
else:
|
|
98
|
+
for key, value in source.walkitems():
|
|
99
|
+
newkey_ordered = reorder(key)
|
|
100
|
+
if skip_target(newkey_ordered):
|
|
101
|
+
if verbose:
|
|
102
|
+
print(f"remap: skip {'.'.join(key)} → {'.'.join(newkey_ordered)}")
|
|
103
|
+
continue
|
|
104
|
+
if verbose:
|
|
105
|
+
print(
|
|
106
|
+
f"remap {'(fcn) ' if has_fcn else ''}{'.'.join(key)} → {'.'.join(newkey_ordered)}"
|
|
107
|
+
)
|
|
108
|
+
target[newkey_ordered] = fcn(value)
|
|
109
|
+
|
|
110
|
+
return target
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _make_skip_fcn(
|
|
114
|
+
skip_indices: Sequence[KeyLike | set] | None,
|
|
115
|
+
) -> Callable[[Key], bool]:
|
|
116
|
+
if skip_indices is not None:
|
|
117
|
+
skip_sets: tuple[set[str], ...] = tuple(
|
|
118
|
+
sq if isinstance(sq, set) else setkey(sq) for sq in skip_indices
|
|
119
|
+
)
|
|
120
|
+
return lambda key: any(ss.issubset(key) for ss in skip_sets)
|
|
121
|
+
|
|
122
|
+
return lambda _: False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def make_reorder_function(
|
|
126
|
+
reorder_indices: (
|
|
127
|
+
tuple[int]
|
|
128
|
+
| tuple[tuple[str, ...], tuple[str, ...]]
|
|
129
|
+
| Sequence[int]
|
|
130
|
+
| Sequence[Sequence[str]]
|
|
131
|
+
| Mapping[str, list[str] | tuple[str]]
|
|
132
|
+
| None
|
|
133
|
+
),
|
|
134
|
+
*,
|
|
135
|
+
allow_skip_items: bool = False,
|
|
136
|
+
) -> Callable:
|
|
137
|
+
match reorder_indices:
|
|
138
|
+
case [int(), *_]:
|
|
139
|
+
index_order = reorder_indices
|
|
140
|
+
len_from = len(index_order)
|
|
141
|
+
case [[str(), *_] as order_from, [str(), *_] as order_to] | {
|
|
142
|
+
"from": [str(), *_] as order_from,
|
|
143
|
+
"to": [str(), *_] as order_to,
|
|
144
|
+
}:
|
|
145
|
+
len_from = len(order_from)
|
|
146
|
+
try:
|
|
147
|
+
index_order = tuple(order_from.index(item) for item in order_to)
|
|
148
|
+
except ValueError:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Inconsistent order definitions {order_from} and {order_to}"
|
|
151
|
+
)
|
|
152
|
+
case None:
|
|
153
|
+
len_from = None
|
|
154
|
+
allow_skip_items = True
|
|
155
|
+
return lambda key: key
|
|
156
|
+
case _:
|
|
157
|
+
raise ValueError(f"Invalid order specification: {reorder_indices}")
|
|
158
|
+
|
|
159
|
+
def reorder_indices(key: Sequence):
|
|
160
|
+
if not allow_skip_items and len(key) != len_from:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"inconsistent index length: {len(index_order)} vs required {len(key)}"
|
|
163
|
+
)
|
|
164
|
+
return key.__class__(key[idx] for idx in index_order)
|
|
165
|
+
|
|
166
|
+
return reorder_indices
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from ordered_set import OrderedSet
|
|
7
|
+
|
|
8
|
+
from ..typing import Key, KeyLike, TupleKey, properkey, setkey
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def match_keys(
|
|
15
|
+
keys_left_seq: Sequence[Sequence[KeyLike]],
|
|
16
|
+
keys_right: Iterable[KeyLike],
|
|
17
|
+
fcn: Callable[[int, TupleKey, TupleKey], Any],
|
|
18
|
+
*,
|
|
19
|
+
fcn_outer_before: Callable[[TupleKey], Any] | None = None,
|
|
20
|
+
fcn_outer_after: Callable[[TupleKey], Any] | None = None,
|
|
21
|
+
fcn_skip: Callable[[int, TupleKey, TupleKey], Any] | None = None,
|
|
22
|
+
left_in_right: bool = True,
|
|
23
|
+
right_in_left: bool = True,
|
|
24
|
+
require_all_right_keys_processed: bool = True,
|
|
25
|
+
require_all_left_keys_processed: bool = True,
|
|
26
|
+
skippable_left_keys_should_contain: Sequence[KeyLike] | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
if isinstance(keys_right, str):
|
|
29
|
+
raise RuntimeError("`keys_right` should be iterable, but not string")
|
|
30
|
+
if not keys_left_seq:
|
|
31
|
+
raise RuntimeError("Left sequence is empty")
|
|
32
|
+
|
|
33
|
+
if left_in_right and right_in_left:
|
|
34
|
+
|
|
35
|
+
def keys_consistent(left: set, right: set) -> bool:
|
|
36
|
+
return left.issubset(right) or right.issubset(left)
|
|
37
|
+
|
|
38
|
+
elif left_in_right:
|
|
39
|
+
|
|
40
|
+
def keys_consistent(left: set, right: set) -> bool:
|
|
41
|
+
return left.issubset(right)
|
|
42
|
+
|
|
43
|
+
elif right_in_left:
|
|
44
|
+
|
|
45
|
+
def keys_consistent(left: set, right: set) -> bool:
|
|
46
|
+
return right.issubset(left)
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
raise RuntimeError("Either right_in_left or left_in_right should be True")
|
|
50
|
+
|
|
51
|
+
skipped_right_keys = []
|
|
52
|
+
processed_left_keys = set()
|
|
53
|
+
skipped_left_keys = set()
|
|
54
|
+
|
|
55
|
+
collect_skipped_left_keys = (
|
|
56
|
+
require_all_left_keys_processed or skippable_left_keys_should_contain
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
for key_right in keys_right:
|
|
60
|
+
key_right_proper = properkey(key_right)
|
|
61
|
+
setkey_right = OrderedSet(key_right_proper)
|
|
62
|
+
if fcn_outer_before is not None:
|
|
63
|
+
fcn_outer_before(key_right_proper)
|
|
64
|
+
|
|
65
|
+
right_processed = False
|
|
66
|
+
for i_left, keys_left in enumerate(keys_left_seq):
|
|
67
|
+
for key_left in keys_left:
|
|
68
|
+
if key_left:
|
|
69
|
+
key_left_proper = properkey(key_left)
|
|
70
|
+
setkey_left = OrderedSet(key_left_proper)
|
|
71
|
+
if not keys_consistent(setkey_left, setkey_right):
|
|
72
|
+
if (
|
|
73
|
+
collect_skipped_left_keys
|
|
74
|
+
and key_left_proper not in processed_left_keys
|
|
75
|
+
):
|
|
76
|
+
skipped_left_keys.add(key_left_proper)
|
|
77
|
+
if fcn_skip:
|
|
78
|
+
fcn_skip(i_left, key_left_proper, key_right_proper)
|
|
79
|
+
continue
|
|
80
|
+
else:
|
|
81
|
+
key_left_proper = ()
|
|
82
|
+
|
|
83
|
+
fcn(i_left, key_left_proper, key_right_proper)
|
|
84
|
+
right_processed = True
|
|
85
|
+
|
|
86
|
+
if collect_skipped_left_keys:
|
|
87
|
+
with suppress(KeyError):
|
|
88
|
+
skipped_left_keys.remove(key_left_proper)
|
|
89
|
+
processed_left_keys.add(key_left_proper)
|
|
90
|
+
|
|
91
|
+
if fcn_outer_after is not None:
|
|
92
|
+
fcn_outer_after(key_right_proper)
|
|
93
|
+
|
|
94
|
+
if not right_processed:
|
|
95
|
+
skipped_right_keys.append(key_right_proper)
|
|
96
|
+
|
|
97
|
+
if skipped_left_keys:
|
|
98
|
+
if require_all_left_keys_processed:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"match_keys: there were unprocessed left keys {skipped_left_keys!s}"
|
|
101
|
+
)
|
|
102
|
+
failure, incorrectly_skipped_key = _check_skipped_keys_incorrect(
|
|
103
|
+
skipped_left_keys, skippable_left_keys_should_contain
|
|
104
|
+
)
|
|
105
|
+
if failure:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"match_keys: there were unprocessed left keys {skipped_left_keys!s}. "
|
|
108
|
+
f"{incorrectly_skipped_key} should not have been skipped."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if require_all_right_keys_processed and skipped_right_keys:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"match_keys: there were unprocessed right keys {skipped_right_keys!s}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _check_skipped_keys_incorrect(
|
|
118
|
+
skipped_keys: Sequence[Key] | set[Key], should_contain: Sequence[KeyLike] | None
|
|
119
|
+
) -> tuple[bool, Key | None]:
|
|
120
|
+
if not should_contain:
|
|
121
|
+
return False, None
|
|
122
|
+
|
|
123
|
+
setkeys = tuple(setkey(keypart) for keypart in should_contain)
|
|
124
|
+
for skipped_key in skipped_keys:
|
|
125
|
+
if not any(setkey.issubset(skipped_key) for setkey in setkeys):
|
|
126
|
+
return True, skipped_key
|
|
127
|
+
|
|
128
|
+
return False, None
|
nested_mapping/typing.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from ordered_set import OrderedSet
|
|
4
|
+
|
|
5
|
+
TupleKey = tuple[str, ...]
|
|
6
|
+
Key = str | tuple[str, ...]
|
|
7
|
+
KeyLike = str | Sequence[str]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def properkey(key: KeyLike, *, sep: str | bool | None = None) -> TupleKey:
|
|
11
|
+
if isinstance(key, str):
|
|
12
|
+
if isinstance(sep, str):
|
|
13
|
+
return tuple(key.split(sep))
|
|
14
|
+
elif sep is True:
|
|
15
|
+
return tuple(key.split("."))
|
|
16
|
+
return (key,)
|
|
17
|
+
if isinstance(key, tuple):
|
|
18
|
+
return key
|
|
19
|
+
|
|
20
|
+
return tuple(key)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def orderedsetkey(key: KeyLike) -> OrderedSet[str]:
|
|
24
|
+
if isinstance(key, str):
|
|
25
|
+
return OrderedSet((key,))
|
|
26
|
+
|
|
27
|
+
return OrderedSet(key)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def setkey(key: KeyLike) -> set[str]:
|
|
31
|
+
if isinstance(key, str):
|
|
32
|
+
return set((key,))
|
|
33
|
+
|
|
34
|
+
return set(key)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def strkey(key: KeyLike, *, sep=".") -> str:
|
|
38
|
+
if isinstance(key, str):
|
|
39
|
+
return key
|
|
40
|
+
|
|
41
|
+
return sep.join(key)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class NestedMappingVisitor:
|
|
2
|
+
def start(self, d):
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
def enterdict(self, k, d):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
def visit(self, k, v):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
def exitdict(self, k, d):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def stop(self, d):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def MakeNestedMappingVisitor(fcn):
|
|
19
|
+
if isinstance(fcn, NestedMappingVisitor):
|
|
20
|
+
return fcn
|
|
21
|
+
|
|
22
|
+
if not callable(fcn):
|
|
23
|
+
raise TypeError(f"Expect function, got {type(fcn).__name__}")
|
|
24
|
+
|
|
25
|
+
ret = NestedMappingVisitor()
|
|
26
|
+
ret.visit = fcn
|
|
27
|
+
return ret
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NestedMappingVisitorDemostrator(NestedMappingVisitor):
|
|
31
|
+
fmt = "{action:7s} {depth!s:>5s} {key!s:<{keylen}s} {vtype!s:<{typelen}s} {value}"
|
|
32
|
+
opts = dict(keylen=20, typelen=15)
|
|
33
|
+
|
|
34
|
+
def typestring(self, v):
|
|
35
|
+
return type(v).__name__
|
|
36
|
+
|
|
37
|
+
def start(self, d):
|
|
38
|
+
v = object.__repr__(d.object)
|
|
39
|
+
print("Start printing dictionary:", v)
|
|
40
|
+
self._print("Action", "Key", "Value", "Type", depth="Depth")
|
|
41
|
+
|
|
42
|
+
def stop(self, d):
|
|
43
|
+
print("Done printing dictionary")
|
|
44
|
+
|
|
45
|
+
def enterdict(self, k, d):
|
|
46
|
+
d = d.object
|
|
47
|
+
v = object.__repr__(d)
|
|
48
|
+
self._print("Enter", k, v, self.typestring(d))
|
|
49
|
+
|
|
50
|
+
def exitdict(self, k, d):
|
|
51
|
+
d = d.object
|
|
52
|
+
v = object.__repr__(d)
|
|
53
|
+
self._print("Exit", k, v, self.typestring(d))
|
|
54
|
+
|
|
55
|
+
def visit(self, k, v):
|
|
56
|
+
self._print("Visit", k, v, self.typestring(v))
|
|
57
|
+
|
|
58
|
+
def _print(self, action, k, v, vtype, *, depth=None):
|
|
59
|
+
if depth is None:
|
|
60
|
+
depth = len(k)
|
|
61
|
+
print(
|
|
62
|
+
self.fmt.format(
|
|
63
|
+
action=action, depth=depth, key=k, vtype=vtype, value=v, **self.opts
|
|
64
|
+
)
|
|
65
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nested-mapping
|
|
3
|
+
Version: 0.10.2
|
|
4
|
+
Summary: Package to work with nested dictionaries supporing list/set as keys
|
|
5
|
+
Author-email: DAGModelling Team <dagflow@jinr.ru>
|
|
6
|
+
Maintainer-email: DAGModelling Team <dagflow@jinr.ru>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/dagflow-team/nested-mapping/issues
|
|
9
|
+
Project-URL: DAGModelling Team, https://github.com/dagflow-team
|
|
10
|
+
Project-URL: documentation, https://github.com/dagflow-team/nested-mapping/wiki
|
|
11
|
+
Project-URL: homepage, https://github.com/dagflow-team/nested-mapping
|
|
12
|
+
Project-URL: repository, https://github.com/dagflow-team/nested-mapping
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy
|
|
21
|
+
Requires-Dist: ordered-set
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: coverage; extra == "test"
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Summary
|
|
29
|
+
|
|
30
|
+
[](https://www.python.org/)
|
|
31
|
+
[](https://git.jinr.ru/dagflow-team/nested-mapping/commits/master)
|
|
32
|
+
[](https://git.jinr.ru/dagflow-team/nested-mapping/-/commits/master)
|
|
33
|
+
<!--- Uncomment here after adding docs!
|
|
34
|
+
[](http://dagflow-team.pages.jinr.ru/nested-mapping)
|
|
35
|
+
-->
|
|
36
|
+
|
|
37
|
+
A package to work with nested dictionaries.
|
|
38
|
+
|
|
39
|
+
# Repositories
|
|
40
|
+
|
|
41
|
+
- Development/CI: https://git.jinr.ru/dagflow-team/nested-mapping
|
|
42
|
+
- Contact/pypi/mirror: https://github.com/dagflow-team/nested-mapping
|
|
43
|
+
- PYPI: https://pypi.org/project/nested-mapping
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
nested_mapping/__init__.py,sha256=7Gq8elq0zdqKY-057qHaqHgegUvKyfmHsrd6NG-VHlI,130
|
|
2
|
+
nested_mapping/class_wrapper.py,sha256=Z3FCAgJRSvs_meJi1bhdmCWvhRSKhydsDUSbDqLRii4,1296
|
|
3
|
+
nested_mapping/ipython.py,sha256=3NrXcLYrKD_kGxqwQp3m2BcQD7Z0AO8PTLWYB0PAaL8,140
|
|
4
|
+
nested_mapping/nested_mapping.py,sha256=rJq-PV60ivoc9DIIW0q-sUMEByr_9u78LLEdY5F0zEA,18647
|
|
5
|
+
nested_mapping/nested_mapping_access.py,sha256=tqZICim76cw2BCVYhHom8i7FdQHSOcqDM87lVxJ6baI,1097
|
|
6
|
+
nested_mapping/typing.py,sha256=1tDcM7oTn84qNfYYqN-pmS1wteuv4G6iv3HCmSkYkuk,884
|
|
7
|
+
nested_mapping/visitor.py,sha256=tWnC0AnaRTUqrh0KWyVkptfOMC92kk_JORgVtJIJAro,1620
|
|
8
|
+
nested_mapping/tools/__init__.py,sha256=oIPmTYpkUX5wVvSQI5iWQM_AmC6fge_thx4mHXPpe_E,136
|
|
9
|
+
nested_mapping/tools/filter.py,sha256=gfRzzB48iOFh38YkZfW1FNAS9EFVBTwSp_GHP4lw5eE,854
|
|
10
|
+
nested_mapping/tools/map.py,sha256=VOVap62aTjo2iZ6AuEoyaJXlULeUZjNYoJSMkiDUpYs,5197
|
|
11
|
+
nested_mapping/tools/match.py,sha256=jWaiETpkYMQ1NMUX5ngpnwBt7hlciO0C5RUG9Nmnk7k,4547
|
|
12
|
+
nested_mapping/tools/reorder_key.py,sha256=OZ3ABamLbjPgZ-khhSn8bmT73udPDyuc8jjTqSjzsRU,231
|
|
13
|
+
nested_mapping-0.10.2.dist-info/licenses/LICENSE,sha256=qtOApKX08hit4Gzn_0_WXOvdk55eFcw9AcWV_7ryXBQ,1088
|
|
14
|
+
nested_mapping-0.10.2.dist-info/METADATA,sha256=vfEJbvDtoNLGdZA7xyvua6JQV8BF5X3yx57yG8azz18,1960
|
|
15
|
+
nested_mapping-0.10.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
nested_mapping-0.10.2.dist-info/top_level.txt,sha256=82tAUnkF7Kr5kXFDpsg49V_CcnjjjlP1TfnN4GUV-IY,15
|
|
17
|
+
nested_mapping-0.10.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gonchar Maxim, Tsegelnik Nikita
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nested_mapping
|