diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2017-06-12 12:56:04 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2017-06-12 16:31:59 -0400 |
| commit | 27a0bdcae0eb534dac9844e2ffa3d6bd3b1e8989 (patch) | |
| tree | 040448c0ac94b7d98b0f8118fe50981a23d2e5e6 /lib/sqlalchemy | |
| parent | ec422fb70e0044ed42dcfda5fb1a7a65db322cf1 (diff) | |
| download | sqlalchemy-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.py | 104 |
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): |
