1
|
|
|
import inspect |
2
|
|
|
from functools import total_ordering |
3
|
|
|
|
4
|
|
|
|
5
|
|
|
def yield_once(iterator): |
6
|
|
|
""" |
7
|
|
|
Decorator to make an iterator yield each result only once. |
8
|
|
|
|
9
|
|
|
:param iterator: Any iterator |
10
|
|
|
:return: An iterator that yields every result only once at most. |
11
|
|
|
""" |
12
|
|
|
def yield_once_generator(*args, **kwargs): |
13
|
|
|
yielded = [] |
14
|
|
|
for item in iterator(*args, **kwargs): |
15
|
|
|
if item in yielded: |
16
|
|
|
pass |
17
|
|
|
else: |
18
|
|
|
yielded.append(item) |
19
|
|
|
yield item |
20
|
|
|
|
21
|
|
|
return yield_once_generator |
22
|
|
|
|
23
|
|
|
|
24
|
|
|
def _to_list(var): |
25
|
|
|
""" |
26
|
|
|
Make variable to list. |
27
|
|
|
|
28
|
|
|
:param var: variable of any type |
29
|
|
|
:return: list |
30
|
|
|
""" |
31
|
|
|
if isinstance(var, list): |
32
|
|
|
return var |
33
|
|
|
elif var is None: |
34
|
|
|
return [] |
35
|
|
|
elif isinstance(var, str) or isinstance(var, dict): |
36
|
|
|
# We dont want to make a list out of those via the default constructor |
37
|
|
|
return [var] |
38
|
|
|
else: |
39
|
|
|
try: |
40
|
|
|
return list(var) |
41
|
|
|
except TypeError: |
42
|
|
|
return [var] |
43
|
|
|
|
44
|
|
|
|
45
|
|
|
def arguments_to_lists(function): |
46
|
|
|
""" |
47
|
|
|
Decorator for a function that converts all arguments to lists. |
48
|
|
|
|
49
|
|
|
:param function: target function |
50
|
|
|
:return: target function with only lists as parameters |
51
|
|
|
""" |
52
|
|
|
def l_function(*args, **kwargs): |
53
|
|
|
l_args = [_to_list(arg) for arg in args] |
54
|
|
|
l_kwargs = {} |
55
|
|
|
|
56
|
|
|
for key, value in kwargs.items(): |
57
|
|
|
l_kwargs[key] = _to_list(value) |
58
|
|
|
return function(*l_args, **l_kwargs) |
59
|
|
|
|
60
|
|
|
return l_function |
61
|
|
|
|
62
|
|
|
|
63
|
|
|
def _get_member(obj, member): |
64
|
|
|
# If not found, pass AttributeError to invoking function. |
65
|
|
|
attribute = getattr(obj, member) |
66
|
|
|
|
67
|
|
|
if callable(attribute) and hasattr(attribute, "__self__"): |
68
|
|
|
# If the value is a bound method, invoke it like a getter and return |
69
|
|
|
# its value. |
70
|
|
|
try: |
71
|
|
|
return attribute() |
72
|
|
|
except TypeError: |
73
|
|
|
# Don't use repr() to display the member more accurately, because |
74
|
|
|
# invoking repr() on a bound method prints in this format: |
75
|
|
|
# <bound method CLASS.METHOD of **repr(instance)**> |
76
|
|
|
# This invokes repr() recursively. |
77
|
|
|
raise TypeError("Given bound method '" + member + "' must be " |
78
|
|
|
"callable like a getter, taking no arguments.") |
79
|
|
|
else: |
80
|
|
|
# Otherwise it's a member variable or property (or any other attribute |
81
|
|
|
# that holds a value). |
82
|
|
|
return attribute |
83
|
|
|
|
84
|
|
|
|
85
|
|
|
def _construct_repr_string(obj, members): |
86
|
|
|
# The passed entries have format (member-name, repr-function). |
87
|
|
|
values = ", ".join(member + "=" + func(_get_member(obj, member)) |
88
|
|
|
for member, func in members) |
89
|
|
|
return ("<" + type(obj).__name__ + " object(" + values + ") at " |
90
|
|
|
+ hex(id(obj)) + ">") |
91
|
|
|
|
92
|
|
|
|
93
|
|
|
def get_public_members(obj): |
94
|
|
|
""" |
95
|
|
|
Retrieves a list of member-like objects (members or properties) that are |
96
|
|
|
publically exposed. |
97
|
|
|
|
98
|
|
|
:param obj: The object to probe. |
99
|
|
|
:return: A list of strings. |
100
|
|
|
""" |
101
|
|
|
return {attr: getattr(obj, attr) for attr in dir(obj) |
102
|
|
|
if not attr.startswith("_") |
103
|
|
|
and not hasattr(getattr(obj, attr), '__call__')} |
104
|
|
|
|
105
|
|
|
|
106
|
|
|
def generate_repr(*members): |
107
|
|
|
""" |
108
|
|
|
Decorator that binds an auto-generated `__repr__()` function to a class. |
109
|
|
|
|
110
|
|
|
The generated `__repr__()` function prints in following format: |
111
|
|
|
<ClassName object(field1=1, field2='A string', field3=[1, 2, 3]) at 0xAAAA> |
112
|
|
|
|
113
|
|
|
Note that this decorator modifies the given class in place! |
114
|
|
|
|
115
|
|
|
:param members: An iterable of member names to include into the |
116
|
|
|
representation-string. Providing no members yields |
117
|
|
|
to inclusion of all member variables and properties |
118
|
|
|
in alphabetical order (except if they start with an |
119
|
|
|
underscore). |
120
|
|
|
|
121
|
|
|
To control the representation of each member, you |
122
|
|
|
can also pass a tuple where the first element |
123
|
|
|
contains the member to print and the second one the |
124
|
|
|
representation function (which defaults to the |
125
|
|
|
built-in `repr()`). Using None as representation |
126
|
|
|
function is the same as using `repr()`. |
127
|
|
|
|
128
|
|
|
Supported members are fields/variables, properties |
129
|
|
|
and getter-like functions (functions that accept no |
130
|
|
|
arguments). |
131
|
|
|
:raises ValueError: Raised when the passed |
132
|
|
|
(member, repr-function)-tuples have not a length of |
133
|
|
|
2. |
134
|
|
|
:raises AttributeError: Raised when a given member/attribute was not found |
135
|
|
|
in class. |
136
|
|
|
:raises TypeError: Raised when a provided member is a bound method |
137
|
|
|
that is not a getter-like function (means it must |
138
|
|
|
accept no parameters). |
139
|
|
|
:return: The class armed with an auto-generated __repr__ |
140
|
|
|
function. |
141
|
|
|
""" |
142
|
|
|
def decorator(cls): |
143
|
|
|
cls.__repr__ = __repr__ |
144
|
|
|
return cls |
145
|
|
|
|
146
|
|
|
if members: |
147
|
|
|
# Prepare members list. |
148
|
|
|
members_to_print = list(members) |
149
|
|
|
for i, member in enumerate(members_to_print): |
150
|
|
|
if isinstance(member, tuple): |
151
|
|
|
# Check tuple dimensions. |
152
|
|
|
length = len(member) |
153
|
|
|
if length == 2: |
154
|
|
|
members_to_print[i] = (member[0], |
155
|
|
|
member[1] if member[1] else repr) |
156
|
|
|
else: |
157
|
|
|
raise ValueError("Passed tuple " + repr(member) + |
158
|
|
|
" needs to be 2-dimensional, but has " + |
159
|
|
|
str(length) + " dimensions.") |
160
|
|
|
else: |
161
|
|
|
members_to_print[i] = (member, repr) |
162
|
|
|
|
163
|
|
|
def __repr__(self): |
164
|
|
|
return _construct_repr_string(self, members_to_print) |
165
|
|
|
else: |
166
|
|
|
def __repr__(self): |
167
|
|
|
# Need to fetch member variables every time since they are unknown |
168
|
|
|
# until class instantation. |
169
|
|
|
members_to_print = get_public_members(self) |
170
|
|
|
|
171
|
|
|
member_repr_list = ((member, repr) for member in |
172
|
|
|
sorted(members_to_print, key=str.lower)) |
173
|
|
|
|
174
|
|
|
return _construct_repr_string(self, member_repr_list) |
175
|
|
|
|
176
|
|
|
return decorator |
177
|
|
|
|
178
|
|
|
|
179
|
|
|
def generate_eq(*members): |
180
|
|
|
""" |
181
|
|
|
Decorator that generates equality and inequality operators for the |
182
|
|
|
decorated class. The given members as well as the type of self and other |
183
|
|
|
will be taken into account. |
184
|
|
|
|
185
|
|
|
Note that this decorator modifies the given class in place! |
186
|
|
|
|
187
|
|
|
:param members: A list of members to compare for equality. |
188
|
|
|
""" |
189
|
|
|
def decorator(cls): |
190
|
|
|
def eq(self, other): |
191
|
|
|
if type(other) is not type(self): |
192
|
|
|
return False |
193
|
|
|
|
194
|
|
|
return all(getattr(self, member) == getattr(other, member) |
195
|
|
|
for member in members) |
196
|
|
|
|
197
|
|
|
def ne(self, other): |
198
|
|
|
return not eq(self, other) |
199
|
|
|
|
200
|
|
|
cls.__eq__ = eq |
201
|
|
|
cls.__ne__ = ne |
202
|
|
|
return cls |
203
|
|
|
|
204
|
|
|
return decorator |
205
|
|
|
|
206
|
|
|
|
207
|
|
|
def generate_ordering(*members): |
208
|
|
|
""" |
209
|
|
|
Decorator that generates ordering operators for the decorated class based |
210
|
|
|
on the given member names. All ordering except equality functions will |
211
|
|
|
raise a TypeError when a comparison with an unrelated class is attempted. |
212
|
|
|
(Comparisons with child classes will thus work fine with the capabilities |
213
|
|
|
of the base class as python will choose the base classes comparison |
214
|
|
|
operator in that case.) |
215
|
|
|
|
216
|
|
|
Note that this decorator modifies the given class in place! |
217
|
|
|
|
218
|
|
|
:param members: A list of members to compare, ordered from high priority to |
219
|
|
|
low. I.e. if the first member is equal the second will be |
220
|
|
|
taken for comparison and so on. If a member is None it is |
221
|
|
|
considered smaller than any other value except None. |
222
|
|
|
""" |
223
|
|
|
def decorator(cls): |
224
|
|
|
def lt(self, other): |
225
|
|
|
if not isinstance(other, cls): |
226
|
|
|
raise TypeError("Comparison with unrelated classes is " |
227
|
|
|
"unsupported.") |
228
|
|
|
|
229
|
|
|
for member in members: |
230
|
|
|
if getattr(self, member) == getattr(other, member): |
231
|
|
|
continue |
232
|
|
|
|
233
|
|
|
if ( |
234
|
|
|
getattr(self, member) is None or |
235
|
|
|
getattr(other, member) is None): |
236
|
|
|
return getattr(self, member) is None |
237
|
|
|
|
238
|
|
|
return getattr(self, member) < getattr(other, member) |
239
|
|
|
|
240
|
|
|
return False |
241
|
|
|
|
242
|
|
|
cls.__lt__ = lt |
243
|
|
|
return total_ordering(generate_eq(*members)(cls)) |
244
|
|
|
|
245
|
|
|
return decorator |
246
|
|
|
|
247
|
|
|
|
248
|
|
|
def _assert_right_type(value, types, argname): |
249
|
|
|
if isinstance(types, type) or types is None: |
250
|
|
|
types = (types,) |
251
|
|
|
|
252
|
|
|
for typ in types: |
253
|
|
|
if value == typ or (isinstance(typ, type) and isinstance(value, typ)): |
254
|
|
|
return |
255
|
|
|
|
256
|
|
|
raise TypeError("{} must be an instance of one of {} (provided value: " |
257
|
|
|
"{})".format(argname, types, repr(value))) |
258
|
|
|
|
259
|
|
|
|
260
|
|
|
def enforce_signature(function): |
261
|
|
|
""" |
262
|
|
|
Enforces the signature of the function by throwing TypeError's if invalid |
263
|
|
|
arguments are provided. The return value is not checked. |
264
|
|
|
|
265
|
|
|
You can annotate any parameter of your function with the desired type or a |
266
|
|
|
tuple of allowed types. If you annotate the function with a value, this |
267
|
|
|
value only will be allowed (useful especially for None). Example: |
268
|
|
|
|
269
|
|
|
>>> @enforce_signature |
270
|
|
|
... def test(arg: bool, another: (int, None)): |
271
|
|
|
... pass |
272
|
|
|
... |
273
|
|
|
>>> test(True, 5) |
274
|
|
|
>>> test(True, None) |
275
|
|
|
|
276
|
|
|
Any string value for any parameter e.g. would then trigger a TypeError. |
277
|
|
|
|
278
|
|
|
:param function: The function to check. |
279
|
|
|
""" |
280
|
|
|
argspec = inspect.getfullargspec(function) |
281
|
|
|
annotations = argspec.annotations |
282
|
|
|
argnames = argspec.args |
283
|
|
|
|
284
|
|
|
unnamed_annotations = {} |
285
|
|
|
for i, arg in enumerate(argnames): |
286
|
|
|
if arg in annotations: |
287
|
|
|
unnamed_annotations[i] = (annotations[arg], arg) |
288
|
|
|
|
289
|
|
|
def decorated(*args, **kwargs): |
290
|
|
|
for i, annotation in unnamed_annotations.items(): |
291
|
|
|
if i < len(args): |
292
|
|
|
_assert_right_type(args[i], annotation[0], annotation[1]) |
293
|
|
|
|
294
|
|
|
for argname, argval in kwargs.items(): |
295
|
|
|
if argname in annotations: |
296
|
|
|
_assert_right_type(argval, annotations[argname], argname) |
297
|
|
|
|
298
|
|
|
return function(*args, **kwargs) |
299
|
|
|
|
300
|
|
|
return decorated |
301
|
|
|
|