# orm/descriptor_props.py # Copyright (C) 2005-2017 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """Descriptor properties are more "auxiliary" properties that exist as configurational elements, but don't participate as actively in the load/persist ORM loop. """ from .interfaces import MapperProperty, PropComparator from .util import _none_set from . import attributes from .. import util, sql, exc as sa_exc, event, schema from ..sql import expression from . import properties from . import query class DescriptorProperty(MapperProperty): """:class:`.MapperProperty` which proxies access to a user-defined descriptor.""" doc = None def instrument_class(self, mapper): prop = self class _ProxyImpl(object): accepts_scalar_loader = False expire_missing = True collection = False def __init__(self, key): self.key = key if hasattr(prop, 'get_history'): def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF): return prop.get_history(state, dict_, passive) if self.descriptor is None: desc = getattr(mapper.class_, self.key, None) if mapper._is_userland_descriptor(desc): self.descriptor = desc if self.descriptor is None: def fset(obj, value): setattr(obj, self.name, value) def fdel(obj): delattr(obj, self.name) def fget(obj): return getattr(obj, self.name) self.descriptor = property( fget=fget, fset=fset, fdel=fdel, ) proxy_attr = attributes.create_proxied_attribute( self.descriptor)( self.parent.class_, self.key, self.descriptor, lambda: self._comparator_factory(mapper), doc=self.doc, original_property=self ) proxy_attr.impl = _ProxyImpl(self.key) mapper.class_manager.instrument_attribute(self.key, proxy_attr) @util.langhelpers.dependency_for("sqlalchemy.orm.properties") class CompositeProperty(DescriptorProperty): """Defines a "composite" mapped attribute, representing a collection of columns as one attribute. :class:`.CompositeProperty` is constructed using the :func:`.composite` function. .. seealso:: :ref:`mapper_composite` """ def __init__(self, class_, *attrs, **kwargs): r"""Return a composite column-based property for use with a Mapper. See the mapping documentation section :ref:`mapper_composite` for a full usage example. The :class:`.MapperProperty` returned by :func:`.composite` is the :class:`.CompositeProperty`. :param class\_: The "composite type" class. :param \*cols: List of Column objects to be mapped. :param active_history=False: When ``True``, indicates that the "previous" value for a scalar attribute should be loaded when replaced, if not already loaded. See the same flag on :func:`.column_property`. .. versionchanged:: 0.7 This flag specifically becomes meaningful - previously it was a placeholder. :param group: A group name for this property when marked as deferred. :param deferred: When True, the column property is "deferred", meaning that it does not load immediately, and is instead loaded when the attribute is first accessed on an instance. See also :func:`~sqlalchemy.orm.deferred`. :param comparator_factory: a class which extends :class:`.CompositeProperty.Comparator` which provides custom SQL clause generation for comparison operations. :param doc: optional string that will be applied as the doc on the class-bound descriptor. :param info: Optional data dictionary which will be populated into the :attr:`.MapperProperty.info` attribute of this object. .. versionadded:: 0.8 :param extension: an :class:`.AttributeExtension` instance, or list of extensions, which will be prepended to the list of attribute listeners for the resulting descriptor placed on the class. **Deprecated.** Please see :class:`.AttributeEvents`. """ super(CompositeProperty, self).__init__() self.attrs = attrs self.composite_class = class_ self.active_history = kwargs.get('active_history', False) self.deferred = kwargs.get('deferred', False) self.group = kwargs.get('group', None) self.comparator_factory = kwargs.pop('comparator_factory', self.__class__.Comparator) if 'info' in kwargs: self.info = kwargs.pop('info') util.set_creation_order(self) self._create_descriptor() def instrument_class(self, mapper): super(CompositeProperty, self).instrument_class(mapper) self._setup_event_handlers() def do_init(self): """Initialization which occurs after the :class:`.CompositeProperty` has been associated with its parent mapper. """ self._setup_arguments_on_columns() def _create_descriptor(self): """Create the Python descriptor that will serve as the access point on instances of the mapped class. """ def fget(instance): dict_ = attributes.instance_dict(instance) state = attributes.instance_state(instance) if self.key not in dict_: # key not present. Iterate through related # attributes, retrieve their values. This # ensures they all load. values = [ getattr(instance, key) for key in self._attribute_keys ] # current expected behavior here is that the composite is # created on access if the object is persistent or if # col attributes have non-None. This would be better # if the composite were created unconditionally, # but that would be a behavioral change. if self.key not in dict_ and ( state.key is not None or not _none_set.issuperset(values) ): dict_[self.key] = self.composite_class(*values) state.manager.dispatch.refresh(state, None, [self.key]) return dict_.get(self.key, None) def fset(instance, value): dict_ = attributes.instance_dict(instance) state = attributes.instance_state(instance) attr = state.manager[self.key] previous = dict_.get(self.key, attributes.NO_VALUE) for fn in attr.dispatch.set: value = fn(state, value, previous, attr.impl) dict_[self.key] = value if value is None: for key in self._attribute_keys: setattr(instance, key, None) else: for key, value in zip( self._attribute_keys, value.__composite_values__()): setattr(instance, key, value) def fdel(instance): state = attributes.instance_state(instance) dict_ = attributes.instance_dict(instance) previous = dict_.pop(self.key, attributes.NO_VALUE) attr = state.manager[self.key] attr.dispatch.remove(state, previous, attr.impl) for key in self._attribute_keys: setattr(instance, key, None) self.descriptor = property(fget, fset, fdel) @util.memoized_property def _comparable_elements(self): return [ getattr(self.parent.class_, prop.key) for prop in self.props ] @util.memoized_property def props(self): props = [] for attr in self.attrs: if isinstance(attr, str): prop = self.parent.get_property( attr, _configure_mappers=False) elif isinstance(attr, schema.Column): prop = self.parent._columntoproperty[attr] elif isinstance(attr, attributes.InstrumentedAttribute): prop = attr.property else: raise sa_exc.ArgumentError( "Composite expects Column objects or mapped " "attributes/attribute names as arguments, got: %r" % (attr,)) props.append(prop) return props @property def columns(self): return [a for a in self.attrs if isinstance(a, schema.Column)] def _setup_arguments_on_columns(self): """Propagate configuration arguments made on this composite to the target columns, for those that apply. """ for prop in self.props: prop.active_history = self.active_history if self.deferred: prop.deferred = self.deferred prop.strategy_key = ( ("deferred", True), ("instrument", True)) prop.group = self.group def _setup_event_handlers(self): """Establish events that populate/expire the composite attribute.""" def load_handler(state, *args): dict_ = state.dict if self.key in dict_: return # if column elements aren't loaded, skip. # __get__() will initiate a load for those # columns for k in self._attribute_keys: if k not in dict_: return # assert self.key not in dict_ dict_[self.key] = self.composite_class( *[state.dict[key] for key in self._attribute_keys] ) def expire_handler(state, keys): if keys is None or set(self._attribute_keys).intersection(keys): state.dict.pop(self.key, None) def insert_update_handler(mapper, connection, state): """After an insert or update, some columns may be expired due to server side defaults, or re-populated due to client side defaults. Pop out the composite value here so that it recreates. """ state.dict.pop(self.key, None) event.listen(self.parent, 'after_insert', insert_update_handler, raw=True) event.listen(self.parent, 'after_update', insert_update_handler, raw=True) event.listen(self.parent, 'load', load_handler, raw=True, propagate=True) event.listen(self.parent, 'refresh', load_handler, raw=True, propagate=True) event.listen(self.parent, 'expire', expire_handler, raw=True, propagate=True) # TODO: need a deserialize hook here @util.memoized_property def _attribute_keys(self): return [ prop.key for prop in self.props ] def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF): """Provided for userland code that uses attributes.get_history().""" added = [] deleted = [] has_history = False for prop in self.props: key = prop.key hist = state.manager[key].impl.get_history(state, dict_) if hist.has_changes(): has_history = True non_deleted = hist.non_deleted() if non_deleted: added.extend(non_deleted) else: added.append(None) if hist.deleted: deleted.extend(hist.deleted) else: deleted.append(None) if has_history: return attributes.History( [self.composite_class(*added)], (), [self.composite_class(*deleted)] ) else: return attributes.History( (), [self.composite_class(*added)], () ) def _comparator_factory(self, mapper): return self.comparator_factory(self, mapper) class CompositeBundle(query.Bundle): def __init__(self, property, expr): self.property = property super(CompositeProperty.CompositeBundle, self).__init__( property.key, *expr) def create_row_processor(self, query, procs, labels): def proc(row): return self.property.composite_class( *[proc(row) for proc in procs]) return proc class Comparator(PropComparator): """Produce boolean, comparison, and other operators for :class:`.CompositeProperty` attributes. See the example in :ref:`composite_operations` for an overview of usage , as well as the documentation for :class:`.PropComparator`. See also: :class:`.PropComparator` :class:`.ColumnOperators` :ref:`types_operators` :attr:`.TypeEngine.comparator_factory` """ __hash__ = None @property def clauses(self): return self.__clause_element__() def __clause_element__(self): return expression.ClauseList( group=False, *self._comparable_elements) def _query_clause_element(self): return CompositeProperty.CompositeBundle( self.prop, self.__clause_element__()) @util.memoized_property def _comparable_elements(self): if self._adapt_to_entity: return [ getattr( self._adapt_to_entity.entity, prop.key ) for prop in self.prop._comparable_elements ] else: return self.prop._comparable_elements def __eq__(self, other): if other is None: values = [None] * len(self.prop._comparable_elements) else: values = other.__composite_values__() comparisons = [ a == b for a, b in zip(self.prop._comparable_elements, values) ] if self._adapt_to_entity: comparisons = [self.adapter(x) for x in comparisons] return sql.and_(*comparisons) def __ne__(self, other): return sql.not_(self.__eq__(other)) def __str__(self): return str(self.parent.class_.__name__) + "." + self.key @util.langhelpers.dependency_for("sqlalchemy.orm.properties") class ConcreteInheritedProperty(DescriptorProperty): """A 'do nothing' :class:`.MapperProperty` that disables an attribute on a concrete subclass that is only present on the inherited mapper, not the concrete classes' mapper. Cases where this occurs include: * When the superclass mapper is mapped against a "polymorphic union", which includes all attributes from all subclasses. * When a relationship() is configured on an inherited mapper, but not on the subclass mapper. Concrete mappers require that relationship() is configured explicitly on each subclass. """ def _comparator_factory(self, mapper): comparator_callable = None for m in self.parent.iterate_to_root(): p = m._props[self.key] if not isinstance(p, ConcreteInheritedProperty): comparator_callable = p.comparator_factory break return comparator_callable def __init__(self): super(ConcreteInheritedProperty, self).__init__() def warn(): raise AttributeError("Concrete %s does not implement " "attribute %r at the instance level. Add " "this property explicitly to %s." % (self.parent, self.key, self.parent)) class NoninheritedConcreteProp(object): def __set__(s, obj, value): warn() def __delete__(s, obj): warn() def __get__(s, obj, owner): if obj is None: return self.descriptor warn() self.descriptor = NoninheritedConcreteProp() @util.langhelpers.dependency_for("sqlalchemy.orm.properties") class SynonymProperty(DescriptorProperty): def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None, doc=None, info=None): """Denote an attribute name as a synonym to a mapped property, in that the attribute will mirror the value and expression behavior of another attribute. :param name: the name of the existing mapped property. This can refer to the string name of any :class:`.MapperProperty` configured on the class, including column-bound attributes and relationships. :param descriptor: a Python :term:`descriptor` that will be used as a getter (and potentially a setter) when this attribute is accessed at the instance level. :param map_column: if ``True``, the :func:`.synonym` construct will locate the existing named :class:`.MapperProperty` based on the attribute name of this :func:`.synonym`, and assign it to a new attribute linked to the name of this :func:`.synonym`. That is, given a mapping like:: class MyClass(Base): __tablename__ = 'my_table' id = Column(Integer, primary_key=True) job_status = Column(String(50)) job_status = synonym("_job_status", map_column=True) The above class ``MyClass`` will now have the ``job_status`` :class:`.Column` object mapped to the attribute named ``_job_status``, and the attribute named ``job_status`` will refer to the synonym itself. This feature is typically used in conjunction with the ``descriptor`` argument in order to link a user-defined descriptor as a "wrapper" for an existing column. :param info: Optional data dictionary which will be populated into the :attr:`.InspectionAttr.info` attribute of this object. .. versionadded:: 1.0.0 :param comparator_factory: A subclass of :class:`.PropComparator` that will provide custom comparison behavior at the SQL expression level. .. note:: For the use case of providing an attribute which redefines both Python-level and SQL-expression level behavior of an attribute, please refer to the Hybrid attribute introduced at :ref:`mapper_hybrids` for a more effective technique. .. seealso:: :ref:`synonyms` - examples of functionality. :ref:`mapper_hybrids` - Hybrids provide a better approach for more complicated attribute-wrapping schemes than synonyms. """ super(SynonymProperty, self).__init__() self.name = name self.map_column = map_column self.descriptor = descriptor self.comparator_factory = comparator_factory self.doc = doc or (descriptor and descriptor.__doc__) or None if info: self.info = info util.set_creation_order(self) # TODO: when initialized, check _proxied_property, # emit a warning if its not a column-based property @util.memoized_property def _proxied_property(self): return getattr(self.parent.class_, self.name).property def _comparator_factory(self, mapper): prop = self._proxied_property if self.comparator_factory: comp = self.comparator_factory(prop, mapper) else: comp = prop.comparator_factory(prop, mapper) return comp def set_parent(self, parent, init): if self.map_column: # implement the 'map_column' option. if self.key not in parent.mapped_table.c: raise sa_exc.ArgumentError( "Can't compile synonym '%s': no column on table " "'%s' named '%s'" % (self.name, parent.mapped_table.description, self.key)) elif parent.mapped_table.c[self.key] in \ parent._columntoproperty and \ parent._columntoproperty[ parent.mapped_table.c[self.key] ].key == self.name: raise sa_exc.ArgumentError( "Can't call map_column=True for synonym %r=%r, " "a ColumnProperty already exists keyed to the name " "%r for column %r" % (self.key, self.name, self.name, self.key) ) p = properties.ColumnProperty(parent.mapped_table.c[self.key]) parent._configure_property( self.name, p, init=init, setparent=True) p._mapped_by_synonym = self.key self.parent = parent @util.langhelpers.dependency_for("sqlalchemy.orm.properties") class ComparableProperty(DescriptorProperty): """Instruments a Python property for use in query expressions.""" def __init__( self, comparator_factory, descriptor=None, doc=None, info=None): """Provides a method of applying a :class:`.PropComparator` to any Python descriptor attribute. .. versionchanged:: 0.7 :func:`.comparable_property` is superseded by the :mod:`~sqlalchemy.ext.hybrid` extension. See the example at :ref:`hybrid_custom_comparators`. Allows any Python descriptor to behave like a SQL-enabled attribute when used at the class level in queries, allowing redefinition of expression operator behavior. In the example below we redefine :meth:`.PropComparator.operate` to wrap both sides of an expression in ``func.lower()`` to produce case-insensitive comparison:: from sqlalchemy.orm import comparable_property from sqlalchemy.orm.interfaces import PropComparator from sqlalchemy.sql import func from sqlalchemy import Integer, String, Column from sqlalchemy.ext.declarative import declarative_base class CaseInsensitiveComparator(PropComparator): def __clause_element__(self): return self.prop def operate(self, op, other): return op( func.lower(self.__clause_element__()), func.lower(other) ) Base = declarative_base() class SearchWord(Base): __tablename__ = 'search_word' id = Column(Integer, primary_key=True) word = Column(String) word_insensitive = comparable_property(lambda prop, mapper: CaseInsensitiveComparator( mapper.c.word, mapper) ) A mapping like the above allows the ``word_insensitive`` attribute to render an expression like:: >>> print SearchWord.word_insensitive == "Trucks" lower(search_word.word) = lower(:lower_1) :param comparator_factory: A PropComparator subclass or factory that defines operator behavior for this property. :param descriptor: Optional when used in a ``properties={}`` declaration. The Python descriptor or property to layer comparison behavior on top of. The like-named descriptor will be automatically retrieved from the mapped class if left blank in a ``properties`` declaration. :param info: Optional data dictionary which will be populated into the :attr:`.InspectionAttr.info` attribute of this object. .. versionadded:: 1.0.0 """ super(ComparableProperty, self).__init__() self.descriptor = descriptor self.comparator_factory = comparator_factory self.doc = doc or (descriptor and descriptor.__doc__) or None if info: self.info = info util.set_creation_order(self) def _comparator_factory(self, mapper): return self.comparator_factory(self, mapper)