Learn more  » Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

aroundthecode / SQLAlchemy   python

Repository URL to install this package:

Version: 1.2.10 

/ orm / descriptor_props.py

# orm/descriptor_props.py
# Copyright (C) 2005-2018 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):
            _load_refresh_handler(state, args, is_refresh=False)

        def refresh_handler(state, *args):
            _load_refresh_handler(state, args, is_refresh=True)

        def _load_refresh_handler(state, args, is_refresh):
            dict_ = state.dict

            if not is_refresh and 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

            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',
                     refresh_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
Loading ...