~jocave/checkbox/hybrid-amd-gpu-mods

« back to all changes in this revision

Viewing changes to plainbox/plainbox/impl/pod.py

  • Committer: Zygmunt Krynicki
  • Date: 2013-05-29 07:50:30 UTC
  • mto: This revision was merged to the branch mainline in revision 2153.
  • Revision ID: zygmunt.krynicki@canonical.com-20130529075030-ngwz245hs2u3y6us
checkbox: move current checkbox code into checkbox-old

This patch cleans up the top-level directory of the project into dedicated
sub-project directories. One for checkbox-old (the current checkbox and all the
associated stuff), one for plainbox and another for checkbox-ng.

There are some associated changes, such as updating the 'source' mode of
checkbox provider in plainbox, and fixing paths in various test scripts that we
have.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# encoding: utf-8
2
 
# This file is part of Checkbox.
3
 
#
4
 
# Copyright 2012-2015 Canonical Ltd.
5
 
# Written by:
6
 
#   Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
7
 
#
8
 
# Checkbox is free software: you can redistribute it and/or modify
9
 
# it under the terms of the GNU General Public License version 3,
10
 
# as published by the Free Software Foundation.
11
 
#
12
 
# Checkbox is distributed in the hope that it will be useful,
13
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 
# GNU General Public License for more details.
16
 
#
17
 
# You should have received a copy of the GNU General Public License
18
 
# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
19
 
"""
20
 
Plain Old Data.
21
 
 
22
 
:mod:`plainbox.impl.pod`
23
 
========================
24
 
 
25
 
This module contains the :class:`POD` and :class:`Field` classes that simplify
26
 
creation of declarative struct-like data holding classes. POD classes get a
27
 
useful repr() method, useful initializer and accessors for each of the fields
28
 
defined inside. POD classes can be inherited (properly detecting any field
29
 
clashes)
30
 
 
31
 
Defining POD classes:
32
 
 
33
 
    >>> class Person(POD):
34
 
    ...     name = Field("name of the person", str, MANDATORY)
35
 
    ...     age = Field("age of the person", int)
36
 
 
37
 
 
38
 
Creating POD instances, positional arguments match field definition order:
39
 
 
40
 
    >>> joe = Person("joe", age=42)
41
 
 
42
 
Full-blown comparison (not only equality):
43
 
 
44
 
    >>> joe == Person("joe", 42)
45
 
    True
46
 
 
47
 
Reading and writing attributes also works (obviously):
48
 
 
49
 
    >>> joe.name
50
 
    'joe'
51
 
    >>> joe.age
52
 
    42
53
 
    >>> joe.age = 24
54
 
    >>> joe.age
55
 
    24
56
 
 
57
 
For a full description check out the documentation of the :class:`POD` and
58
 
:class:`Field`.
59
 
"""
60
 
from collections import OrderedDict
61
 
from collections import namedtuple
62
 
from functools import total_ordering
63
 
from logging import getLogger
64
 
from textwrap import dedent
65
 
 
66
 
from plainbox.i18n import gettext as _
67
 
from plainbox.vendor import morris
68
 
 
69
 
__all__ = ('POD', 'PODBase', 'podify', 'Field', 'MANDATORY', 'UNSET',
70
 
           'read_only_assign_filter', 'type_convert_assign_filter',
71
 
           'type_check_assign_filter', 'modify_field_docstring')
72
 
 
73
 
 
74
 
_logger = getLogger("plainbox.pod")
75
 
 
76
 
 
77
 
class _Singleton:
78
 
 
79
 
    """A simple object()-like singleton that has a more useful repr()."""
80
 
 
81
 
    def __repr__(self):
82
 
        return self.__class__.__name__
83
 
 
84
 
 
85
 
class MANDATORY(_Singleton):
86
 
 
87
 
    """
88
 
    Class for the special MANDATORY object.
89
 
 
90
 
    This object can be used as a value in :attr:`Field.initial`.
91
 
 
92
 
    Using ``MANDATORY`` on a field like that makes the explicit initialization
93
 
    of the field mandatory during POD initialization. Please use this value to
94
 
    require that the caller supplies a given argument to the POD you are
95
 
    working with.
96
 
    """
97
 
 
98
 
 
99
 
MANDATORY = MANDATORY()
100
 
 
101
 
 
102
 
class UNSET(_Singleton):
103
 
 
104
 
    """
105
 
    Class of the special UNSET object.
106
 
 
107
 
    Singleton that is implicitly assigned to the values of all fields during
108
 
    POD initialization. This way all fields will have a value, even early at
109
 
    the time a POD is initialized. This can be important if the POD is somehow
110
 
    repr()-ed or inspected in other means.
111
 
 
112
 
    This object is also used by the :func:`read_only_assign_filter` function.
113
 
    """
114
 
 
115
 
 
116
 
UNSET = UNSET()
117
 
 
118
 
 
119
 
class Field:
120
 
 
121
 
    """
122
 
    A field in a plain-old-data class.
123
 
 
124
 
    Each field declares one attribute that can be read and written to. Just
125
 
    like a C structure. Attributes are readable _and_ writable but there is a
126
 
    lot of flexibility in what happens.
127
 
 
128
 
    :attr name:
129
 
        Name of the field (this is how this field can be accessed on the class
130
 
        or instance that contains it). This gets set by
131
 
        :meth:`_FieldCollection.inspect_namespace()`
132
 
    :attr instance_attr:
133
 
        Name of the POD dictionary entry used as backing store. This is set the
134
 
        same as ``name`` above. By default that's just name prepended with the
135
 
        ``'_'`` character.
136
 
    :attr type:
137
 
        An optional type hit. This is not used by default but assign filters
138
 
        can inspect and use this for type checking. It can also be used for
139
 
        documenting the intent of the field.
140
 
    :attr __doc__:
141
 
        The docstring of the field, as initialized by the caller.
142
 
    :attr initial:
143
 
        Initial value of this field, can be changed by passing arguments to
144
 
        :meth:`POD.__init__()`. May be set to ``MANDATORY`` for a special
145
 
        meaning (see below).
146
 
    :attr initial_fn:
147
 
        If not None this is a callable that produces the ``initial`` value for
148
 
        each new POD object.
149
 
    :attr notify:
150
 
        If True, a on_{name}_changed
151
 
        A flag controlling if notification events are sent for each
152
 
        modification of POD data through field.
153
 
    :attr notify_fn:
154
 
        An (optional) function to use as the first responder to the change
155
 
        notification signal. This field is only used if the ``notify``
156
 
        attribute is set to ``True``.
157
 
    :attr assign_filter_list:
158
 
        An (optional) list of assignment filter functions.
159
 
 
160
 
    A field is initialized based on the arguments passed to the POD
161
 
    initializer. If no argument is passed that would correspond to a given
162
 
    field the *initial* value is used. The *initial* value is either a constant
163
 
    (reference) stored in the ``initial`` property of the field or the return
164
 
    value of the callable in ``initial_fn``. Please make sure to use
165
 
    ``initial_fn`` if the value is not immutable as otherwise the produced
166
 
    value may be unintentionally shared by multiple objects.
167
 
 
168
 
    If the ``initial`` value is the special constant ``MANDATORY`` then the
169
 
    corresponding field must be explicitly initialized by the POD initializer
170
 
    argument list or a TypeError is raised.
171
 
 
172
 
    The ``notify`` flag controls the existence of the ``on_{name}_changed(old,
173
 
    new)`` signal on the class that includes the field. Applications can
174
 
    connect to that signal to observe changes. The signal is fired whenever the
175
 
    newly-assigned value compares *unequal* to the value currently stored in
176
 
    the POD.
177
 
 
178
 
    The ``notify_fn`` is an optional function that is used instead of the
179
 
    default (internal) :meth:`on_changed()` method of the Field class itself.
180
 
    If specified it must have the same three-argument signature. It will be
181
 
    called whenever the value of the field changes. Note that it will also be
182
 
    called on the initial assignment, when the ``old`` argument it receives
183
 
    will be set to the special ``UNSET`` object.
184
 
 
185
 
    Lastly a docstring and type hint can be provided for documentation. The
186
 
    type check is not enforced.
187
 
 
188
 
    Assignment filters are used to inspect and optionally modify a value during
189
 
    assignment (including the assignment done on object initialization) and can
190
 
    be used for various operations (including type conversions and validation).
191
 
    Assignment filters are called whenever a field is used to write to a POD.
192
 
 
193
 
    Since assignment filters are arranged in a list and executed in-order, they
194
 
    can also be used to modify the value as it gets propagated through the list
195
 
    of filters.
196
 
 
197
 
    The signature of each filter is ``fn(pod, field, old_value, new_value)``.
198
 
    The return value is the value shown to the subsequent filter or finally
199
 
    assigned to the POD.
200
 
    """
201
 
 
202
 
    _counter = 0
203
 
 
204
 
    def __init__(self, doc=None, type=None, initial=None, initial_fn=None,
205
 
                 notify=False, notify_fn=None, assign_filter_list=None):
206
 
        """Initialize (define) a new POD field."""
207
 
        self.__doc__ = dedent(doc) if doc is not None else None
208
 
        self.type = type
209
 
        self.initial = initial
210
 
        self.initial_fn = initial_fn
211
 
        self.notify = notify
212
 
        self.notify_fn = notify_fn
213
 
        self.assign_filter_list = assign_filter_list
214
 
        self.name = None  # Set via :meth:`gain_name()`
215
 
        self.instance_attr = None  # ditto
216
 
        self.signal_name = None  # ditto
217
 
        doc_extra = []
218
 
        for fn in self.assign_filter_list or ():
219
 
            if hasattr(fn, 'field_docstring_ext'):
220
 
                doc_extra.append(fn.field_docstring_ext.format(field=self))
221
 
        if doc_extra:
222
 
            self.__doc__ += (
223
 
                '\n\nSide effects of assign filters:\n'
224
 
                + '\n'.join('  - {}'.format(extra) for extra in doc_extra))
225
 
        self.counter = self.__class__._counter
226
 
        self.__class__._counter += 1
227
 
 
228
 
    @property
229
 
    def change_notifier(self):
230
 
        """
231
 
        Decorator for changing the change notification function.
232
 
 
233
 
        This decorator can be used to define all the fields in one block and
234
 
        all the notification function in another block. It helps to make the
235
 
        code easier to read.
236
 
 
237
 
        Example::
238
 
 
239
 
            >>> class Person(POD):
240
 
            ...     name = Field()
241
 
            ...
242
 
            ...     @name.change_notifier
243
 
            ...     def _name_changed(self, old, new):
244
 
            ...         print("changed from {!r} to {!r}".format(old, new))
245
 
            >>> person = Person()
246
 
            changed from UNSET to None
247
 
            >>> person.name = "bob"
248
 
            changed from None to 'bob'
249
 
 
250
 
        .. note::
251
 
            Keep in mind that the decorated function is converted to a signal
252
 
            automatically. The name of the function is also irrelevant, the POD
253
 
            core automatically creates signals that have consistent names of
254
 
            ``on_{field}_changed()``.
255
 
        """
256
 
        def decorator(fn):
257
 
            self.notify = True
258
 
            self.notify_fn = fn
259
 
            return fn
260
 
        return decorator
261
 
 
262
 
    def __repr__(self):
263
 
        """Get a debugging representation of a field."""
264
 
        return "<{} name:{!r}>".format(self.__class__.__name__, self.name)
265
 
 
266
 
    @property
267
 
    def is_mandatory(self) -> bool:
268
 
        """Flag indicating if the field needs a mandatory initializer."""
269
 
        return self.initial is MANDATORY
270
 
 
271
 
    def gain_name(self, name: str) -> None:
272
 
        """
273
 
        Set field name.
274
 
 
275
 
        :param name:
276
 
            Name of the field as it appears in a class definition
277
 
 
278
 
        Method called at most once on each Field instance embedded in a
279
 
        :class:`POD` subclass. This method informs the field of the name it was
280
 
        assigned to in the class.
281
 
        """
282
 
        self.name = name
283
 
        self.instance_attr = "_{}".format(name)
284
 
        self.signal_name = "on_{}_changed".format(name)
285
 
 
286
 
    def alter_cls(self, cls: type) -> None:
287
 
        """
288
 
        Modify class definition this field belongs to.
289
 
 
290
 
        This method is called during class construction. It allows the field to
291
 
        alter the class and add the on_{field.name}_changed signal. The signal
292
 
        is only added if notification is enabled *and* if there is no such
293
 
        signal in the first place (this allows inheritance not to create
294
 
        separate but identically-named signals and allows signal handlers
295
 
        connected via the base class to work on child classes.
296
 
        """
297
 
        if not self.notify:
298
 
            return
299
 
        assert self.signal_name is not None
300
 
        if not hasattr(cls, self.signal_name):
301
 
            signal_def = morris.signal(
302
 
                self.notify_fn if self.notify_fn is not None
303
 
                else self.on_changed,
304
 
                signal_name='{}.{}'.format(cls.__name__, self.signal_name))
305
 
            setattr(cls, self.signal_name, signal_def)
306
 
 
307
 
    def __get__(self, instance: object, owner: type) -> "Any":
308
 
        """
309
 
        Get field value from an object or from a class.
310
 
 
311
 
        This method is part of the Python descriptor protocol.
312
 
        """
313
 
        if instance is None:
314
 
            return self
315
 
        else:
316
 
            return getattr(instance, self.instance_attr)
317
 
 
318
 
    def __set__(self, instance: object, new_value: "Any") -> None:
319
 
        """
320
 
        Set field value from on an object.
321
 
 
322
 
        This method is part of the Python descriptor protocol.
323
 
 
324
 
        Assignments respect the assign filter chain, that is, the new value is
325
 
        being pushed through the chain of callbacks (each has a chance to alter
326
 
        the value) until it is finally assigned. Any of the callbacks can raise
327
 
        an exception and abort the setting process.
328
 
 
329
 
        This can be used to implement simple type checking, value checking or
330
 
        even type and value conversions.
331
 
        """
332
 
        if self.assign_filter_list is not None or self.notify:
333
 
            old_value = getattr(instance, self.instance_attr, UNSET)
334
 
        # Run the value through assign filters
335
 
        if self.assign_filter_list is not None:
336
 
            for assign_filter in self.assign_filter_list:
337
 
                new_value = assign_filter(instance, self, old_value, new_value)
338
 
        # Do value modification check if change notification is enabled
339
 
        if self.notify and hasattr(instance, self.instance_attr):
340
 
            if new_value != old_value:
341
 
                setattr(instance, self.instance_attr, new_value)
342
 
                on_field_change = getattr(instance, self.signal_name)
343
 
                on_field_change(old_value, new_value)
344
 
        else:
345
 
            # Or just fire away
346
 
            setattr(instance, self.instance_attr, new_value)
347
 
 
348
 
    def on_changed(self, pod: "POD", old: "Any", new: "Any") -> None:
349
 
        """
350
 
        The first responder of the per-field modification signal.
351
 
 
352
 
        :param pod:
353
 
            The object that contains the modified values
354
 
        :param old:
355
 
            The old value of the field
356
 
        :param new:
357
 
            The new value of the field
358
 
        """
359
 
        _logger.debug("<%s %s>.%s(%r, %r)", pod.__class__.__name__, id(pod),
360
 
                      self.signal_name, old, new)
361
 
 
362
 
 
363
 
@total_ordering
364
 
class PODBase:
365
 
 
366
 
    """Base class for POD-like classes."""
367
 
 
368
 
    field_list = []
369
 
    namedtuple_cls = namedtuple('PODBase', '')
370
 
 
371
 
    def __init__(self, *args, **kwargs):
372
 
        """
373
 
        Initialize a new POD object.
374
 
 
375
 
        Positional arguments bind to fields in declaration order. Keyword
376
 
        arguments bind to fields in any order but fields cannot be initialized
377
 
        twice.
378
 
 
379
 
        :raises TypeError:
380
 
            If there are more positional arguments than fields to initialize
381
 
        :raises TypeError:
382
 
            If a keyword argument doesn't correspond to a field name.
383
 
        :raises TypeError:
384
 
            If a field is initialized twice (first with positional arguments,
385
 
            then again with keyword arguments).
386
 
        :raises TypeError:
387
 
            If a ``MANDATORY`` field is not initialized.
388
 
        """
389
 
        field_list = self.__class__.field_list
390
 
        # Set all of the instance attributes to the special UNSET value, this
391
 
        # is useful if something fails and the object is inspected somehow.
392
 
        # Then all the attributes will be still UNSET.
393
 
        for field in field_list:
394
 
            setattr(self, field.instance_attr, UNSET)
395
 
        # Check if the number of positional arguments is correct
396
 
        if len(args) > len(field_list):
397
 
            raise TypeError("too many arguments")
398
 
        # Initialize mandatory fields using positional arguments
399
 
        for field, field_value in zip(field_list, args):
400
 
            setattr(self, field.name, field_value)
401
 
        # Initialize fields using keyword arguments
402
 
        for field_name, field_value in kwargs.items():
403
 
            field = getattr(self.__class__, field_name, None)
404
 
            if not isinstance(field, Field):
405
 
                raise TypeError("no such field: {}".format(field_name))
406
 
            if getattr(self, field.instance_attr) is not UNSET:
407
 
                raise TypeError(
408
 
                    "field initialized twice: {}".format(field_name))
409
 
            setattr(self, field_name, field_value)
410
 
        # Initialize remaining fields using their default initializers
411
 
        for field in field_list:
412
 
            if getattr(self, field.instance_attr) is not UNSET:
413
 
                continue
414
 
            if field.is_mandatory:
415
 
                raise TypeError(
416
 
                    "mandatory argument missing: {}".format(field.name))
417
 
            if field.initial_fn is not None:
418
 
                field_value = field.initial_fn()
419
 
            else:
420
 
                field_value = field.initial
421
 
            setattr(self, field.name, field_value)
422
 
 
423
 
    def __repr__(self):
424
 
        """Get a debugging representation of a POD object."""
425
 
        return "{}({})".format(
426
 
            self.__class__.__name__,
427
 
            ', '.join([
428
 
                '{}={!r}'.format(field.name, getattr(self, field.name))
429
 
                for field in self.__class__.field_list]))
430
 
 
431
 
    def __eq__(self, other: "POD") -> bool:
432
 
        """
433
 
        Check that this POD is equal to another POD.
434
 
 
435
 
        POD comparison is implemented by converting them to tuples and
436
 
        comparing the two tuples.
437
 
        """
438
 
        if not isinstance(other, POD):
439
 
            return NotImplemented
440
 
        return self.as_tuple() == other.as_tuple()
441
 
 
442
 
    def __lt__(self, other: "POD") -> bool:
443
 
        """
444
 
        Check that this POD is "less" than an another POD.
445
 
 
446
 
        POD comparison is implemented by converting them to tuples and
447
 
        comparing the two tuples.
448
 
        """
449
 
        if not isinstance(other, POD):
450
 
            return NotImplemented
451
 
        return self.as_tuple() < other.as_tuple()
452
 
 
453
 
    def as_tuple(self) -> tuple:
454
 
        """
455
 
        Return the data in this POD as a tuple.
456
 
 
457
 
        Order of elements in the tuple corresponds to the order of field
458
 
        declarations.
459
 
        """
460
 
        return self.__class__.namedtuple_cls(*[
461
 
            getattr(self, field.name)
462
 
            for field in self.__class__.field_list
463
 
        ])
464
 
 
465
 
    def as_dict(self) -> dict:
466
 
        """
467
 
        Return the data in this POD as a dictionary.
468
 
 
469
 
        .. note::
470
 
            UNSET values are not added to the dictionary.
471
 
        """
472
 
        return {
473
 
            field.name: getattr(self, field.name)
474
 
            for field in self.__class__.field_list
475
 
            if getattr(self, field.name) is not UNSET
476
 
        }
477
 
 
478
 
 
479
 
class _FieldCollection:
480
 
 
481
 
    """
482
 
    Support class for constructing POD meta-data information.
483
 
 
484
 
    Helper class that simplifies :class:`PODMeta` code that harvests
485
 
    :class:`Field` instances during class construction. Looking at the
486
 
    namespace and a list of base classes come up with a list of Field objects
487
 
    that belong to the given POD.
488
 
 
489
 
    :attr field_list:
490
 
        A list of :class:`Field` instances
491
 
    :attr field_origin_map:
492
 
        A dictionary mapping from field name to the *name* of the class that
493
 
        defines it.
494
 
    """
495
 
 
496
 
    def __init__(self):
497
 
        self.field_list = []
498
 
        self.field_origin_map = {}  # field name -> defining class name
499
 
 
500
 
    def inspect_cls_for_decorator(self, cls: type) -> None:
501
 
        """Analyze a bare POD class."""
502
 
        self.inspect_base_classes(cls.__bases__)
503
 
        self.inspect_namespace(cls.__dict__, cls.__name__)
504
 
 
505
 
    def inspect_base_classes(self, base_cls_list: "List[type]") -> None:
506
 
        """
507
 
        Analyze base classes of a POD class.
508
 
 
509
 
        Analyze a list of base classes and check if they have consistent
510
 
        fields.  All analyzed fields are added to the internal data structures.
511
 
 
512
 
        :param base_cls_list:
513
 
            A list of classes to inspect. Only subclasses of POD are inspected.
514
 
        """
515
 
        for base_cls in base_cls_list:
516
 
            if not issubclass(base_cls, PODBase):
517
 
                continue
518
 
            base_cls_name = base_cls.__name__
519
 
            for field in base_cls.field_list:
520
 
                self.add_field(field, base_cls_name)
521
 
 
522
 
    def inspect_namespace(self, namespace: dict, cls_name: str) -> None:
523
 
        """
524
 
        Analyze namespace of a POD class.
525
 
 
526
 
        Analyze a namespace of a newly (being formed) class and check if it has
527
 
        consistent fields. All analyzed fields are added to the internal data
528
 
        structures.
529
 
 
530
 
        .. note::
531
 
            This method calls :meth:`Field.gain_name()` on all fields it finds.
532
 
        """
533
 
        fields = []
534
 
        for field_name, field in namespace.items():
535
 
            if not isinstance(field, Field):
536
 
                continue
537
 
            field.gain_name(field_name)
538
 
            fields.append(field)
539
 
        fields.sort(key=lambda field: field.counter)
540
 
        for field in fields:
541
 
            self.add_field(field, cls_name)
542
 
 
543
 
    def get_namedtuple_cls(self, name: str) -> type:
544
 
        """
545
 
        Create a new namedtuple that corresponds to the fields seen so far.
546
 
 
547
 
        :parm name:
548
 
            Name of the namedtuple class
549
 
        :returns:
550
 
            A new namedtuple class
551
 
        """
552
 
        return namedtuple(name, [field.name for field in self.field_list])
553
 
 
554
 
    def add_field(self, field: Field, base_cls_name: str) -> None:
555
 
        """
556
 
        Add a field to the collection.
557
 
 
558
 
        :param field:
559
 
            A :class:`Field` instance
560
 
        :param base_cls_name:
561
 
            The name of the class that defines the field
562
 
        :raises TypeError:
563
 
            If any of the base classes have overlapping fields.
564
 
        """
565
 
        assert field.name is not None
566
 
        field_name = field.name
567
 
        if field_name not in self.field_origin_map:
568
 
            self.field_origin_map[field_name] = base_cls_name
569
 
            self.field_list.append(field)
570
 
        else:
571
 
            raise TypeError("field {1}.{0} clashes with {2}.{0}".format(
572
 
                field_name, base_cls_name, self.field_origin_map[field_name]))
573
 
 
574
 
 
575
 
class PODMeta(type):
576
 
 
577
 
    """
578
 
    Meta-class for all POD classes.
579
 
 
580
 
    This meta-class is responsible for correctly handling field inheritance.
581
 
    This class sets up ``field_list`` and ``namedtuple_cls`` attributes on the
582
 
    newly-created class.
583
 
    """
584
 
 
585
 
    def __new__(mcls, name, bases, namespace):
586
 
        fc = _FieldCollection()
587
 
        fc.inspect_base_classes(bases)
588
 
        fc.inspect_namespace(namespace, name)
589
 
        namespace['field_list'] = fc.field_list
590
 
        namespace['namedtuple_cls'] = fc.get_namedtuple_cls(name)
591
 
        cls = super().__new__(mcls, name, bases, namespace)
592
 
        for field in fc.field_list:
593
 
            field.alter_cls(cls)
594
 
        return cls
595
 
 
596
 
    @classmethod
597
 
    def __prepare__(mcls, name, bases, **kwargs):
598
 
        """
599
 
        Get a namespace for defining new POD classes.
600
 
 
601
 
        Prepare the namespace for the definition of a class using PODMeta as a
602
 
        meta-class. Since we want to observe the order of fields, using an
603
 
        OrderedDict makes that task trivial.
604
 
        """
605
 
        return OrderedDict()
606
 
 
607
 
 
608
 
def podify(cls):
609
 
    """
610
 
    Decorator for POD classes.
611
 
 
612
 
    The decorator offers an alternative from using the POD class (with the
613
 
    PODMeta meta-class). Instead of using that, one can use the ``@podify``
614
 
    decorator on a PODBase-derived class.
615
 
    """
616
 
    if not isinstance(cls, type) or not issubclass(cls, PODBase):
617
 
        raise TypeError("cls must be a subclass of PODBase")
618
 
    fc = _FieldCollection()
619
 
    fc.inspect_cls_for_decorator(cls)
620
 
    cls.field_list = fc.field_list
621
 
    cls.namedtuple_cls = fc.get_namedtuple_cls(cls.__name__)
622
 
    for field in fc.field_list:
623
 
        field.alter_cls(cls)
624
 
    return cls
625
 
 
626
 
 
627
 
@total_ordering
628
 
class POD(PODBase, metaclass=PODMeta):
629
 
 
630
 
    """
631
 
    Base class that removes boilerplate from plain-old-data classes.
632
 
 
633
 
    Use POD as your base class and define :class:`Field` objects inside.  Don't
634
 
    define any __init__() (unless you really, really have to have one) and
635
 
    instead set appropriate attributes on the initializer of a particular field
636
 
    object.
637
 
 
638
 
    What you get for *free* is, all the properties (for each field),
639
 
    documentation, initializer, comparison methods (PODs have total ordering)
640
 
    and the __repr__() method.
641
 
 
642
 
    There are some additional methods, such as :meth:`as_tuple()` and
643
 
    :meth:`as_dict()` that may be of use in some circumstances.
644
 
 
645
 
    All fields in a single POD subclass are collected (including all of the
646
 
    fields in the parent classes) and arranged in a list. That list is
647
 
    available as ``POD.field_list``.
648
 
 
649
 
    In addition each POD class has an unique named tuple that corresponds to
650
 
    each field stored inside the POD, the named tuple is available as
651
 
    ``POD.namedtuple_cls``. The return value of :meth:`as_tuple()` actually
652
 
    uses that type.
653
 
    """
654
 
 
655
 
 
656
 
def modify_field_docstring(field_docstring_ext: str):
657
 
    """
658
 
    Decorator for altering field docstrings via assign filter functions.
659
 
 
660
 
    A decorator for assign filter functions that allows them to declaratively
661
 
    modify the docstring of the field they are used on.
662
 
 
663
 
    :param field_docstring_ext:
664
 
        A string compatible with python's str.format() method. The string
665
 
        should be one line long (newlines will look odd) and may reference any
666
 
        of the field attributes, as exposed by the {field} named format
667
 
        attribute.
668
 
 
669
 
    Example:
670
 
 
671
 
        >>> @modify_field_docstring("not even")
672
 
        ... def not_even(instance, field, old, new):
673
 
        ...     if new % 2 == 0:
674
 
        ...         raise ValueError("value cannot be even")
675
 
        ...     return new
676
 
    """
677
 
    def decorator(fn):
678
 
        fn.field_docstring_ext = field_docstring_ext
679
 
        return fn
680
 
    return decorator
681
 
 
682
 
 
683
 
@modify_field_docstring("constant (read-only after initialization)")
684
 
def read_only_assign_filter(
685
 
        instance: POD, field: Field, old: "Any", new: "Any") -> "Any":
686
 
    """
687
 
    An assign filter that makes a field read-only.
688
 
 
689
 
    The field can be only assigned if the old value is ``UNSET``, that is,
690
 
    during the initial construction of a POD object.
691
 
 
692
 
    :param instance:
693
 
        A subclass of :class:`POD` that contains ``field``
694
 
    :param field:
695
 
        The :class:`Field` being assigned to
696
 
    :param old:
697
 
        The current value of the field
698
 
    :param new:
699
 
        The proposed value of the field
700
 
    :returns:
701
 
        ``new``, as-is
702
 
    :raises AttributeError:
703
 
        if ``old`` is anything but the special object ``UNSET``
704
 
    """
705
 
    if old is UNSET:
706
 
        return new
707
 
    raise AttributeError(_(
708
 
        "{}.{} is read-only"
709
 
    ).format(instance.__class__.__name__, field.name))
710
 
 
711
 
 
712
 
const = read_only_assign_filter
713
 
 
714
 
 
715
 
@modify_field_docstring(
716
 
    "type-converted (value must be convertible to {field.type.__name__})")
717
 
def type_convert_assign_filter(
718
 
        instance: POD, field: Field, old: "Any", new: "Any") -> "Any":
719
 
    """
720
 
    An assign filter that converts the value to the field type.
721
 
 
722
 
    The field must have a valid python type object stored in the .type field.
723
 
 
724
 
    :param instance:
725
 
        A subclass of :class:`POD` that contains ``field``
726
 
    :param field:
727
 
        The :class:`Field` being assigned to
728
 
    :param old:
729
 
        The current value of the field
730
 
    :param new:
731
 
        The proposed value of the field
732
 
    :returns:
733
 
        ``new`` type-converted to ``field.type``.
734
 
    :raises ValueError:
735
 
        if ``new`` cannot be converted to ``field.type``
736
 
    """
737
 
    return field.type(new)
738
 
 
739
 
 
740
 
@modify_field_docstring(
741
 
    "type-checked (value must be of type {field.type.__name__})")
742
 
def type_check_assign_filter(
743
 
        instance: POD, field: Field, old: "Any", new: "Any") -> "Any":
744
 
    """
745
 
    An assign filter that type-checks the value according to the field type.
746
 
 
747
 
    The field must have a valid python type object stored in the .type field.
748
 
 
749
 
    :param instance:
750
 
        A subclass of :class:`POD` that contains ``field``
751
 
    :param field:
752
 
        The :class:`Field` being assigned to
753
 
    :param old:
754
 
        The current value of the field
755
 
    :param new:
756
 
        The proposed value of the field
757
 
    :returns:
758
 
        ``new``, as-is
759
 
    :raises TypeError:
760
 
        if ``new`` is not an instance of ``field.type``
761
 
    """
762
 
    if isinstance(new, field.type):
763
 
        return new
764
 
    raise TypeError("{}.{} requires objects of type {}".format(
765
 
        instance.__class__.__name__, field.name, field.type.__name__))
766
 
 
767
 
 
768
 
typed = type_check_assign_filter
769
 
 
770
 
 
771
 
@modify_field_docstring(
772
 
    "unset or type-checked (value must be of type {field.type.__name__})")
773
 
def unset_or_type_check_assign_filter(
774
 
        instance: POD, field: Field, old: "Any", new: "Any") -> "Any":
775
 
    """
776
 
    An assign filter that type-checks the value according to the field type.
777
 
 
778
 
    .. note::
779
 
        This filter allows (passes through) the special ``UNSET`` value as-is.
780
 
 
781
 
    The field must have a valid python type object stored in the .type field.
782
 
 
783
 
    :param instance:
784
 
        A subclass of :class:`POD` that contains ``field``
785
 
    :param field:
786
 
        The :class:`Field` being assigned to
787
 
    :param old:
788
 
        The current value of the field
789
 
    :param new:
790
 
        The proposed value of the field
791
 
    :returns:
792
 
        ``new``, as-is
793
 
    :raises TypeError:
794
 
        if ``new`` is not an instance of ``field.type``
795
 
    """
796
 
    if new is UNSET:
797
 
        return new
798
 
    return type_check_assign_filter(instance, field, old, new)
799
 
 
800
 
 
801
 
unset_or_typed = unset_or_type_check_assign_filter
802
 
 
803
 
 
804
 
class sequence_type_check_assign_filter:
805
 
 
806
 
    """
807
 
    Assign filter for typed sequences.
808
 
 
809
 
    An assign filter for typed sequences (lists or tuples) that must contain an
810
 
    object of the given type.
811
 
    """
812
 
 
813
 
    def __init__(self, item_type: type):
814
 
        """
815
 
        Initialize the assign filter with the given sequence item type.
816
 
 
817
 
        :param item_type:
818
 
            Desired type of each sequence item.
819
 
        """
820
 
        self.item_type = item_type
821
 
 
822
 
    @property
823
 
    def field_docstring_ext(self) -> str:
824
 
        return "type-checked sequence (items must be of type {})".format(
825
 
            self.item_type.__name__)
826
 
 
827
 
    def __call__(
828
 
            self, instance: POD, field: Field, old: "Any", new: "Any"
829
 
    ) -> "Any":
830
 
        """
831
 
        An assign filter that type-checks the value of all sequence elements.
832
 
 
833
 
        :param instance:
834
 
            A subclass of :class:`POD` that contains ``field``
835
 
        :param field:
836
 
            The :class:`Field` being assigned to
837
 
        :param old:
838
 
            The current value of the field
839
 
        :param new:
840
 
            The proposed value of the field
841
 
        :returns:
842
 
            ``new``, as-is
843
 
        :raises TypeError:
844
 
            if ``new`` is not an instance of ``field.type``
845
 
        """
846
 
        for item in new:
847
 
            if not isinstance(item, self.item_type):
848
 
                raise TypeError(
849
 
                    "{}.{} requires all sequence elements of type {}".format(
850
 
                        instance.__class__.__name__, field.name,
851
 
                        self.item_type.__name__))
852
 
        return new
853
 
 
854
 
 
855
 
typed.sequence = sequence_type_check_assign_filter
856
 
 
857
 
 
858
 
class unset_or_sequence_type_check_assign_filter(typed.sequence):
859
 
 
860
 
    """
861
 
    Assign filter for typed sequences.
862
 
 
863
 
    .. note::
864
 
        This filter allows (passes through) the special ``UNSET`` value as-is.
865
 
 
866
 
    An assign filter for typed sequences (lists or tuples) that must contain an
867
 
    object of the given type.
868
 
    """
869
 
 
870
 
    @property
871
 
    def field_docstring_ext(self) -> str:
872
 
        return (
873
 
            "unset or type-checked sequence (items must be of type {})"
874
 
        ).format(self.item_type.__name__)
875
 
 
876
 
    def __call__(
877
 
            self, instance: POD, field: Field, old: "Any", new: "Any"
878
 
    ) -> "Any":
879
 
        """
880
 
        An assign filter that type-checks the value of all sequence elements.
881
 
 
882
 
        .. note::
883
 
            This filter allows (passes through) the special ``UNSET`` value
884
 
            as-is.
885
 
 
886
 
        :param instance:
887
 
            A subclass of :class:`POD` that contains ``field``
888
 
        :param field:
889
 
            The :class:`Field` being assigned to
890
 
        :param old:
891
 
            The current value of the field
892
 
        :param new:
893
 
            The proposed value of the field
894
 
        :returns:
895
 
            ``new``, as-is
896
 
        :raises TypeError:
897
 
            if ``new`` is not an instance of ``field.type``
898
 
        """
899
 
        if new is UNSET:
900
 
            return new
901
 
        return super().__call__(instance, field, old, new)
902
 
 
903
 
 
904
 
unset_or_typed.sequence = unset_or_sequence_type_check_assign_filter
905
 
 
906
 
 
907
 
@modify_field_docstring("unique elements (sequence elements cannot repeat)")
908
 
def unique_elements_assign_filter(
909
 
        instance: POD, field: Field, old: "Any", new: "Any") -> "Any":
910
 
    """
911
 
    An assign filter that ensures a sequence has non-repeating items.
912
 
 
913
 
    :param instance:
914
 
        A subclass of :class:`POD` that contains ``field``
915
 
    :param field:
916
 
        The :class:`Field` being assigned to
917
 
    :param old:
918
 
        The current value of the field
919
 
    :param new:
920
 
        The proposed value of the field
921
 
    :returns:
922
 
        ``new``, as-is
923
 
    :raises ValueError:
924
 
        if ``new`` contains any duplicates
925
 
    """
926
 
    seen = set()
927
 
    for item in new:
928
 
        if new in seen:
929
 
            raise ValueError("Duplicate element: {!r}".format(item))
930
 
        seen.add(item)
931
 
    return new
932
 
 
933
 
unique = unique_elements_assign_filter