Merge lp:~zyga/checkbox/fix-1444996 into lp:checkbox
- fix-1444996
- Merge into trunk
Proposed by
Zygmunt Krynicki
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Maciej Kisielewski | ||||
Approved revision: | 3703 | ||||
Merged at revision: | 3702 | ||||
Proposed branch: | lp:~zyga/checkbox/fix-1444996 | ||||
Merge into: | lp:checkbox | ||||
Diff against target: |
489 lines (+186/-121) 1 file modified
plainbox/plainbox/impl/pod.py (+186/-121) |
||||
To merge this branch: | bzr merge lp:~zyga/checkbox/fix-1444996 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Maciej Kisielewski | Approve | ||
Review via email: mp+256508@code.launchpad.net |
Commit message
Description of the change
d66ee14 plainbox:pod: fix PEP257 issues
47ab7c6 plainbox:pod: fix PEP-8 issues
b8e68dd plainbox:pod: add .counter to each Field.
836d41c plainbox:pod: use field.counter to determine field ordering
f8b9993 plainbox:pod: move POD methods to new PODBase class
6470a6a plainbox:pod: add @podify decorator
5d52f5b plainbox:pod: refresh __all__
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'plainbox/plainbox/impl/pod.py' |
2 | --- plainbox/plainbox/impl/pod.py 2015-03-27 20:26:58 +0000 |
3 | +++ plainbox/plainbox/impl/pod.py 2015-04-16 14:45:30 +0000 |
4 | @@ -16,8 +16,10 @@ |
5 | # You should have received a copy of the GNU General Public License |
6 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
7 | """ |
8 | -:mod:`plainbox.impl.pod` -- Plain Old Data |
9 | -========================================== |
10 | +Plain Old Data. |
11 | + |
12 | +:mod:`plainbox.impl.pod` |
13 | +======================== |
14 | |
15 | This module contains the :class:`POD` and :class:`Field` classes that simplify |
16 | creation of declarative struct-like data holding classes. POD classes get a |
17 | @@ -63,26 +65,28 @@ |
18 | from plainbox.i18n import gettext as _ |
19 | from plainbox.vendor.morris import signal |
20 | |
21 | -__all__ = ['POD', 'Field', 'MANDATORY', 'UNSET', 'read_only_assign_filter', |
22 | - 'type_convert_assign_filter', 'type_check_assign_filter', |
23 | - 'modify_field_docstring'] |
24 | +__all__ = ('POD', 'PODBase', 'podify', 'Field', 'MANDATORY', 'UNSET', |
25 | + 'read_only_assign_filter', 'type_convert_assign_filter', |
26 | + 'type_check_assign_filter', 'modify_field_docstring') |
27 | |
28 | |
29 | _logger = getLogger("plainbox.pod") |
30 | |
31 | |
32 | class _Singleton: |
33 | - """ |
34 | - A simple object()-like singleton that has a more useful repr() |
35 | - """ |
36 | + |
37 | + """ A simple object()-like singleton that has a more useful repr(). """ |
38 | |
39 | def __repr__(self): |
40 | return self.__class__.__name__ |
41 | |
42 | |
43 | class MANDATORY(_Singleton): |
44 | + |
45 | """ |
46 | - Singleton that can be used as a value in :attr:`Field.initial`. |
47 | + Class for the special MANDATORY object. |
48 | + |
49 | + This object can be used as a value in :attr:`Field.initial`. |
50 | |
51 | Using ``MANDATORY`` on a field like that makes the explicit initialization |
52 | of the field mandatory during POD initialization. Please use this value to |
53 | @@ -95,7 +99,10 @@ |
54 | |
55 | |
56 | class UNSET(_Singleton): |
57 | + |
58 | """ |
59 | + Class of the special UNSET object. |
60 | + |
61 | Singleton that is implicitly assigned to the values of all fields during |
62 | POD initialization. This way all fields will have a value, even early at |
63 | the time a POD is initialized. This can be important if the POD is somehow |
64 | @@ -109,6 +116,7 @@ |
65 | |
66 | |
67 | class Field: |
68 | + |
69 | """ |
70 | A field in a plain-old-data class. |
71 | |
72 | @@ -190,8 +198,11 @@ |
73 | assigned to the POD. |
74 | """ |
75 | |
76 | + _counter = 0 |
77 | + |
78 | def __init__(self, doc=None, type=None, initial=None, initial_fn=None, |
79 | notify=False, notify_fn=None, assign_filter_list=None): |
80 | + """ Initialize (define) a new POD field. """ |
81 | self.__doc__ = dedent(doc) if doc is not None else None |
82 | self.type = type |
83 | self.initial = initial |
84 | @@ -210,15 +221,16 @@ |
85 | self.__doc__ += ( |
86 | '\n\nSide effects of assign filters:\n' |
87 | + '\n'.join(' - {}'.format(extra) for extra in doc_extra)) |
88 | + self.counter = self.__class__._counter |
89 | + self.__class__._counter += 1 |
90 | |
91 | def __repr__(self): |
92 | + """ Get a debugging representation of a field. """ |
93 | return "<{} name:{!r}>".format(self.__class__.__name__, self.name) |
94 | |
95 | @property |
96 | def is_mandatory(self) -> bool: |
97 | - """ |
98 | - Flag indicating if the field needs a mandatory initializer. |
99 | - """ |
100 | + """ Flag indicating if the field needs a mandatory initializer. """ |
101 | return self.initial is MANDATORY |
102 | |
103 | def gain_name(self, name: str) -> None: |
104 | @@ -252,7 +264,8 @@ |
105 | assert self.signal_name is not None |
106 | if not hasattr(cls, self.signal_name): |
107 | signal_def = signal( |
108 | - self.notify_fn if self.notify_fn is not None else self.on_changed, |
109 | + self.notify_fn if self.notify_fn is not None |
110 | + else self.on_changed, |
111 | signal_name='{}.{}'.format(cls.__name__, self.signal_name)) |
112 | setattr(cls, self.signal_name, signal_def) |
113 | |
114 | @@ -312,8 +325,121 @@ |
115 | self.signal_name, old, new) |
116 | |
117 | |
118 | +@total_ordering |
119 | +class PODBase: |
120 | + |
121 | + """ Base class for POD-like classes. """ |
122 | + |
123 | + field_list = [] |
124 | + namedtuple_cls = namedtuple('PODBase', '') |
125 | + |
126 | + def __init__(self, *args, **kwargs): |
127 | + """ |
128 | + Initialize a new POD object. |
129 | + |
130 | + Positional arguments bind to fields in declaration order. Keyword |
131 | + arguments bind to fields in any order but fields cannot be initialized |
132 | + twice. |
133 | + |
134 | + :raises TypeError: |
135 | + If there are more positional arguments than fields to initialize |
136 | + :raises TypeError: |
137 | + If a keyword argument doesn't correspond to a field name. |
138 | + :raises TypeError: |
139 | + If a field is initialized twice (first with positional arguments, |
140 | + then again with keyword arguments). |
141 | + :raises TypeError: |
142 | + If a ``MANDATORY`` field is not initialized. |
143 | + """ |
144 | + field_list = self.__class__.field_list |
145 | + # Set all of the instance attributes to the special UNSET value, this |
146 | + # is useful if something fails and the object is inspected somehow. |
147 | + # Then all the attributes will be still UNSET. |
148 | + for field in field_list: |
149 | + setattr(self, field.instance_attr, UNSET) |
150 | + # Check if the number of positional arguments is correct |
151 | + if len(args) > len(field_list): |
152 | + raise TypeError("too many arguments") |
153 | + # Initialize mandatory fields using positional arguments |
154 | + for field, field_value in zip(field_list, args): |
155 | + setattr(self, field.name, field_value) |
156 | + # Initialize fields using keyword arguments |
157 | + for field_name, field_value in kwargs.items(): |
158 | + field = getattr(self.__class__, field_name, None) |
159 | + if not isinstance(field, Field): |
160 | + raise TypeError("no such field: {}".format(field_name)) |
161 | + if getattr(self, field.instance_attr) is not UNSET: |
162 | + raise TypeError( |
163 | + "field initialized twice: {}".format(field_name)) |
164 | + setattr(self, field_name, field_value) |
165 | + # Initialize remaining fields using their default initializers |
166 | + for field in field_list: |
167 | + if getattr(self, field.instance_attr) is not UNSET: |
168 | + continue |
169 | + if field.is_mandatory: |
170 | + raise TypeError( |
171 | + "mandatory argument missing: {}".format(field.name)) |
172 | + if field.initial_fn is not None: |
173 | + field_value = field.initial_fn() |
174 | + else: |
175 | + field_value = field.initial |
176 | + setattr(self, field.name, field_value) |
177 | + |
178 | + def __repr__(self): |
179 | + """ Get a debugging representation of a POD object. """ |
180 | + return "{}({})".format( |
181 | + self.__class__.__name__, |
182 | + ', '.join([ |
183 | + '{}={!r}'.format(field.name, getattr(self, field.name)) |
184 | + for field in self.__class__.field_list])) |
185 | + |
186 | + def __eq__(self, other: "POD") -> bool: |
187 | + """ |
188 | + Check that this POD is equal to another POD. |
189 | + |
190 | + POD comparison is implemented by converting them to tuples and |
191 | + comparing the two tuples. |
192 | + """ |
193 | + if not isinstance(other, POD): |
194 | + return NotImplemented |
195 | + return self.as_tuple() == other.as_tuple() |
196 | + |
197 | + def __lt__(self, other: "POD") -> bool: |
198 | + """ |
199 | + Check that this POD is "less" than an another POD. |
200 | + |
201 | + POD comparison is implemented by converting them to tuples and |
202 | + comparing the two tuples. |
203 | + """ |
204 | + if not isinstance(other, POD): |
205 | + return NotImplemented |
206 | + return self.as_tuple() < other.as_tuple() |
207 | + |
208 | + def as_tuple(self) -> tuple: |
209 | + """ |
210 | + Return the data in this POD as a tuple. |
211 | + |
212 | + Order of elements in the tuple corresponds to the order of field |
213 | + declarations. |
214 | + """ |
215 | + return self.__class__.namedtuple_cls(*[ |
216 | + getattr(self, field.name) |
217 | + for field in self.__class__.field_list |
218 | + ]) |
219 | + |
220 | + def as_dict(self) -> dict: |
221 | + """ Return the data in this POD as a dictionary. """ |
222 | + return { |
223 | + field.name: getattr(self, field.name) |
224 | + for field in self.__class__.field_list |
225 | + } |
226 | + |
227 | + |
228 | class _FieldCollection: |
229 | + |
230 | """ |
231 | + Support class for constructing POD meta-data information. |
232 | + |
233 | Helper class that simplifies :class:`PODMeta` code that harvests |
234 | :class:`Field` instances during class construction. Looking at the |
235 | namespace and a list of base classes come up with a list of Field objects |
236 | @@ -330,8 +456,15 @@ |
237 | self.field_list = [] |
238 | self.field_origin_map = {} # field name -> defining class name |
239 | |
240 | + def inspect_cls_for_decorator(self, cls: type) -> None: |
241 | + """ Analyze a bare POD class. """ |
242 | + self.inspect_base_classes(cls.__bases__) |
243 | + self.inspect_namespace(cls.__dict__, cls.__name__) |
244 | + |
245 | def inspect_base_classes(self, base_cls_list: "List[type]") -> None: |
246 | """ |
247 | + Analyze base classes of a POD class. |
248 | + |
249 | Analyze a list of base classes and check if they have consistent |
250 | fields. All analyzed fields are added to the internal data structures. |
251 | |
252 | @@ -339,7 +472,7 @@ |
253 | A list of classes to inspect. Only subclasses of POD are inspected. |
254 | """ |
255 | for base_cls in base_cls_list: |
256 | - if not issubclass(base_cls, POD): |
257 | + if not issubclass(base_cls, PODBase): |
258 | continue |
259 | base_cls_name = base_cls.__name__ |
260 | for field in base_cls.field_list: |
261 | @@ -347,6 +480,8 @@ |
262 | |
263 | def inspect_namespace(self, namespace: dict, cls_name: str) -> None: |
264 | """ |
265 | + Analyze namespace of a POD class. |
266 | + |
267 | Analyze a namespace of a newly (being formed) class and check if it has |
268 | consistent fields. All analyzed fields are added to the internal data |
269 | structures. |
270 | @@ -354,15 +489,19 @@ |
271 | .. note:: |
272 | This method calls :meth:`Field.gain_name()` on all fields it finds. |
273 | """ |
274 | + fields = [] |
275 | for field_name, field in namespace.items(): |
276 | if not isinstance(field, Field): |
277 | continue |
278 | field.gain_name(field_name) |
279 | + fields.append(field) |
280 | + fields.sort(key=lambda field: field.counter) |
281 | + for field in fields: |
282 | self.add_field(field, cls_name) |
283 | |
284 | def get_namedtuple_cls(self, name: str) -> type: |
285 | """ |
286 | - Create a new namedtuple that corresponds to the fields seen so far |
287 | + Create a new namedtuple that corresponds to the fields seen so far. |
288 | |
289 | :parm name: |
290 | Name of the namedtuple class |
291 | @@ -393,6 +532,7 @@ |
292 | |
293 | |
294 | class PODMeta(type): |
295 | + |
296 | """ |
297 | Meta-class for all POD classes. |
298 | |
299 | @@ -415,6 +555,8 @@ |
300 | @classmethod |
301 | def __prepare__(mcls, name, bases, **kwargs): |
302 | """ |
303 | + Get a namespace for defining new POD classes. |
304 | + |
305 | Prepare the namespace for the definition of a class using PODMeta as a |
306 | meta-class. Since we want to observe the order of fields, using an |
307 | OrderedDict makes that task trivial. |
308 | @@ -422,8 +564,28 @@ |
309 | return OrderedDict() |
310 | |
311 | |
312 | +def podify(cls): |
313 | + """ |
314 | + Decorator for POD classes. |
315 | + |
316 | + The decorator offers an alternative from using the POD class (with the |
317 | + PODMeta meta-class). Instead of using that, one can use the ``@podify`` |
318 | + decorator on a PODBase-derived class. |
319 | + """ |
320 | + if not isinstance(cls, type) or not issubclass(cls, PODBase): |
321 | + raise TypeError("cls must be a subclass of PODBase") |
322 | + fc = _FieldCollection() |
323 | + fc.inspect_cls_for_decorator(cls) |
324 | + cls.field_list = fc.field_list |
325 | + cls.namedtuple_cls = fc.get_namedtuple_cls(cls.__name__) |
326 | + for field in fc.field_list: |
327 | + field.alter_cls(cls) |
328 | + return cls |
329 | + |
330 | + |
331 | @total_ordering |
332 | -class POD(metaclass=PODMeta): |
333 | +class POD(PODBase, metaclass=PODMeta): |
334 | + |
335 | """ |
336 | Base class that removes boilerplate from plain-old-data classes. |
337 | |
338 | @@ -449,111 +611,11 @@ |
339 | uses that type. |
340 | """ |
341 | |
342 | - def __init__(self, *args, **kwargs): |
343 | - """ |
344 | - Initialize a new POD object. |
345 | - |
346 | - Positional arguments bind to fields in declaration order. Keyword |
347 | - arguments bind to fields in any order but fields cannot be initialized |
348 | - twice. |
349 | - |
350 | - :raises TypeError: |
351 | - If there are more positional arguments than fields to initialize |
352 | - :raises TypeError: |
353 | - If a keyword argument doesn't correspond to a field name. |
354 | - :raises TypeError: |
355 | - If a field is initialized twice (first with positional arguments, |
356 | - then again with keyword arguments). |
357 | - :raises TypeError: |
358 | - If a ``MANDATORY`` field is not initialized. |
359 | - """ |
360 | - field_list = self.__class__.field_list |
361 | - # Set all of the instance attributes to the special UNSET value, this |
362 | - # is useful if something fails and the object is inspected somehow. |
363 | - # Then all the attributes will be still UNSET. |
364 | - for field in field_list: |
365 | - setattr(self, field.instance_attr, UNSET) |
366 | - # Check if the number of positional arguments is correct |
367 | - if len(args) > len(field_list): |
368 | - raise TypeError("too many arguments") |
369 | - # Initialize mandatory fields using positional arguments |
370 | - for field, field_value in zip(field_list, args): |
371 | - setattr(self, field.name, field_value) |
372 | - # Initialize fields using keyword arguments |
373 | - for field_name, field_value in kwargs.items(): |
374 | - field = getattr(self.__class__, field_name, None) |
375 | - if not isinstance(field, Field): |
376 | - raise TypeError("no such field: {}".format(field_name)) |
377 | - if getattr(self, field.instance_attr) is not UNSET: |
378 | - raise TypeError( |
379 | - "field initialized twice: {}".format(field_name)) |
380 | - setattr(self, field_name, field_value) |
381 | - # Initialize remaining fields using their default initializers |
382 | - for field in field_list: |
383 | - if getattr(self, field.instance_attr) is not UNSET: |
384 | - continue |
385 | - if field.is_mandatory: |
386 | - raise TypeError( |
387 | - "mandatory argument missing: {}".format(field.name)) |
388 | - if field.initial_fn is not None: |
389 | - field_value = field.initial_fn() |
390 | - else: |
391 | - field_value = field.initial |
392 | - setattr(self, field.name, field_value) |
393 | - |
394 | - def __repr__(self): |
395 | - return "{}({})".format( |
396 | - self.__class__.__name__, |
397 | - ', '.join([ |
398 | - '{}={!r}'.format(field.name, getattr(self, field.name)) |
399 | - for field in self.__class__.field_list])) |
400 | - |
401 | - def __eq__(self, other: "POD") -> bool: |
402 | - """ |
403 | - Check that this POD is equal to another POD |
404 | - |
405 | - POD comparison is implemented by converting them to tuples and |
406 | - comparing the two tuples. |
407 | - """ |
408 | - if not isinstance(other, POD): |
409 | - return NotImplemented |
410 | - return self.as_tuple() == other.as_tuple() |
411 | - |
412 | - def __lt__(self, other: "POD") -> bool: |
413 | - """ |
414 | - Check that this POD is "less" than an another POD. |
415 | - |
416 | - POD comparison is implemented by converting them to tuples and |
417 | - comparing the two tuples. |
418 | - """ |
419 | - if not isinstance(other, POD): |
420 | - return NotImplemented |
421 | - return self.as_tuple() < other.as_tuple() |
422 | - |
423 | - def as_tuple(self) -> tuple: |
424 | - """ |
425 | - Return the data in this POD as a tuple. |
426 | - |
427 | - Order of elements in the tuple corresponds to the order of field |
428 | - declarations. |
429 | - """ |
430 | - return self.__class__.namedtuple_cls(*[ |
431 | - getattr(self, field.name) |
432 | - for field in self.__class__.field_list |
433 | - ]) |
434 | - |
435 | - def as_dict(self) -> dict: |
436 | - """ |
437 | - Return the data in this POD as a dictionary |
438 | - """ |
439 | - return { |
440 | - field.name: getattr(self, field.name) |
441 | - for field in self.__class__.field_list |
442 | - } |
443 | - |
444 | |
445 | def modify_field_docstring(field_docstring_ext: str): |
446 | """ |
447 | + Decorator for altering field docstrings via assign filter functions. |
448 | + |
449 | A decorator for assign filter functions that allows them to declaratively |
450 | modify the docstring of the field they are used on. |
451 | |
452 | @@ -581,7 +643,7 @@ |
453 | def read_only_assign_filter( |
454 | instance: POD, field: Field, old: "Any", new: "Any") -> "Any": |
455 | """ |
456 | - An assign filter that makes a field read-only |
457 | + An assign filter that makes a field read-only. |
458 | |
459 | The field can be only assigned if the old value is ``UNSET``, that is, |
460 | during the initial construction of a POD object. |
461 | @@ -639,7 +701,7 @@ |
462 | def type_check_assign_filter( |
463 | instance: POD, field: Field, old: "Any", new: "Any") -> "Any": |
464 | """ |
465 | - An assign filter that type-checks the value according to the field type |
466 | + An assign filter that type-checks the value according to the field type. |
467 | |
468 | The field must have a valid python type object stored in the .type field. |
469 | |
470 | @@ -666,7 +728,10 @@ |
471 | |
472 | |
473 | class sequence_type_check_assign_filter: |
474 | + |
475 | """ |
476 | + Assign filter for typed sequences. |
477 | + |
478 | An assign filter for typed sequences (lists or tuples) that must contain an |
479 | object of the given type. |
480 | """ |
481 | @@ -689,7 +754,7 @@ |
482 | self, instance: POD, field: Field, old: "Any", new: "Any" |
483 | ) -> "Any": |
484 | """ |
485 | - An assign filter that type-checks the value of all sequence elements |
486 | + An assign filter that type-checks the value of all sequence elements. |
487 | |
488 | :param instance: |
489 | A subclass of :class:`POD` that contains ``field`` |
Good stuff, +1