Completed
Push — master ( 6de098...be5965 )
by Ionel Cristian
53s
created

src.fields.make_init_func()   F

Complexity

Conditions 12

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 38
cc 12
rs 2.7855

How to fix   Complexity   

Complexity

Complex classes like src.fields.make_init_func() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
How it works: the library is composed of 2 major parts:
3
4
* The `sealers`. They return a class that implements a container according the given specification (list of field names
5
  and default values).
6
* The `factory`. A metaclass that implements attribute/item access, so you can do ``Fields.a.b.c``. On each
7
  getattr/getitem it returns a new instance with the new state. Its ``__new__`` method takes extra arguments to store
8
  the contruction state and it works in two ways:
9
10
  * Construction phase (there are no bases). Make new instances of the `Factory` with new state.
11
  * Usage phase. When subclassed (there are bases) it will use the sealer to return the final class.
12
"""
13
import sys
14
from itertools import chain
15
from operator import itemgetter
16
try:
17
    from collections import OrderedDict
18
except ImportError:
19
    from .py2ordereddict import OrderedDict
20
21
__version__ = "3.0.0"
22
__all__ = (
23
    'BareFields',
24
    'ComparableMixin',
25
    'ConvertibleFields',
26
    'ConvertibleMixin',
27
    'Fields',
28
    'PrintableMixin',
29
    'SlotsFields',
30
    'Tuple',
31
    # advanced stuff
32
    'factory',
33
    'make_init_func',
34
    'class_sealer',
35
    'slots_class_sealer',
36
    'tuple_sealer',
37
    # convenience things
38
    'Namespace'
39
)
40
PY2 = sys.version_info[0] == 2
41
MISSING = object()
42
43
44
def _with_metaclass(meta, *bases):
45
    # See: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/#metaclass-syntax-changes
46
47
    class metaclass(meta):
48
        __call__ = type.__call__
49
        __init__ = type.__init__
50
51
        def __new__(cls, name, this_bases, d):
52
            if this_bases is None:
53
                return type.__new__(cls, name, (), d)
54
            return meta(name, bases, d)
55
    return metaclass('temporary_class', None, {})
56
57
58
class __base__(object):
59
    def __init__(self, *args, **kwargs):
60
        pass
61
62
63
def make_init_func(fields, defaults,
64
                   header='def __init__(self',
65
                   super_call_start='super(FieldsBase, self).__init__(',
66
                   super_call_end=')\n',
67
                   super_call=True,
68
                   super_call_kw=True, set_attributes=True):
69
    parts = [header]
70
    still_positional = True
71
    for var in fields:
72
        if var in defaults:
73
            still_positional = False
74
            parts.append(', {0}={0}'.format(var))
75
        elif still_positional:
76
            parts.append(', {0}'.format(var))
77
        else:
78
            raise ValueError("Cannot have positional fields after fields with defaults. "
79
                             "Field {0!r} is missing a default value!".format(var))
80
    parts.append('):\n')
81
    if set_attributes:
82
        for var in fields:
83
            parts.append('    self.{0} = {0}\n'.format(var))
84
    if super_call:
85
        parts.append('    ')
86
        parts.append(super_call_start)
87
        if super_call_kw:
88
            parts.append(', '.join('{0}={0}'.format(var) for var in fields))
89
        else:
90
            parts.append(', '.join('{0}'.format(var) for var in fields))
91
        parts.append(super_call_end)
92
    local_namespace = dict(defaults)
93
    global_namespace = dict(super=super) if super_call else {}
94
    code = ''.join(parts)
95
96
    if PY2:
97
        exec("exec code in global_namespace, local_namespace")
98
    else:
99
        exec(code, global_namespace, local_namespace)
100
    return global_namespace, local_namespace
101
102
103
def class_sealer(required, defaults, everything,
104
                 base=__base__, make_init_func=make_init_func,
105
                 initializer=True, comparable=True, printable=True, convertible=False):
106
    """
107
    This sealer makes a normal container class. It's mutable and supports arguments with default values.
108
    """
109
    if initializer:
110
        global_namespace, local_namespace = make_init_func(everything, defaults)
111
112
    class FieldsBase(base):
113
        if initializer:
114
            __init__ = local_namespace['__init__']
115
116
        if comparable:
117
            def __eq__(self, other):
118
                if isinstance(other, self.__class__):
119
                    return tuple(getattr(self, a) for a in everything) == tuple(getattr(other, a) for a in everything)
120
                else:
121
                    return NotImplemented
122
123
            def __ne__(self, other):
124
                result = self.__eq__(other)
125
                if result is NotImplemented:
126
                    return NotImplemented
127
                else:
128
                    return not result
129
130
            def __lt__(self, other):
131
                if isinstance(other, self.__class__):
132
                    return tuple(getattr(self, a) for a in everything) < tuple(getattr(other, a) for a in everything)
133
                else:
134
                    return NotImplemented
135
136
            def __le__(self, other):
137
                if isinstance(other, self.__class__):
138
                    return tuple(getattr(self, a) for a in everything) <= tuple(getattr(other, a) for a in everything)
139
                else:
140
                    return NotImplemented
141
142
            def __gt__(self, other):
143
                if isinstance(other, self.__class__):
144
                    return tuple(getattr(self, a) for a in everything) > tuple(getattr(other, a) for a in everything)
145
                else:
146
                    return NotImplemented
147
148
            def __ge__(self, other):
149
                if isinstance(other, self.__class__):
150
                    return tuple(getattr(self, a) for a in everything) >= tuple(getattr(other, a) for a in everything)
151
                else:
152
                    return NotImplemented
153
154
            def __hash__(self):
155
                return hash(tuple(getattr(self, a) for a in everything))
156
157
        if printable:
158
            def __repr__(self):
159
                return "{0}({1})".format(
160
                    self.__class__.__name__,
161
                    ", ".join("{0}={1}".format(attr, repr(getattr(self, attr))) for attr in everything)
162
                )
163
        if convertible:
164
            @property
165
            def as_dict(self):
166
                return dict((attr, getattr(self, attr)) for attr in everything)
167
168
            @property
169
            def as_tuple(self):
170
                return tuple(getattr(self, attr) for attr in everything)
171
172
    if initializer:
173
        global_namespace['FieldsBase'] = FieldsBase
174
    return FieldsBase
175
176
177
def slots_class_sealer(required, defaults, everything):
178
    """
179
    This sealer makes a container class that uses ``__slots__`` (it uses :func:`class_sealer` internally).
180
181
    The resulting class has a metaclass that forcibly sets ``__slots__`` on subclasses.
182
    """
183
    class __slots_meta__(type):
184
        def __new__(mcs, name, bases, namespace):
185
            if "__slots__" not in namespace:
186
                namespace["__slots__"] = everything
187
            return type.__new__(mcs, name, bases, namespace)
188
189
    class __slots_base__(_with_metaclass(__slots_meta__, object)):
190
        __slots__ = ()
191
192
        def __init__(self, *args, **kwargs):
193
            pass
194
195
    return class_sealer(required, defaults, everything, base=__slots_base__)
196
197
198
def tuple_sealer(required, defaults, everything):
199
    """
200
    This sealer returns an equivalent of a ``namedtuple``.
201
    """
202
    global_namespace, local_namespace = make_init_func(
203
        everything, defaults,
204
        header='def __new__(cls',
205
        super_call_start='return tuple.__new__(cls, (',
206
        super_call_end='))\n',
207
        super_call_kw=False, set_attributes=False,
208
    )
209
210
    def __getnewargs__(self):
211
        return tuple(self)
212
213
    def __repr__(self):
214
        return "{0}({1})".format(
215
            self.__class__.__name__,
216
            ", ".join(a + "=" + repr(getattr(self, a)) for a in everything)
217
        )
218
219
    return type("TupleBase", (tuple,), dict(
220
        [(name, property(itemgetter(i))) for i, name in enumerate(everything)],
221
        __new__=local_namespace['__new__'],
222
        __getnewargs__=__getnewargs__,
223
        __repr__=__repr__,
224
        __slots__=(),
225
    ))
226
227
228
class _SealerWrapper(object):
229
    """
230
    Primitive wrapper around a function that makes it `un-bindable`.
231
232
    When you add a function in the namespace of a class it will be bound (become a method) when you try to access it.
233
    This class prevents that.
234
    """
235
    def __init__(self, func, **kwargs):
236
        self.func = func
237
        self.kwargs = kwargs
238
239
    @property
240
    def __name__(self):
241
        return self.func.__name__
242
243
    def __call__(self, *args, **kwargs):
244
        return self.func(*args, **dict(self.kwargs, **kwargs))
245
246
247
class _Factory(type):
248
    """
249
    This class makes everything work. It a metaclass for the class that users are going to use. Each new chain
250
    rebuilds everything.
251
    """
252
253
    __required = ()
254
    __defaults = ()
255
    __all_fields = ()
256
    __last_field = None
257
    __full_required = ()
258
    __sealer = None
259
    __concrete = None
260
261
    def __getattr__(cls, name):
262
        if name.startswith("__") and name.endswith("__"):
263
            return type.__getattribute__(cls, name)
264
        if name in cls.__required:
265
            raise TypeError("Field %r is already specified as required." % name)
266
        if name in cls.__defaults:
267
            raise TypeError("Field %r is already specified with a default value (%r)." % (
268
                name, cls.__defaults[name]
269
            ))
270
        if name == cls.__last_field:
271
            raise TypeError("Field %r is already specified as required." % name)
272
        if cls.__defaults and cls.__last_field is not None:
273
            raise TypeError("Can't add required fields after fields with defaults.")
274
275
        return _Factory(
276
            required=cls.__full_required,
277
            defaults=cls.__defaults,
278
            last_field=name,
279
            sealer=cls.__sealer,
280
        )
281
282
    def __getitem__(cls, default):
283
        if cls.__last_field is None:
284
            raise TypeError("Can't set default %r. There's no previous field." % default)
285
286
        new_defaults = OrderedDict(cls.__defaults)
287
        new_defaults[cls.__last_field] = default
288
        return _Factory(
289
            required=cls.__required,
290
            defaults=new_defaults,
291
            sealer=cls.__sealer,
292
        )
293
294
    def __new__(mcs, name="__blank__", bases=(), namespace=None, last_field=None, required=(), defaults=(),
295
                sealer=_SealerWrapper(class_sealer)):
296
297
        if not bases:
298
            assert isinstance(sealer, _SealerWrapper)
299
300
            full_required = tuple(required)
301
            if last_field is not None:
302
                full_required += last_field,
303
            all_fields = list(chain(full_required, defaults))
304
305
            return type.__new__(
306
                _Factory,
307
                "Fields<%s>.%s" % (sealer.__name__, ".".join(all_fields))
308
                if all_fields else "Fields<%s>" % sealer.__name__,
309
                bases,
310
                dict(
311
                    _Factory__required=required,
312
                    _Factory__defaults=defaults,
313
                    _Factory__all_fields=all_fields,
314
                    _Factory__last_field=last_field,
315
                    _Factory__full_required=full_required,
316
                    _Factory__sealer=sealer,
317
                )
318
            )
319
        else:
320
            for pos, names in enumerate(zip(*[
321
                k.__all_fields
322
                for k in bases
323
                if isinstance(k, _Factory)
324
            ])):
325
                if names:
326
                    if len(set(names)) != 1:
327
                        raise TypeError("Field layout conflict: fields in position %s have different names: %s" % (
328
                            pos,
329
                            ', '.join(repr(name) for name in names)
330
                        ))
331
332
            return type(name, tuple(
333
                    ~k if isinstance(k, _Factory) else k for k in bases
334
            ), {} if namespace is None else namespace)
335
336
    def __init__(cls, *args, **kwargs):
337
        pass
338
339
    def __call__(cls, *args, **kwargs):
340
        return (~cls)(*args, **kwargs)
341
342
    def __invert__(cls):
343
        if cls.__concrete is None:
344
            if not cls.__all_fields:
345
                raise TypeError("You're trying to use an empty Fields factory !")
346
            if cls.__defaults and cls.__last_field is not None:
347
                raise TypeError("Can't add required fields after fields with defaults.")
348
349
            cls.__concrete = cls.__sealer(cls.__full_required, cls.__defaults, cls.__all_fields)
350
        return cls.__concrete
351
352
353
class Namespace(object):
354
    """
355
    A backport of Python 3.3's ``types.SimpleNamespace``.
356
    """
357
    def __init__(self, **kwargs):
358
        self.__dict__.update(kwargs)
359
360
    def __repr__(self):
361
        keys = sorted(self.__dict__)
362
        items = ("{0}={1!r}".format(k, self.__dict__[k]) for k in keys)
363
        return "{0}({1})".format(type(self).__name__, ", ".join(items))
364
365
    def __eq__(self, other):
366
        return self.__dict__ == other.__dict__
367
368
369
def factory(sealer, **sealer_options):
370
    """
371
    Create a factory that will produce a class using the given ``sealer``.
372
373
    Args:
374
        sealer (func): A function that takes ``required, defaults, everything`` as arguments, where:
375
376
            * required (list): A list of strings
377
            * defaults (dict): A dict with all the defaults
378
            * everything (list): A list with all the field names in the declared order.
379
        sealer_options: Optional keyword arguments passed to ``sealer``.
380
    Return:
381
        A class on which you can do `.field1.field2.field3...`. When it's subclassed it "seals", and whatever the
382
        ``sealer`` returned for the given fields is used as the baseclass.
383
384
        Example:
385
386
        .. sourcecode:: pycon
387
388
389
            >>> def sealer(required, defaults, everything):
390
            ...     print("Creating class with:")
391
            ...     print("  required = {0}".format(required))
392
            ...     print("  defaults = {0}".format(defaults))
393
            ...     print("  everything = {0}".format(everything))
394
            ...     return object
395
            ...
396
            >>> Fields = factory(sealer)
397
            >>> class Foo(Fields.foo.bar.lorem[1].ipsum[2]):
398
            ...     pass
399
            ...
400
            Creating class with:
401
              required = ('foo', 'bar')
402
              defaults = OrderedDict([('lorem', 1), ('ipsum', 2)])
403
              everything = ['foo', 'bar', 'lorem', 'ipsum']
404
            >>> Foo
405
            <class '...Foo'>
406
            >>> Foo.__bases__
407
            (<... 'object'>,)
408
    """
409
    return _Factory(sealer=_SealerWrapper(sealer, **sealer_options))
410
411
Fields = _Factory()
412
ConvertibleFields = factory(class_sealer, convertible=True)
413
SlotsFields = factory(slots_class_sealer)
414
BareFields = factory(class_sealer, comparable=False, printable=False)
415
416
Tuple = factory(tuple_sealer)
417
418
PrintableMixin = factory(class_sealer, initializer=False, base=object, comparable=False)
419
ComparableMixin = factory(class_sealer, initializer=False, base=object, printable=False)
420
ConvertibleMixin = factory(
421
    class_sealer, initializer=False, base=object, printable=False, comparable=False, convertible=True
422
)
423