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 / strategy_options.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

"""

"""

from .interfaces import MapperOption, PropComparator, MapperProperty
from .attributes import QueryableAttribute
from .. import util
from ..sql.base import _generative, Generative
from .. import exc as sa_exc, inspect
from .base import _is_aliased_class, _class_to_mapper, _is_mapped_class, \
    InspectionAttr
from . import util as orm_util
from .path_registry import PathRegistry, TokenRegistry, \
    _WILDCARD_TOKEN, _DEFAULT_TOKEN


class Load(Generative, MapperOption):
    """Represents loader options which modify the state of a
    :class:`.Query` in order to affect how various mapped attributes are
    loaded.

    The :class:`.Load` object is in most cases used implicitly behind the
    scenes when one makes use of a query option like :func:`.joinedload`,
    :func:`.defer`, or similar.   However, the :class:`.Load` object
    can also be used directly, and in some cases can be useful.

    To use :class:`.Load` directly, instantiate it with the target mapped
    class as the argument.   This style of usage is
    useful when dealing with a :class:`.Query` that has multiple entities::

        myopt = Load(MyClass).joinedload("widgets")

    The above ``myopt`` can now be used with :meth:`.Query.options`, where it
    will only take effect for the ``MyClass`` entity::

        session.query(MyClass, MyOtherClass).options(myopt)

    One case where :class:`.Load` is useful as public API is when specifying
    "wildcard" options that only take effect for a certain class::

        session.query(Order).options(Load(Order).lazyload('*'))

    Above, all relationships on ``Order`` will be lazy-loaded, but other
    attributes on those descendant objects will load using their normal
    loader strategy.

    .. seealso::

        :ref:`loading_toplevel`

    """

    def __init__(self, entity):
        insp = inspect(entity)
        self.path = insp._path_registry
        # note that this .context is shared among all descendant
        # Load objects
        self.context = util.OrderedDict()
        self.local_opts = {}
        self._of_type = None
        self.is_class_strategy = False

    @classmethod
    def for_existing_path(cls, path):
        load = cls.__new__(cls)
        load.path = path
        load.context = {}
        load.local_opts = {}
        load._of_type = None
        return load

    def _generate_cache_key(self, path):
        if path.path[0].is_aliased_class:
            return False

        serialized = []
        for (key, loader_path), obj in self.context.items():
            if key != "loader":
                continue

            for local_elem, obj_elem in zip(self.path.path, loader_path):
                if local_elem is not obj_elem:
                    break
            else:
                endpoint = obj._of_type or obj.path.path[-1]
                chopped = self._chop_path(loader_path, path)

                if (
                    # means loader_path and path are unrelated,
                    # this does not need to be part of a cache key
                    chopped is None
                ) or (
                    # means no additional path with loader_path + path
                    # and the endpoint isn't using of_type so isn't modified
                    # into an alias or other unsafe entity
                    not chopped and not obj._of_type
                ):
                    continue

                serialized_path = []

                for token in chopped:
                    if isinstance(token, util.string_types):
                        serialized_path.append(token)
                    elif token.is_aliased_class:
                        return False
                    elif token.is_property:
                        serialized_path.append(token.key)
                    else:
                        assert token.is_mapper
                        serialized_path.append(token.class_)

                if not serialized_path or endpoint != serialized_path[-1]:
                    if endpoint.is_mapper:
                        serialized_path.append(endpoint.class_)
                    elif endpoint.is_aliased_class:
                        return False

                serialized.append(
                    (
                        tuple(serialized_path) +
                        (obj.strategy or ()) +
                        (tuple([
                            (key, obj.local_opts[key])
                            for key in sorted(obj.local_opts)
                        ]) if obj.local_opts else ())
                    )
                )
        if not serialized:
            return None
        else:
            return tuple(serialized)

    def _generate(self):
        cloned = super(Load, self)._generate()
        cloned.local_opts = {}
        return cloned

    is_opts_only = False
    is_class_strategy = False
    strategy = None
    propagate_to_loaders = False

    def process_query(self, query):
        self._process(query, True)

    def process_query_conditionally(self, query):
        self._process(query, False)

    def _process(self, query, raiseerr):
        current_path = query._current_path
        if current_path:
            for (token, start_path), loader in self.context.items():
                chopped_start_path = self._chop_path(start_path, current_path)
                if chopped_start_path is not None:
                    query._attributes[(token, chopped_start_path)] = loader
        else:
            query._attributes.update(self.context)

    def _generate_path(self, path, attr, wildcard_key, raiseerr=True):
        self._of_type = None

        if raiseerr and not path.has_entity:
            if isinstance(path, TokenRegistry):
                raise sa_exc.ArgumentError(
                    "Wildcard token cannot be followed by another entity")
            else:
                raise sa_exc.ArgumentError(
                    "Attribute '%s' of entity '%s' does not "
                    "refer to a mapped entity" %
                    (path.prop.key, path.parent.entity)
                )

        if isinstance(attr, util.string_types):
            default_token = attr.endswith(_DEFAULT_TOKEN)
            if attr.endswith(_WILDCARD_TOKEN) or default_token:
                if default_token:
                    self.propagate_to_loaders = False
                if wildcard_key:
                    attr = "%s:%s" % (wildcard_key, attr)
                path = path.token(attr)
                self.path = path
                return path

            try:
                # use getattr on the class to work around
                # synonyms, hybrids, etc.
                attr = getattr(path.entity.class_, attr)
            except AttributeError:
                if raiseerr:
                    raise sa_exc.ArgumentError(
                        "Can't find property named '%s' on the "
                        "mapped entity %s in this Query. " % (
                            attr, path.entity)
                    )
                else:
                    return None
            else:
                attr = attr.property

            path = path[attr]
        elif _is_mapped_class(attr):
            if not attr.common_parent(path.mapper):
                if raiseerr:
                    raise sa_exc.ArgumentError(
                        "Attribute '%s' does not "
                        "link from element '%s'" % (attr, path.entity))
                else:
                    return None
        else:
            prop = attr.property

            if not prop.parent.common_parent(path.mapper):
                if raiseerr:
                    raise sa_exc.ArgumentError(
                        "Attribute '%s' does not "
                        "link from element '%s'" % (attr, path.entity))
                else:
                    return None

            if getattr(attr, '_of_type', None):
                ac = attr._of_type
                ext_info = of_type_info = inspect(ac)

                existing = path.entity_path[prop].get(
                    self.context, "path_with_polymorphic")
                if not ext_info.is_aliased_class:
                    ac = orm_util.with_polymorphic(
                        ext_info.mapper.base_mapper,
                        ext_info.mapper, aliased=True,
                        _use_mapper_path=True,
                        _existing_alias=existing)
                    ext_info = inspect(ac)
                elif not ext_info.with_polymorphic_mappers:
                    ext_info = orm_util.AliasedInsp(
                        ext_info.entity,
                        ext_info.mapper.base_mapper,
                        ext_info.selectable,
                        ext_info.name,
                        ext_info.with_polymorphic_mappers or [ext_info.mapper],
                        ext_info.polymorphic_on,
                        ext_info._base_alias,
                        ext_info._use_mapper_path,
                        ext_info._adapt_on_names,
                        ext_info.represents_outer_join
                    )

                path.entity_path[prop].set(
                    self.context, "path_with_polymorphic", ext_info)

                # the path here will go into the context dictionary and
                # needs to match up to how the class graph is traversed.
                # so we can't put an AliasedInsp in the path here, needs
                # to be the base mapper.
                path = path[prop][ext_info.mapper]

                # but, we need to know what the original of_type()
                # argument is for cache key purposes.  so....store that too.
                # it might be better for "path" to really represent,
                # "the path", but trying to keep the impact of the cache
                # key feature localized for now
                self._of_type = of_type_info
            else:
                path = path[prop]

        if path.has_entity:
            path = path.entity_path
        self.path = path
        return path

    def __str__(self):
        return "Load(strategy=%r)" % (self.strategy, )

    def _coerce_strat(self, strategy):
        if strategy is not None:
            strategy = tuple(sorted(strategy.items()))
        return strategy

    @_generative
    def set_relationship_strategy(
            self, attr, strategy, propagate_to_loaders=True):
        strategy = self._coerce_strat(strategy)

        self.is_class_strategy = False
        self.propagate_to_loaders = propagate_to_loaders
        # if the path is a wildcard, this will set propagate_to_loaders=False
        self._generate_path(self.path, attr, "relationship")
        self.strategy = strategy
        if strategy is not None:
            self._set_path_strategy()

    @_generative
    def set_column_strategy(self, attrs, strategy, opts=None, opts_only=False):
        strategy = self._coerce_strat(strategy)

        self.is_class_strategy = False
        for attr in attrs:
            cloned = self._generate()
            cloned.strategy = strategy
            cloned._generate_path(self.path, attr, "column")
            cloned.propagate_to_loaders = True
            if opts:
                cloned.local_opts.update(opts)
            if opts_only:
                cloned.is_opts_only = True
            cloned._set_path_strategy()
        self.is_class_strategy = False

    @_generative
    def set_generic_strategy(self, attrs, strategy):
        strategy = self._coerce_strat(strategy)

        for attr in attrs:
            path = self._generate_path(self.path, attr, None)
            cloned = self._generate()
            cloned.strategy = strategy
            cloned.path = path
            cloned.propagate_to_loaders = True
            cloned._set_path_strategy()

    @_generative
    def set_class_strategy(self, strategy, opts):
        strategy = self._coerce_strat(strategy)
        cloned = self._generate()
        cloned.is_class_strategy = True
        path = cloned._generate_path(self.path, None, None)
        cloned.strategy = strategy
        cloned.path = path
        cloned.propagate_to_loaders = True
        cloned._set_path_strategy()
        cloned.local_opts.update(opts)

    def _set_for_path(self, context, path, replace=True, merge_opts=False):
        if merge_opts or not replace:
            existing = path.get(self.context, "loader")

            if existing:
                if merge_opts:
                    existing.local_opts.update(self.local_opts)
Loading ...