__base__   A
last analyzed

Complexity

Total Complexity 1

Size/Duplication

Total Lines 3
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 3
rs 10
wmc 1

1 Method

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