~cypressyew/checkbox/no-s3-adv

« back to all changes in this revision

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

  • Committer: Po-Hsu Lin
  • Date: 2015-04-20 08:39:36 UTC
  • mfrom: (3694.1.17 checkbox)
  • Revision ID: po-hsu.lin@canonical.com-20150420083936-8gva6p4jfyb0a8wu
merge with trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
# You should have received a copy of the GNU General Public License
17
17
# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
18
18
"""
19
 
:mod:`plainbox.impl.pod` -- Plain Old Data
20
 
==========================================
 
19
Plain Old Data.
 
20
 
 
21
:mod:`plainbox.impl.pod`
 
22
========================
21
23
 
22
24
This module contains the :class:`POD` and :class:`Field` classes that simplify
23
25
creation of declarative struct-like data holding classes. POD classes get a
61
63
from textwrap import dedent
62
64
 
63
65
from plainbox.i18n import gettext as _
64
 
from plainbox.vendor.morris import signal
 
66
from plainbox.vendor import morris
65
67
 
66
 
__all__ = ['POD', 'Field', 'MANDATORY', 'UNSET', 'read_only_assign_filter',
67
 
           'type_convert_assign_filter', 'type_check_assign_filter',
68
 
           'modify_field_docstring']
 
68
__all__ = ('POD', 'PODBase', 'podify', 'Field', 'MANDATORY', 'UNSET',
 
69
           'read_only_assign_filter', 'type_convert_assign_filter',
 
70
           'type_check_assign_filter', 'modify_field_docstring')
69
71
 
70
72
 
71
73
_logger = getLogger("plainbox.pod")
72
74
 
73
75
 
74
76
class _Singleton:
75
 
    """
76
 
    A simple object()-like singleton that has a more useful repr()
77
 
    """
 
77
 
 
78
    """ A simple object()-like singleton that has a more useful repr(). """
78
79
 
79
80
    def __repr__(self):
80
81
        return self.__class__.__name__
81
82
 
82
83
 
83
84
class MANDATORY(_Singleton):
 
85
 
84
86
    """
85
 
    Singleton that can be used as a value in :attr:`Field.initial`.
 
87
    Class for the special MANDATORY object.
 
88
 
 
89
    This object can be used as a value in :attr:`Field.initial`.
86
90
 
87
91
    Using ``MANDATORY`` on a field like that makes the explicit initialization
88
92
    of the field mandatory during POD initialization. Please use this value to
95
99
 
96
100
 
97
101
class UNSET(_Singleton):
 
102
 
98
103
    """
 
104
    Class of the special UNSET object.
 
105
 
99
106
    Singleton that is implicitly assigned to the values of all fields during
100
107
    POD initialization. This way all fields will have a value, even early at
101
108
    the time a POD is initialized. This can be important if the POD is somehow
109
116
 
110
117
 
111
118
class Field:
 
119
 
112
120
    """
113
121
    A field in a plain-old-data class.
114
122
 
190
198
    assigned to the POD.
191
199
    """
192
200
 
 
201
    _counter = 0
 
202
 
193
203
    def __init__(self, doc=None, type=None, initial=None, initial_fn=None,
194
204
                 notify=False, notify_fn=None, assign_filter_list=None):
 
205
        """ Initialize (define) a new POD field. """
195
206
        self.__doc__ = dedent(doc) if doc is not None else None
196
207
        self.type = type
197
208
        self.initial = initial
210
221
            self.__doc__ += (
211
222
                '\n\nSide effects of assign filters:\n'
212
223
                + '\n'.join('  - {}'.format(extra) for extra in doc_extra))
 
224
        self.counter = self.__class__._counter
 
225
        self.__class__._counter += 1
213
226
 
214
227
    def __repr__(self):
 
228
        """ Get a debugging representation of a field. """
215
229
        return "<{} name:{!r}>".format(self.__class__.__name__, self.name)
216
230
 
217
231
    @property
218
232
    def is_mandatory(self) -> bool:
219
 
        """
220
 
        Flag indicating if the field needs a mandatory initializer.
221
 
        """
 
233
        """ Flag indicating if the field needs a mandatory initializer. """
222
234
        return self.initial is MANDATORY
223
235
 
224
236
    def gain_name(self, name: str) -> None:
251
263
            return
252
264
        assert self.signal_name is not None
253
265
        if not hasattr(cls, self.signal_name):
254
 
            signal_def = signal(
255
 
                self.notify_fn if self.notify_fn is not None else self.on_changed,
 
266
            signal_def = morris.signal(
 
267
                self.notify_fn if self.notify_fn is not None
 
268
                else self.on_changed,
256
269
                signal_name='{}.{}'.format(cls.__name__, self.signal_name))
257
270
            setattr(cls, self.signal_name, signal_def)
258
271
 
312
325
                      self.signal_name, old, new)
313
326
 
314
327
 
 
328
@total_ordering
 
329
class PODBase:
 
330
 
 
331
    """ Base class for POD-like classes. """
 
332
 
 
333
    field_list = []
 
334
    namedtuple_cls = namedtuple('PODBase', '')
 
335
 
 
336
    def __init__(self, *args, **kwargs):
 
337
        """
 
338
        Initialize a new POD object.
 
339
 
 
340
        Positional arguments bind to fields in declaration order. Keyword
 
341
        arguments bind to fields in any order but fields cannot be initialized
 
342
        twice.
 
343
 
 
344
        :raises TypeError:
 
345
            If there are more positional arguments than fields to initialize
 
346
        :raises TypeError:
 
347
            If a keyword argument doesn't correspond to a field name.
 
348
        :raises TypeError:
 
349
            If a field is initialized twice (first with positional arguments,
 
350
            then again with keyword arguments).
 
351
        :raises TypeError:
 
352
            If a ``MANDATORY`` field is not initialized.
 
353
        """
 
354
        field_list = self.__class__.field_list
 
355
        # Set all of the instance attributes to the special UNSET value, this
 
356
        # is useful if something fails and the object is inspected somehow.
 
357
        # Then all the attributes will be still UNSET.
 
358
        for field in field_list:
 
359
            setattr(self, field.instance_attr, UNSET)
 
360
        # Check if the number of positional arguments is correct
 
361
        if len(args) > len(field_list):
 
362
            raise TypeError("too many arguments")
 
363
        # Initialize mandatory fields using positional arguments
 
364
        for field, field_value in zip(field_list, args):
 
365
            setattr(self, field.name, field_value)
 
366
        # Initialize fields using keyword arguments
 
367
        for field_name, field_value in kwargs.items():
 
368
            field = getattr(self.__class__, field_name, None)
 
369
            if not isinstance(field, Field):
 
370
                raise TypeError("no such field: {}".format(field_name))
 
371
            if getattr(self, field.instance_attr) is not UNSET:
 
372
                raise TypeError(
 
373
                    "field initialized twice: {}".format(field_name))
 
374
            setattr(self, field_name, field_value)
 
375
        # Initialize remaining fields using their default initializers
 
376
        for field in field_list:
 
377
            if getattr(self, field.instance_attr) is not UNSET:
 
378
                continue
 
379
            if field.is_mandatory:
 
380
                raise TypeError(
 
381
                    "mandatory argument missing: {}".format(field.name))
 
382
            if field.initial_fn is not None:
 
383
                field_value = field.initial_fn()
 
384
            else:
 
385
                field_value = field.initial
 
386
            setattr(self, field.name, field_value)
 
387
 
 
388
    def __repr__(self):
 
389
        """ Get a debugging representation of a POD object. """
 
390
        return "{}({})".format(
 
391
            self.__class__.__name__,
 
392
            ', '.join([
 
393
                '{}={!r}'.format(field.name, getattr(self, field.name))
 
394
                for field in self.__class__.field_list]))
 
395
 
 
396
    def __eq__(self, other: "POD") -> bool:
 
397
        """
 
398
        Check that this POD is equal to another POD.
 
399
 
 
400
        POD comparison is implemented by converting them to tuples and
 
401
        comparing the two tuples.
 
402
        """
 
403
        if not isinstance(other, POD):
 
404
            return NotImplemented
 
405
        return self.as_tuple() == other.as_tuple()
 
406
 
 
407
    def __lt__(self, other: "POD") -> bool:
 
408
        """
 
409
        Check that this POD is "less" than an another POD.
 
410
 
 
411
        POD comparison is implemented by converting them to tuples and
 
412
        comparing the two tuples.
 
413
        """
 
414
        if not isinstance(other, POD):
 
415
            return NotImplemented
 
416
        return self.as_tuple() < other.as_tuple()
 
417
 
 
418
    def as_tuple(self) -> tuple:
 
419
        """
 
420
        Return the data in this POD as a tuple.
 
421
 
 
422
        Order of elements in the tuple corresponds to the order of field
 
423
        declarations.
 
424
        """
 
425
        return self.__class__.namedtuple_cls(*[
 
426
            getattr(self, field.name)
 
427
            for field in self.__class__.field_list
 
428
        ])
 
429
 
 
430
    def as_dict(self) -> dict:
 
431
        """ Return the data in this POD as a dictionary. """
 
432
        return {
 
433
            field.name: getattr(self, field.name)
 
434
            for field in self.__class__.field_list
 
435
        }
 
436
 
 
437
 
315
438
class _FieldCollection:
 
439
 
316
440
    """
 
441
    Support class for constructing POD meta-data information.
 
442
 
317
443
    Helper class that simplifies :class:`PODMeta` code that harvests
318
444
    :class:`Field` instances during class construction. Looking at the
319
445
    namespace and a list of base classes come up with a list of Field objects
330
456
        self.field_list = []
331
457
        self.field_origin_map = {}  # field name -> defining class name
332
458
 
 
459
    def inspect_cls_for_decorator(self, cls: type) -> None:
 
460
        """ Analyze a bare POD class. """
 
461
        self.inspect_base_classes(cls.__bases__)
 
462
        self.inspect_namespace(cls.__dict__, cls.__name__)
 
463
 
333
464
    def inspect_base_classes(self, base_cls_list: "List[type]") -> None:
334
465
        """
 
466
        Analyze base classes of a POD class.
 
467
 
335
468
        Analyze a list of base classes and check if they have consistent
336
469
        fields.  All analyzed fields are added to the internal data structures.
337
470
 
339
472
            A list of classes to inspect. Only subclasses of POD are inspected.
340
473
        """
341
474
        for base_cls in base_cls_list:
342
 
            if not issubclass(base_cls, POD):
 
475
            if not issubclass(base_cls, PODBase):
343
476
                continue
344
477
            base_cls_name = base_cls.__name__
345
478
            for field in base_cls.field_list:
347
480
 
348
481
    def inspect_namespace(self, namespace: dict, cls_name: str) -> None:
349
482
        """
 
483
        Analyze namespace of a POD class.
 
484
 
350
485
        Analyze a namespace of a newly (being formed) class and check if it has
351
486
        consistent fields. All analyzed fields are added to the internal data
352
487
        structures.
354
489
        .. note::
355
490
            This method calls :meth:`Field.gain_name()` on all fields it finds.
356
491
        """
 
492
        fields = []
357
493
        for field_name, field in namespace.items():
358
494
            if not isinstance(field, Field):
359
495
                continue
360
496
            field.gain_name(field_name)
 
497
            fields.append(field)
 
498
        fields.sort(key=lambda field: field.counter)
 
499
        for field in fields:
361
500
            self.add_field(field, cls_name)
362
501
 
363
502
    def get_namedtuple_cls(self, name: str) -> type:
364
503
        """
365
 
        Create a new namedtuple that corresponds to the fields seen so far
 
504
        Create a new namedtuple that corresponds to the fields seen so far.
366
505
 
367
506
        :parm name:
368
507
            Name of the namedtuple class
393
532
 
394
533
 
395
534
class PODMeta(type):
 
535
 
396
536
    """
397
537
    Meta-class for all POD classes.
398
538
 
415
555
    @classmethod
416
556
    def __prepare__(mcls, name, bases, **kwargs):
417
557
        """
 
558
        Get a namespace for defining new POD classes.
 
559
 
418
560
        Prepare the namespace for the definition of a class using PODMeta as a
419
561
        meta-class. Since we want to observe the order of fields, using an
420
562
        OrderedDict makes that task trivial.
422
564
        return OrderedDict()
423
565
 
424
566
 
 
567
def podify(cls):
 
568
    """
 
569
    Decorator for POD classes.
 
570
 
 
571
    The decorator offers an alternative from using the POD class (with the
 
572
    PODMeta meta-class). Instead of using that, one can use the ``@podify``
 
573
    decorator on a PODBase-derived class.
 
574
    """
 
575
    if not isinstance(cls, type) or not issubclass(cls, PODBase):
 
576
        raise TypeError("cls must be a subclass of PODBase")
 
577
    fc = _FieldCollection()
 
578
    fc.inspect_cls_for_decorator(cls)
 
579
    cls.field_list = fc.field_list
 
580
    cls.namedtuple_cls = fc.get_namedtuple_cls(cls.__name__)
 
581
    for field in fc.field_list:
 
582
        field.alter_cls(cls)
 
583
    return cls
 
584
 
 
585
 
425
586
@total_ordering
426
 
class POD(metaclass=PODMeta):
 
587
class POD(PODBase, metaclass=PODMeta):
 
588
 
427
589
    """
428
590
    Base class that removes boilerplate from plain-old-data classes.
429
591
 
449
611
    uses that type.
450
612
    """
451
613
 
452
 
    def __init__(self, *args, **kwargs):
453
 
        """
454
 
        Initialize a new POD object.
455
 
 
456
 
        Positional arguments bind to fields in declaration order. Keyword
457
 
        arguments bind to fields in any order but fields cannot be initialized
458
 
        twice.
459
 
 
460
 
        :raises TypeError:
461
 
            If there are more positional arguments than fields to initialize
462
 
        :raises TypeError:
463
 
            If a keyword argument doesn't correspond to a field name.
464
 
        :raises TypeError:
465
 
            If a field is initialized twice (first with positional arguments,
466
 
            then again with keyword arguments).
467
 
        :raises TypeError:
468
 
            If a ``MANDATORY`` field is not initialized.
469
 
        """
470
 
        field_list = self.__class__.field_list
471
 
        # Set all of the instance attributes to the special UNSET value, this
472
 
        # is useful if something fails and the object is inspected somehow.
473
 
        # Then all the attributes will be still UNSET.
474
 
        for field in field_list:
475
 
            setattr(self, field.instance_attr, UNSET)
476
 
        # Check if the number of positional arguments is correct
477
 
        if len(args) > len(field_list):
478
 
            raise TypeError("too many arguments")
479
 
        # Initialize mandatory fields using positional arguments
480
 
        for field, field_value in zip(field_list, args):
481
 
            setattr(self, field.name, field_value)
482
 
        # Initialize fields using keyword arguments
483
 
        for field_name, field_value in kwargs.items():
484
 
            field = getattr(self.__class__, field_name, None)
485
 
            if not isinstance(field, Field):
486
 
                raise TypeError("no such field: {}".format(field_name))
487
 
            if getattr(self, field.instance_attr) is not UNSET:
488
 
                raise TypeError(
489
 
                    "field initialized twice: {}".format(field_name))
490
 
            setattr(self, field_name, field_value)
491
 
        # Initialize remaining fields using their default initializers
492
 
        for field in field_list:
493
 
            if getattr(self, field.instance_attr) is not UNSET:
494
 
                continue
495
 
            if field.is_mandatory:
496
 
                raise TypeError(
497
 
                    "mandatory argument missing: {}".format(field.name))
498
 
            if field.initial_fn is not None:
499
 
                field_value = field.initial_fn()
500
 
            else:
501
 
                field_value = field.initial
502
 
            setattr(self, field.name, field_value)
503
 
 
504
 
    def __repr__(self):
505
 
        return "{}({})".format(
506
 
            self.__class__.__name__,
507
 
            ', '.join([
508
 
                '{}={!r}'.format(field.name, getattr(self, field.name))
509
 
                for field in self.__class__.field_list]))
510
 
 
511
 
    def __eq__(self, other: "POD") -> bool:
512
 
        """
513
 
        Check that this POD is equal to another POD
514
 
 
515
 
        POD comparison is implemented by converting them to tuples and
516
 
        comparing the two tuples.
517
 
        """
518
 
        if not isinstance(other, POD):
519
 
            return NotImplemented
520
 
        return self.as_tuple() == other.as_tuple()
521
 
 
522
 
    def __lt__(self, other: "POD") -> bool:
523
 
        """
524
 
        Check that this POD is "less" than an another POD.
525
 
 
526
 
        POD comparison is implemented by converting them to tuples and
527
 
        comparing the two tuples.
528
 
        """
529
 
        if not isinstance(other, POD):
530
 
            return NotImplemented
531
 
        return self.as_tuple() < other.as_tuple()
532
 
 
533
 
    def as_tuple(self) -> tuple:
534
 
        """
535
 
        Return the data in this POD as a tuple.
536
 
 
537
 
        Order of elements in the tuple corresponds to the order of field
538
 
        declarations.
539
 
        """
540
 
        return self.__class__.namedtuple_cls(*[
541
 
            getattr(self, field.name)
542
 
            for field in self.__class__.field_list
543
 
        ])
544
 
 
545
 
    def as_dict(self) -> dict:
546
 
        """
547
 
        Return the data in this POD as a dictionary
548
 
        """
549
 
        return {
550
 
            field.name: getattr(self, field.name)
551
 
            for field in self.__class__.field_list
552
 
        }
553
 
 
554
614
 
555
615
def modify_field_docstring(field_docstring_ext: str):
556
616
    """
 
617
    Decorator for altering field docstrings via assign filter functions.
 
618
 
557
619
    A decorator for assign filter functions that allows them to declaratively
558
620
    modify the docstring of the field they are used on.
559
621
 
581
643
def read_only_assign_filter(
582
644
        instance: POD, field: Field, old: "Any", new: "Any") -> "Any":
583
645
    """
584
 
    An assign filter that makes a field read-only
 
646
    An assign filter that makes a field read-only.
585
647
 
586
648
    The field can be only assigned if the old value is ``UNSET``, that is,
587
649
    during the initial construction of a POD object.
639
701
def type_check_assign_filter(
640
702
        instance: POD, field: Field, old: "Any", new: "Any") -> "Any":
641
703
    """
642
 
    An assign filter that type-checks the value according to the field type
 
704
    An assign filter that type-checks the value according to the field type.
643
705
 
644
706
    The field must have a valid python type object stored in the .type field.
645
707
 
666
728
 
667
729
 
668
730
class sequence_type_check_assign_filter:
 
731
 
669
732
    """
 
733
    Assign filter for typed sequences.
 
734
 
670
735
    An assign filter for typed sequences (lists or tuples) that must contain an
671
736
    object of the given type.
672
737
    """
689
754
            self, instance: POD, field: Field, old: "Any", new: "Any"
690
755
    ) -> "Any":
691
756
        """
692
 
        An assign filter that type-checks the value of all sequence elements
 
757
        An assign filter that type-checks the value of all sequence elements.
693
758
 
694
759
        :param instance:
695
760
            A subclass of :class:`POD` that contains ``field``