Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
singlestore-labs
GitHub Repository: singlestore-labs/singlestoredb-python
Path: blob/main/singlestoredb/utils/xdict.py
469 views
1
#!/usr/bin/env python
2
#
3
# Copyright SAS Institute
4
#
5
# Licensed under the Apache License, Version 2.0 (the License);
6
# you may not use this file except in compliance with the License.
7
# You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
#
17
# This file originally copied from https://github.com/sassoftware/python-swat
18
#
19
"""Dictionary that allows setting nested keys by period (.) delimited strings."""
20
import copy
21
import re
22
from typing import Any
23
from typing import Dict
24
from typing import ItemsView
25
from typing import Iterable
26
from typing import KeysView
27
from typing import List
28
from typing import Tuple
29
from typing import ValuesView
30
31
32
def _is_compound_key(key: str) -> bool:
33
"""
34
Check for a compound key name.
35
36
Parameters
37
----------
38
key : string
39
The key name to check
40
41
Returns
42
-------
43
True
44
If the key is compound (i.e., contains a '.')
45
False
46
If the key is not compound
47
48
"""
49
return isinstance(key, str) and '.' in key
50
51
52
class xdict(Dict[str, Any]):
53
"""
54
Nested dictionary that allows setting of nested keys using '.' delimited strings.
55
56
Keys with a '.' in them automatically get split into separate keys.
57
Each '.' in a key represents another level of nesting in the resulting
58
dictionary.
59
60
Parameters
61
----------
62
*args, **kwargs : Arbitrary arguments and keyword arguments
63
Same arguments as `dict`
64
65
Returns
66
-------
67
xdict object
68
69
Examples
70
--------
71
>>> dct = xdict()
72
>>> dct['a.b.c'] = 100
73
{'a': {'b': {'c': 100}}}
74
75
"""
76
77
_dir: List[str]
78
79
def __init__(self, *args: Any, **kwargs: Any):
80
super(xdict, self).__init__()
81
self.update(*args, **kwargs)
82
83
def __dir__(self) -> Iterable[str]:
84
"""Return keys in the dict."""
85
if hasattr(self, '_dir') and self._dir:
86
return list(self._dir)
87
return super(xdict, self).__dir__()
88
89
def set_dir_values(self, values: List[str]) -> None:
90
"""
91
Set the valid values for keys to display in tab-completion.
92
93
Parameters
94
----------
95
values : list of strs
96
The values to display
97
98
"""
99
super(xdict, self).__setattr__('_dir', values)
100
101
def set_doc(self, docstring: str) -> None:
102
"""Set the docstring for the xdict."""
103
super(xdict, self).__setattr__('__doc__', docstring)
104
105
def __copy__(self) -> 'xdict':
106
"""Return a copy of self."""
107
return type(self)(**self)
108
109
def __deepcopy__(self, memo: Any) -> 'xdict':
110
"""Return a deep copy of self."""
111
out = type(self)()
112
for key, value in self.items():
113
if isinstance(value, (dict, list, tuple, set)):
114
value = copy.deepcopy(value)
115
out[key] = value
116
return out
117
118
@classmethod
119
def from_json(cls, jsonstr: str) -> 'xdict':
120
"""
121
Create an xdict object from a JSON string.
122
123
Parameters
124
----------
125
jsonstr : string
126
Valid JSON string that represents an object
127
128
Returns
129
-------
130
xdict object
131
132
"""
133
import json
134
out = cls()
135
out.update(json.loads(jsonstr))
136
return out
137
138
def __setitem__(self, key: str, value: Any) -> Any:
139
"""Set a key/value pair in an xdict object."""
140
if isinstance(value, dict) and not isinstance(value, type(self)):
141
value = type(self)(value)
142
if _is_compound_key(key):
143
return self._xset(key, value)
144
return super(xdict, self).__setitem__(key, value)
145
146
def _xset(self, key: str, value: Any) -> Any:
147
"""
148
Set a key/value pair allowing nested levels in the key.
149
150
Parameters
151
----------
152
key : any
153
Key value, if it is a string delimited by periods (.), each
154
period represents another level of nesting of xdict objects.
155
value : any
156
Data value
157
158
"""
159
if isinstance(value, dict) and not isinstance(value, type(self)):
160
value = type(self)(value)
161
if _is_compound_key(key):
162
current, key = key.split('.', 1)
163
if current not in self:
164
self[current] = type(self)()
165
return self[current]._xset(key, value)
166
self[key] = value
167
return None
168
169
def setdefault(self, key: str, *default: Any) -> Any:
170
"""Return keyed value, or set it to `default` if missing."""
171
if _is_compound_key(key):
172
try:
173
return self[key]
174
except KeyError:
175
if default:
176
new_default = default[0]
177
if isinstance(default, dict) and not isinstance(default, type(self)):
178
new_default = type(self)(default)
179
else:
180
new_default = None
181
self[key] = new_default
182
return new_default
183
return super(xdict, self).setdefault(key, *default)
184
185
def __contains__(self, key: object) -> bool:
186
"""Does the xdict contain `key`?."""
187
if super(xdict, self).__contains__(key):
188
return True
189
return key in self.allkeys()
190
191
has_key = __contains__
192
193
def __getitem__(self, key: str) -> Any:
194
"""Get value stored at `key`."""
195
if _is_compound_key(key):
196
return self._xget(key)
197
return super(xdict, self).__getitem__(key)
198
199
def _xget(self, key: str, *default: Any) -> Any:
200
"""
201
Return keyed value, or `default` if missing
202
203
Parameters
204
----------
205
key : any
206
Key to look up
207
*default : any
208
Default value to return if key is missing
209
210
Returns
211
-------
212
Any
213
214
"""
215
if _is_compound_key(key):
216
current, key = key.split('.', 1)
217
try:
218
return self[current]._xget(key)
219
except KeyError:
220
if default:
221
return default[0]
222
raise KeyError(key)
223
return self[key]
224
225
def get(self, key: str, *default: Any) -> Any:
226
"""Return keyed value, or `default` if missing."""
227
if _is_compound_key(key):
228
return self._xget(key, *default)
229
return super(xdict, self).get(key, *default)
230
231
def __delitem__(self, key: str) -> Any:
232
"""Deleted keyed item."""
233
if _is_compound_key(key):
234
return self._xdel(key)
235
super(xdict, self).__delitem__(key)
236
237
def _xdel(self, key: str) -> Any:
238
"""
239
Delete keyed item.
240
241
Parameters
242
----------
243
key : any
244
Key to delete. If it is a string that is period (.) delimited,
245
each period represents another level of nesting of xdict objects.
246
247
"""
248
if _is_compound_key(key):
249
current, key = key.split('.', 1)
250
try:
251
return self[current]._xdel(key)
252
except KeyError:
253
raise KeyError(key)
254
del self[key]
255
256
def pop(self, key: str, *default: Any) -> Any:
257
"""Remove and return value stored at `key`."""
258
try:
259
out = self[key]
260
del self[key]
261
return out
262
except KeyError:
263
if default:
264
return default[0]
265
raise KeyError(key)
266
267
def _flatten(
268
self,
269
dct: Dict[str, Any],
270
output: Dict[str, Any],
271
prefix: str = '',
272
) -> None:
273
"""
274
Create a new dict with keys flattened to period (.) delimited keys
275
276
Parameters
277
----------
278
dct : dict
279
The dictionary to flatten
280
output : dict
281
The resulting dictionary (used internally in recursion)
282
prefix : string
283
Key prefix built from upper levels of nesting
284
285
Returns
286
-------
287
dict
288
289
"""
290
if prefix:
291
prefix = prefix + '.'
292
for key, value in dct.items():
293
if isinstance(value, dict):
294
if isinstance(key, int):
295
intkey = '%s[%s]' % (re.sub(r'\.$', r'', prefix), key)
296
self._flatten(value, prefix=intkey, output=output)
297
else:
298
self._flatten(value, prefix=prefix + key, output=output)
299
else:
300
if isinstance(key, int):
301
intkey = '%s[%s]' % (re.sub(r'\.$', r'', prefix), key)
302
output[intkey] = value
303
else:
304
output[prefix + key] = value
305
306
def flattened(self) -> Dict[str, Any]:
307
"""Return an xdict with keys flattened to period (.) delimited strings."""
308
output: Dict[str, Any] = {}
309
self._flatten(self, output)
310
return output
311
312
def allkeys(self) -> List[str]:
313
"""Return a list of all possible keys (even sub-keys) in the xdict."""
314
out = set()
315
for key in self.flatkeys():
316
out.add(key)
317
while '.' in key:
318
key = key.rsplit('.', 1)[0]
319
out.add(key)
320
if '[' in key:
321
out.add(re.sub(r'\[\d+\]', r'', key))
322
return list(out)
323
324
def flatkeys(self) -> List[str]:
325
"""Return a list of flattened keys in the xdict."""
326
return list(self.flattened().keys())
327
328
def flatvalues(self) -> List[Any]:
329
"""Return a list of flattened values in the xdict."""
330
return list(self.flattened().values())
331
332
def flatitems(self) -> List[Tuple[str, Any]]:
333
"""Return tuples of flattened key/value pairs."""
334
return list(self.flattened().items())
335
336
def iterflatkeys(self) -> Iterable[str]:
337
"""Return iterator of flattened keys."""
338
return iter(self.flattened().keys())
339
340
def iterflatvalues(self) -> Iterable[Any]:
341
"""Return iterator of flattened values."""
342
return iter(self.flattened().values())
343
344
def iterflatitems(self) -> Iterable[Tuple[str, Any]]:
345
"""Return iterator of flattened items."""
346
return iter(self.flattened().items())
347
348
def viewflatkeys(self) -> KeysView[str]:
349
"""Return view of flattened keys."""
350
return self.flattened().keys()
351
352
def viewflatvalues(self) -> ValuesView[Any]:
353
"""Return view of flattened values."""
354
return self.flattened().values()
355
356
def viewflatitems(self) -> ItemsView[str, Any]:
357
"""Return view of flattened items."""
358
return self.flattened().items()
359
360
def update(self, *args: Any, **kwargs: Any) -> None:
361
"""Merge the key/value pairs into `self`."""
362
for arg in args:
363
if isinstance(arg, dict):
364
for key, value in arg.items():
365
self._xset(key, value)
366
else:
367
for key, value in arg:
368
self._xset(key, value)
369
for key, value in kwargs.items():
370
self._xset(key, value)
371
372
def to_json(self) -> str:
373
"""
374
Convert an xdict object to a JSON string.
375
376
Returns
377
-------
378
str
379
380
"""
381
import json
382
return json.dumps(self)
383
384
385
class xadict(xdict):
386
"""An xdict that also allows setting/getting/deleting keys as attributes."""
387
388
getdoc = None
389
trait_names = None
390
391
def _getAttributeNames(self) -> None:
392
"""Block this from creating attributes."""
393
return
394
395
def __delattr__(self, key: str) -> Any:
396
"""Delete the attribute stored at `key`."""
397
if key.startswith('_') and key.endswith('_'):
398
return super(xadict, self).__delattr__(key)
399
del self[key]
400
401
def __getattr__(self, key: str) -> Any:
402
"""Get the attribute store at `key`."""
403
if key.startswith('_') and key.endswith('_'):
404
return super(xadict, self).__getattr__(key) # type: ignore
405
try:
406
return self[key]
407
except KeyError:
408
dct = type(self)()
409
self[key] = dct
410
return dct
411
return None
412
413
def __getitem__(self, key: str) -> Any:
414
"""Get item of an integer creates a new dict."""
415
if isinstance(key, int) and key not in self:
416
out = type(self)()
417
self[key] = out
418
return out
419
return super(xadict, self).__getitem__(key)
420
421
def __setattr__(self, key: str, value: Any) -> Any:
422
"""Set the attribute stored at `key`."""
423
if key.startswith('_') and key.endswith('_'):
424
return super(xadict, self).__setattr__(key, value)
425
self[key] = value
426
427