summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2018-09-27 17:10:21 -0400
committerGerrit Code Review <gerrit@ci.zzzcomputing.com>2018-09-27 17:10:21 -0400
commitd0c4873dc7a609728f805308f0e3e23a9d3d2bda (patch)
treeefcbf9cffa5de94c0e8e629353bdbb07aa8ddb3f /lib/sqlalchemy
parent5f2a934e3d359ec46a0c9554c9a89210a44807b7 (diff)
parent6446e0dfd3e3bb60754bad81c4d52345733d94e3 (diff)
downloadsqlalchemy-d0c4873dc7a609728f805308f0e3e23a9d3d2bda.tar.gz
Merge "Break association proxy into a descriptor + per-class accessor"
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/ext/associationproxy.py353
-rw-r--r--lib/sqlalchemy/orm/instrumentation.py35
2 files changed, 220 insertions, 168 deletions
diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py
index 3c27cb59f..acf13df46 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -162,25 +162,161 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
self.proxy_bulk_set = proxy_bulk_set
self.cascade_scalar_deletes = cascade_scalar_deletes
- self.owning_class = None
self.key = '_%s_%s_%s' % (
type(self).__name__, target_collection, id(self))
- self.collection_class = None
if info:
self.info = info
+ def __get__(self, obj, class_):
+ if class_ is None:
+ return self
+ inst = self._as_instance(class_)
+ if inst:
+ return inst.get(obj)
+ else:
+ return self
+
+ def __set__(self, obj, values):
+ class_ = type(obj)
+ return self._as_instance(class_).set(obj, values)
+
+ def __delete__(self, obj):
+ class_ = type(obj)
+ return self._as_instance(class_).delete(obj)
+
+ def for_class(self, class_):
+ """Return the internal state local to a specific mapped class.
+
+ E.g., given a class ``User``::
+
+ class User(Base):
+ # ...
+
+ keywords = association_proxy('kws', 'keyword')
+
+ If we access this :class:`.AssociationProxy` from
+ :attr:`.Mapper.all_orm_descriptors`, and we want to view the
+ target class for this proxy as mapped by ``User``::
+
+ inspect(User).all_orm_descriptors["keywords"].for_class(User).target_class
+
+ This returns an instance of :class:`.AssociationProxyInstance` that
+ is specific to the ``User`` class. The :class:`.AssociationProxy`
+ object remains agnostic of its parent class.
+
+ .. versionadded:: 1.3 - :class:`.AssociationProxy` no longer stores
+ any state specific to a particular parent class; the state is now
+ stored in per-class :class:`.AssociationProxyInstance` objects.
+
+
+ """
+ return self._as_instance(class_)
+
+ def _as_instance(self, class_):
+ try:
+ return class_.__dict__[self.key + "_inst"]
+ except KeyError:
+ owner = self._calc_owner(class_)
+ if owner is not None:
+ result = AssociationProxyInstance(self, owner)
+ setattr(class_, self.key + "_inst", result)
+ return result
+ else:
+ return None
+
+ def _calc_owner(self, target_cls):
+ # we might be getting invoked for a subclass
+ # that is not mapped yet, in some declarative situations.
+ # save until we are mapped
+ try:
+ insp = inspect(target_cls)
+ except exc.NoInspectionAvailable:
+ # can't find a mapper, don't set owner. if we are a not-yet-mapped
+ # subclass, we can also scan through __mro__ to find a mapped
+ # class, but instead just wait for us to be called again against a
+ # mapped class normally.
+ return None
+ else:
+ return insp.mapper.class_manager.class_
+
+ def _default_getset(self, collection_class):
+ attr = self.value_attr
+ _getter = operator.attrgetter(attr)
+
+ def getter(target):
+ return _getter(target) if target is not None else None
+ if collection_class is dict:
+ def setter(o, k, v):
+ setattr(o, attr, v)
+ else:
+ def setter(o, v):
+ setattr(o, attr, v)
+ return getter, setter
+
+
+class AssociationProxyInstance(object):
+ """A per-class object that serves class- and object-specific results.
+
+ This is used by :class:`.AssociationProxy` when it is invoked
+ in terms of a specific class or instance of a class, i.e. when it is
+ used as a regular Python descriptor.
+
+ When referring to the :class:`.AssociationProxy` as a normal Python
+ descriptor, the :class:`.AssociationProxyInstance` is the object that
+ actually serves the information. Under normal circumstances, its presence
+ is transparent::
+
+ >>> User.keywords.scalar
+ False
+
+ In the special case that the :class:`.AssociationProxy` object is being
+ accessed directly, in order to get an explicit handle to the
+ :class:`.AssociationProxyInstance`, use the
+ :meth:`.AssociationProxy.for_class` method::
+
+ proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
+
+ # view if proxy object is scalar or not
+ >>> proxy_state.scalar
+ False
+
+ .. versionadded:: 1.3
+
+ """
+
+ def __init__(self, parent, owning_class):
+ self.parent = parent
+ self.key = parent.key
+ self.owning_class = owning_class
+ self.target_collection = parent.target_collection
+ self.value_attr = parent.value_attr
+ self.collection_class = None
+
+ def _get_property(self):
+ return orm.class_mapper(self.owning_class).\
+ get_property(self.target_collection)
+
+ @property
+ def _comparator(self):
+ return self._get_property().comparator
+
+ @util.memoized_property
+ def _unwrap_target_assoc_proxy(self):
+ attr = getattr(self.target_class, self.value_attr)
+ if isinstance(attr, (AssociationProxy, AssociationProxyInstance)):
+ return attr
+ return None
+
@property
def remote_attr(self):
"""The 'remote' :class:`.MapperProperty` referenced by this
- :class:`.AssociationProxy`.
-
- .. versionadded:: 0.7.3
+ :class:`.AssociationProxyInstance`.
- See also:
+ ..seealso::
- :attr:`.AssociationProxy.attr`
+ :attr:`.AssociationProxyInstance.attr`
- :attr:`.AssociationProxy.local_attr`
+ :attr:`.AssociationProxyInstance.local_attr`
"""
return getattr(self.target_class, self.value_attr)
@@ -188,15 +324,13 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
@property
def local_attr(self):
"""The 'local' :class:`.MapperProperty` referenced by this
- :class:`.AssociationProxy`.
-
- .. versionadded:: 0.7.3
+ :class:`.AssociationProxyInstance`.
- See also:
+ .. seealso::
- :attr:`.AssociationProxy.attr`
+ :attr:`.AssociationProxyInstance.attr`
- :attr:`.AssociationProxy.remote_attr`
+ :attr:`.AssociationProxyInstance.remote_attr`
"""
return getattr(self.owning_class, self.target_collection)
@@ -210,29 +344,19 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
sess.query(Parent).join(*Parent.proxied.attr)
- .. versionadded:: 0.7.3
+ .. seealso::
- See also:
+ :attr:`.AssociationProxyInstance.local_attr`
- :attr:`.AssociationProxy.local_attr`
-
- :attr:`.AssociationProxy.remote_attr`
+ :attr:`.AssociationProxyInstance.remote_attr`
"""
return (self.local_attr, self.remote_attr)
- def _get_property(self):
- owning_class = self.owning_class
- if owning_class is None:
- raise exc.InvalidRequestError(
- "This association proxy has no mapped owning class; "
- "can't locate a mapped property")
- return (orm.class_mapper(owning_class).
- get_property(self.target_collection))
-
@util.memoized_property
def target_class(self):
- """The intermediary class handled by this :class:`.AssociationProxy`.
+ """The intermediary class handled by this
+ :class:`.AssociationProxyInstance`.
Intercepted append/set/assignment events will result
in the generation of new instances of this class.
@@ -242,8 +366,8 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
@util.memoized_property
def scalar(self):
- """Return ``True`` if this :class:`.AssociationProxy` proxies a scalar
- relationship on the local side."""
+ """Return ``True`` if this :class:`.AssociationProxyInstance`
+ proxies a scalar relationship on the local side."""
scalar = not self._get_property().uselist
if scalar:
@@ -259,39 +383,32 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
def _target_is_object(self):
return getattr(self.target_class, self.value_attr).impl.uses_objects
- def _calc_owner(self, obj, class_):
- if obj is not None and class_ is None:
- target_cls = type(obj)
- elif class_ is not None:
- target_cls = class_
+ def _initialize_scalar_accessors(self):
+ if self.parent.getset_factory:
+ get, set = self.parent.getset_factory(None, self)
else:
- return
+ get, set = self.parent._default_getset(None)
+ self._scalar_get, self._scalar_set = get, set
- # we might be getting invoked for a subclass
- # that is not mapped yet, in some declarative situations.
- # save until we are mapped
- try:
- insp = inspect(target_cls)
- except exc.NoInspectionAvailable:
- # can't find a mapper, don't set owner. if we are a not-yet-mapped
- # subclass, we can also scan through __mro__ to find a mapped
- # class, but instead just wait for us to be called again against a
- # mapped class normally.
- return
+ def _default_getset(self, collection_class):
+ attr = self.value_attr
+ _getter = operator.attrgetter(attr)
- # note we can get our real .key here too
- owner = insp.mapper.class_manager._locate_owning_manager(self)
- if owner is not None:
- self.owning_class = owner.class_
+ def getter(target):
+ return _getter(target) if target is not None else None
+ if collection_class is dict:
+ def setter(o, k, v):
+ return setattr(o, attr, v)
else:
- # the proxy is attached to a class that is not mapped
- # (like a mixin), we are mapped, so, it's us.
- self.owning_class = target_cls
+ def setter(o, v):
+ return setattr(o, attr, v)
+ return getter, setter
- def __get__(self, obj, class_):
- if self.owning_class is None:
- self._calc_owner(obj, class_)
+ @property
+ def info(self):
+ return self.parent.info
+ def get(self, obj):
if obj is None:
return self
@@ -302,21 +419,23 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
try:
# If the owning instance is reborn (orm session resurrect,
# etc.), refresh the proxy cache.
- creator_id, proxy = getattr(obj, self.key)
- if id(obj) == creator_id:
- return proxy
+ creator_id, self_id, proxy = getattr(obj, self.key)
except AttributeError:
pass
- proxy = self._new(_lazy_collection(obj, self.target_collection))
- setattr(obj, self.key, (id(obj), proxy))
- return proxy
+ else:
+ if id(obj) == creator_id and id(self) == self_id:
+ assert self.collection_class is not None
+ return proxy
- def __set__(self, obj, values):
- if self.owning_class is None:
- self._calc_owner(obj, None)
+ self.collection_class, proxy = self._new(
+ _lazy_collection(obj, self.target_collection))
+ setattr(obj, self.key, (id(obj), id(self), proxy))
+ return proxy
+ def set(self, obj, values):
if self.scalar:
- creator = self.creator and self.creator or self.target_class
+ creator = self.parent.creator \
+ if self.parent.creator else self.target_class
target = getattr(obj, self.target_collection)
if target is None:
if values is None:
@@ -324,15 +443,16 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
setattr(obj, self.target_collection, creator(values))
else:
self._scalar_set(target, values)
- if values is None and self.cascade_scalar_deletes:
+ if values is None and self.parent.cascade_scalar_deletes:
setattr(obj, self.target_collection, None)
else:
- proxy = self.__get__(obj, None)
+ proxy = self.get(obj)
+ assert self.collection_class is not None
if proxy is not values:
proxy.clear()
self._set(proxy, values)
- def __delete__(self, obj):
+ def delete(self, obj):
if self.owning_class is None:
self._calc_owner(obj, None)
@@ -342,49 +462,29 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
delattr(target, self.value_attr)
delattr(obj, self.target_collection)
- def _initialize_scalar_accessors(self):
- if self.getset_factory:
- get, set = self.getset_factory(None, self)
- else:
- get, set = self._default_getset(None)
- self._scalar_get, self._scalar_set = get, set
-
- def _default_getset(self, collection_class):
- attr = self.value_attr
- _getter = operator.attrgetter(attr)
-
- def getter(target):
- return _getter(target) if target is not None else None
-
- if collection_class is dict:
- def setter(o, k, v):
- setattr(o, attr, v)
- else:
- def setter(o, v):
- setattr(o, attr, v)
- return getter, setter
-
def _new(self, lazy_collection):
- creator = self.creator and self.creator or self.target_class
- self.collection_class = util.duck_type_collection(lazy_collection())
+ creator = self.parent.creator if self.parent.creator else \
+ self.target_class
+ collection_class = util.duck_type_collection(lazy_collection())
- if self.proxy_factory:
- return self.proxy_factory(
+ if self.parent.proxy_factory:
+ return collection_class, self.parent.proxy_factory(
lazy_collection, creator, self.value_attr, self)
- if self.getset_factory:
- getter, setter = self.getset_factory(self.collection_class, self)
+ if self.parent.getset_factory:
+ getter, setter = self.parent.getset_factory(
+ collection_class, self)
else:
- getter, setter = self._default_getset(self.collection_class)
+ getter, setter = self.parent._default_getset(collection_class)
- if self.collection_class is list:
- return _AssociationList(
+ if collection_class is list:
+ return collection_class, _AssociationList(
lazy_collection, creator, getter, setter, self)
- elif self.collection_class is dict:
- return _AssociationDict(
+ elif collection_class is dict:
+ return collection_class, _AssociationDict(
lazy_collection, creator, getter, setter, self)
- elif self.collection_class is set:
- return _AssociationSet(
+ elif collection_class is set:
+ return collection_class, _AssociationSet(
lazy_collection, creator, getter, setter, self)
else:
raise exc.ArgumentError(
@@ -393,21 +493,9 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
'proxy_factory and proxy_bulk_set manually' %
(self.collection_class.__name__, self.target_collection))
- def _inflate(self, proxy):
- creator = self.creator and self.creator or self.target_class
-
- if self.getset_factory:
- getter, setter = self.getset_factory(self.collection_class, self)
- else:
- getter, setter = self._default_getset(self.collection_class)
-
- proxy.creator = creator
- proxy.getter = getter
- proxy.setter = setter
-
def _set(self, proxy, values):
- if self.proxy_bulk_set:
- self.proxy_bulk_set(proxy, values)
+ if self.parent.proxy_bulk_set:
+ self.parent.proxy_bulk_set(proxy, values)
elif self.collection_class is list:
proxy.extend(values)
elif self.collection_class is dict:
@@ -419,16 +507,19 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
'no proxy_bulk_set supplied for custom '
'collection_class implementation')
- @property
- def _comparator(self):
- return self._get_property().comparator
+ def _inflate(self, proxy):
+ creator = self.parent.creator and \
+ self.parent.creator or self.target_class
- @util.memoized_property
- def _unwrap_target_assoc_proxy(self):
- attr = getattr(self.target_class, self.value_attr)
- if isinstance(attr, AssociationProxy):
- return attr
- return None
+ if self.parent.getset_factory:
+ getter, setter = self.parent.getset_factory(
+ self.collection_class, self)
+ else:
+ getter, setter = self.parent._default_getset(self.collection_class)
+
+ proxy.creator = creator
+ proxy.getter = getter
+ proxy.setter = setter
def _criterion_exists(self, criterion=None, **kwargs):
is_has = kwargs.pop('is_has', None)
@@ -543,10 +634,6 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
return self._comparator.has(
getattr(self.target_class, self.value_attr) != obj)
- def __repr__(self):
- return "AssociationProxy(%r, %r)" % (
- self.target_collection, self.value_attr)
-
class _lazy_collection(object):
def __init__(self, obj, target):
diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py
index 1b839cf5c..d34326e0f 100644
--- a/lib/sqlalchemy/orm/instrumentation.py
+++ b/lib/sqlalchemy/orm/instrumentation.py
@@ -115,41 +115,6 @@ class ClassManager(dict):
# raises unless self.mapper has been assigned
raise exc.UnmappedClassError(self.class_)
- def _locate_owning_manager(self, attribute):
- """Scan through all instrumented classes in our hierarchy
- searching for the given object as an attribute, and return
- the bottommost owner.
-
- E.g.::
-
- foo = foobar()
-
- class Parent:
- attr = foo
-
- class Child(Parent):
- pass
-
- Child.manager._locate_owning_manager(foo) would
- give us Parent.
-
- Needed by association proxy to correctly figure out the
- owning class when the attribute is accessed.
-
- """
-
- stack = [None]
- for supercls in self.class_.__mro__:
- mgr = manager_of_class(supercls)
- if not mgr:
- continue
- for key in set(supercls.__dict__):
- val = supercls.__dict__[key]
- if val is attribute:
- stack.append(mgr)
- continue
- return stack[-1]
-
def _all_sqla_attributes(self, exclude=None):
"""return an iterator of all classbound attributes that are
implement :class:`.InspectionAttr`.