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

src.fields._Factory.__call__()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

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