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.
- datatypes/__init__.py +169 -0
- datatypes/collections/__init__.py +25 -0
- datatypes/collections/container.py +248 -0
- datatypes/collections/mapping.py +1226 -0
- datatypes/collections/sequence.py +311 -0
- datatypes/command.py +596 -0
- datatypes/compat.py +206 -0
- datatypes/config/__init__.py +13 -0
- datatypes/config/environ/__init__.py +33 -0
- datatypes/config/environ/base.py +332 -0
- datatypes/config/environ/token.py +173 -0
- datatypes/config/parser.py +256 -0
- datatypes/config/settings.py +352 -0
- datatypes/copy.py +151 -0
- datatypes/csv.py +547 -0
- datatypes/datetime.py +1243 -0
- datatypes/decorators/__init__.py +31 -0
- datatypes/decorators/base.py +449 -0
- datatypes/decorators/descriptor.py +490 -0
- datatypes/decorators/misc.py +152 -0
- datatypes/email.py +439 -0
- datatypes/enum.py +176 -0
- datatypes/event.py +299 -0
- datatypes/exception.py +13 -0
- datatypes/filetypes/__init__.py +10 -0
- datatypes/filetypes/html.py +540 -0
- datatypes/filetypes/toml.py +275 -0
- datatypes/http.py +1443 -0
- datatypes/logging.py +475 -0
- datatypes/number.py +586 -0
- datatypes/parse.py +362 -0
- datatypes/path.py +4358 -0
- datatypes/profile.py +246 -0
- datatypes/reflection/__init__.py +24 -0
- datatypes/reflection/inspect.py +3475 -0
- datatypes/reflection/other.py +843 -0
- datatypes/reflection/path.py +639 -0
- datatypes/server.py +642 -0
- datatypes/service.py +212 -0
- datatypes/ssh.py +812 -0
- datatypes/string.py +1632 -0
- datatypes/token/__init__.py +16 -0
- datatypes/token/abnf.py +1696 -0
- datatypes/token/base.py +921 -0
- datatypes/token/word.py +259 -0
- datatypes/typing.py +126 -0
- datatypes/url.py +988 -0
- datatypes/utils.py +179 -0
- datatypes-0.31.0.dist-info/METADATA +38 -0
- datatypes-0.31.0.dist-info/RECORD +53 -0
- datatypes-0.31.0.dist-info/WHEEL +5 -0
- datatypes-0.31.0.dist-info/licenses/LICENSE.txt +21 -0
- 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
|
+
|