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,160 @@
1
+ __all__ = [
2
+ 'str_norm',
3
+ 'str_norm_eq',
4
+ 'maxlen',
5
+ 'slugify',
6
+ 'dequote',
7
+ ]
8
+
9
+ """String Related Type Utilities
10
+
11
+ Utilities for manipulating strings, i.e. the input AND output of functions here
12
+ should both only be strings (str and/or unicode).
13
+
14
+ These should be pretty basic though, e.g. HTML stuff or formatting stuff etc.
15
+ should probably live somewhere else.
16
+
17
+ This should NOT contain functions that:
18
+
19
+ - ...take in a string and output something else, e.g. convert or cast a
20
+ string to some other object type (that's "casting").
21
+ - ...take in some other object type and output a string, e.g. to renders
22
+ stuff readable (that's "formatting").
23
+ """
24
+ from batutils.tpu.casting import decode_bytes
25
+ from batutils.tpu.structs import *
26
+
27
+ _SLUG_SPACER = re.compile(r'[\s_\-:;,.]+')
28
+ _SLUGGER = re.compile(r'[^a-z0-9\-]+')
29
+
30
+
31
+ def str_norm(string: Union[str, bytes], default='') -> str:
32
+ """Normalize a string, i.e. ensure that it is indeed a string (if possible)
33
+ and strip leading and trailing whitespaces and make it lowercase.
34
+
35
+ Returns a default value if the string is a None or if an exception is
36
+ encountered while trying to turn a non-string into a string via the str()
37
+ method.
38
+
39
+ :param string:
40
+ :type string:
41
+ :param default:
42
+ :type default: any
43
+ :return:
44
+ :rtype:
45
+ """
46
+ if isinstance(string, bytes):
47
+ try:
48
+ string = decode_bytes(string)
49
+ except Exception:
50
+ return default
51
+
52
+ if not isinstance(string, str):
53
+ if string is None:
54
+ return default
55
+ try:
56
+ string = str(string)
57
+ except Exception:
58
+ return default
59
+ return string.strip().lower()
60
+
61
+
62
+ def str_norm_eq(string, another_string):
63
+ """Checks if two strings are the same if normalized (i.e. stripped of
64
+ leading and trailing whitespaces and made lowercase).
65
+
66
+ Returns False if one or both can't be turned into a string.
67
+
68
+ :param string:
69
+ :type string:
70
+ :param another_string:
71
+ :type another_string:
72
+ :return:
73
+ :rtype: bool
74
+ """
75
+ string = str_norm(string, default=None)
76
+ another_string = str_norm(another_string, default=None)
77
+ if string is not None and another_string is not None:
78
+ return string == another_string
79
+ return False
80
+
81
+
82
+ def maxlen(string: str, max_length: int) -> str:
83
+ """Trims the back of a string if it exceeds a certain length.
84
+
85
+ If the given parameter is not a string, it'll be returned unchanged.
86
+
87
+ :param string:
88
+ :type string: str, unicode or any
89
+ :param max_length:
90
+ :type max_length: int
91
+ :return:
92
+ :rtype: str, unicode or any
93
+ """
94
+ if string and isinstance(string, str) and len(string) > max_length:
95
+ string = string[:max_length]
96
+ return string
97
+
98
+
99
+ def slugify(string):
100
+ """Returns a "slug" version of a string (replacing any whitespaces
101
+ and separators with - and removing any character that is not a-z or 0-9)
102
+
103
+ :param string:
104
+ :type string:
105
+ :return:
106
+ :rtype:
107
+ """
108
+ string = str_norm(string)
109
+ string = _SLUG_SPACER.sub('-', string)
110
+ return _SLUGGER.sub('', string)
111
+
112
+
113
+ def dequote(string: str, symbol_list: Optional[Union[str, List[str]]] = None) -> str:
114
+ """Removes single or double quote pairs (or any custom given quotation
115
+ character symbold) from a string if it is encompassed.
116
+ in those.
117
+
118
+ Example:
119
+
120
+ >>> dequote('"foo"')
121
+ 'foo'
122
+ >>> dequote("'bar and another bar'")
123
+ 'bar and another bar'
124
+
125
+ By default only single `'` or double quotes `"` are removed but you can pass
126
+ in a different character symbol or a list of symbols to act as quotes.
127
+
128
+ >>> dequote('$foo$', '$')
129
+ 'foo'
130
+ >>> dequote('%bar and another bar%', ['%', '$', '"'])
131
+ 'bar and another bar'
132
+
133
+ The string must start and end with the same type of quote in order for it
134
+ to be removed. In all other cases the same string will be returned unchanged
135
+ (even if the value isn't an actual string').
136
+
137
+ >>> dequote('"foo')
138
+ '"foo'
139
+ >>> dequote('%bar and another bar$', ['%', '$', '"'])
140
+ '%bar and another bar$'
141
+ >>> dequote(None)
142
+ None
143
+ >>> dequote(42)
144
+ 42
145
+
146
+ :param string: String to remove surrounding quotes from
147
+ :type string: str | unicode
148
+ :param symbol_list: A list of symbols that should act as quotes (optional)
149
+ :type symbol_list: None | list | tuple | set | str | unicode
150
+ :return: The string input without surrounding quotes or whatever was passed
151
+ in unchanged.
152
+ :rtype: str | unicode
153
+ """
154
+ if string and isinstance(string, str):
155
+ symbol_list = symbol_list or ('"', "'")
156
+ if not isinstance(symbol_list, (list, tuple, set)):
157
+ symbol_list = [symbol_list]
158
+ if string[0] == string[-1] and string[0] in symbol_list:
159
+ return string[1:-1]
160
+ return string
@@ -0,0 +1 @@
1
+ from batutils.tpu.structs._base import *
@@ -0,0 +1,3 @@
1
+ from batutils.structs import *
2
+
3
+ import datetime
@@ -0,0 +1 @@
1
+ from batutils.tpu.structs.empty._empty import *
@@ -0,0 +1,387 @@
1
+ __all__ = [
2
+ 'Empty',
3
+ 'EmptyDict',
4
+ 'if_empty',
5
+ ]
6
+
7
+ import collections
8
+
9
+
10
+ class _Empty(object):
11
+ """
12
+ This is essentially just a glorified None that tries not to break any code
13
+ that makes certain wild assumptions.
14
+
15
+ The typical use case here would be to return Empty instead of None as a
16
+ default value for something not found so you don't have to check again and
17
+ again if you got a "None" before calling an attribute so as to not raise
18
+ an AttributeError as long as you don't care if the results of whatever
19
+ you're doing don't make sense until later.
20
+
21
+ Totally made up use case example:
22
+
23
+ character = get_character(123)
24
+ if character:
25
+ if hasattr(character, 'user'):
26
+ user = character.user
27
+ if user:
28
+ if hasattr(user, 'email'):
29
+ email = user.email
30
+ if email:
31
+ if isinstance(email, (str, unicode)):
32
+ if email.startswith('_'):
33
+ raise ValueError('Emails can not start with underscores')
34
+
35
+ Totally made up example using Empty as default values for failed lookups:
36
+
37
+ if get_character(123).user.email.startswith('_'):
38
+ raise ValueError('Emails can not start with underscores')
39
+
40
+ Here get_character returns Empty when it failed to find a character so the
41
+ attribute 'user' of Empty will also return Empty.
42
+
43
+ Ditto for the 'email' attribute of 'user'.
44
+
45
+ The 'startswith' of 'email' will also be Empty and that will call the
46
+ Empty.__call__('_') which ALSO return Empty and then finally the if
47
+ statement will call Empty.__nonzero__() which returns False.
48
+
49
+ So we can super quickly make sure the email doesn't start with an
50
+ underscore IF that's the only thing we care about and don't mind if any
51
+ of the stuff in that chain failed or wasn't found.
52
+
53
+ After all, we can always check later explicitly for the validity of any
54
+ and/or all of these attributes.
55
+
56
+ Another super simple use case (and probably more likely) would be to use this for quick "throwaway" lookups:
57
+
58
+ E.g. printing the domain of the email address of the example above:
59
+
60
+ print 'Email:', character.user.email[character.user.email.index('@')+1:]
61
+
62
+ If everything was found here, we'll get:
63
+ "Email: ccpgames.com"
64
+
65
+ If not, we'll get:
66
+ "Email: Empty"
67
+
68
+ Could also do this:
69
+ print 'Email:', character.user.email[character.user.email.index('@')+1:] or '-'
70
+ "Email: -"
71
+ """
72
+ ___inst = None
73
+
74
+ def __new__(cls, *args, **kwargs):
75
+ if cls.___inst is None:
76
+ cls.___inst = super(_Empty, cls).__new__(cls, *args, **kwargs)
77
+ return cls.___inst
78
+
79
+ def __cmp__(self, other):
80
+ if other is Empty:
81
+ return 0
82
+ return -1
83
+
84
+ def __lt__(self, other):
85
+ return False
86
+
87
+ def __gt__(self, other):
88
+ return False
89
+
90
+ def __le__(self, other):
91
+ if other is Empty:
92
+ return True
93
+ return False
94
+
95
+ def __ge__(self, other):
96
+ if other is Empty:
97
+ return True
98
+ return False
99
+
100
+ def __eq__(self, other):
101
+ if other is Empty:
102
+ return True
103
+ return False
104
+
105
+ def __ne__(self, other):
106
+ if other is Empty:
107
+ return False
108
+ return True
109
+
110
+ def __nonzero__(self):
111
+ return False
112
+
113
+ def __getattr__(self, name):
114
+ return Empty
115
+
116
+ def __setattr__(self, name, value):
117
+ pass
118
+
119
+ def __delattr__(self, name):
120
+ pass
121
+
122
+ def __instancecheck__(self, instance):
123
+ return False
124
+
125
+ def __subclasscheck__(self, subclass):
126
+ return False
127
+
128
+ def __str__(self):
129
+ return 'Empty'
130
+
131
+ def __unicode__(self):
132
+ return u'Empty'
133
+
134
+ def __repr__(self):
135
+ return '<Empty>'
136
+
137
+ def __call__(self, *args, **kwargs):
138
+ return Empty
139
+
140
+ def __len__(self):
141
+ return 0
142
+
143
+ def __getitem__(self, key):
144
+ return Empty
145
+
146
+ def __contains__(self, item):
147
+ return False
148
+
149
+ def __missing__(self, key):
150
+ return Empty
151
+
152
+ def __int__(self):
153
+ return 0
154
+
155
+ def __float__(self):
156
+ return 0.0
157
+
158
+ def __complex__(self):
159
+ return complex(0, 0)
160
+
161
+ def __oct__(self):
162
+ return 00
163
+
164
+ def __hex__(self):
165
+ return 0x0
166
+
167
+ def __index__(self):
168
+ return 0
169
+
170
+ def __trunc__(self):
171
+ return 0
172
+
173
+ def __format__(self, formatstr):
174
+ return 'Empty'
175
+
176
+ def __pos__(self):
177
+ return Empty
178
+
179
+ def __neg__(self):
180
+ return Empty
181
+
182
+ def __abs__(self):
183
+ return Empty
184
+
185
+ def __invert__(self):
186
+ return Empty
187
+
188
+ def __round__(self, n):
189
+ return Empty
190
+
191
+ def __floor__(self):
192
+ return Empty
193
+
194
+ def __ceil__(self):
195
+ return Empty
196
+
197
+ def __copy__(self):
198
+ return Empty
199
+
200
+ def __deepcopy__(self):
201
+ return Empty
202
+
203
+ def __add__(self, other):
204
+ return Empty
205
+
206
+ def __radd__(self, other):
207
+ return Empty
208
+
209
+ def __sub__(self, other):
210
+ return Empty
211
+
212
+ def __rsub__(self, other):
213
+ return Empty
214
+
215
+ def __mul__(self, other):
216
+ return Empty
217
+
218
+ def __rmul__(self, other):
219
+ return Empty
220
+
221
+ def __floordiv__(self, other):
222
+ return Empty
223
+
224
+ def __rfloordiv__(self, other):
225
+ return Empty
226
+
227
+ def __div__(self, other):
228
+ return Empty
229
+
230
+ def __rdiv__(self, other):
231
+ return Empty
232
+
233
+ def __truediv__(self, other):
234
+ return Empty
235
+
236
+ def __rtruediv__(self, other):
237
+ return Empty
238
+
239
+ def __mod__(self, other):
240
+ return Empty
241
+
242
+ def __rmod__(self, other):
243
+ return Empty
244
+
245
+ def __divmod__(self, other):
246
+ return Empty
247
+
248
+ def __rdivmod__(self, other):
249
+ return Empty
250
+
251
+ def __pow__(self):
252
+ return Empty
253
+
254
+ def __rpow__(self):
255
+ return Empty
256
+
257
+ def __lshift__(self, other):
258
+ return Empty
259
+
260
+ def __rlshift__(self, other):
261
+ return Empty
262
+
263
+ def __rshift__(self, other):
264
+ return Empty
265
+
266
+ def __rrshift__(self, other):
267
+ return Empty
268
+
269
+ def __and__(self, other):
270
+ return Empty
271
+
272
+ def __rand__(self, other):
273
+ return Empty
274
+
275
+ def __or__(self, other):
276
+ return Empty
277
+
278
+ def __ror__(self, other):
279
+ return Empty
280
+
281
+ def __xor__(self, other):
282
+ return Empty
283
+
284
+ def __rxor__(self, other):
285
+ return Empty
286
+
287
+ def __hash__(self):
288
+ return object.__hash__(self)
289
+
290
+ def __iter__(self):
291
+ return self
292
+
293
+ def __next__(self):
294
+ raise StopIteration()
295
+
296
+
297
+ Empty = _Empty()
298
+
299
+
300
+ class EmptyDict(collections.defaultdict):
301
+ def __init__(self, *args, **kwargs):
302
+ """A defaultdict who's default values are an Empty and that can be
303
+ accessed via attributes as well as normal dict methods.
304
+
305
+ Useful for wrapping around nested dicts who's keys you can't be sure of
306
+ and when you don't want to have to code too defensively.
307
+ """
308
+ super(EmptyDict, self).__init__(lambda x: Empty, *args, **kwargs)
309
+
310
+ def __getattribute__(self, name):
311
+ if hasattr(collections.defaultdict, name):
312
+ return collections.defaultdict.__getattribute__(self, name)
313
+ if collections.defaultdict.__contains__(self, name):
314
+ data = collections.defaultdict.__getitem__(self, name)
315
+ if isinstance(data, dict):
316
+ return EmptyDict(**data)
317
+ elif data is None:
318
+ return Empty
319
+ else:
320
+ return data
321
+ return Empty
322
+
323
+ def __getitem__(self, item):
324
+ if collections.defaultdict.__contains__(self, item):
325
+ data = collections.defaultdict.__getitem__(self, item)
326
+ if isinstance(data, dict):
327
+ return EmptyDict(**data)
328
+ elif data is None:
329
+ return Empty
330
+ else:
331
+ return data
332
+ return Empty
333
+
334
+ def __missing__(self, key):
335
+ return Empty
336
+
337
+ def __repr__(self):
338
+ return dict.__repr__(self)
339
+
340
+
341
+ def if_empty(value, return_if_empty=None):
342
+ """Returns the value of the `return_if_empty` parameter (`None` by default)
343
+ if the given `value` parameter is either an `Empty`.
344
+
345
+ The point of this is to quickly default some value that may be an Empty OR
346
+ some other value that evaluates as False into the correct type (e.g. before
347
+ passing a possible `Empty` on to code that may bug out with an `Empty` but
348
+ expect something like a `None` or a 0 or an empty string) without bothering
349
+ with more cumbersome type checks.
350
+
351
+ This can usually be done with a simple `or` fallback like so:
352
+
353
+ >>> bar = Empty
354
+ >>> foo = bar or None
355
+ >>> assert foo is None # GOOD: We wanted a None by default
356
+
357
+ This ensures that if `bar` is an `Empty`, `foo` will be a
358
+ `None` by default, however, if the expected real value of `bar` can be
359
+ something that evaluates to `False` in an `or` check (eg. `None`, `False`,
360
+ `0`, `0.0`, `""`, etc. this check will also always overwrite such real
361
+ values with a `None`:
362
+
363
+ >>> bar = 0
364
+ >>> foo = if_empty(bar, None)
365
+ >>> assert foo is None # GOOD
366
+
367
+ So we can guard agains this while skipping overly complex checks (that would
368
+ negated the utility of the `Empty` stuff) like so:
369
+
370
+ >>> bar = Empty
371
+ >>> foo = if_empty(bar, None)
372
+ >>> assert foo is None # GOOD: Default None if no value is set
373
+ >>> bar = 0
374
+ >>> foo = if_empty(bar, None)
375
+ >>> assert foo == 0 # GOOD: Value of 0 is cool
376
+
377
+ :param value: Value to check if is an `Empty`
378
+ :type value: Any
379
+ :param return_if_empty: Value to return (as default) if the `value` is empty
380
+ :type return_if_empty: Any
381
+ :return: The `return_if_empty` param if `value` is an `Empty`, otherwise,
382
+ the `value` unchanged
383
+ :rtype: Any
384
+ """
385
+ if value is Empty:
386
+ return return_if_empty
387
+ return value
@@ -0,0 +1 @@
1
+ from ._enumextra import *
@@ -0,0 +1,25 @@
1
+ __all__ = [
2
+ 'EnumEx',
3
+ ]
4
+
5
+ import enum
6
+ from typing import *
7
+
8
+ _T_ENUM = TypeVar('_T_ENUM', bound='EnumEx')
9
+
10
+ _NOT_SUPPLIED = object()
11
+
12
+
13
+ class EnumEx(enum.Enum):
14
+ @classmethod
15
+ def from_any(cls: Type[_T_ENUM], value: Any,
16
+ default: Union[_T_ENUM, None] = _NOT_SUPPLIED) -> _T_ENUM:
17
+ """Casts any sensible value to an Enum of this type.
18
+
19
+ :param value: The value to cast to an Enum
20
+ :param default: Default value to return on failed eval if any. Defaults
21
+ to _NOT_SUPPLIED and makes this method raise an
22
+ exception on failure
23
+ """
24
+ from batutils.tpu.casting import enum_eval
25
+ return enum_eval(value, cls, default=default, raise_on_fail=(default is _NOT_SUPPLIED))
@@ -0,0 +1 @@
1
+ from ._attrfluid import *
@@ -0,0 +1,77 @@
1
+ __all__ = [
2
+ 'AttributeFluidObject',
3
+ ]
4
+
5
+ import logging
6
+ _default_log = logging.getLogger(__name__)
7
+
8
+
9
+ class AttributeFluidObject(object):
10
+ _log_missing_attributes = False
11
+ _log_level = logging.WARNING
12
+ _logger = _default_log
13
+ _default_value = None
14
+
15
+ def __new__(cls, *args, **kwargs):
16
+ # This makes sure we don't log anything set in the __init__ methods
17
+ # as missing until AFTER calling AttributeFluidObject's super init.
18
+ instance = super(AttributeFluidObject, cls).__new__(cls)
19
+
20
+ object.__setattr__(instance, '_log_missing_attributes', False)
21
+ return instance
22
+
23
+ def __init__(self, **kwargs):
24
+ """Base class for "Attribute-Fluid" object, i.e. objects that
25
+ self-correct instead of raising an attribute error when an attribute
26
+ called is missing (whether it's set or get), i.e. create within itself
27
+ any missing attribute on the fly.
28
+
29
+ Useful for making error tolerant objects, e.g. when external resources
30
+ (that aren't guaranteed to be stable) are used to act on the object. A
31
+ good example would be a Class that read configuration values from a
32
+ file where you don't want the code to break even if the file has borked
33
+ values.
34
+
35
+ The class has three class variables that determine how to handle
36
+ missing attributes specifically:
37
+
38
+ - _log_missing_attributes: If True, the object will log any missing
39
+ attribute call (for the first time). This will be logged to the
40
+ tpu.bases logger. Default is False.
41
+ - _logger: The logger to log to (if _log_missing_attributes is
42
+ True). Default is the 'tpu.bases'.
43
+ - _log_level: The log level to log missing attributes as (if
44
+ _log_missing_attributes is True). Default is logging.WARNING.
45
+ - _default_value: The default value to give to missing attributes
46
+ created. Default is None.
47
+
48
+ IMPORTANT: If logging missing attributes, make sure you call:
49
+ `super(self.__class__, self).__init__(**kwargs)` in your classes
50
+ `__init__` method AFTER setting all expected attributes (generally at
51
+ the bottom of the __init__ method). Otherwise missing attributes will
52
+ not get logged AND is you call the super's __init__ before initializing
53
+ all self.whatever attributes, those attributes in the init that you set
54
+ after calling the super will get logged as missing, which is both
55
+ incorrect and annoying :).
56
+
57
+ :param kwargs: Any keywords that will be set as attributes of the
58
+ object.
59
+ :type kwargs: dict[any,any]
60
+ """
61
+ object.__setattr__(self, '_log_missing_attributes', self.__class__._log_missing_attributes)
62
+ if kwargs:
63
+ for k, v in kwargs.items():
64
+ if not k.startswith('_'):
65
+ setattr(self, k, v)
66
+
67
+ def __getattr__(self, item):
68
+ if self._log_missing_attributes:
69
+ self._logger.log(self._log_level, u'Attribute "%s" of Class "%s" does not exist! Setting it to default value=%r', item, self.__class__.__name__, self._default_value)
70
+ object.__setattr__(self, item, self._default_value)
71
+ return self._default_value
72
+
73
+ def __setattr__(self, name, value):
74
+ if self._log_missing_attributes:
75
+ if not hasattr(self, name):
76
+ self._logger.log(self._log_level, u'Attribute "%s" of Class "%s" does not exist but is now set to %r!', name, self.__class__.__name__, value)
77
+ object.__setattr__(self, name, value)
@@ -0,0 +1 @@
1
+ from ._builder import *