batutils 2.0.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 (73) hide show
  1. batutils/__init__.py +5 -0
  2. batutils/_common/__init__.py +6 -0
  3. batutils/_common/_decode.py +40 -0
  4. batutils/dtu/__init__.py +19 -0
  5. batutils/dtu/casting/__init__.py +5 -0
  6. batutils/dtu/casting/_any.py +136 -0
  7. batutils/dtu/casting/_filetime.py +36 -0
  8. batutils/dtu/casting/_instant.py +31 -0
  9. batutils/dtu/casting/_string.py +81 -0
  10. batutils/dtu/casting/_timestamp.py +50 -0
  11. batutils/dtu/formatting/__init__.py +2 -0
  12. batutils/dtu/formatting/_delta.py +62 -0
  13. batutils/dtu/formatting/_serialize.py +26 -0
  14. batutils/dtu/parsers/__init__.py +2 -0
  15. batutils/dtu/parsers/_string.py +44 -0
  16. batutils/dtu/parsers/_timedelta.py +161 -0
  17. batutils/dtu/shortcuts/__init__.py +3 -0
  18. batutils/dtu/shortcuts/_aliases.py +29 -0
  19. batutils/dtu/shortcuts/_checkers.py +21 -0
  20. batutils/dtu/shortcuts/_finders.py +80 -0
  21. batutils/dtu/structs/__init__.py +5 -0
  22. batutils/dtu/structs/_base.py +5 -0
  23. batutils/dtu/structs/_deltasplit.py +278 -0
  24. batutils/dtu/structs/_types.py +24 -0
  25. batutils/dtu/structs/aliases.py +21 -0
  26. batutils/dtu/structs/consts.py +35 -0
  27. batutils/structs/__init__.py +7 -0
  28. batutils/structs/_base.py +21 -0
  29. batutils/tpu/__init__.py +0 -0
  30. batutils/tpu/casting/__init__.py +9 -0
  31. batutils/tpu/casting/_currency.py +52 -0
  32. batutils/tpu/casting/_evals.py +247 -0
  33. batutils/tpu/casting/_ip.py +58 -0
  34. batutils/tpu/casting/_mask.py +34 -0
  35. batutils/tpu/casting/_params.py +108 -0
  36. batutils/tpu/casting/_serialize.py +20 -0
  37. batutils/tpu/comparison/__init__.py +1 -0
  38. batutils/tpu/comparison/_mixin.py +30 -0
  39. batutils/tpu/insp/__init__.py +2 -0
  40. batutils/tpu/insp/_insp_old.py +347 -0
  41. batutils/tpu/insp/_typecheck.py +89 -0
  42. batutils/tpu/iters/__init__.py +4 -0
  43. batutils/tpu/iters/_attrs.py +84 -0
  44. batutils/tpu/iters/_dicts.py +130 -0
  45. batutils/tpu/iters/_group.py +50 -0
  46. batutils/tpu/iters/_lists.py +178 -0
  47. batutils/tpu/size/__init__.py +1 -0
  48. batutils/tpu/size/_total.py +52 -0
  49. batutils/tpu/strimp/__init__.py +1 -0
  50. batutils/tpu/strimp/_getters.py +285 -0
  51. batutils/tpu/string/__init__.py +1 -0
  52. batutils/tpu/string/_string.py +160 -0
  53. batutils/tpu/structs/__init__.py +1 -0
  54. batutils/tpu/structs/_base.py +3 -0
  55. batutils/tpu/structs/empty/__init__.py +1 -0
  56. batutils/tpu/structs/empty/_empty.py +387 -0
  57. batutils/tpu/structs/enumex/__init__.py +1 -0
  58. batutils/tpu/structs/enumex/_enumextra.py +25 -0
  59. batutils/tpu/structs/fluid/__init__.py +1 -0
  60. batutils/tpu/structs/fluid/_attrfluid.py +77 -0
  61. batutils/tpu/structs/sentience/__init__.py +1 -0
  62. batutils/tpu/structs/sentience/_builder.py +87 -0
  63. batutils/tpu/structs/serializers/__init__.py +3 -0
  64. batutils/tpu/structs/serializers/_json.py +12 -0
  65. batutils/tpu/structs/serializers/_jsonsafe.py +69 -0
  66. batutils/tpu/structs/serializers/_universal.py +318 -0
  67. batutils/tpu/structs/singleton/__init__.py +1 -0
  68. batutils/tpu/structs/singleton/_singleton.py +35 -0
  69. batutils-2.0.0.dist-info/LICENSE +22 -0
  70. batutils-2.0.0.dist-info/METADATA +125 -0
  71. batutils-2.0.0.dist-info/RECORD +73 -0
  72. batutils-2.0.0.dist-info/WHEEL +5 -0
  73. batutils-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,347 @@
1
+ __all__ = [
2
+ 'is_methfunc',
3
+ 'is_callable',
4
+ 'get_argspec',
5
+ 'fits_callable_profile',
6
+ 'get_any_name',
7
+ 'get_first_attr',
8
+ 'resolve_attr',
9
+ 'resolve_first',
10
+ ]
11
+
12
+ from batutils.tpu.structs import empty
13
+ import inspect
14
+ from typing import *
15
+
16
+ _NOT_SUPPLIED = object()
17
+
18
+
19
+ def is_methfunc(function_or_method):
20
+ """Checks if the given parameter is a function/method or not.
21
+
22
+ This simply joins python's inspect.isfunction and inspect.ismethod in one
23
+ call.
24
+
25
+ A function is:
26
+ - Anything declared via def on the top level of a module (or inside
27
+ another function or method)
28
+ - Anything created with lambda
29
+ - Any method defined in a class with the @staticmethod decorator
30
+ A method is:
31
+ - Anything declared via def inside a class definition (except those
32
+ that have the @staticmethod decorator)
33
+
34
+ :param function_or_method: The thing to check
35
+ :type function_or_method: function or method or any
36
+ :return: True if the given parameter is a function or a method, false if not
37
+ :rtype: bool
38
+ """
39
+ return inspect.isfunction(function_or_method) or inspect.ismethod(function_or_method)
40
+
41
+
42
+ def is_callable(something):
43
+ """Checks if the given parameter is callable or not.
44
+
45
+ Callable means a function, method or object of a class that implements the
46
+ __call__ method and can therefor be called like a function.
47
+
48
+ See is_methfunc() for more details.
49
+
50
+ :param something: The thing to check
51
+ :type something: function or method or object or any
52
+ :return: True if the given parameter is callable, false if not
53
+ :rtype: bool
54
+ """
55
+ if is_methfunc(something):
56
+ return True
57
+ else:
58
+ call_func = getattr(something, '__call__', None)
59
+ if call_func:
60
+ return is_methfunc(call_func)
61
+ return False
62
+
63
+
64
+ def get_argspec(somecallable):
65
+ """Gets the argument specifications of a callable.
66
+
67
+ This simply extends the inspect.getargspec function to also work for
68
+ objects of classes that implement the __call__ method.
69
+
70
+ Returns None if the given parameter isn't callable.
71
+
72
+ :param somecallable: The thing to fetch the argument spec for
73
+ :type somecallable: function or method or object or any
74
+ :return: The argspecs or None if the given parameter wasn't callable
75
+ :rtype: inspect.ArgSpec or collections.namedtuple or None
76
+ """
77
+ if is_methfunc(somecallable):
78
+ return inspect.getfullargspec(somecallable)
79
+ else:
80
+ call_func = getattr(somecallable, '__call__', None)
81
+ if call_func:
82
+ if is_methfunc(call_func):
83
+ return inspect.getfullargspec(call_func)
84
+ return None
85
+
86
+
87
+ def fits_callable_profile(somecallable, required_arg_count=None,
88
+ total_arg_count=None, default_count=None,
89
+ has_varargs=None, has_kwargs=None, arg_names=None,
90
+ raise_error=False):
91
+ """Checks if a callable (function, method or object with __call__) fits
92
+ the given argument profile.
93
+
94
+ Returns False if the given parameter to check is not callable.
95
+
96
+ All check parameters default to None which skips that particular check.
97
+
98
+ Required arguments means total number of arguments minus the number of
99
+ arguments with default values.
100
+
101
+ If the arg_names parameter is a list (NOT a tuple), this will only check
102
+ if the argument names there in are present regardless of order.
103
+
104
+ If the arg_names parameter is a tuple (NOT a list), this will check if the
105
+ order pf the argument names there in are present and in the same order
106
+ although you can use '*' in the tuple to skip the check for the argument
107
+ in that place (e.g. if you don't care what the "self" parameter in a bound
108
+ method is called but you need a certain argument set to be present in order
109
+ after that). The arg_names tuple can be shorter than the number of
110
+ arguments in the callable though without that failing (use the argument
111
+ count checks if that is needed).
112
+
113
+ :param somecallable: The thing to check
114
+ :type somecallable: function or method or object or any
115
+ :param required_arg_count: The number of required arguments (if given),
116
+ either the exact number or a 2-tuple with
117
+ the minimum and maximum allowed number.
118
+ :type required_arg_count: int or (int, int) or None
119
+ :param total_arg_count: The total number of arguments (if given), either
120
+ the exact number or a 2-tuple with the minimum
121
+ and maximum allowed number.
122
+ :type total_arg_count: int or (int, int) or None
123
+ :param default_count: The number of arguments with default values (if
124
+ given), either the exact number or a 2-tuple with
125
+ the minimum and maximum allowed number.
126
+ :type default_count: int or (int, int) or None
127
+ :param has_varargs: Should the callable accept a variable number of
128
+ arguments (*args)? If this is a string, then
129
+ the args argument must be called that to pass.
130
+ :type has_varargs: bool or str or None
131
+ :param has_kwargs: Should the callable accept a variable number of
132
+ named arguments (**kwargs)? If this is a string, then
133
+ the kwargs argument must be called that to pass.
134
+ :type has_kwargs: bool or str or None
135
+ :param arg_names: A list of argument names the callable should have or a
136
+ tuple of argument names the callable should have in that
137
+ exact order (using "*" to skip individual name checks).
138
+ :type arg_names: list[str] or tuple[str] or None
139
+ :param raise_error: Should an ValueError be raised if a check fails
140
+ (with the details)?
141
+ :type raise_error: bool
142
+ :return: True if the given callable fits all the given profile checks,
143
+ False if not.
144
+ :rtype: bool
145
+ """
146
+ # TODO(thordurm@ccpgames.com) 2024-03-20: This could probably use a "modern"
147
+ # overhaul and to include type annotations and such!... I think this was
148
+ # written for Python 2.7 :D
149
+ specs = get_argspec(somecallable)
150
+ if not specs:
151
+ if raise_error:
152
+ raise ValueError('Not a callable!')
153
+ return False
154
+ def_count = 0
155
+ if specs.defaults:
156
+ def_count = len(specs.defaults)
157
+ arg_count = 0
158
+ if specs.args:
159
+ arg_count = len(specs.args)
160
+ req_args = arg_count - def_count
161
+
162
+ if required_arg_count is not None:
163
+ if isinstance(required_arg_count, (list, tuple)):
164
+ mi = required_arg_count[0]
165
+ ma = required_arg_count[1]
166
+ else:
167
+ mi = required_arg_count
168
+ ma = required_arg_count
169
+ if not mi <= req_args <= ma:
170
+ if raise_error:
171
+ raise ValueError('Required arguments check fail: min=%s >= count=%s >= max=%s' % (mi, req_args, ma))
172
+ return False # Required argument count fail! :(
173
+
174
+ if total_arg_count is not None:
175
+ if isinstance(total_arg_count, (list, tuple)):
176
+ mi = total_arg_count[0]
177
+ ma = total_arg_count[1]
178
+ else:
179
+ mi = total_arg_count
180
+ ma = total_arg_count
181
+ if not mi <= arg_count <= ma:
182
+ if raise_error:
183
+ raise ValueError('Arguments count check fail: min=%s >= count=%s >= max=%s' % (mi, arg_count, ma))
184
+ return False # Total argument count fail! :(
185
+
186
+ if default_count is not None:
187
+ if isinstance(default_count, (list, tuple)):
188
+ mi = default_count[0]
189
+ ma = default_count[1]
190
+ else:
191
+ mi = default_count
192
+ ma = default_count
193
+ if not mi <= def_count <= ma:
194
+ if raise_error:
195
+ raise ValueError('Default argument check fail: min=%s >= count=%s >= max=%s' % (mi, def_count, ma))
196
+ return False # Default argument count fail! :(
197
+
198
+ if has_varargs is not None:
199
+ if isinstance(has_varargs, str):
200
+ if has_varargs != specs.varargs:
201
+ if raise_error:
202
+ raise ValueError('Variable arguments name fail: is=%s, should be=%s' % (specs.varargs, has_varargs))
203
+ return False # Has variable arguments fail! :(
204
+ if bool(has_varargs) != bool(specs.varargs):
205
+ if raise_error:
206
+ raise ValueError('Variable arguments fail: is=%s, should be=%s' % (bool(specs.varargs), bool(has_varargs)))
207
+ return False # Has variable arguments fail! :(
208
+
209
+ if has_kwargs is not None:
210
+ kwarg_name = specs.varkw
211
+ if isinstance(has_kwargs, str):
212
+ # if has_kwargs != specs.keywords:
213
+ if has_kwargs != kwarg_name:
214
+ if raise_error:
215
+ raise ValueError('Keyword arguments name fail: is=%s, should be=%s' % (kwarg_name, has_kwargs))
216
+ return False # Has variable keyword arguments fail! :(
217
+ # if bool(has_kwargs) != bool(specs.keywords):
218
+ if bool(has_kwargs) != bool(kwarg_name):
219
+ if raise_error:
220
+ raise ValueError('Keyword arguments fail: is=%s, should be=%s' % (bool(kwarg_name), bool(has_kwargs)))
221
+ return False # Has variable keyword arguments fail! :(
222
+
223
+ if arg_names:
224
+ if not specs.args:
225
+ if raise_error:
226
+ raise ValueError('Named arguments fail: No named arguments found')
227
+ return False # Has argument name fail! :(
228
+ if isinstance(arg_names, list): # Order doesn't matter!
229
+ for arg_name in arg_names:
230
+ if arg_name not in specs.args:
231
+ if raise_error:
232
+ raise ValueError('Named arguments fail: Argument "%s" not found' % arg_name)
233
+ return False # Missing required argument! :(
234
+ elif isinstance(arg_names, tuple):
235
+ for i, arg_name in enumerate(arg_names):
236
+ if specs.args[i] != arg_name and arg_name != '*':
237
+ if raise_error:
238
+ raise ValueError('Named arguments fail: Argument "%s" not found in correct order' % arg_name)
239
+ return False # Missing required argument in order! :(
240
+ else:
241
+ if raise_error:
242
+ raise ValueError('Named arguments fail: arg_names must be a list or tuple')
243
+ return False
244
+
245
+ return True # Looks good! :D
246
+
247
+
248
+ def get_any_name(anything: Any) -> str:
249
+ if anything is None:
250
+ return 'None'
251
+
252
+ if anything is empty.Empty:
253
+ return f'{get_any_name(empty)}.Empty'
254
+
255
+ if inspect.ismodule(anything):
256
+ return getattr(anything, '__name__', f'?m:{anything!r}?')
257
+
258
+ if isinstance(anything, property):
259
+ anything = anything.fget
260
+
261
+ if inspect.isfunction(anything) or inspect.ismethod(anything):
262
+ module = getattr(anything, '__module__', None)
263
+ if module and module != 'builtins':
264
+ return f"{module}.{getattr(anything, '__qualname__', getattr(anything, '__name__', '?f1:noname?'))}"
265
+ else:
266
+ return f"{getattr(anything, '__qualname__', getattr(anything, '__name__', '?f1:noname?'))}"
267
+
268
+ cls = None
269
+ module = None
270
+ if isinstance(anything, Type): # noqa
271
+ # Got a class/type/metaclass etc.
272
+ cls = anything
273
+
274
+ else:
275
+ cls = getattr(anything, '__class__', None)
276
+
277
+ if not cls:
278
+ return f'?{anything!r}?' # Worth a shot!
279
+
280
+ name = getattr(cls, '__name__', None)
281
+ if not name:
282
+ name = f'??{cls!r}??'
283
+
284
+ module = getattr(cls, '__module__', None)
285
+ if module == 'builtins':
286
+ module = ''
287
+
288
+ if module is None:
289
+ module = '?nomodule?'
290
+
291
+ if module:
292
+ return f'{module}.{name}'
293
+
294
+ else:
295
+ return f'{name}'
296
+
297
+
298
+ def get_first_attr(obj: Any, list_of_attributes: Union[str, List[str]], default: Any = None,
299
+ skip_nones: bool = False) -> Any:
300
+ """Gets the first attribute of an object found from the given list of
301
+ attribute names.
302
+
303
+ This is similar to looping through a list using `getattr` although this will
304
+ also work on dicts and dict like mapping objects.
305
+ """
306
+ from batutils.tpu.iters import any_getter
307
+ for attr_name in list_of_attributes:
308
+ attr = any_getter(obj, attr_name, _NOT_SUPPLIED)
309
+ if attr != _NOT_SUPPLIED:
310
+ if skip_nones and attr is None:
311
+ continue
312
+ else:
313
+ return attr
314
+
315
+ return default
316
+
317
+
318
+ def resolve_attr(attr: Union[Callable, property, Any]) -> Any:
319
+ """Resolves an attribute that may be a property or something callable to
320
+ it's post-call value and returns.
321
+
322
+ If the attr given is not a property or a callable entity, it's assumed to
323
+ be a proper value to begin with and is returned unchanged.
324
+ """
325
+ if isinstance(attr, property):
326
+ attr = attr.fget
327
+
328
+ if callable(attr):
329
+ return attr()
330
+
331
+ return attr
332
+
333
+
334
+ def resolve_first(obj: Any, list_of_attributes: Union[str, List[str]], default: Any = None,
335
+ skip_nones: bool = False) -> Any:
336
+ """Resolves the first attribute of an object found from the given list of
337
+ attribute names that may be a property or something callable to it's
338
+ post-call value and returns.
339
+
340
+ Basically just a shortcut for using `get_first_attr` and `resolve_attr` at
341
+ once.
342
+ """
343
+ attr = get_first_attr(obj, list_of_attributes, _NOT_SUPPLIED, skip_nones=skip_nones)
344
+ if attr == _NOT_SUPPLIED:
345
+ return default
346
+ else:
347
+ return resolve_attr(attr)
@@ -0,0 +1,89 @@
1
+ __all__ = [
2
+ 'check_type',
3
+ ]
4
+ from typing import *
5
+ import typing
6
+ import sys
7
+ import logging
8
+ log = logging.getLogger(__name__)
9
+
10
+ _POST_39 = sys.version_info >= (3, 9)
11
+
12
+
13
+ def check_type(val: Any, type_or_annotation: Any) -> bool:
14
+ def _is_special(toa: Any) -> bool:
15
+ if _POST_39:
16
+ if isinstance(toa, typing._SpecialGenericAlias): # noqa
17
+ return True
18
+ else:
19
+ return toa._special # noqa
20
+
21
+ if val is None and type_or_annotation is None:
22
+ return True
23
+
24
+ m = getattr(type_or_annotation, '__module__', None)
25
+ if m == 'typing':
26
+ if type_or_annotation == Any:
27
+ return True
28
+ if isinstance(type_or_annotation, typing._GenericAlias): # noqa
29
+ if _is_special(type_or_annotation): # noqa Not subscripted
30
+ return isinstance(val, type_or_annotation.__origin__)
31
+
32
+ else:
33
+ if type_or_annotation.__origin__ == Union:
34
+ if not type_or_annotation.__args__:
35
+ return True
36
+ if Any in type_or_annotation.__args__:
37
+ return True
38
+ return isinstance(val, type_or_annotation.__args__)
39
+
40
+ elif type_or_annotation.__origin__ in (list, set): # Check list and set
41
+ if not isinstance(val, type_or_annotation.__origin__):
42
+ return False
43
+ if not type_or_annotation.__args__ or Any in type_or_annotation.__args__:
44
+ return True
45
+ for sub_val in val:
46
+ if not check_type(sub_val, Union[type_or_annotation.__args__]):
47
+ return False
48
+ return True # Should be good now! :)
49
+
50
+ elif type_or_annotation.__origin__ == tuple: # Check tuple!
51
+ if not isinstance(val, tuple):
52
+ return False
53
+ if not type_or_annotation.__args__:
54
+ return True
55
+
56
+ if type_or_annotation.__args__[-1] is ... and len(type_or_annotation.__args__) == 2:
57
+ for sub_val in val:
58
+ if not check_type(sub_val, Union[type_or_annotation.__args__[0]]):
59
+ return False
60
+ return True # Should be good now! :)
61
+
62
+ if len(type_or_annotation.__args__) != len(val):
63
+ return False
64
+
65
+ for i, sub_val in enumerate(val):
66
+ if not check_type(sub_val, Union[type_or_annotation.__args__[i]]):
67
+ return False
68
+
69
+ return True # Should be good now! :)
70
+
71
+ elif issubclass(type_or_annotation.__origin__, Mapping):
72
+ if not isinstance(val, type_or_annotation.__origin__):
73
+ return False
74
+ if not type_or_annotation.__args__ or type_or_annotation.__args__ == (Any, Any):
75
+ return True
76
+
77
+ for k, v in val.items():
78
+ if not (check_type(k, Union[type_or_annotation.__args__[0]]) and check_type(v, Union[type_or_annotation.__args__[1]])):
79
+ return False
80
+ return True
81
+ else:
82
+ log.warning('I do not know how to check type %r of typing module:(', type_or_annotation)
83
+
84
+ if isinstance(type_or_annotation, TypeVar):
85
+ # TODO(thordurm@ccpgames.com>): Not really tested and supported yet officially...
86
+ # TODO(thordurm@ccpgames.com>): Main issue is nested types (if they don't flatten)
87
+ return check_type(val, Union[type_or_annotation.__constraints__])
88
+
89
+ return isinstance(val, type_or_annotation)
@@ -0,0 +1,4 @@
1
+ from ._lists import *
2
+ from ._dicts import *
3
+ from ._attrs import *
4
+ from ._group import *
@@ -0,0 +1,84 @@
1
+ __all__ = [
2
+ 'any_getter',
3
+ 'any_setter',
4
+ 'nested_get',
5
+ 'nested_set',
6
+ ]
7
+ from batutils.tpu.structs import *
8
+
9
+
10
+ def any_getter(obj: Any, key: Union[str, int], default: Any = None, eval_call: bool = False) -> Optional[Any]:
11
+ if isinstance(key, int):
12
+ try:
13
+ val = obj[key]
14
+ except (IndexError, KeyError):
15
+ return default
16
+ else:
17
+ if isinstance(obj, Mapping):
18
+ val = obj.get(key, default)
19
+ else:
20
+ val = getattr(obj, key, default)
21
+ if eval_call:
22
+ if val and isinstance(val, Callable):
23
+ val = val()
24
+ return val
25
+
26
+
27
+ def any_setter(obj: Any, key: Union[str, int], value: Any):
28
+ if isinstance(obj, MutableSequence):
29
+ obj[key] = value
30
+
31
+ elif isinstance(obj, MutableMapping):
32
+ obj[key] = value
33
+
34
+ elif isinstance(key, int):
35
+ obj[key] = value
36
+
37
+ else:
38
+ setattr(obj, key, value)
39
+
40
+
41
+ def nested_get(obj: Union[Mapping, Sequence],
42
+ key_list: Sequence[Union[str, int]],
43
+ default: Any = None) -> Any:
44
+ """Given a nested Mapping, Sequence or any indexable/subscriptable object of
45
+ other such, and a Sequence of keys and/or indexes, fetches the object at the
46
+ end of the Sequence of keys.
47
+
48
+ Returns the given default value if any key/index isn't found (defaults to
49
+ None if nothing is given).
50
+
51
+ Example:
52
+ >>> nested_get({'a': {7: {'c': ['apple', 'banana', 'cantaloupe']}, 5: 'other'}}, ('a', 7, 'c', -2))
53
+ 'banana'
54
+ """
55
+ next_obj = obj
56
+ for key in key_list:
57
+ next_obj = any_getter(next_obj, key, None)
58
+ if next_obj is None:
59
+ return default
60
+ return next_obj
61
+
62
+
63
+ def nested_set(obj: Union[Mapping, Sequence],
64
+ key_list: Sequence[Union[str, int]],
65
+ value: Any):
66
+ """Given a nested Mapping, Sequence or any indexable/subscriptable object of
67
+ other such, and a Sequence of keys and/or indexes, sets the value the those
68
+ keys/indexes point to.
69
+
70
+ If any key/index doesn't exist, it will be created/set if possible
71
+
72
+ """
73
+ next_obj = obj
74
+ last = key_list[-1]
75
+ path = key_list[:-1]
76
+ for key in path:
77
+ next_next_obj = any_getter(next_obj, key, None)
78
+ if next_next_obj is None:
79
+ next_next_obj = {}
80
+ any_setter(next_obj, key, next_next_obj)
81
+ next_obj = next_next_obj
82
+
83
+ any_setter(next_obj, last, value)
84
+
@@ -0,0 +1,130 @@
1
+ __all__ = [
2
+ 'nested_dict_update',
3
+ 'nest_dict',
4
+ 'str_nest_dict',
5
+ 'flatten_dict',
6
+ 'str_flatten_dict',
7
+ ]
8
+
9
+ from batutils.tpu.structs import *
10
+
11
+
12
+ def nested_dict_update(base_map: Dict[Any, Any], update_map: Dict[Any, Any]):
13
+ """Updates a nested dicts. This means that if the `base_map` and
14
+ `update_map` contain the same key that is a dict in both, that nested dict
15
+ will be updated the same way `base_map.update(update_map)` works, i.e. it
16
+ will preserve any pre-existing keys in `base_map` and only update the values
17
+ of keys in the `update_map` (again, in a nested way).
18
+
19
+ Example of normal update:
20
+ >>> a = {'foo': 1, 'bar': {'alpha': 2, 'beta': 3}, 'oof': 4}
21
+ >>> b = {'foo': 101, 'bar': {'beta': 103}}
22
+ >>> a.update(b)
23
+ >>> print(a)
24
+ {'foo': 101, 'bar': {'beta': 103}, 'oof': 4}
25
+
26
+ Example of nested update:
27
+ >>> a = {'foo': 1, 'bar': {'alpha': 2, 'beta': 3}, 'oof': 4}
28
+ >>> b = {'foo': 101, 'bar': {'beta': 103}}
29
+ >>> nested_dict_update(a, b)
30
+ >>> print(a)
31
+ {'foo': 101, 'bar': {'alpha': 2, 'beta': 103}, 'oof': 4}
32
+
33
+ Note that if a key (or nested key) who's value is a dict in `base_map` is
34
+ NOT a dict in `update_map`, that dict value will be overwritten completely
35
+ with the new value and type.
36
+ """
37
+ for k, v in update_map.items():
38
+ if k in base_map:
39
+ base_val = base_map[k]
40
+ if isinstance(base_val, dict) and isinstance(v, dict):
41
+ nested_dict_update(base_val, v)
42
+ base_map[k] = base_val
43
+ else:
44
+ base_map[k] = v
45
+ else:
46
+ base_map[k] = v
47
+
48
+
49
+ def nest_dict(key_list: List[Any], value: Any = None) -> Dict[Any, Any]:
50
+ """Creates a nested dict using a list of keys to construct an ever deepening
51
+ dict of a dict of a dict etc.
52
+
53
+ Example:
54
+ >>> nest_dict(['a', 'b', 'c'], 'value')
55
+ {'a': {'b': {'c': 'value'}}}
56
+ """
57
+ if not key_list:
58
+ return value
59
+ k = key_list.pop(0)
60
+ return {k: nest_dict(key_list, value)}
61
+
62
+
63
+ def str_nest_dict(nested_key_string: str, value: Any = None, key_seperator: str = '.') -> Dict[str, Any]:
64
+ """Creates a nested dict using a string with some separator symbol to split
65
+ it into a list of keys to construct an ever deepening dict of a dict of a
66
+ dict etc.
67
+
68
+ Example:
69
+ >>> str_nest_dict('a.b.c', 'value')
70
+ {'a': {'b': {'c': 'value'}}}
71
+ """
72
+ return nest_dict(nested_key_string.split(key_seperator), value)
73
+
74
+
75
+ def _denest_dict(some_dict: Dict[Any, Any]) -> List[Tuple[List[Any], Any]]:
76
+ """Takes a potentially nested dict and returns a list of Tuple pairs
77
+ containing all "leaf" values (values that aren't dicts) as each Tuples
78
+ second value while the first value will be a list of the "key-path" through
79
+ the nested dict to that value.
80
+
81
+ Example:
82
+ >>> _denest_dict({'a': {7: {'c': 'value'}, 5: 'other'}})
83
+ [(['a', 7, 'c'], 'value'), (['a', 5], 'other')]
84
+ """
85
+ buff = []
86
+ for k, v in some_dict.items():
87
+ if isinstance(v, dict):
88
+ for nk, nv in _denest_dict(v):
89
+ buff.append(([k]+nk, nv))
90
+ else:
91
+ buff.append(([k], v))
92
+ return buff
93
+
94
+
95
+ def flatten_dict(some_dict: Dict[Any, Any]) -> Dict[Tuple, Any]:
96
+ """Takes a dict that may (or may not) be nested (contain other dicts) and
97
+ flattens it out by keys, returning a single dict with one value entry per
98
+ "leaf" value (any value in the nested dict tree that is NOT another dict)
99
+ where they key for each value is a tuple of the key "path" to it in the
100
+ original dict.
101
+
102
+ >>> flatten_dict({'a': {7: {'c': 'value'}, 5: 'other'}})
103
+ {('a', 7, 'c'): 'value', ('a', 5): 'other'}
104
+ """
105
+ buff = {}
106
+ for k, v in _denest_dict(some_dict):
107
+ buff[tuple(k)] = v
108
+ return buff
109
+
110
+
111
+ def str_flatten_dict(some_dict: Dict[str, Any], joiner: str = '.', caster: Callable = str) -> Dict[str, Any]:
112
+ """Does the same as `flatten_dict` except instead of keys of tuples, keys
113
+ are converted to str and joined with a dot character (by default).
114
+
115
+ Both the joining character (or string) and how each value in the key tuple
116
+ is converted to a str is customizable.
117
+
118
+ This can be especially useful if keys contain objects who's hash values
119
+ differ but who's str representations are the same, e.g. the int 1 and the
120
+ string '1' in {1: 'a', '1': 'b'}. That dict will turn into {'1': 'b'}.
121
+
122
+ :type some_dict: dict
123
+ :type joiner: str
124
+ :type caster: function
125
+ :rtype: dict[str,Any]
126
+ """
127
+ buff = {}
128
+ for k, v in _denest_dict(some_dict):
129
+ buff[joiner.join([caster(ki) for ki in k])] = v
130
+ return buff