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.
@@ -0,0 +1,2 @@
1
+ from .nested_mapping import NestedMapping, walkitems, walkkeys, walkvalues
2
+ from .nested_mapping_access import NestedMappingAccess
@@ -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,3 @@
1
+ def repr_pretty(self, p, cycle):
2
+ """Pretty repr for IPython. To be used as __repr__ method"""
3
+ p.text("..." if cycle else str(self))
@@ -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,4 @@
1
+ from .filter import filter_items
2
+ from .map import remap_items, mkmap
3
+ from .match import match_keys
4
+ from .reorder_key import reorder_key
@@ -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
@@ -0,0 +1,9 @@
1
+ from collections.abc import Sequence
2
+
3
+ from ..typing import TupleKey
4
+
5
+
6
+ def reorder_key(key: TupleKey, key_order: Sequence[int] | None = None):
7
+ if key_order is None:
8
+ return key
9
+ return tuple(key[i] for i in key_order)
@@ -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
+ [![python](https://img.shields.io/badge/python-3.11-purple.svg)](https://www.python.org/)
31
+ [![pipeline](https://git.jinr.ru/dagflow-team/nested-mapping/badges/master/pipeline.svg)](https://git.jinr.ru/dagflow-team/nested-mapping/commits/master)
32
+ [![coverage report](https://git.jinr.ru/dagflow-team/nested-mapping/badges/master/coverage.svg)](https://git.jinr.ru/dagflow-team/nested-mapping/-/commits/master)
33
+ <!--- Uncomment here after adding docs!
34
+ [![pages](https://img.shields.io/badge/pages-link-white.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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