Source code for properties.base.union

"""union.py: Union property"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

from warnings import warn

from six import PY2

from ..base import GENERIC_ERRORS, HasProperties, Instance
from .. import basic
from .. import utils

if PY2:
    from types import ClassType                                                #pylint: disable=no-name-in-module
    CLASS_TYPES = (type, ClassType)
else:
    CLASS_TYPES = (type,)


[docs]class Union(basic.Property): """Property with multiple valid Property types **Union** Properties contain a list of :ref:`property` instances. Validation, serialization, etc. cycle through the corresponding method on the each Property instance sequentially until one succeeds. If all Property types raise an error, the Union Property will also raise an error. .. note:: When specifying Property types, the order matters; if multiple types are valid, the earlier type will be favored. For example, .. code:: import properties union_0 = properties.Union( doc='String and Color', props=(properties.String(''), properties.Color('')), ) union_1 = properties.Union( doc='String and Color', props=(properties.Color(''), properties.String('')), ) union_0.validate(None, 'red') == 'red' # Validates to string union_1.validate(None, 'red') == (255, 0, 0) # Validates to color **Available keywords** (in addition to those inherited from :ref:`Property <property>`): * **props** - A list of Property instances that each specify a valid type for the Union Property. HasProperties classes may also be specified; these are coerced to Instance Properties of the respective class. """ class_info = 'a union of multiple property types' def __init__(self, doc, props, **kwargs): self.props = props super(Union, self).__init__(doc, **kwargs) self._unused_default_warning() @property def props(self): """List of valid property types or HasProperties classes""" return self._props @props.setter def props(self, value): if not isinstance(value, (tuple, list)): raise TypeError('props must be a list') new_props = tuple() for prop in value: if (isinstance(prop, CLASS_TYPES) and issubclass(prop, HasProperties)): prop = Instance('', prop) if not isinstance(prop, basic.Property): raise TypeError('props must be Property instances or ' 'HasProperties classes') new_props += (prop,) self._props = new_props @property def strict_instances(self): """Require input dictionaries for instances to be valid If True, this passes :code:`strict=True` and :code:`assert_valid=True` to the instance deserializer, ensuring the instance is valid. Default is False. """ return getattr(self, '_strict_instances', False) @strict_instances.setter def strict_instances(self, value): if not isinstance(value, bool): raise TypeError('strict_instances must be a boolean') self._strict_instances = value @property def info(self): """Description of the property, supplemental to the basic doc""" return ' or '.join([p.info or 'any value' for p in self.props]) @property def name(self): """The name of the property on a HasProperties class This is set in the metaclass. For Unions, props inherit the name. """ return getattr(self, '_name', '') @name.setter def name(self, value): for prop in self.props: prop.name = value self._name = value @property def default(self): """Default value of the property""" prop_def = getattr(self, '_default', utils.undefined) for prop in self.props: if prop.default is utils.undefined: continue if prop_def is utils.undefined: prop_def = prop.default break return prop_def @default.setter def default(self, value): if value is utils.undefined: self._default = value return for prop in self.props: try: if callable(value): prop.validate(None, value()) else: prop.validate(None, value) self._default = value return except GENERIC_ERRORS: continue raise TypeError('Invalid default for Union property') def _unused_default_warning(self): prop_def = getattr(self, '_default', utils.undefined) for prop in self.props: if prop.default is utils.undefined: continue if prop_def is utils.undefined: prop_def = prop.default elif prop_def != prop.default: warn('Union prop default ignored: {}'.format(prop.default), RuntimeWarning) def validate(self, instance, value): """Check if value is a valid type of one of the Union props""" for prop in self.props: try: return prop.validate(instance, value) except GENERIC_ERRORS: continue self.error(instance, value) def assert_valid(self, instance, value=None): """Check if the Union has a valid value""" valid = super(Union, self).assert_valid(instance, value) if not valid: return False if value is None: value = instance._get(self.name) if value is None: return True for prop in self.props: try: return prop.assert_valid(instance, value) except GENERIC_ERRORS: continue message = ( 'The "{name}" property of a {cls} instance has not been set ' 'correctly'.format( name=self.name, cls=instance.__class__.__name__ ) ) raise utils.ValidationError(message, 'invalid', self.name, instance) def serialize(self, value, **kwargs): """Return a serialized value If no serializer is provided, it uses the serialize method of the prop corresponding to the value """ kwargs.update({'include_class': kwargs.get('include_class', True)}) if self.serializer is not None: return self.serializer(value, **kwargs) if value is None: return None for prop in self.props: try: prop.validate(None, value) except GENERIC_ERRORS: continue return prop.serialize(value, **kwargs) return self.to_json(value, **kwargs) def deserialize(self, value, **kwargs): """Return a deserialized value If no deserializer is provided, it uses the deserialize method of the prop corresponding to the value """ kwargs.update({'trusted': kwargs.get('trusted', False)}) if self.deserializer is not None: return self.deserializer(value, **kwargs) if value is None: return None instance_props = [ prop for prop in self.props if isinstance(prop, Instance) ] kwargs = kwargs.copy() kwargs.update({ 'strict': kwargs.get('strict') or self.strict_instances, 'assert_valid': self.strict_instances, }) if isinstance(value, dict) and value.get('__class__'): clsname = value.get('__class__') for prop in instance_props: if clsname == prop.instance_class.__name__: return prop.deserialize(value, **kwargs) for prop in self.props: try: out_val = prop.deserialize(value, **kwargs) prop.validate(None, out_val) return out_val except GENERIC_ERRORS: continue return self.from_json(value, **kwargs) def equal(self, value_a, value_b): return any((prop.equal(value_a, value_b) for prop in self.props)) @staticmethod def to_json(value, **kwargs): """Return value, serialized if value is a HasProperties instance""" if isinstance(value, HasProperties): return value.serialize(**kwargs) return value def sphinx_class(self): """Redefine sphinx class to provide doc links to types of props""" return ', '.join(p.sphinx_class() for p in self.props)