61
63
from textwrap import dedent
63
65
from plainbox.i18n import gettext as _
64
from plainbox.vendor.morris import signal
66
from plainbox.vendor import morris
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')
71
73
_logger = getLogger("plainbox.pod")
76
A simple object()-like singleton that has a more useful repr()
78
""" A simple object()-like singleton that has a more useful repr(). """
79
80
def __repr__(self):
80
81
return self.__class__.__name__
83
84
class MANDATORY(_Singleton):
85
Singleton that can be used as a value in :attr:`Field.initial`.
87
Class for the special MANDATORY object.
89
This object can be used as a value in :attr:`Field.initial`.
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
312
325
self.signal_name, old, new)
331
""" Base class for POD-like classes. """
334
namedtuple_cls = namedtuple('PODBase', '')
336
def __init__(self, *args, **kwargs):
338
Initialize a new POD object.
340
Positional arguments bind to fields in declaration order. Keyword
341
arguments bind to fields in any order but fields cannot be initialized
345
If there are more positional arguments than fields to initialize
347
If a keyword argument doesn't correspond to a field name.
349
If a field is initialized twice (first with positional arguments,
350
then again with keyword arguments).
352
If a ``MANDATORY`` field is not initialized.
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:
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:
379
if field.is_mandatory:
381
"mandatory argument missing: {}".format(field.name))
382
if field.initial_fn is not None:
383
field_value = field.initial_fn()
385
field_value = field.initial
386
setattr(self, field.name, field_value)
389
""" Get a debugging representation of a POD object. """
390
return "{}({})".format(
391
self.__class__.__name__,
393
'{}={!r}'.format(field.name, getattr(self, field.name))
394
for field in self.__class__.field_list]))
396
def __eq__(self, other: "POD") -> bool:
398
Check that this POD is equal to another POD.
400
POD comparison is implemented by converting them to tuples and
401
comparing the two tuples.
403
if not isinstance(other, POD):
404
return NotImplemented
405
return self.as_tuple() == other.as_tuple()
407
def __lt__(self, other: "POD") -> bool:
409
Check that this POD is "less" than an another POD.
411
POD comparison is implemented by converting them to tuples and
412
comparing the two tuples.
414
if not isinstance(other, POD):
415
return NotImplemented
416
return self.as_tuple() < other.as_tuple()
418
def as_tuple(self) -> tuple:
420
Return the data in this POD as a tuple.
422
Order of elements in the tuple corresponds to the order of field
425
return self.__class__.namedtuple_cls(*[
426
getattr(self, field.name)
427
for field in self.__class__.field_list
430
def as_dict(self) -> dict:
431
""" Return the data in this POD as a dictionary. """
433
field.name: getattr(self, field.name)
434
for field in self.__class__.field_list
315
438
class _FieldCollection:
441
Support class for constructing POD meta-data information.
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
422
564
return OrderedDict()
569
Decorator for POD classes.
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.
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:
426
class POD(metaclass=PODMeta):
587
class POD(PODBase, metaclass=PODMeta):
428
590
Base class that removes boilerplate from plain-old-data classes.
452
def __init__(self, *args, **kwargs):
454
Initialize a new POD object.
456
Positional arguments bind to fields in declaration order. Keyword
457
arguments bind to fields in any order but fields cannot be initialized
461
If there are more positional arguments than fields to initialize
463
If a keyword argument doesn't correspond to a field name.
465
If a field is initialized twice (first with positional arguments,
466
then again with keyword arguments).
468
If a ``MANDATORY`` field is not initialized.
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:
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:
495
if field.is_mandatory:
497
"mandatory argument missing: {}".format(field.name))
498
if field.initial_fn is not None:
499
field_value = field.initial_fn()
501
field_value = field.initial
502
setattr(self, field.name, field_value)
505
return "{}({})".format(
506
self.__class__.__name__,
508
'{}={!r}'.format(field.name, getattr(self, field.name))
509
for field in self.__class__.field_list]))
511
def __eq__(self, other: "POD") -> bool:
513
Check that this POD is equal to another POD
515
POD comparison is implemented by converting them to tuples and
516
comparing the two tuples.
518
if not isinstance(other, POD):
519
return NotImplemented
520
return self.as_tuple() == other.as_tuple()
522
def __lt__(self, other: "POD") -> bool:
524
Check that this POD is "less" than an another POD.
526
POD comparison is implemented by converting them to tuples and
527
comparing the two tuples.
529
if not isinstance(other, POD):
530
return NotImplemented
531
return self.as_tuple() < other.as_tuple()
533
def as_tuple(self) -> tuple:
535
Return the data in this POD as a tuple.
537
Order of elements in the tuple corresponds to the order of field
540
return self.__class__.namedtuple_cls(*[
541
getattr(self, field.name)
542
for field in self.__class__.field_list
545
def as_dict(self) -> dict:
547
Return the data in this POD as a dictionary
550
field.name: getattr(self, field.name)
551
for field in self.__class__.field_list
555
615
def modify_field_docstring(field_docstring_ext: str):
617
Decorator for altering field docstrings via assign filter functions.
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.