123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699 |
- # orm/descriptor_props.py
- # Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # 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)
|