summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2017-06-12 12:56:04 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2017-06-12 16:31:59 -0400
commit27a0bdcae0eb534dac9844e2ffa3d6bd3b1e8989 (patch)
tree040448c0ac94b7d98b0f8118fe50981a23d2e5e6 /lib/sqlalchemy
parentec422fb70e0044ed42dcfda5fb1a7a65db322cf1 (diff)
downloadsqlalchemy-27a0bdcae0eb534dac9844e2ffa3d6bd3b1e8989.tar.gz
Support AssociationProxy any() / has() / contains() to another AssociationProxy
The :meth:`.AssociationProxy.any`, :meth:`.AssociationProxy.has` and :meth:`.AssociationProxy.contains` comparison methods now support linkage to an attribute that is itself also an :class:`.AssociationProxy`, recursively. After some initial attempts it's clear that the any() / has() of AssociationProxy needed to be reworked into a generic _criterion_exists() to allow this to work recursively without excess complexity. For the case of the multi-linked associationproxy, the usual checks of "any()" / "has()" correctness simply don't take place; for a single-link association proxy the error checking logic that takes place in relationship() has been ported to the local any() / has() methods. Change-Id: Ic5aed2a4e910b8138a737d215430113c31cce856 Fixes: #3769
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/ext/associationproxy.py104
1 files changed, 68 insertions, 36 deletions
diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py
index 1c735ca4d..16a4f3540 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -363,6 +363,41 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
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):
+ return attr
+ return None
+
+ def _criterion_exists(self, criterion=None, **kwargs):
+ is_has = kwargs.pop('is_has', None)
+
+ target_assoc = self._unwrap_target_assoc_proxy
+ if target_assoc is not None:
+ inner = target_assoc._criterion_exists(
+ criterion=criterion, **kwargs)
+ return self._comparator._criterion_exists(inner)
+
+ if self._target_is_object:
+ prop = getattr(self.target_class, self.value_attr)
+ value_expr = prop._criterion_exists(criterion, **kwargs)
+ else:
+ if kwargs:
+ raise exc.ArgumentError(
+ "Can't apply keyword arguments to column-targeted "
+ "association proxy; use =="
+ )
+ elif is_has and criterion is not None:
+ raise exc.ArgumentError(
+ "Non-empty has() not allowed for "
+ "column-targeted association proxy; use =="
+ )
+
+ value_expr = criterion
+
+ return self._comparator._criterion_exists(value_expr)
+
def any(self, criterion=None, **kwargs):
"""Produce a proxied 'any' expression using EXISTS.
@@ -372,29 +407,16 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
operators of the underlying proxied attributes.
"""
- if self._target_is_object:
- if self._value_is_scalar:
- value_expr = getattr(
- self.target_class, self.value_attr).has(
- criterion, **kwargs)
- else:
- value_expr = getattr(
- self.target_class, self.value_attr).any(
- criterion, **kwargs)
- else:
- value_expr = criterion
-
- # check _value_is_scalar here, otherwise
- # we're scalar->scalar - call .any() so that
- # the "can't call any() on a scalar" msg is raised.
- if self.scalar and not self._value_is_scalar:
- return self._comparator.has(
- value_expr
- )
- else:
- return self._comparator.any(
- value_expr
+ if self._unwrap_target_assoc_proxy is None and (
+ self.scalar and (
+ not self._target_is_object or self._value_is_scalar)
+ ):
+ raise exc.InvalidRequestError(
+ "'any()' not implemented for scalar "
+ "attributes. Use has()."
)
+ return self._criterion_exists(
+ criterion=criterion, is_has=False, **kwargs)
def has(self, criterion=None, **kwargs):
"""Produce a proxied 'has' expression using EXISTS.
@@ -405,18 +427,15 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
operators of the underlying proxied attributes.
"""
-
- if self._target_is_object:
- return self._comparator.has(
- getattr(self.target_class, self.value_attr).
- has(criterion, **kwargs)
- )
- else:
- if criterion is not None or kwargs:
- raise exc.ArgumentError(
- "Non-empty has() not allowed for "
- "column-targeted association proxy; use ==")
- return self._comparator.has()
+ if self._unwrap_target_assoc_proxy is None and (
+ not self.scalar or (
+ self._target_is_object and not self._value_is_scalar)
+ ):
+ raise exc.InvalidRequestError(
+ "'has()' not implemented for collections. "
+ "Use any().")
+ return self._criterion_exists(
+ criterion=criterion, is_has=True, **kwargs)
def contains(self, obj):
"""Produce a proxied 'contains' expression using EXISTS.
@@ -428,12 +447,23 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
operators of the underlying proxied attributes.
"""
- if self.scalar and not self._value_is_scalar:
+ target_assoc = self._unwrap_target_assoc_proxy
+ if target_assoc is not None:
+ return self._comparator._criterion_exists(
+ target_assoc.contains(obj)
+ )
+ elif self._target_is_object and self.scalar and \
+ not self._value_is_scalar:
return self._comparator.has(
getattr(self.target_class, self.value_attr).contains(obj)
)
+ elif self._target_is_object and self.scalar and \
+ self._value_is_scalar:
+ raise exc.InvalidRequestError(
+ "contains() doesn't apply to a scalar endpoint; use ==")
else:
- return self._comparator.any(**{self.value_attr: obj})
+
+ return self._comparator._criterion_exists(**{self.value_attr: obj})
def __eq__(self, obj):
# note the has() here will fail for collections; eq_()
@@ -452,6 +482,8 @@ 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):