Failed Conditions
Pull Request — master (#1311)
by Sudheesh
01:49
created

coalib.misc.decorator()   B

Complexity

Conditions 5

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 5
dl 0
loc 3
rs 8.5454
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