From 59141d360e70d1a762719206e3cb0220b4c53fef Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 14 Aug 2013 19:58:34 -0400 Subject: - apply an import refactoring to the ORM as well - rework the event system so that event modules load after their targets, dependencies are reversed - create an improved strategy lookup system for the ORM - rework the ORM to have very few import cycles - move out "importlater" to just util.dependency - other tricks to cross-populate modules in as clear a way as possible --- lib/sqlalchemy/orm/interfaces.py | 131 ++++++++++----------------------------- 1 file changed, 34 insertions(+), 97 deletions(-) (limited to 'lib/sqlalchemy/orm/interfaces.py') diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 150277be2..00906a262 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -21,9 +21,12 @@ from __future__ import absolute_import from .. import exc as sa_exc, util, inspect from ..sql import operators from collections import deque +from .base import _is_aliased_class, _class_to_mapper +from .base import ONETOMANY, MANYTOONE, MANYTOMANY, EXT_CONTINUE, EXT_STOP, NOT_EXTENSION +from .base import _InspectionAttr, _MappedAttribute +from .path_registry import PathRegistry +import collections -orm_util = util.importlater('sqlalchemy.orm', 'util') -collections = util.importlater('sqlalchemy.orm', 'collections') __all__ = ( 'AttributeExtension', @@ -42,97 +45,6 @@ __all__ = ( 'StrategizedProperty', ) -EXT_CONTINUE = util.symbol('EXT_CONTINUE') -EXT_STOP = util.symbol('EXT_STOP') - -ONETOMANY = util.symbol('ONETOMANY') -MANYTOONE = util.symbol('MANYTOONE') -MANYTOMANY = util.symbol('MANYTOMANY') - -from .deprecated_interfaces import AttributeExtension, \ - SessionExtension, \ - MapperExtension - - -NOT_EXTENSION = util.symbol('NOT_EXTENSION') -"""Symbol indicating an :class:`_InspectionAttr` that's - not part of sqlalchemy.ext. - - Is assigned to the :attr:`._InspectionAttr.extension_type` - attibute. - -""" - -class _InspectionAttr(object): - """A base class applied to all ORM objects that can be returned - by the :func:`.inspect` function. - - The attributes defined here allow the usage of simple boolean - checks to test basic facts about the object returned. - - While the boolean checks here are basically the same as using - the Python isinstance() function, the flags here can be used without - the need to import all of these classes, and also such that - the SQLAlchemy class system can change while leaving the flags - here intact for forwards-compatibility. - - """ - - is_selectable = False - """Return True if this object is an instance of :class:`.Selectable`.""" - - is_aliased_class = False - """True if this object is an instance of :class:`.AliasedClass`.""" - - is_instance = False - """True if this object is an instance of :class:`.InstanceState`.""" - - is_mapper = False - """True if this object is an instance of :class:`.Mapper`.""" - - is_property = False - """True if this object is an instance of :class:`.MapperProperty`.""" - - is_attribute = False - """True if this object is a Python :term:`descriptor`. - - This can refer to one of many types. Usually a - :class:`.QueryableAttribute` which handles attributes events on behalf - of a :class:`.MapperProperty`. But can also be an extension type - such as :class:`.AssociationProxy` or :class:`.hybrid_property`. - The :attr:`._InspectionAttr.extension_type` will refer to a constant - identifying the specific subtype. - - .. seealso:: - - :attr:`.Mapper.all_orm_descriptors` - - """ - - is_clause_element = False - """True if this object is an instance of :class:`.ClauseElement`.""" - - extension_type = NOT_EXTENSION - """The extension type, if any. - Defaults to :data:`.interfaces.NOT_EXTENSION` - - .. versionadded:: 0.8.0 - - .. seealso:: - - :data:`.HYBRID_METHOD` - - :data:`.HYBRID_PROPERTY` - - :data:`.ASSOCIATION_PROXY` - - """ - -class _MappedAttribute(object): - """Mixin for attributes which should be replaced by mapper-assigned - attributes. - - """ class MapperProperty(_MappedAttribute, _InspectionAttr): @@ -542,6 +454,30 @@ class StrategizedProperty(MapperProperty): self.strategy.init_class_attribute(mapper) + _strategies = collections.defaultdict(dict) + + @classmethod + def _strategy_for(cls, *keys): + def decorate(dec_cls): + for key in keys: + key = tuple(sorted(key.items())) + cls._strategies[cls][key] = dec_cls + return dec_cls + return decorate + + @classmethod + def _strategy_lookup(cls, **kw): + key = tuple(sorted(kw.items())) + for prop_cls in cls.__mro__: + if prop_cls in cls._strategies: + strategies = cls._strategies[prop_cls] + try: + return strategies[key] + except KeyError: + pass + raise Exception("can't locate strategy for %s %s" % (cls, kw)) + + class MapperOption(object): """Describe a modification to a Query.""" @@ -608,10 +544,10 @@ class PropertyOption(MapperOption): self.__dict__ = state def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): - if orm_util._is_aliased_class(mapper): + if _is_aliased_class(mapper): searchfor = mapper else: - searchfor = orm_util._class_to_mapper(mapper) + searchfor = _class_to_mapper(mapper) for ent in query._mapper_entities: if ent.corresponds_to(searchfor): return ent @@ -650,14 +586,15 @@ class PropertyOption(MapperOption): else: return None - def _process_paths(self, query, raiseerr): + @util.dependencies("sqlalchemy.orm.util") + def _process_paths(self, orm_util, query, raiseerr): """reconcile the 'key' for this PropertyOption with the current path and entities of the query. Return a list of affected paths. """ - path = orm_util.PathRegistry.root + path = PathRegistry.root entity = None paths = [] no_result = [] -- cgit v1.2.1 From 4268ea06b3efe116361157fa2ad155b2347d2bba Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 21 Aug 2013 18:48:34 -0400 Subject: move FAQ to the docs, [ticket:2133] --- lib/sqlalchemy/orm/interfaces.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/orm/interfaces.py') diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 00906a262..4a1a1823d 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -147,7 +147,26 @@ class MapperProperty(_MappedAttribute, _InspectionAttr): @property def class_attribute(self): """Return the class-bound descriptor corresponding to this - MapperProperty.""" + :class:`.MapperProperty`. + + This is basically a ``getattr()`` call:: + + return getattr(self.parent.class_, self.key) + + I.e. if this :class:`.MapperProperty` were named ``addresses``, + and the class to which it is mapped is ``User``, this sequence + is possible:: + + >>> from sqlalchemy import inspect + >>> mapper = inspect(User) + >>> addresses_property = mapper.attrs.addresses + >>> addresses_property.class_attribute is User.addresses + True + >>> User.addresses.property is addresses_property + True + + + """ return getattr(self.parent.class_, self.key) -- cgit v1.2.1 From a83378b64005971fe97dff270641bce4967dbb53 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 3 Oct 2013 17:06:55 -0400 Subject: - A new construct :class:`.Bundle` is added, which allows for specification of groups of column expressions to a :class:`.Query` construct. The group of columns are returned as a single tuple by default. The behavior of :class:`.Bundle` can be overridden however to provide any sort of result processing to the returned row. One example included is :attr:`.Composite.Comparator.bundle`, which applies a bundled form of a "composite" mapped attribute. [ticket:2824] - The :func:`.composite` construct now maintains the return object when used in a column-oriented :class:`.Query`, rather than expanding out into individual columns. This makes use of the new :class:`.Bundle` feature internally. This behavior is backwards incompatible; to select from a composite column which will expand out, use ``MyClass.some_composite.clauses``. --- lib/sqlalchemy/orm/interfaces.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/sqlalchemy/orm/interfaces.py') diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 4a1a1823d..2f4aa5208 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -320,6 +320,9 @@ class PropComparator(operators.ColumnOperators): def __clause_element__(self): raise NotImplementedError("%r" % self) + def _query_clause_element(self): + return self.__clause_element__() + def adapt_to_entity(self, adapt_to_entity): """Return a copy of this PropComparator which will use the given :class:`.AliasedInsp` to produce corresponding expressions. -- cgit v1.2.1 From 1b25ed907fb7311d28d2273c9b9858b50c1a7afc Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 6 Oct 2013 20:29:08 -0400 Subject: - merge ticket_1418 branch, [ticket:1418] - The system of loader options has been entirely rearchitected to build upon a much more comprehensive base, the :class:`.Load` object. This base allows any common loader option like :func:`.joinedload`, :func:`.defer`, etc. to be used in a "chained" style for the purpose of specifying options down a path, such as ``joinedload("foo").subqueryload("bar")``. The new system supersedes the usage of dot-separated path names, multiple attributes within options, and the usage of ``_all()`` options. - Added a new load option :func:`.orm.load_only`. This allows a series of column names to be specified as loading "only" those attributes, deferring the rest. --- lib/sqlalchemy/orm/interfaces.py | 321 ++++++--------------------------------- 1 file changed, 45 insertions(+), 276 deletions(-) (limited to 'lib/sqlalchemy/orm/interfaces.py') diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 2f4aa5208..18723e4f6 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -21,7 +21,6 @@ from __future__ import absolute_import from .. import exc as sa_exc, util, inspect from ..sql import operators from collections import deque -from .base import _is_aliased_class, _class_to_mapper from .base import ONETOMANY, MANYTOONE, MANYTOMANY, EXT_CONTINUE, EXT_STOP, NOT_EXTENSION from .base import _InspectionAttr, _MappedAttribute from .path_registry import PathRegistry @@ -424,51 +423,57 @@ class StrategizedProperty(MapperProperty): strategy_wildcard_key = None - @util.memoized_property - def _wildcard_path(self): - if self.strategy_wildcard_key: - return ('loaderstrategy', (self.strategy_wildcard_key,)) - else: - return None + def _get_context_loader(self, context, path): + load = None - def _get_context_strategy(self, context, path): - strategy_cls = path._inlined_get_for(self, context, 'loaderstrategy') + # use EntityRegistry.__getitem__()->PropRegistry here so + # that the path is stated in terms of our base + search_path = dict.__getitem__(path, self) - if not strategy_cls: - wc_key = self._wildcard_path - if wc_key and wc_key in context.attributes: - strategy_cls = context.attributes[wc_key] + # search among: exact match, "attr.*", "default" strategy + # if any. + for path_key in ( + search_path._loader_key, + search_path._wildcard_path_loader_key, + search_path._default_path_loader_key + ): + if path_key in context.attributes: + load = context.attributes[path_key] + break - if strategy_cls: - try: - return self._strategies[strategy_cls] - except KeyError: - return self.__init_strategy(strategy_cls) - return self.strategy + return load - def _get_strategy(self, cls): + def _get_strategy(self, key): try: - return self._strategies[cls] + return self._strategies[key] except KeyError: - return self.__init_strategy(cls) + cls = self._strategy_lookup(*key) + self._strategies[key] = self._strategies[cls] = strategy = cls(self) + return strategy - def __init_strategy(self, cls): - self._strategies[cls] = strategy = cls(self) - return strategy + def _get_strategy_by_cls(self, cls): + return self._get_strategy(cls._strategy_keys[0]) def setup(self, context, entity, path, adapter, **kwargs): - self._get_context_strategy(context, path).\ - setup_query(context, entity, path, - adapter, **kwargs) + loader = self._get_context_loader(context, path) + if loader and loader.strategy: + strat = self._get_strategy(loader.strategy) + else: + strat = self.strategy + strat.setup_query(context, entity, path, loader, adapter, **kwargs) def create_row_processor(self, context, path, mapper, row, adapter): - return self._get_context_strategy(context, path).\ - create_row_processor(context, path, + loader = self._get_context_loader(context, path) + if loader and loader.strategy: + strat = self._get_strategy(loader.strategy) + else: + strat = self.strategy + return strat.create_row_processor(context, path, loader, mapper, row, adapter) def do_init(self): self._strategies = {} - self.strategy = self.__init_strategy(self.strategy_class) + self.strategy = self._get_strategy_by_cls(self.strategy_class) def post_instrument_class(self, mapper): if self.is_primary() and \ @@ -479,17 +484,17 @@ class StrategizedProperty(MapperProperty): _strategies = collections.defaultdict(dict) @classmethod - def _strategy_for(cls, *keys): + def strategy_for(cls, **kw): def decorate(dec_cls): - for key in keys: - key = tuple(sorted(key.items())) - cls._strategies[cls][key] = dec_cls + dec_cls._strategy_keys = [] + key = tuple(sorted(kw.items())) + cls._strategies[cls][key] = dec_cls + dec_cls._strategy_keys.append(key) return dec_cls return decorate @classmethod - def _strategy_lookup(cls, **kw): - key = tuple(sorted(kw.items())) + def _strategy_lookup(cls, *key): for prop_cls in cls.__mro__: if prop_cls in cls._strategies: strategies = cls._strategies[prop_cls] @@ -497,7 +502,7 @@ class StrategizedProperty(MapperProperty): return strategies[key] except KeyError: pass - raise Exception("can't locate strategy for %s %s" % (cls, kw)) + raise Exception("can't locate strategy for %s %s" % (cls, key)) class MapperOption(object): @@ -521,242 +526,6 @@ class MapperOption(object): self.process_query(query) -class PropertyOption(MapperOption): - """A MapperOption that is applied to a property off the mapper or - one of its child mappers, identified by a dot-separated key - or list of class-bound attributes. """ - - def __init__(self, key, mapper=None): - self.key = key - self.mapper = mapper - - def process_query(self, query): - self._process(query, True) - - def process_query_conditionally(self, query): - self._process(query, False) - - def _process(self, query, raiseerr): - paths = self._process_paths(query, raiseerr) - if paths: - self.process_query_property(query, paths) - - def process_query_property(self, query, paths): - pass - - def __getstate__(self): - d = self.__dict__.copy() - d['key'] = ret = [] - for token in util.to_list(self.key): - if isinstance(token, PropComparator): - ret.append((token._parentmapper.class_, token.key)) - else: - ret.append(token) - return d - - def __setstate__(self, state): - ret = [] - for key in state['key']: - if isinstance(key, tuple): - cls, propkey = key - ret.append(getattr(cls, propkey)) - else: - ret.append(key) - state['key'] = tuple(ret) - self.__dict__ = state - - def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): - if _is_aliased_class(mapper): - searchfor = mapper - else: - searchfor = _class_to_mapper(mapper) - for ent in query._mapper_entities: - if ent.corresponds_to(searchfor): - return ent - else: - if raiseerr: - if not list(query._mapper_entities): - raise sa_exc.ArgumentError( - "Query has only expression-based entities - " - "can't find property named '%s'." - % (token, ) - ) - else: - raise sa_exc.ArgumentError( - "Can't find property '%s' on any entity " - "specified in this Query. Note the full path " - "from root (%s) to target entity must be specified." - % (token, ",".join(str(x) for - x in query._mapper_entities)) - ) - else: - return None - - def _find_entity_basestring(self, query, token, raiseerr): - for ent in query._mapper_entities: - # return only the first _MapperEntity when searching - # based on string prop name. Ideally object - # attributes are used to specify more exactly. - return ent - else: - if raiseerr: - raise sa_exc.ArgumentError( - "Query has only expression-based entities - " - "can't find property named '%s'." - % (token, ) - ) - else: - return None - - @util.dependencies("sqlalchemy.orm.util") - def _process_paths(self, orm_util, query, raiseerr): - """reconcile the 'key' for this PropertyOption with - the current path and entities of the query. - - Return a list of affected paths. - - """ - path = PathRegistry.root - entity = None - paths = [] - no_result = [] - - # _current_path implies we're in a - # secondary load with an existing path - current_path = list(query._current_path.path) - - tokens = deque(self.key) - while tokens: - token = tokens.popleft() - if isinstance(token, str): - # wildcard token - if token.endswith(':*'): - return [path.token(token)] - sub_tokens = token.split(".", 1) - token = sub_tokens[0] - tokens.extendleft(sub_tokens[1:]) - - # exhaust current_path before - # matching tokens to entities - if current_path: - if current_path[1].key == token: - current_path = current_path[2:] - continue - else: - return no_result - - if not entity: - entity = self._find_entity_basestring( - query, - token, - raiseerr) - if entity is None: - return no_result - path_element = entity.entity_zero - mapper = entity.mapper - - if hasattr(mapper.class_, token): - prop = getattr(mapper.class_, token).property - else: - if raiseerr: - raise sa_exc.ArgumentError( - "Can't find property named '%s' on the " - "mapped entity %s in this Query. " % ( - token, mapper) - ) - else: - return no_result - elif isinstance(token, PropComparator): - prop = token.property - - # exhaust current_path before - # matching tokens to entities - if current_path: - if current_path[0:2] == \ - [token._parententity, prop]: - current_path = current_path[2:] - continue - else: - return no_result - - if not entity: - entity = self._find_entity_prop_comparator( - query, - prop.key, - token._parententity, - raiseerr) - if not entity: - return no_result - - path_element = entity.entity_zero - mapper = entity.mapper - else: - raise sa_exc.ArgumentError( - "mapper option expects " - "string key or list of attributes") - assert prop is not None - if raiseerr and not prop.parent.common_parent(mapper): - raise sa_exc.ArgumentError("Attribute '%s' does not " - "link from element '%s'" % (token, path_element)) - - path = path[path_element][prop] - - paths.append(path) - - if getattr(token, '_of_type', None): - ac = token._of_type - ext_info = inspect(ac) - path_element = mapper = ext_info.mapper - 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) - ext_info = inspect(ac) - path.set(query._attributes, "path_with_polymorphic", ext_info) - else: - path_element = mapper = getattr(prop, 'mapper', None) - if mapper is None and tokens: - raise sa_exc.ArgumentError( - "Attribute '%s' of entity '%s' does not " - "refer to a mapped entity" % - (token, entity) - ) - - if current_path: - # ran out of tokens before - # current_path was exhausted. - assert not tokens - return no_result - - return paths - - -class StrategizedOption(PropertyOption): - """A MapperOption that affects which LoaderStrategy will be used - for an operation by a StrategizedProperty. - """ - - chained = False - - def process_query_property(self, query, paths): - strategy = self.get_strategy_class() - if self.chained: - for path in paths: - path.set( - query._attributes, - "loaderstrategy", - strategy - ) - else: - paths[-1].set( - query._attributes, - "loaderstrategy", - strategy - ) - - def get_strategy_class(self): - raise NotImplementedError() class LoaderStrategy(object): @@ -791,10 +560,10 @@ class LoaderStrategy(object): def init_class_attribute(self, mapper): pass - def setup_query(self, context, entity, path, adapter, **kwargs): + def setup_query(self, context, entity, path, loadopt, adapter, **kwargs): pass - def create_row_processor(self, context, path, mapper, + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): """Return row processing functions which fulfill the contract specified by MapperProperty.create_row_processor. -- cgit v1.2.1 From 22d5a1e415d603d253870719466152b9e817e1e5 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 6 Oct 2013 20:12:28 -0400 Subject: 11th hour realization that Load() needs to do the _chop_path() thing as well. this probably has some bugs --- lib/sqlalchemy/orm/interfaces.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/sqlalchemy/orm/interfaces.py') diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 18723e4f6..f61396750 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -430,6 +430,9 @@ class StrategizedProperty(MapperProperty): # that the path is stated in terms of our base search_path = dict.__getitem__(path, self) + #if self.key == "email_address": + # import pdb + # pdb.set_trace() # search among: exact match, "attr.*", "default" strategy # if any. for path_key in ( -- cgit v1.2.1 From d91449361654385edde382776e0dc5639047a1a8 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 6 Oct 2013 21:07:20 -0400 Subject: - add some tests for propagate of wildcard lazyload --- lib/sqlalchemy/orm/interfaces.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'lib/sqlalchemy/orm/interfaces.py') diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index f61396750..18723e4f6 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -430,9 +430,6 @@ class StrategizedProperty(MapperProperty): # that the path is stated in terms of our base search_path = dict.__getitem__(path, self) - #if self.key == "email_address": - # import pdb - # pdb.set_trace() # search among: exact match, "attr.*", "default" strategy # if any. for path_key in ( -- cgit v1.2.1 From f89d4d216bd7605c920b7b8a10ecde6bfea2238c Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 5 Jan 2014 16:57:05 -0500 Subject: - happy new year --- lib/sqlalchemy/orm/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/orm/interfaces.py') diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 18723e4f6..3d5559be9 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -1,5 +1,5 @@ # orm/interfaces.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 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 -- cgit v1.2.1