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
|
|
|
|