Merge lp:~zyga/checkbox/fix-1444996 into lp:checkbox

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
Reviewer Review Type Date Requested Status
Maciej Kisielewski Approve
Review via email: mp+256508@code.launchpad.net

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.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Good stuff, +1

review: Approve

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

Subscribers

People subscribed via source and target branches