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
|
|
|
Fields = _Factory() |
439
|
|
|
ConvertibleFields = factory(class_sealer, convertible=True) |
440
|
|
|
SlotsFields = factory(slots_class_sealer) |
441
|
|
|
BareFields = factory(class_sealer, comparable=False, printable=False) |
442
|
|
|
InheritableFields = factory(class_sealer, base=object, pass_kwargs=True) |
443
|
|
|
|
444
|
|
|
Tuple = factory(tuple_sealer) |
445
|
|
|
|
446
|
|
|
PrintableMixin = factory(class_sealer, initializer=False, base=object, comparable=False) |
447
|
|
|
ComparableMixin = factory(class_sealer, initializer=False, base=object, printable=False) |
448
|
|
|
ConvertibleMixin = factory( |
449
|
|
|
class_sealer, initializer=False, base=object, printable=False, comparable=False, convertible=True |
450
|
|
|
) |
451
|
|
|
|