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.
- batutils/__init__.py +5 -0
- batutils/_common/__init__.py +6 -0
- batutils/_common/_decode.py +40 -0
- batutils/dtu/__init__.py +19 -0
- batutils/dtu/casting/__init__.py +5 -0
- batutils/dtu/casting/_any.py +136 -0
- batutils/dtu/casting/_filetime.py +36 -0
- batutils/dtu/casting/_instant.py +31 -0
- batutils/dtu/casting/_string.py +81 -0
- batutils/dtu/casting/_timestamp.py +50 -0
- batutils/dtu/formatting/__init__.py +2 -0
- batutils/dtu/formatting/_delta.py +62 -0
- batutils/dtu/formatting/_serialize.py +26 -0
- batutils/dtu/parsers/__init__.py +2 -0
- batutils/dtu/parsers/_string.py +44 -0
- batutils/dtu/parsers/_timedelta.py +161 -0
- batutils/dtu/shortcuts/__init__.py +3 -0
- batutils/dtu/shortcuts/_aliases.py +29 -0
- batutils/dtu/shortcuts/_checkers.py +21 -0
- batutils/dtu/shortcuts/_finders.py +80 -0
- batutils/dtu/structs/__init__.py +5 -0
- batutils/dtu/structs/_base.py +5 -0
- batutils/dtu/structs/_deltasplit.py +278 -0
- batutils/dtu/structs/_types.py +24 -0
- batutils/dtu/structs/aliases.py +21 -0
- batutils/dtu/structs/consts.py +35 -0
- batutils/structs/__init__.py +7 -0
- batutils/structs/_base.py +21 -0
- batutils/tpu/__init__.py +0 -0
- batutils/tpu/casting/__init__.py +9 -0
- batutils/tpu/casting/_currency.py +52 -0
- batutils/tpu/casting/_evals.py +247 -0
- batutils/tpu/casting/_ip.py +58 -0
- batutils/tpu/casting/_mask.py +34 -0
- batutils/tpu/casting/_params.py +108 -0
- batutils/tpu/casting/_serialize.py +20 -0
- batutils/tpu/comparison/__init__.py +1 -0
- batutils/tpu/comparison/_mixin.py +30 -0
- batutils/tpu/insp/__init__.py +2 -0
- batutils/tpu/insp/_insp_old.py +347 -0
- batutils/tpu/insp/_typecheck.py +89 -0
- batutils/tpu/iters/__init__.py +4 -0
- batutils/tpu/iters/_attrs.py +84 -0
- batutils/tpu/iters/_dicts.py +130 -0
- batutils/tpu/iters/_group.py +50 -0
- batutils/tpu/iters/_lists.py +178 -0
- batutils/tpu/size/__init__.py +1 -0
- batutils/tpu/size/_total.py +52 -0
- batutils/tpu/strimp/__init__.py +1 -0
- batutils/tpu/strimp/_getters.py +285 -0
- batutils/tpu/string/__init__.py +1 -0
- batutils/tpu/string/_string.py +160 -0
- batutils/tpu/structs/__init__.py +1 -0
- batutils/tpu/structs/_base.py +3 -0
- batutils/tpu/structs/empty/__init__.py +1 -0
- batutils/tpu/structs/empty/_empty.py +387 -0
- batutils/tpu/structs/enumex/__init__.py +1 -0
- batutils/tpu/structs/enumex/_enumextra.py +25 -0
- batutils/tpu/structs/fluid/__init__.py +1 -0
- batutils/tpu/structs/fluid/_attrfluid.py +77 -0
- batutils/tpu/structs/sentience/__init__.py +1 -0
- batutils/tpu/structs/sentience/_builder.py +87 -0
- batutils/tpu/structs/serializers/__init__.py +3 -0
- batutils/tpu/structs/serializers/_json.py +12 -0
- batutils/tpu/structs/serializers/_jsonsafe.py +69 -0
- batutils/tpu/structs/serializers/_universal.py +318 -0
- batutils/tpu/structs/singleton/__init__.py +1 -0
- batutils/tpu/structs/singleton/_singleton.py +35 -0
- batutils-2.0.0.dist-info/LICENSE +22 -0
- batutils-2.0.0.dist-info/METADATA +125 -0
- batutils-2.0.0.dist-info/RECORD +73 -0
- batutils-2.0.0.dist-info/WHEEL +5 -0
- 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 @@
|
|
|
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 *
|