datatypes 0.31.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. datatypes/__init__.py +169 -0
  2. datatypes/collections/__init__.py +25 -0
  3. datatypes/collections/container.py +248 -0
  4. datatypes/collections/mapping.py +1226 -0
  5. datatypes/collections/sequence.py +311 -0
  6. datatypes/command.py +596 -0
  7. datatypes/compat.py +206 -0
  8. datatypes/config/__init__.py +13 -0
  9. datatypes/config/environ/__init__.py +33 -0
  10. datatypes/config/environ/base.py +332 -0
  11. datatypes/config/environ/token.py +173 -0
  12. datatypes/config/parser.py +256 -0
  13. datatypes/config/settings.py +352 -0
  14. datatypes/copy.py +151 -0
  15. datatypes/csv.py +547 -0
  16. datatypes/datetime.py +1243 -0
  17. datatypes/decorators/__init__.py +31 -0
  18. datatypes/decorators/base.py +449 -0
  19. datatypes/decorators/descriptor.py +490 -0
  20. datatypes/decorators/misc.py +152 -0
  21. datatypes/email.py +439 -0
  22. datatypes/enum.py +176 -0
  23. datatypes/event.py +299 -0
  24. datatypes/exception.py +13 -0
  25. datatypes/filetypes/__init__.py +10 -0
  26. datatypes/filetypes/html.py +540 -0
  27. datatypes/filetypes/toml.py +275 -0
  28. datatypes/http.py +1443 -0
  29. datatypes/logging.py +475 -0
  30. datatypes/number.py +586 -0
  31. datatypes/parse.py +362 -0
  32. datatypes/path.py +4358 -0
  33. datatypes/profile.py +246 -0
  34. datatypes/reflection/__init__.py +24 -0
  35. datatypes/reflection/inspect.py +3475 -0
  36. datatypes/reflection/other.py +843 -0
  37. datatypes/reflection/path.py +639 -0
  38. datatypes/server.py +642 -0
  39. datatypes/service.py +212 -0
  40. datatypes/ssh.py +812 -0
  41. datatypes/string.py +1632 -0
  42. datatypes/token/__init__.py +16 -0
  43. datatypes/token/abnf.py +1696 -0
  44. datatypes/token/base.py +921 -0
  45. datatypes/token/word.py +259 -0
  46. datatypes/typing.py +126 -0
  47. datatypes/url.py +988 -0
  48. datatypes/utils.py +179 -0
  49. datatypes-0.31.0.dist-info/METADATA +38 -0
  50. datatypes-0.31.0.dist-info/RECORD +53 -0
  51. datatypes-0.31.0.dist-info/WHEEL +5 -0
  52. datatypes-0.31.0.dist-info/licenses/LICENSE.txt +21 -0
  53. datatypes-0.31.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1226 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Dict and Map like objects
4
+
5
+ https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping
6
+ https://docs.python.org/3/library/stdtypes.html#dict
7
+
8
+ class Dictionary(dict):
9
+ def __init__(self, iterable_or_mapping=None, **kwargs):
10
+ if iterable_or_mapping:
11
+ super().__init__(iterable_or_mapping, **kwargs)
12
+
13
+ else:
14
+ super().__init__(**kwargs)
15
+
16
+ def clear(self):
17
+ return super().clear()
18
+
19
+ def copy(self):
20
+ return super().copy()
21
+
22
+ def get(key, default=None, /):
23
+ return super().get(key, default)
24
+
25
+ def items(self):
26
+ return super().items()
27
+
28
+ def keys(self):
29
+ return super().keys()
30
+
31
+ def values(self):
32
+ return super().values()
33
+
34
+ def pop(self, key, *default):
35
+ return super().pop(key, *default)
36
+
37
+ def popitem(self):
38
+ return super().popitem()
39
+
40
+ def setdefault(self, key, default=None, /):
41
+ return super().setdefault(key, default)
42
+
43
+ def update(self, *args, **kwargs):
44
+ super().update(*args, **kwargs)
45
+
46
+ def __setitem__(self, key, value, /):
47
+ return super().__setitem__(key, value)
48
+
49
+ def __getitem__(self, key, /):
50
+ return super().__getitem__(key)
51
+
52
+ def __delitem__(self, key, /):
53
+ return super().__delitem__(key)
54
+
55
+ """
56
+ from __future__ import annotations
57
+ from contextlib import contextmanager, AbstractContextManager
58
+
59
+ from ..compat import *
60
+ from .container import HotSet
61
+ from .sequence import Stack
62
+ from collections.abc import Mapping, Generator, Iterable
63
+ from typing import Any
64
+
65
+
66
+ class Pool(dict):
67
+ """Generic pool of some values bounded by size, this means when size is
68
+ reached then the least used item will be silently dropped from the pool.
69
+
70
+ This class gets interesting when you extend it and add __missing__() so
71
+ you can use this as kind of a hot cache of the maxsize most used items in
72
+ the Pool
73
+
74
+ :Example:
75
+ class HotPool(Pool):
76
+ def __missing__(self, key):
77
+ value = get_some_value_here(key)
78
+ self[key] = value # cache value at key
79
+ return value
80
+ """
81
+ def __init__(self, maxsize=0):
82
+ super().__init__()
83
+ self.pq = HotSet(maxsize=maxsize)
84
+
85
+ def __getitem__(self, key):
86
+ shuffle = key in self
87
+ value = super().__getitem__(key)
88
+
89
+ # since this is being accessed again, we refresh it
90
+ if shuffle:
91
+ self.pq.add(key)
92
+
93
+ return value
94
+
95
+ def __setitem__(self, key, value):
96
+ if len(self.pq) == self.pq.maxsize:
97
+ self.popitem()
98
+
99
+ super().__setitem__(key, value)
100
+
101
+ self.pq.add(key)
102
+
103
+ def __delitem__(self, key):
104
+ super().__delitem__(key)
105
+ self.pq.remove(key)
106
+
107
+ def popitem(self):
108
+ if self:
109
+ dead_key = self.pq.pop()
110
+ dead_value = super().__getitem__(dead_key)
111
+ super().__delitem__(dead_key)
112
+ return dead_key, dead_value
113
+
114
+ else:
115
+ raise KeyError("Pool is empty")
116
+
117
+ def pop(self, key, *args):
118
+ if key in self:
119
+ self.pq.remove(key)
120
+ return super().pop(key, *args)
121
+
122
+ def setdefault(self, key, value):
123
+ if key not in self:
124
+ self[key] = value
125
+
126
+ def get(self, key, default=None):
127
+ value = default
128
+ if key in self:
129
+ value = self[key]
130
+ return value
131
+
132
+ def update(self, d):
133
+ for k, v in d.items():
134
+ self[k] = v
135
+
136
+
137
+ class Dict(dict):
138
+ def __init__(self, *args, **kwargs):
139
+ if len(args) > 1:
140
+ super().__init__(args[0], **kwargs)
141
+ for arg in args[1:]:
142
+ self.update(arg)
143
+
144
+ else:
145
+ super().__init__(*args, **kwargs)
146
+
147
+ def ritems(self, *keys):
148
+ """Iterate through the dict and all sub dicts
149
+
150
+ :param *keys: str, if you only want to find and iterate certain keys
151
+ :yields: tuple, (list, mixed), the list is the key path, all the keys
152
+ it would take to get to this key (eg, ["foo", "bar"]) and mixed is
153
+ the value found at the end of the key path
154
+ """
155
+ def iterdict(d, keypath, keys):
156
+ for k, v in d.items():
157
+ kp = keypath + [k]
158
+ if k in keys:
159
+ yield kp, v
160
+
161
+ else:
162
+ if not keys:
163
+ yield kp, v
164
+
165
+ if isinstance(v, Mapping):
166
+ for kp, d in iterdict(v, kp, keys):
167
+ yield kp, d
168
+
169
+ keys = set(keys)
170
+ for kp, d in iterdict(self, [], keys):
171
+ yield kp, d
172
+
173
+ def rkeys(self, *keys):
174
+ """Iterate through all key paths
175
+
176
+ :param *keys: str, if you only want to find and iterate certain keys
177
+ :yields: list, the key paths
178
+ """
179
+ for kp, _ in self.ritems(*keys):
180
+ yield kp
181
+
182
+ def rvalues(self, *keys):
183
+ """Iterate through all values
184
+
185
+ :param *keys: str, if you only want to find and iterate certain keys
186
+ :yields: mixed, the found values
187
+ """
188
+ for _, v in self.ritems(*keys):
189
+ yield v
190
+
191
+ def rget(self, key, default=None):
192
+ """Return the first matching value found at key anywhere in the dict or
193
+ sub dicts
194
+
195
+ :param key: string, the key to find in this or any sub dicts
196
+ :param default: mixed, if key isn't found, return this value
197
+ :returns: mixed, the value found at the first key/subkey
198
+ """
199
+ v = default
200
+ for v in self.rvalues(key):
201
+ break
202
+ return v
203
+
204
+ def gets(self, keys, default=None):
205
+ """Check every key in the keys list, first found key is returned, if
206
+ none of the keys exist then return default
207
+
208
+ :Example:
209
+ d = Dict({
210
+ "foo": 1,
211
+ "bar": 2,
212
+ "che": 3,
213
+ })
214
+
215
+ d.get(["does", "not", "exist", "foo"], 5) # 1
216
+ d.get(["does", "bar", "exist", "foo"], 5) # 2
217
+
218
+ :param keys: list, a list of keys to check in the order they should be
219
+ checked
220
+ :param *default: mixed, what to return if none of the keys exist
221
+ :returns: mixed, either the value of the first key that exists or
222
+ default_val
223
+ """
224
+ for k in keys:
225
+ if k in self:
226
+ return self[k]
227
+ return default
228
+
229
+ def pops(self, keys, *default):
230
+ """Check every key in the keys list, first found key will be returned,
231
+ if none of the keys exist then return default, all keys in the keys
232
+ list will be popped, even the ones after a value is found
233
+
234
+ :Example:
235
+ d = Dict({
236
+ "foo": 1,
237
+ "bar": 2,
238
+ "che": 3,
239
+ })
240
+
241
+ d.pop(["foo"], 5) # 1
242
+ d.pop(["foo", "bar"], 5) # 2
243
+
244
+ :param keys: list, a list of keys to check in the order they should be
245
+ checked
246
+ :param *default: mixed, what to return if none of the keys exist
247
+ :returns: mixed, either the value of the first key that exists or
248
+ default_val
249
+ """
250
+ ret = None
251
+ key = None
252
+ for k in keys:
253
+ if k in self:
254
+ if key is None:
255
+ key = k
256
+ ret = self.pop(k)
257
+
258
+ else:
259
+ self.pop(k)
260
+
261
+ if key is None:
262
+ if default:
263
+ ret = default[0]
264
+ else:
265
+ raise KeyError(", ".join(keys))
266
+
267
+ return ret
268
+
269
+ def merge(self, other):
270
+ """Very similar to .update() but merges the dicts instead of overriding
271
+
272
+ :Example:
273
+ d = {
274
+ "foo": {
275
+ "bar": 1
276
+ }
277
+ }
278
+ d2 = {
279
+ "foo": {
280
+ "che": 2
281
+ }
282
+ }
283
+
284
+ d.merge(d2)
285
+ print(d["foo"]["che"]) # 2
286
+ print(d["foo"]["bar"]) # 1
287
+
288
+ :param other: Mapping, the other dict to merge into this dict, if
289
+ self[key] is not a Mapping, then other[key] will override, if both
290
+ self[key] and other[key] are Mapping instances then they will be
291
+ merged
292
+ """
293
+ for k in other.keys():
294
+ if isinstance(other[k], Mapping) and (k in self):
295
+ if isinstance(self[k], Dict):
296
+ self[k].merge(other[k])
297
+
298
+ elif isinstance(self[k], Mapping):
299
+ self[k] = type(self)(self[k])
300
+ self[k].merge(other[k])
301
+
302
+ else:
303
+ self[k] = other[k]
304
+
305
+ else:
306
+ self[k] = other[k]
307
+
308
+
309
+ class NormalizeMixin(object):
310
+ """Mixin to create a normalizing dictionary
311
+
312
+ This class must come before the mapping base class when defining the
313
+ custom child class
314
+
315
+ :example:
316
+ class Foo(NormalizeMixin, dict):
317
+ def normalize_key(self, k):
318
+ return k
319
+
320
+ def normalize_value(self, v):
321
+ return v
322
+
323
+ You can override the .normalize_key and .normalize_value methods, which get
324
+ called anytime you set/get a value. You can check out the idict class for
325
+ an implementation that uses those methods to allow case-insensitive keys
326
+
327
+ Taken from `herd.utils.NormalizeDict`
328
+ """
329
+ def __init__(self, *args, **kwargs):
330
+ super().__init__()
331
+ self.update(*args, **kwargs)
332
+
333
+ def __setitem__(self, k, v):
334
+ k = self.normalize_key(k)
335
+ v = self.normalize_value(v)
336
+ return super().__setitem__(k, v)
337
+
338
+ def __delitem__(self, k):
339
+ k = self.normalize_key(k)
340
+ return super().__delitem__(k)
341
+
342
+ def __getitem__(self, k):
343
+ k = self.normalize_key(k)
344
+ return super().__getitem__(k)
345
+
346
+ def __contains__(self, k):
347
+ k = self.normalize_key(k)
348
+ return super().__contains__(k)
349
+
350
+ def setdefault(self, k, default=None):
351
+ k = self.normalize_key(k)
352
+ v = self.normalize_value(default)
353
+ return super().setdefault(k, v)
354
+
355
+ def update(self, *args, **kwargs):
356
+ # create temp dictionary so I don't have to mess with the arguments
357
+ d = dict(*args, **kwargs)
358
+ for k, v in d.items():
359
+ self[k] = v
360
+
361
+ def pop(self, k, *default):
362
+ k = self.normalize_key(k)
363
+ if default:
364
+ default = [self.normalize_value(default[0])]
365
+ return super().pop(k, *default)
366
+
367
+ def get(self, k, default=None):
368
+ k = self.normalize_key(k)
369
+ v = self.normalize_value(default)
370
+ return super().get(k, v)
371
+
372
+ def normalize_key(self, k):
373
+ return k
374
+
375
+ def normalize_value(self, v):
376
+ return v
377
+
378
+ def ritems(self, *keys):
379
+ keys = map(self.normalize_key, keys)
380
+ return super().ritems(*keys)
381
+
382
+
383
+ class idict(NormalizeMixin, Dict):
384
+ """A case insensitive dictionary, adapted from `herd.utils.NormalizeDict`,
385
+ naming convention is meant to mimic python's use of i* for case-insensitive
386
+ functions and python's built-in dict class (which is why the classname is
387
+ all lowercase)"""
388
+ def __init__(self, *args, **kwargs):
389
+ # lookup table holding key variations to the actual key
390
+ self.key_lookup = {}
391
+
392
+ super().__init__(*args, **kwargs)
393
+
394
+ def normalize_key(self, k):
395
+ if k in self.key_lookup:
396
+ k = self.key_lookup[k]
397
+
398
+ else:
399
+ self.key_lookup[k] = k
400
+
401
+ nk = k.lower()
402
+ if nk in self.key_lookup:
403
+ k = self.key_lookup[nk]
404
+ self.key_lookup[nk] = k
405
+
406
+ return k
407
+
408
+
409
+ class NamespaceMixin(object):
410
+ """Mixin to create an attribute dictionary
411
+
412
+ This makes `self.foo` work like `self["foo"]
413
+
414
+ This class must come before the mapping base class when defining the
415
+ custom child class
416
+
417
+ :example:
418
+ class Foo(NamespaceMixin, dict):
419
+ pass
420
+ """
421
+ def __setattr__(self, k, v):
422
+ if k.startswith("__"):
423
+ return super().__setattr__(k, v)
424
+
425
+ else:
426
+ return self.__setitem__(k, v)
427
+
428
+ def __getattr__(self, k):
429
+ """Treat a missing attribute like a `.__getitem__` key request
430
+
431
+ :raises KeyError: When the attribute `k` is missing, this is to
432
+ hint to the developer that a dictionary key using an alternate
433
+ syntax was being requested and means that `self.foo` and
434
+ `self["foo"]` can be handled the same way
435
+ """
436
+ if k.startswith("__"):
437
+ return super().__getattr__(k)
438
+
439
+ else:
440
+ return self.__getitem__(k)
441
+
442
+ def __delattr__(self, k):
443
+ if k.startswith("__"):
444
+ return super().__delattr__(k)
445
+
446
+ else:
447
+ return self.__delitem__(k)
448
+
449
+
450
+ class Namespace(NamespaceMixin, Dict):
451
+ """Allows both dictionary syntax (eg foo["keyname"]) and object syntax
452
+ (eg foo.keyname)
453
+ """
454
+ pass
455
+
456
+
457
+ class StackNamespace(Mapping):
458
+ """Nested namespaces that can be changed for the given context and then
459
+ reverted to the previous context when the context is done
460
+
461
+ :example:
462
+ n = StackNamespace()
463
+ n["foo"] = 1
464
+ with n:
465
+ n["foo"] = 2
466
+ with n("named context"):
467
+ n["foo"] = 3
468
+ print(n["foo"]) # 3
469
+
470
+ print(n["foo"]) # 2
471
+
472
+ print(n["foo"]) # 1
473
+
474
+ Internally, an instance keeps track of a stack of contexts and pops off
475
+ the last context created every time a context block is finished. This
476
+ class quacks like a mapping and always references the last context on
477
+ the stack
478
+ """
479
+ def __init__(self, name: str = "", cascade: bool = True):
480
+ """
481
+ :param name: str, If you want to customize the default context name
482
+ then pass it in
483
+ :param cascade: bool, if True then gets cascade through the stack of
484
+ contexts, if False then this contexts completely switch
485
+ """
486
+ super().__init__()
487
+
488
+ self._contexts = {
489
+ "active": [],
490
+ "cascade": cascade,
491
+ }
492
+
493
+ self.push_context(name, source="__init__")
494
+
495
+ def __enter__(self) -> ContextNamespace:
496
+ """Allow `with instance:` context invocation"""
497
+ context_tuple = self._contexts["active"][-1]
498
+ if context_tuple[2] == "__call__":
499
+ context_tuple[2] = "__enter__"
500
+
501
+ else:
502
+ self.push_context("", source="__enter__")
503
+
504
+ return self
505
+
506
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
507
+ self.pop_context()
508
+ return None
509
+
510
+ def __call__(self, name: str = "", **kwargs):
511
+ """Allow `with instance(...):` context invocation"""
512
+ self.push_context(name, kwargs, "__call__")
513
+ return self
514
+
515
+ @contextmanager
516
+ def context(
517
+ self,
518
+ name: str = "",
519
+ **kwargs
520
+ ) -> AbstractContextManager[ContextNamespace]:
521
+ """This is meant to be used with the "with ..." command, its purpose is
522
+ to make it easier to change the context and restore it back to the
523
+ previous context when it is done
524
+
525
+ :example:
526
+ with instance.context("foo"):
527
+ # anything in this block will use the foo configuration
528
+ pass
529
+ # anything outside this block will *NOT* use the foo configuration
530
+ """
531
+ with self(name, **kwargs) as s:
532
+ yield s
533
+
534
+ def context_name(self) -> str:
535
+ """Get the current context name"""
536
+ return self._current_context_tuple()[0]
537
+ #return self._contexts["active"][-1][0]
538
+
539
+ def _current_context_tuple(self) -> tuple[str, Mapping, str]:
540
+ """Internal method. Gets the full context tuple for the current
541
+ context"""
542
+ #return self._contexts["active"][-1]
543
+ # we pull directly from the __dict__ so __setattr__
544
+ # doesn't infinite loop
545
+ if contexts := self.__dict__.get("_contexts", None):
546
+ return contexts["active"][-1]
547
+ raise IndexError("No current contexts")
548
+
549
+ def current_context(self) -> Mapping:
550
+ """get the current context"""
551
+ return self._current_context_tuple()[1]
552
+ #return self._contexts["active"][-1][1]
553
+
554
+ def has_context(self) -> bool:
555
+ try:
556
+ self._current_context_tuple()
557
+ return True
558
+
559
+ except IndexError:
560
+ return False
561
+
562
+ # if contexts := self.__dict__.get("_contexts", None):
563
+ # return len(contexts["active"]) > 0
564
+ # return False
565
+
566
+ def push_context(
567
+ self,
568
+ name: str,
569
+ context: Mapping|None = None,
570
+ source: str = "",
571
+ ) -> str:
572
+ """push a context named name onto the context stack"""
573
+ self._contexts["active"].append([name, context or {}, source])
574
+ return name
575
+
576
+ def pop_context(self) -> tuple[str, Mapping, str]:
577
+ """Pop the last context from the stack"""
578
+ if self.has_context():
579
+ return self._contexts["active"].pop(-1)
580
+
581
+ def active_contexts(self) -> Generator[Mapping]:
582
+ """yield all the contexts taking into account the cascade setting
583
+ """
584
+ for context_tuple in reversed(self._contexts["active"]):
585
+ yield context_tuple[1]
586
+
587
+ if not self._contexts["cascade"]:
588
+ break
589
+
590
+ def __setattr__(self, k, v):
591
+ if self.has_context():
592
+ self.__setitem__(k, v)
593
+
594
+ else:
595
+ super().__setattr__(k, v)
596
+
597
+ def __setitem__(self, k, v):
598
+ self.current_context().__setitem__(k, v)
599
+
600
+ def __delattr__(self, k):
601
+ if self.has_context():
602
+ self.__delitem__(k)
603
+
604
+ else:
605
+ super().__delattr__(k)
606
+
607
+ def __delitem__(self, k):
608
+ self.current_context().__delitem__(k)
609
+
610
+ def __getattr__(self, k):
611
+ if self.has_context():
612
+ return self.__getitem__(k)
613
+
614
+ else:
615
+ raise KeyError(k)
616
+ #return super().__getattr__(k)
617
+
618
+ def __getitem__(self, k):
619
+ """Most of the context LIFO magic happens here, this will work through
620
+ the contexts looking for k"""
621
+ for context in self.active_contexts():
622
+ try:
623
+ return context[k]
624
+
625
+ except KeyError:
626
+ pass
627
+
628
+ return self.__missing__(k)
629
+
630
+ def __missing__(self, k):
631
+ raise KeyError(k)
632
+
633
+ def __contains__(self, k):
634
+ for context in self.active_contexts():
635
+ if k in context:
636
+ return True
637
+
638
+ return False
639
+
640
+ def __len__(self):
641
+ """Unlike a normal dict this is O(n)"""
642
+ return len(list(self.keys()))
643
+
644
+ def __ior__(self, other):
645
+ """self |= other"""
646
+ self.update(other)
647
+ return self
648
+
649
+ def __or__(self, other):
650
+ """self | other"""
651
+ d = {i[0]: i[1] for i in self.items()}
652
+ d.update(other)
653
+ return d
654
+
655
+ def setdefault(self, k, v):
656
+ if k not in self:
657
+ self[k] = v
658
+
659
+ def get(self, k, default=None):
660
+ try:
661
+ return self[k]
662
+
663
+ except KeyError:
664
+ return default
665
+
666
+ def pop(self, k, *default):
667
+ """Pop works a little differently than other key access methods, pop
668
+ will only return a value if it is actually in the current context, if
669
+ `k` is not in the current context then this will return default. It
670
+ does this because it would violate the principal of least surprise if
671
+ popping something in the current context actually popped it from a
672
+ previous context
673
+ """
674
+ try:
675
+ d = self.current_context()
676
+ v = d[k]
677
+ del d[k]
678
+
679
+ except (KeyError, IndexError):
680
+ if default:
681
+ v = default[0]
682
+
683
+ else:
684
+ raise
685
+
686
+ return v
687
+
688
+ def popitem(self):
689
+ return self.current_context().popitem()
690
+
691
+ def clear(self):
692
+ return self.current_context().clear()
693
+
694
+ def update(self, *args: Iterable[tuple[Any, Any]]|Mapping, **kwargs):
695
+ # create temp dictionary so I don't have to mess with the arguments
696
+ self.current_context().update(*args, **kwargs)
697
+
698
+ def copy(self):
699
+ """return a dict of all active values in the config at the moment"""
700
+ return dict(item for item in self.items())
701
+
702
+ def items(self):
703
+ seen_keys = set()
704
+ for context in self.active_contexts():
705
+ for k, v in context.items():
706
+ if k not in seen_keys:
707
+ yield k, v
708
+ seen_keys.add(k)
709
+
710
+ def __iter__(self):
711
+ yield from self.keys()
712
+
713
+ def keys(self):
714
+ for k, _ in self.items():
715
+ yield k
716
+
717
+ def values(self):
718
+ for _, v in self.items():
719
+ yield v
720
+
721
+
722
+ class ContextNamespace(StackNamespace):
723
+ """A context aware namespace where you can override values in later
724
+ contexts and then revert back to the original context when the with
725
+ statement is done
726
+
727
+ values are retrieved in LIFO order of the pushed contexts when
728
+ `cascade=True`
729
+
730
+ Based on `bang.config.Config` moved here and expanded on 1-10-2023
731
+
732
+ Similar to a ChainMap:
733
+ https://docs.python.org/3/library/collections.html#chainmap-objects
734
+
735
+ :example:
736
+ n = ContextNamespace()
737
+
738
+ n.foo = 1
739
+ with n.context("one"):
740
+ n.foo # 1
741
+ n.foo = 2
742
+ n.foo # 2
743
+ with n.context("two"):
744
+ n.foo # 2
745
+ n.foo = 3
746
+ n.foo #3
747
+
748
+ n.foo # 2
749
+
750
+ n.foo # 1
751
+
752
+ # calling the context again restores the values
753
+ with n.context("one"):
754
+ n.foo # 2
755
+
756
+ n.foo # 1
757
+
758
+ # you can also turn cascading off, so it switches contexts but doesn't
759
+ # cascade the values
760
+
761
+ n = ContextNamespace(cascade=False)
762
+
763
+ n.foo = 1
764
+ with n.context("one"):
765
+ "foo" in n.foo # False
766
+ n.foo = 2
767
+ "foo" in n.foo # True
768
+
769
+ n.foo # 1
770
+
771
+ Internally, this class keeps a stack of active contexts and when the
772
+ context blocks are done, the context is moved to an inactive mapping,
773
+ if the context name is used again, that context will be moved from the
774
+ inactive mapping back to the active stack
775
+ """
776
+ def push_context(
777
+ self,
778
+ name: str,
779
+ context: Mapping|None = None,
780
+ source: str = "",
781
+ ) -> str:
782
+ inactive = self._contexts.get("inactive", {})
783
+ d = inactive.get(name, {})
784
+
785
+ if context:
786
+ d.update(context)
787
+
788
+ return super().push_context(name, d, source)
789
+
790
+ def pop_context(self) -> Mapping:
791
+ """Pop the last context from the stack"""
792
+ context_tuple = super().pop_context()
793
+ self._contexts.setdefault("inactive", {})
794
+
795
+ self._contexts["inactive"][context_tuple[0]] = context_tuple[1]
796
+ return context_tuple
797
+
798
+
799
+ class DictTree(Dict):
800
+ """A dict/tree hybrid, what that means is you can pass in a list of keys
801
+ and it will create sub DictTrees at each key so you can nest values. This
802
+ could also be called a NestingDict, hierarchyDict, or something like that
803
+
804
+ https://github.com/Jaymon/datatypes/issues/29
805
+
806
+ :example:
807
+ d = DictTree()
808
+
809
+ d.set(["foo", "bar", "che"], 1)
810
+ d.get(["foo", "bar", "che"]) # 1
811
+
812
+ d.set([], 2) # set a value on the root node
813
+ d.value # 2
814
+ d[[]] # 2
815
+
816
+ This is the terminology (with tree_ prefix) I'm trying to adhere to:
817
+ https://en.wikipedia.org/wiki/Tree_(data_structure)#Terminology
818
+
819
+ Each tree node in the tree has these properties:
820
+
821
+ * .parent: DictTree, points to the tree node above this tree, this
822
+ will be None if this is the absolute head tree
823
+ * .key: str, the name of the key this tree is in (so
824
+ `self == self.parent[self.name]`), this will be "" if this
825
+ node is the absolute .root node of the tree
826
+ * .pathkeys: list[str], similar to .name but returns the entire set of
827
+ names in the order needed to traverse from the .root node back
828
+ to this node. This isn't named .keys to avoid a naming collision
829
+ with the dict.keys method
830
+ * .root: DictTree, the absolute head node of the tree
831
+ * .value: Any, the value at this node of the tree, None if the node
832
+ has no value
833
+ """
834
+ @property
835
+ def pathkeys(self):
836
+ """Get the path of keys from the absolute head tree to this tree
837
+
838
+ :returns: list[str]
839
+ """
840
+ keys = [self.key]
841
+ parent = self
842
+ while parent := parent.parent:
843
+ keys.append(parent.key)
844
+ return list(reversed(keys[:-1]))
845
+
846
+ def __init__(self, *args, **kwargs):
847
+ """This should support all the standard dict init signatures"""
848
+ self.root = self
849
+ self.parent = None
850
+ self.key = None
851
+ self.value = None
852
+
853
+ super().__init__()
854
+ self.update(*args, **kwargs)
855
+
856
+ def update(self, mapping_or_iterable=None, **kwargs):
857
+ """
858
+ https://docs.python.org/3/library/stdtypes.html#dict.update
859
+ """
860
+ if mapping_or_iterable:
861
+ if isinstance(mapping_or_iterable, Mapping):
862
+ mapping_or_iterable = mapping_or_iterable.items()
863
+
864
+ for k, v in mapping_or_iterable:
865
+ self.set(k, v)
866
+
867
+ if kwargs:
868
+ for k, v in kwargs.items():
869
+ self.set(k, v)
870
+
871
+ def create_instance(self):
872
+ """Internal method. Called from .add_node and is only responsible
873
+ for creating an instance of this class.
874
+
875
+ The reason this exists is because the __init__ might be customized
876
+ in a child class and so this can also be customized so nothing fails
877
+ and .add_node can be left alone since it sets the node properties
878
+
879
+ :returns: DictTree instance
880
+ """
881
+ return type(self)()
882
+
883
+ def __getitem__(self, keys):
884
+ """Allow list access in normal dict interactions
885
+
886
+ :param keys: list[hashable]|hashable
887
+ :returns: Any, the value found at the end of keys
888
+ """
889
+ node = self.get_node(keys)
890
+ return node.value
891
+
892
+ def __setitem__(self, keys, value):
893
+ """Allow list access in normal dict interactions"""
894
+ self.set(keys, value)
895
+
896
+ def __delitem__(self, keys):
897
+ """Allow list access in normal dict interactions"""
898
+ self.pop_node(keys)
899
+
900
+ def __contains__(self, keys):
901
+ """Allow list access when checking a key"""
902
+ try:
903
+ self.get_node(keys)
904
+ ret = True
905
+
906
+ except KeyError:
907
+ ret = False
908
+
909
+ return ret
910
+
911
+ def set(self, keys, value):
912
+ """Set value into the last key in keys, creating intermediate dicts
913
+ along the way
914
+
915
+ :param keys: list[hashable]|hashable, a list of keys or the key you
916
+ want to set value in
917
+ :param value: Any
918
+ """
919
+ keys = self.normalize_keys(keys)
920
+
921
+ if len(keys) > 1:
922
+ key = self.normalize_key(keys[0])
923
+ if key not in self:
924
+ self.add_node(key, self.create_instance(), None)
925
+
926
+ super().__getitem__(key).set(keys[1:], value)
927
+
928
+ else:
929
+ if keys:
930
+ key = self.normalize_key(keys[0])
931
+ value = self.normalize_value(value)
932
+ if key not in self:
933
+ self.add_node(key, self.create_instance(), value)
934
+
935
+ else:
936
+ self.update_node(
937
+ key,
938
+ super().__getitem__(key),
939
+ value
940
+ )
941
+
942
+ else:
943
+ # root of the tree
944
+ self.update_node(
945
+ self.key,
946
+ self,
947
+ self.normalize_value(value)
948
+ )
949
+
950
+ def add_node(self, key, node, value):
951
+ """Add `node` into `self` at `key` with `value`
952
+
953
+ NOTE -- this is never called for the root node since the root node
954
+ is never added into another node
955
+
956
+ :param key: Hashable, already ran through .normalize_key
957
+ :param node: ClasspathFinder, freshly created with .create_instance
958
+ :param value: Any, already ran through .normalize_value
959
+ """
960
+ node.parent = self
961
+ node.key = key
962
+ node.root = self.root
963
+ node.value = value
964
+ super().__setitem__(key, node)
965
+
966
+ def update_node(self, key, node, value):
967
+ """Update `node` of `self` at `key` with `value`
968
+
969
+ NOTE -- this only updates the value of node, the key is only here to
970
+ keep the signature the same as .add_node and self and node can be the
971
+ same node when the root node is being updated
972
+
973
+ :param key: Hashable, already ran through .normalize_key
974
+ :param node: ClasspathFinder, the node to update
975
+ :param value: Any, already ran through .normalize_value
976
+ """
977
+ node.value = value
978
+
979
+ def normalize_keys(self, keys):
980
+ """Internal method. This makes sure keys is a sequence
981
+
982
+ Called on both set and get
983
+
984
+ :param keys: list[hashable]|tuple[hashable, ...]|hashable
985
+ :returns: sequence[hashable]
986
+ """
987
+ if not isinstance(keys, (list, tuple)):
988
+ keys = [keys]
989
+
990
+ return keys
991
+
992
+ def normalize_key(self, key):
993
+ """Internal method. Hook that is called right before key is used
994
+ to access
995
+
996
+ Called on both set and get
997
+
998
+ :param key: Hashable
999
+ :returns: Hashable
1000
+ """
1001
+ return key
1002
+
1003
+ def normalize_value(self, value):
1004
+ """Internal method. Hook that is called right before value is set
1005
+ into the node.
1006
+
1007
+ Called *only* on set
1008
+
1009
+ :param value: Any
1010
+ :returns: Any
1011
+ """
1012
+ return value
1013
+
1014
+ def get_node(self, keys):
1015
+ """Gets the node found at keys, most of the magic happens in this
1016
+ method and is a key building block of all the other methods
1017
+
1018
+ This method is recursive
1019
+
1020
+ :param keys: see .normalize_key
1021
+ :returns: DictTree
1022
+ :raises: KeyError, if the key doesn't exist
1023
+ """
1024
+ keys = self.normalize_keys(keys)
1025
+ if keys:
1026
+ node = super().__getitem__(self.normalize_key(keys[0]))
1027
+
1028
+ if len(keys) > 1:
1029
+ node = node.get_node(keys[1:])
1030
+
1031
+ else:
1032
+ node = self
1033
+
1034
+ return node
1035
+
1036
+ def get(self, keys, default=None):
1037
+ """Get the value of the last key in keys, otherwise return default
1038
+
1039
+ :param keys: list[hashable]|hashable, the path to return
1040
+ :param default: Any, if value isn't found at the end of keys then
1041
+ return this value
1042
+ :returns: Any
1043
+ """
1044
+ try:
1045
+ return self[keys]
1046
+
1047
+ except KeyError:
1048
+ return default
1049
+
1050
+ def pop(self, keys, *default):
1051
+ """Pop a value from the tree
1052
+
1053
+ This does not trim the tree if it isn't a leaf, so if this is a
1054
+ waypoint value it will just be set to None and the branches will remain
1055
+ intact.
1056
+
1057
+ :param keys: Hashable|list[Hashable], As with .set and .get this allows
1058
+ a list of keys or key
1059
+ :param *default: Any, if present returned instead of rasing a KeyError
1060
+ :returns: Any, whatever was the value
1061
+ :raises: KeyError, if keys isn't found in the tree or if the node's
1062
+ value is None (meaning it didn't have a value)
1063
+ """
1064
+ try:
1065
+ node = self.get_node(keys)
1066
+
1067
+ if len(node) == 0:
1068
+ # we trim the node since it has no children
1069
+ node.parent.pop_node(node.key)
1070
+
1071
+ if node.value is None:
1072
+ raise KeyError(keys)
1073
+
1074
+ v = node.value
1075
+ node.value = None
1076
+ return v
1077
+
1078
+ except KeyError:
1079
+ if default:
1080
+ return default[0]
1081
+
1082
+ else:
1083
+ raise
1084
+
1085
+ def pop_node(self, keys, *default):
1086
+ """Pop the node and trim the tree
1087
+
1088
+ NOTE -- this trims the tree, so if you pop a tree node it will trim
1089
+ all the leaves of that node from the root tree. In fact, I first named
1090
+ this .trim, then .trim_node, but I think it is more cohesive to be
1091
+ named .pop_node, like .get/.get_node
1092
+
1093
+ :param keys: Hashable|list[Hashable], As with .set and .get this allows
1094
+ a list of keys or key
1095
+ :param *default: Any, if present returned instead of rasing a KeyError
1096
+ :returns: DictTree, the returned node is completely intact with all
1097
+ down stream keys and pointers but .root will no longer contain this
1098
+ node
1099
+ :raises: KeyError, if keys isn't found in the tree
1100
+ """
1101
+ keys = self.normalize_keys(keys)
1102
+
1103
+ try:
1104
+ if len(keys) > 1:
1105
+ return self.get_node(keys[:-1]).pop_node(keys[-1])
1106
+
1107
+ else:
1108
+ return super().pop(self.normalize_key(keys[-1]))
1109
+
1110
+ except KeyError:
1111
+ if default:
1112
+ return default[0]
1113
+
1114
+ else:
1115
+ raise
1116
+
1117
+ def setdefault(self, keys, default=None):
1118
+ """As with .set and .get this allows a list of keys or key"""
1119
+ if keys in self:
1120
+ return self[keys]
1121
+
1122
+ else:
1123
+ self.set(keys, default)
1124
+ return default
1125
+
1126
+ def trees(self, depth=-1):
1127
+ """Iterate through the trees in this tree
1128
+
1129
+ A tree is a node that contains other trees
1130
+
1131
+ :param depth: int, If above 0 only proceed to that depth. If -1 then
1132
+ don't worry about depth
1133
+ :returns: generator[tuple(list, DictTree)], index 0 is the path of keys
1134
+ to get to this tree, and empty list is the root tree. Index 1 is
1135
+ the tree
1136
+ """
1137
+ if depth > 0 or depth < 0:
1138
+ if len(self):
1139
+ yield [], self
1140
+
1141
+ for k, d in self.items():
1142
+ for sk, sd in d.trees(depth=depth-1):
1143
+ if sk:
1144
+ yield [k] + sk, sd
1145
+
1146
+ else:
1147
+ yield [k], sd
1148
+
1149
+ def leaves(self, depth=-1):
1150
+ """Iterate through the leaves in this tree
1151
+
1152
+ A leaf is a node that is the end of a tree
1153
+
1154
+ :param depth: int, see .trees
1155
+ :returns: generator[tuple(list, DictTree)], Index 0 is the path of
1156
+ keys. Index 1 is the value of the leaf node
1157
+ """
1158
+ for ks, tree in self.trees(depth=depth):
1159
+ for k, d in tree.items():
1160
+ if not len(d):
1161
+ yield ks + [k], d
1162
+
1163
+ def walk(self, keys, set_missing=False):
1164
+ """Walk each node of the tree in keys.
1165
+
1166
+ Basically, if you passed in ["foo", "bar", "che"] it would first
1167
+ yield the node at ["foo"], then ["foo", "bar"], and finally the
1168
+ node at ["foo", "bar", "che"]
1169
+
1170
+ :param keys: list[str]|str, the path key(s) to walk
1171
+ :returns: generator[DictTree]
1172
+ """
1173
+ keys = self.normalize_keys(keys)
1174
+
1175
+ d = self
1176
+ i = -(len(keys) - 1)
1177
+ while i < 1:
1178
+ if i < 0:
1179
+ ks = keys[:i]
1180
+
1181
+ else:
1182
+ ks = keys
1183
+
1184
+ i += 1
1185
+
1186
+ k = ks[-1]
1187
+ if k in d:
1188
+ d = d.get_node(k)
1189
+ yield ks, d
1190
+
1191
+ else:
1192
+ if set_missing:
1193
+ d.setdefault(k)
1194
+ d = d.get_node(k)
1195
+ yield ks, d
1196
+
1197
+ else:
1198
+ raise KeyError(k)
1199
+
1200
+ def nodes(self, depth=-1):
1201
+ """Iterate all the rows in depth order, so do the first row of keys,
1202
+ then the second row of keys, etc. It's similar to how you would
1203
+ recursively iterate a folder, first you do all the files in the
1204
+ root folder, then you do all the files of all the folders in the
1205
+ root folder, etc.
1206
+
1207
+ :param depth: int, see .trees
1208
+ :returns: generator[DictTree], each row of nodes starting from the
1209
+ root node and iteratin all keys of each level of nodes until the
1210
+ leaf nodes
1211
+ """
1212
+ if depth > 0 or depth < 0:
1213
+ yield [], self
1214
+
1215
+ nodes = []
1216
+ for k, d in self.items():
1217
+ yield [k], d
1218
+
1219
+ if len(d):
1220
+ nodes.append((k, d))
1221
+
1222
+ for k, n in nodes:
1223
+ for sk, sd in n.nodes(depth=depth-1):
1224
+ if sk:
1225
+ yield [k] + sk, sd
1226
+