# 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 ...