Source code for normalize.property

#
# This file is a part of the normalize python library
#
# normalize is free software: you can redistribute it and/or modify
# it under the terms of the MIT License.
#
# normalize is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# MIT License for more details.
#
# You should have received a copy of the MIT license along with
# normalize.  If not, refer to the upstream repository at
# http://github.com/hearsaycorp/normalize
#


"""Property objects are "new"-style object *data descriptors*.  They define
*getters* and *setters* for object attribute access, and allow the module to
hang on extra information and customize behavior.

For information on data descriptors, see the `Descriptor HowTo Guide
<https://docs.python.org/2/howto/descriptor.html>`_ in the main python
documentation.
"""

from __future__ import absolute_import

import inspect
import warnings
import weakref

import normalize.empty as empty
import normalize.exc as exc
from normalize.property.meta import MetaProperty


class _Default(object):
    def __repr__(self):
        return "<not set>"


_none = _Default()


[docs]class Property(object): """This is the base class for all property types. It is a data descriptor, so care should be taken before adding any ``SPECIALMETHODS`` which might change the way it behaves. """ __metaclass__ = MetaProperty __safe_unless_ro__ = False
[docs] def __init__(self, isa=None, coerce=None, check=None, required=False, default=_none, traits=None, extraneous=False, empty_attr=_none, empty=None, doc=None): """Declares a new standard Property. Note: if you pass arguments which are not understood by this constructor, or pass extra property traits to ``traits``, then the call will be redirected to a sub-class; see :py:mod:`normalize.property.meta` for more. Because of this magic, all ``Property`` arguments *must* be passed in keyword argument form. All arguments are optional. ``isa=``\ *TYPE|TUPLE* Any assigned property must be one of these types according to ``isinstance()``. Also used by visitor functions which are missing an instance, such as marshal in. If ``isa`` is not set, then *any* value (including ``None``) is acceptable. ``coerce=``\ *FUNCTION* If the value fails the ``isa`` isinstance check, then this function is called with the value, and should return a value of a conformant type or throw an exception. ``check=``\ *FUNCTION* Once the value is of the correct type, this function is called and should return something true (according to ``bool()``) if the value is acceptable. ``required=``\ *BOOL* If ``True``, then the value must be passed during construction, and may not be ``None`` (this is only meaningful if ``isa=`` is not passed) ``default=``\ *VALUE|FUNCTION* If no value is passed during construction, then use this value instead. If the argument is a function, then the function is called and the value it returns used as the default. ``traits=``\ *LIST|SEQUENCE* Manually specify a list of named Property traits. The default is ``["safe"]``, and any unknown keyword arguments will add extra traits on. ``empty_attr=``\ *METHODNAME* Specify an auxiliary method name which returns the value if the attribute is set, otherwise an ``empty`` proxy value. Defaults to the name of the attribute with a ``0`` appended, or ``None`` if the attribute already ends with a number (disabling the accessor) ``empty=``\ *IGNORED* For partial compatibility with normalize 0.7.x. This used to specify the value returned by the ``empty_attr`` attribute; now that attribute always returns a Falsy placeholder. ``extraneous=``\ *BOOL* This Property is considered *denormalized* and does not affect the ``Record`` equality operator. Visitor functions typically ignore extraneous properties or require an extra option to process them. ``doc=``\ *STR* Specify a docstring for the property. """ self.name = None self.class_ = None self.__doc__ = doc super(Property, self).__init__() self.default = default if callable(default): is_method, nargs = self.func_info(default) if nargs: if not is_method and nargs == 1: # backwards compatibility; default=lambda x: x.foo was # permitted previously. stacklevel = 1 # walk stack back to the actual caller of the original # constructor stack = inspect.stack() while stacklevel <= len(stack): loc = stack[stacklevel - 1][0].f_locals if 'self' not in loc or loc['self'] != self: break stacklevel += 1 warnings.warn( "'default' first argument should be called 'self'", stacklevel=stacklevel, ) is_method = True else: raise exc.DefaultSignatureError( func=default, module=default.__module__, nargs=nargs, ) self.default_is_method = is_method self.required = required self.check = check self.valuetype = isa self.coerce = coerce or isa if self.coerce and not self.valuetype: raise exc.CoerceWithoutType() self.empty_attr = empty_attr self.extraneous = extraneous
def func_info(self, func): args = inspect.getargspec(func) is_method = False if not args.args: required_args = 0 else: required_args = len(args.args) if args.defaults: required_args -= len(args.defaults) if required_args and args.args[0] == "self": is_method = True required_args -= 1 return is_method, required_args @property def bound(self): return bool(self.class_) def set_name(self, name): self.name = name if self.empty_attr is _none: self.empty_attr = ( (name + "0") if name[-1] not in "0123456789" else None ) def bind(self, class_): self.class_ = weakref.ref(class_) @property
[docs] def fullname(self): """Returns the name of the ``Record`` class this ``Property`` is attached to, and attribute name it is attached as.""" if not self.bound: if self.name is not None: return "(unbound).%s" % self.name else: return "(unbound)" elif not self.class_(): classname = "(GC'd class)" else: classname = self.class_().__name__ return "%s.%s" % (classname, self.name)
def type_safe_value(self, value, _none_ok=False): if value is None and self.required and not self.valuetype: raise ValueError("%s is required" % self.fullname) if self.valuetype and not isinstance(value, self.valuetype): try: new_value = self.coerce(value) except Exception as e: raise exc.CoerceError( prop=self.fullname, value=repr(value), exc=e, func=( "%s constructor" % self.coerce.__name__ if isinstance(self.coerce, type) else self.coerce ), valuetype=( "(" + ", ".join( x.__name__ for x in self.valuetype ) + ")" if isinstance(self.valuetype, tuple) else self.valuetype.__name__ ), ) if not isinstance(new_value, self.valuetype): if _none_ok and new_value is None and not self.required: # allow coerce functions to return 'None' to silently # swallow optional properties on initialization return _none else: raise exc.ValueCoercionError( prop=self.fullname, value=repr(value), coerced=repr(new_value), ) else: value = new_value if self.check and not self.check(value): raise ValueError( "%s value '%r' failed type check" % (self.fullname, value) ) return value def get_default(self, obj): if callable(self.default): if self.default_is_method: # XXX - only 'lazy' properties should be allowed to do this. rv = self.default(obj) else: rv = self.default() else: rv = self.default return rv def init_prop(self, obj, value=_Default): if value is _Default: value = self.get_default(obj) new_value = ( _none if value is _none else self.type_safe_value(value, _none_ok=True) ) if new_value is _none: if self.required: raise ValueError("%s is required" % self.fullname) else: obj.__dict__[self.name] = new_value def eager_init(self): return self.required or self.default is not _none
[docs] def __get__(self, obj, type_=None): """Default getter; does NOT fall back to regular descriptor behavior """ if obj is None: return self if self.name not in obj.__dict__: raise AttributeError(self.fullname) return obj.__dict__[self.name]
def __str__(self): metaclass = str(type(self).__name__) return "<%s %s>" % (metaclass, self.fullname)
[docs] def aux_props(self): """This method is available for property traits to provide extra class attributes which are added to the class they are defined in during class creation. The default implementation is responsible for defining ``empty_attr`` attributes. The return value should be an iterable list of 2-tuples, with the first member of each 2-tuple being the attribute name and the second being the value to insert. """ if self.empty_attr is not None: return ((self.empty_attr, EmptyAuxProp(self)), ) else: return ()
class EmptyAuxProp(object): def __init__(self, prop): self.prop = prop self.valuetype = prop.valuetype or any def __get__(self, obj, type_=None): try: return self.prop.__get__(obj) except AttributeError: return empty.placeholder(self.valuetype)
[docs]class LazyProperty(Property): """This declares a property which has late evaluation using its 'default' method. This type uses the support built-in to python for lazy attribute setting, which means subsequent attribute assignments will not be prevented or checked. See LazySafeProperty for the descriptor version. """ __trait__ = "lazy"
[docs] def __init__(self, lazy=True, **kwargs): """Creates a Lazy property. In addition to the standard Property arguments, accepts: ``lazy=``\ *BOOL* Must be ``True``. Used as a "distinguishing argument" to request a lazy Property. Not required if you call ``LazyProperty()`` directly. ``default=``\ *FUNCTION|METHOD* The default value for the property. Unlike a standard ``Property``, the value can also be set to a method, which can reference other object properties. """ if not lazy: raise exc.LazyIsFalse() super(LazyProperty, self).__init__(**kwargs)
def init_prop(self, obj, value=_Default): if value is _Default: return super(LazyProperty, self).init_prop(obj, value) def eager_init(self): return False
[docs] def __get__(self, obj, type_=None): """This getter is called when there is no value set, and calls the default method/function. """ if obj is None: return self if self.name not in obj.__dict__: value = self.get_default(obj) obj.__dict__[self.name] = self.type_safe_value(value) return super(LazyProperty, self).__get__(obj, type_)
def __hasattr__(self, obj): return True
[docs]class ROProperty(Property): """A read-only property throws an exception when the attribute slot is assigned to""" __trait__ = "ro" def __set__(self, obj, value): """Raises ``ReadOnlyAttributeError``""" raise exc.ReadOnlyAttributeError(attrname=self.fullname) def __delete__(self, obj): """Raises ``ReadOnlyAttributeError``""" raise exc.ReadOnlyAttributeError(attrname=self.fullname)
[docs]class ROLazyProperty(LazyProperty, ROProperty):
[docs] def __get__(self, obj, type_=None): """This getter checks to see if the slot is already set in the object and if so, returns it.""" if obj is None: return self if self.name in obj.__dict__: return obj.__dict__[self.name] return super(ROLazyProperty, self).__get__(obj, type_)
[docs]class SafeProperty(Property): """A version of Property which always checks all assignments to properties. Normalize gives you safe properties by default; if you want unsafe properties, then you (currently) need to pass ``traits=["unsafe"]`` to the ``Property()`` declaration. """ __trait__ = "safe" def __set__(self, obj, value): """This setter checks the type of the value before allowing it to be set.""" obj.__dict__[self.name] = self.type_safe_value(value) def __delete__(self, obj): """Checks the property's ``required`` setting, and allows the delete if it is false""" if self.required: raise ValueError("%s is required" % self.fullname) del obj.__dict__[self.name]
[docs]class LazySafeProperty(SafeProperty, LazyProperty):
[docs] def __get__(self, obj, type_=None): """This getter checks to see if the slot is already set in the object and if so, returns it.""" if obj is None: return self if self.name in obj.__dict__: return obj.__dict__[self.name] return super(LazySafeProperty, self).__get__(obj, type_)
class DiffasProperty(Property): __trait__ = "diffas" def __init__(self, compare_as=None, **kwargs): """Specify ``compare_as=`` to pass a clean-up function which is applied to the value in the slot, but only during comparison. The function can be a method, and can choose to either accept or not accept an argument. """ super(DiffasProperty, self).__init__(**kwargs) self.compare_as = compare_as is_method, nargs = self.func_info(compare_as) self.compare_as_info = is_method, nargs if nargs > 1: raise exc.CompareAsSignatureError( func=compare_as, module=compare_as.__module__, nargs=nargs, ) trait_num = 0
[docs]def make_property_type(name, base_type=Property, attrs=None, trait_name=None, **default_kwargs): """Makes a new ``Property`` type, which supplies the given arguments as defaults to the ``Property()`` constructor. The typical use of this function is to make types for the API you are mapping so that, for instance, any time they use a date you can convert it in a consistent way to a ``datetime.date``. It's also used by :py:mod:`normalize.property.types` to create all of its Property subclasses. Args: ``name=``\ *STR* Specifies the name of the new property type. This is entirely cosmetic, but it is probably a good idea to make this exactly the same as the symbol you are assigning the result to. ``base_type=``\ *Property sub-class* Specifies which property type you are adding defaults to. You can pass in a tuple of types here. ``attrs=``\ *DICT* This lets you pass in a dictionary that will be used as the new Property type's class dictionary. i.e., it gets passed as the third argument to ``type(NAME, BASES, ATTRS)``, after the properties necessary to implement the defaults are added to it. If you use this for anything less than trivial, it may be simpler just to make a whole class definition. ``trait_name=``\ *STR* Specify the unique identifier of the trait that is created. This probably doesn't matter, unless you want to use the ``traits=`` keyword to ``Property()``. The default is to make up a new numbered trait name, starting with "``trait1``". ``**kwargs`` Everything not known is used as defaults for the eventual call to ``Property()``. If the user of the Property type passes it as well, this overrides the defaults passed to ``make_property_type``. """ if not attrs: attrs = {} bases = base_type if isinstance(base_type, tuple) else (base_type,) self_type = [] if not trait_name: global trait_num trait_num += 1 trait_name = "trait%d" % trait_num def __init__(self, **kwargs): for arg, val in default_kwargs.iteritems(): if arg not in kwargs: kwargs[arg] = val return super(self_type[0], self).__init__(**kwargs) attrs['default_kwargs'] = default_kwargs attrs['__init__'] = __init__ attrs['__trait__'] = trait_name new_property_type = type(name, bases, attrs) self_type.append(new_property_type) return new_property_type