codekingpro/portable-devtools
115k
1# ext/associationproxy.py
2# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""Contain the ``AssociationProxy`` class.
9
10The ``AssociationProxy`` is a Python property object which provides
11transparent proxied access to the endpoint of an association object.
12
13See the example ``examples/association/proxied_association.py``.
14
15"""
16from __future__ import annotations
17
18import operator
19import typing
20from typing import AbstractSet
21from typing import Any
22from typing import Callable
23from typing import cast
24from typing import Collection
25from typing import Dict
26from typing import Generic
27from typing import ItemsView
28from typing import Iterable
29from typing import Iterator
30from typing import KeysView
31from typing import List
32from typing import Mapping
33from typing import MutableMapping
34from typing import MutableSequence
35from typing import MutableSet
36from typing import NoReturn
37from typing import Optional
38from typing import overload
39from typing import Set
40from typing import Tuple
41from typing import Type
42from typing import TypeVar
43from typing import Union
44from typing import ValuesView
45
46from .. import ColumnElement
47from .. import exc
48from .. import inspect
49from .. import orm
50from .. import util
51from ..orm import collections
52from ..orm import InspectionAttrExtensionType
53from ..orm import interfaces
54from ..orm import ORMDescriptor
55from ..orm.base import SQLORMOperations
56from ..orm.interfaces import _AttributeOptions
57from ..orm.interfaces import _DCAttributeOptions
58from ..orm.interfaces import _DEFAULT_ATTRIBUTE_OPTIONS
59from ..sql import operators
60from ..sql import or_
61from ..sql.base import _NoArg
62from ..util.typing import Literal
63from ..util.typing import Protocol
64from ..util.typing import Self
65from ..util.typing import SupportsIndex
66from ..util.typing import SupportsKeysAndGetItem
67
68if typing.TYPE_CHECKING:
69 from ..orm.interfaces import MapperProperty
70 from ..orm.interfaces import PropComparator
71 from ..orm.mapper import Mapper
72 from ..sql._typing import _ColumnExpressionArgument
73 from ..sql._typing import _InfoType
74
75
76_T = TypeVar("_T", bound=Any)
77_T_co = TypeVar("_T_co", bound=Any, covariant=True)
78_T_con = TypeVar("_T_con", bound=Any, contravariant=True)
79_S = TypeVar("_S", bound=Any)
80_KT = TypeVar("_KT", bound=Any)
81_VT = TypeVar("_VT", bound=Any)
82
83
84def association_proxy(
85 target_collection: str,
86 attr: str,
87 *,
88 creator: Optional[_CreatorProtocol] = None,
89 getset_factory: Optional[_GetSetFactoryProtocol] = None,
90 proxy_factory: Optional[_ProxyFactoryProtocol] = None,
91 proxy_bulk_set: Optional[_ProxyBulkSetProtocol] = None,
92 info: Optional[_InfoType] = None,
93 cascade_scalar_deletes: bool = False,
94 create_on_none_assignment: bool = False,
95 init: Union[_NoArg, bool] = _NoArg.NO_ARG,
96 repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
97 default: Optional[Any] = _NoArg.NO_ARG,
98 default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
99 compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
100 kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
101 hash: Union[_NoArg, bool, None] = _NoArg.NO_ARG, # noqa: A002
102 dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG,
103) -> AssociationProxy[Any]:
104 r"""Return a Python property implementing a view of a target
105 attribute which references an attribute on members of the
106 target.
107
108 The returned value is an instance of :class:`.AssociationProxy`.
109
110 Implements a Python property representing a relationship as a collection
111 of simpler values, or a scalar value. The proxied property will mimic
112 the collection type of the target (list, dict or set), or, in the case of
113 a one to one relationship, a simple scalar value.
114
115 :param target_collection: Name of the attribute that is the immediate
116 target. This attribute is typically mapped by
117 :func:`~sqlalchemy.orm.relationship` to link to a target collection, but
118 can also be a many-to-one or non-scalar relationship.
119
120 :param attr: Attribute on the associated instance or instances that
121 are available on instances of the target object.
122
123 :param creator: optional.
124
125 Defines custom behavior when new items are added to the proxied
126 collection.
127
128 By default, adding new items to the collection will trigger a
129 construction of an instance of the target object, passing the given
130 item as a positional argument to the target constructor. For cases
131 where this isn't sufficient, :paramref:`.association_proxy.creator`
132 can supply a callable that will construct the object in the
133 appropriate way, given the item that was passed.
134
135 For list- and set- oriented collections, a single argument is
136 passed to the callable. For dictionary oriented collections, two
137 arguments are passed, corresponding to the key and value.
138
139 The :paramref:`.association_proxy.creator` callable is also invoked
140 for scalar (i.e. many-to-one, one-to-one) relationships. If the
141 current value of the target relationship attribute is ``None``, the
142 callable is used to construct a new object. If an object value already
143 exists, the given attribute value is populated onto that object.
144
145 .. seealso::
146
147 :ref:`associationproxy_creator`
148
149 :param cascade_scalar_deletes: when True, indicates that setting
150 the proxied value to ``None``, or deleting it via ``del``, should
151 also remove the source object. Only applies to scalar attributes.
152 Normally, removing the proxied target will not remove the proxy
153 source, as this object may have other state that is still to be
154 kept.
155
156 .. versionadded:: 1.3
157
158 .. seealso::
159
160 :ref:`cascade_scalar_deletes` - complete usage example
161
162 :param create_on_none_assignment: when True, indicates that setting
163 the proxied value to ``None`` should **create** the source object
164 if it does not exist, using the creator. Only applies to scalar
165 attributes. This is mutually exclusive
166 vs. the :paramref:`.association_proxy.cascade_scalar_deletes`.
167
168 .. versionadded:: 2.0.18
169
170 :param init: Specific to :ref:`orm_declarative_native_dataclasses`,
171 specifies if the mapped attribute should be part of the ``__init__()``
172 method as generated by the dataclass process.
173
174 .. versionadded:: 2.0.0b4
175
176 :param repr: Specific to :ref:`orm_declarative_native_dataclasses`,
177 specifies if the attribute established by this :class:`.AssociationProxy`
178 should be part of the ``__repr__()`` method as generated by the dataclass
179 process.
180
181 .. versionadded:: 2.0.0b4
182
183 :param default_factory: Specific to
184 :ref:`orm_declarative_native_dataclasses`, specifies a default-value
185 generation function that will take place as part of the ``__init__()``
186 method as generated by the dataclass process.
187
188 .. versionadded:: 2.0.0b4
189
190 :param compare: Specific to
191 :ref:`orm_declarative_native_dataclasses`, indicates if this field
192 should be included in comparison operations when generating the
193 ``__eq__()`` and ``__ne__()`` methods for the mapped class.
194
195 .. versionadded:: 2.0.0b4
196
197 :param kw_only: Specific to :ref:`orm_declarative_native_dataclasses`,
198 indicates if this field should be marked as keyword-only when generating
199 the ``__init__()`` method as generated by the dataclass process.
200
201 .. versionadded:: 2.0.0b4
202
203 :param hash: Specific to
204 :ref:`orm_declarative_native_dataclasses`, controls if this field
205 is included when generating the ``__hash__()`` method for the mapped
206 class.
207
208 .. versionadded:: 2.0.36
209
210 :param dataclass_metadata: Specific to
211 :ref:`orm_declarative_native_dataclasses`, supplies metadata
212 to be attached to the generated dataclass field.
213
214 .. versionadded:: 2.0.42
215
216 :param info: optional, will be assigned to
217 :attr:`.AssociationProxy.info` if present.
218
219
220 The following additional parameters involve injection of custom behaviors
221 within the :class:`.AssociationProxy` object and are for advanced use
222 only:
223
224 :param getset_factory: Optional. Proxied attribute access is
225 automatically handled by routines that get and set values based on
226 the `attr` argument for this proxy.
227
228 If you would like to customize this behavior, you may supply a
229 `getset_factory` callable that produces a tuple of `getter` and
230 `setter` functions. The factory is called with two arguments, the
231 abstract type of the underlying collection and this proxy instance.
232
233 :param proxy_factory: Optional. The type of collection to emulate is
234 determined by sniffing the target collection. If your collection
235 type can't be determined by duck typing or you'd like to use a
236 different collection implementation, you may supply a factory
237 function to produce those collections. Only applicable to
238 non-scalar relationships.
239
240 :param proxy_bulk_set: Optional, use with proxy_factory.
241
242
243 """
244 return AssociationProxy(
245 target_collection,
246 attr,
247 creator=creator,
248 getset_factory=getset_factory,
249 proxy_factory=proxy_factory,
250 proxy_bulk_set=proxy_bulk_set,
251 info=info,
252 cascade_scalar_deletes=cascade_scalar_deletes,
253 create_on_none_assignment=create_on_none_assignment,
254 attribute_options=_AttributeOptions(
255 init,
256 repr,
257 default,
258 default_factory,
259 compare,
260 kw_only,
261 hash,
262 dataclass_metadata,
263 ),
264 )
265
266
267class AssociationProxyExtensionType(InspectionAttrExtensionType):
268 ASSOCIATION_PROXY = "ASSOCIATION_PROXY"
269 """Symbol indicating an :class:`.InspectionAttr` that's
270 of type :class:`.AssociationProxy`.
271
272 Is assigned to the :attr:`.InspectionAttr.extension_type`
273 attribute.
274
275 """
276
277
278class _GetterProtocol(Protocol[_T_co]):
279 def __call__(self, instance: Any) -> _T_co: ...
280
281
282# mypy 0.990 we are no longer allowed to make this Protocol[_T_con]
283class _SetterProtocol(Protocol): ...
284
285
286class _PlainSetterProtocol(_SetterProtocol, Protocol[_T_con]):
287 def __call__(self, instance: Any, value: _T_con) -> None: ...
288
289
290class _DictSetterProtocol(_SetterProtocol, Protocol[_T_con]):
291 def __call__(self, instance: Any, key: Any, value: _T_con) -> None: ...
292
293
294# mypy 0.990 we are no longer allowed to make this Protocol[_T_con]
295class _CreatorProtocol(Protocol): ...
296
297
298class _PlainCreatorProtocol(_CreatorProtocol, Protocol[_T_con]):
299 def __call__(self, value: _T_con) -> Any: ...
300
301
302class _KeyCreatorProtocol(_CreatorProtocol, Protocol[_T_con]):
303 def __call__(self, key: Any, value: Optional[_T_con]) -> Any: ...
304
305
306class _LazyCollectionProtocol(Protocol[_T]):
307 def __call__(
308 self,
309 ) -> Union[
310 MutableSet[_T], MutableMapping[Any, _T], MutableSequence[_T]
311 ]: ...
312
313
314class _GetSetFactoryProtocol(Protocol):
315 def __call__(
316 self,
317 collection_class: Optional[Type[Any]],
318 assoc_instance: AssociationProxyInstance[Any],
319 ) -> Tuple[_GetterProtocol[Any], _SetterProtocol]: ...
320
321
322class _ProxyFactoryProtocol(Protocol):
323 def __call__(
324 self,
325 lazy_collection: _LazyCollectionProtocol[Any],
326 creator: _CreatorProtocol,
327 value_attr: str,
328 parent: AssociationProxyInstance[Any],
329 ) -> Any: ...
330
331
332class _ProxyBulkSetProtocol(Protocol):
333 def __call__(
334 self, proxy: _AssociationCollection[Any], collection: Iterable[Any]
335 ) -> None: ...
336
337
338class _AssociationProxyProtocol(Protocol[_T]):
339 """describes the interface of :class:`.AssociationProxy`
340 without including descriptor methods in the interface."""
341
342 creator: Optional[_CreatorProtocol]
343 key: str
344 target_collection: str
345 value_attr: str
346 cascade_scalar_deletes: bool
347 create_on_none_assignment: bool
348 getset_factory: Optional[_GetSetFactoryProtocol]
349 proxy_factory: Optional[_ProxyFactoryProtocol]
350 proxy_bulk_set: Optional[_ProxyBulkSetProtocol]
351
352 @util.ro_memoized_property
353 def info(self) -> _InfoType: ...
354
355 def for_class(
356 self, class_: Type[Any], obj: Optional[object] = None
357 ) -> AssociationProxyInstance[_T]: ...
358
359 def _default_getset(
360 self, collection_class: Any
361 ) -> Tuple[_GetterProtocol[Any], _SetterProtocol]: ...
362
363
364class AssociationProxy(
365 interfaces.InspectionAttrInfo,
366 ORMDescriptor[_T],
367 _DCAttributeOptions,
368 _AssociationProxyProtocol[_T],
369):
370 """A descriptor that presents a read/write view of an object attribute."""
371
372 is_attribute = True
373 extension_type = AssociationProxyExtensionType.ASSOCIATION_PROXY
374
375 def __init__(
376 self,
377 target_collection: str,
378 attr: str,
379 *,
380 creator: Optional[_CreatorProtocol] = None,
381 getset_factory: Optional[_GetSetFactoryProtocol] = None,
382 proxy_factory: Optional[_ProxyFactoryProtocol] = None,
383 proxy_bulk_set: Optional[_ProxyBulkSetProtocol] = None,
384 info: Optional[_InfoType] = None,
385 cascade_scalar_deletes: bool = False,
386 create_on_none_assignment: bool = False,
387 attribute_options: Optional[_AttributeOptions] = None,
388 ):
389 """Construct a new :class:`.AssociationProxy`.
390
391 The :class:`.AssociationProxy` object is typically constructed using
392 the :func:`.association_proxy` constructor function. See the
393 description of :func:`.association_proxy` for a description of all
394 parameters.
395
396
397 """
398 self.target_collection = target_collection
399 self.value_attr = attr
400 self.creator = creator
401 self.getset_factory = getset_factory
402 self.proxy_factory = proxy_factory
403 self.proxy_bulk_set = proxy_bulk_set
404
405 if cascade_scalar_deletes and create_on_none_assignment:
406 raise exc.ArgumentError(
407 "The cascade_scalar_deletes and create_on_none_assignment "
408 "parameters are mutually exclusive."
409 )
410 self.cascade_scalar_deletes = cascade_scalar_deletes
411 self.create_on_none_assignment = create_on_none_assignment
412
413 self.key = "_%s_%s_%s" % (
414 type(self).__name__,
415 target_collection,
416 id(self),
417 )
418 if info:
419 self.info = info # type: ignore
420
421 if (
422 attribute_options
423 and attribute_options != _DEFAULT_ATTRIBUTE_OPTIONS
424 ):
425 self._has_dataclass_arguments = True
426 self._attribute_options = attribute_options
427 else:
428 self._has_dataclass_arguments = False
429 self._attribute_options = _DEFAULT_ATTRIBUTE_OPTIONS
430
431 @overload
432 def __get__(
433 self, instance: Literal[None], owner: Literal[None]
434 ) -> Self: ...
435
436 @overload
437 def __get__(
438 self, instance: Literal[None], owner: Any
439 ) -> AssociationProxyInstance[_T]: ...
440
441 @overload
442 def __get__(self, instance: object, owner: Any) -> _T: ...
443
444 def __get__(
445 self, instance: object, owner: Any
446 ) -> Union[AssociationProxyInstance[_T], _T, AssociationProxy[_T]]:
447 if owner is None:
448 return self
449 inst = self._as_instance(owner, instance)
450 if inst:
451 return inst.get(instance)
452
453 assert instance is None
454
455 return self
456
457 def __set__(self, instance: object, values: _T) -> None:
458 class_ = type(instance)
459 self._as_instance(class_, instance).set(instance, values)
460
461 def __delete__(self, instance: object) -> None:
462 class_ = type(instance)
463 self._as_instance(class_, instance).delete(instance)
464
465 def for_class(
466 self, class_: Type[Any], obj: Optional[object] = None
467 ) -> AssociationProxyInstance[_T]:
468 r"""Return the internal state local to a specific mapped class.
469
470 E.g., given a class ``User``::
471
472 class User(Base):
473 # ...
474
475 keywords = association_proxy("kws", "keyword")
476
477 If we access this :class:`.AssociationProxy` from
478 :attr:`_orm.Mapper.all_orm_descriptors`, and we want to view the
479 target class for this proxy as mapped by ``User``::
480
481 inspect(User).all_orm_descriptors["keywords"].for_class(User).target_class
482
483 This returns an instance of :class:`.AssociationProxyInstance` that
484 is specific to the ``User`` class. The :class:`.AssociationProxy`
485 object remains agnostic of its parent class.
486
487 :param class\_: the class that we are returning state for.
488
489 :param obj: optional, an instance of the class that is required
490 if the attribute refers to a polymorphic target, e.g. where we have
491 to look at the type of the actual destination object to get the
492 complete path.
493
494 .. versionadded:: 1.3 - :class:`.AssociationProxy` no longer stores
495 any state specific to a particular parent class; the state is now
496 stored in per-class :class:`.AssociationProxyInstance` objects.
497
498
499 """
500 return self._as_instance(class_, obj)
501
502 def _as_instance(
503 self, class_: Any, obj: Any
504 ) -> AssociationProxyInstance[_T]:
505 try:
506 inst = class_.__dict__[self.key + "_inst"]
507 except KeyError:
508 inst = None
509
510 # avoid exception context
511 if inst is None:
512 owner = self._calc_owner(class_)
513 if owner is not None:
514 inst = AssociationProxyInstance.for_proxy(self, owner, obj)
515 setattr(class_, self.key + "_inst", inst)
516 else:
517 inst = None
518
519 if inst is not None and not inst._is_canonical:
520 # the AssociationProxyInstance can't be generalized
521 # since the proxied attribute is not on the targeted
522 # class, only on subclasses of it, which might be
523 # different. only return for the specific
524 # object's current value
525 return inst._non_canonical_get_for_object(obj) # type: ignore
526 else:
527 return inst # type: ignore # TODO
528
529 def _calc_owner(self, target_cls: Any) -> Any:
530 # we might be getting invoked for a subclass
531 # that is not mapped yet, in some declarative situations.
532 # save until we are mapped
533 try:
534 insp = inspect(target_cls)
535 except exc.NoInspectionAvailable:
536 # can't find a mapper, don't set owner. if we are a not-yet-mapped
537 # subclass, we can also scan through __mro__ to find a mapped
538 # class, but instead just wait for us to be called again against a
539 # mapped class normally.
540 return None
541 else:
542 return insp.mapper.class_manager.class_
543
544 def _default_getset(
545 self, collection_class: Any
546 ) -> Tuple[_GetterProtocol[Any], _SetterProtocol]:
547 attr = self.value_attr
548 _getter = operator.attrgetter(attr)
549
550 def getter(instance: Any) -> Optional[Any]:
551 return _getter(instance) if instance is not None else None
552
553 if collection_class is dict:
554
555 def dict_setter(instance: Any, k: Any, value: Any) -> None:
556 setattr(instance, attr, value)
557
558 return getter, dict_setter
559
560 else:
561
562 def plain_setter(o: Any, v: Any) -> None:
563 setattr(o, attr, v)
564
565 return getter, plain_setter
566
567 def __repr__(self) -> str:
568 return "AssociationProxy(%r, %r)" % (
569 self.target_collection,
570 self.value_attr,
571 )
572
573
574# the pep-673 Self type does not work in Mypy for a "hybrid"
575# style method that returns type or Self, so for one specific case
576# we still need to use the pre-pep-673 workaround.
577_Self = TypeVar("_Self", bound="AssociationProxyInstance[Any]")
578
579
580class AssociationProxyInstance(SQLORMOperations[_T]):
581 """A per-class object that serves class- and object-specific results.
582
583 This is used by :class:`.AssociationProxy` when it is invoked
584 in terms of a specific class or instance of a class, i.e. when it is
585 used as a regular Python descriptor.
586
587 When referring to the :class:`.AssociationProxy` as a normal Python
588 descriptor, the :class:`.AssociationProxyInstance` is the object that
589 actually serves the information. Under normal circumstances, its presence
590 is transparent::
591
592 >>> User.keywords.scalar
593 False
594
595 In the special case that the :class:`.AssociationProxy` object is being
596 accessed directly, in order to get an explicit handle to the
597 :class:`.AssociationProxyInstance`, use the
598 :meth:`.AssociationProxy.for_class` method::
599
600 proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
601
602 # view if proxy object is scalar or not
603 >>> proxy_state.scalar
604 False
605
606 .. versionadded:: 1.3
607
608 """ # noqa
609
610 collection_class: Optional[Type[Any]]
611 parent: _AssociationProxyProtocol[_T]
612
613 def __init__(
614 self,
615 parent: _AssociationProxyProtocol[_T],
616 owning_class: Type[Any],
617 target_class: Type[Any],
618 value_attr: str,
619 ):
620 self.parent = parent
621 self.key = parent.key
622 self.owning_class = owning_class
623 self.target_collection = parent.target_collection
624 self.collection_class = None
625 self.target_class = target_class
626 self.value_attr = value_attr
627
628 target_class: Type[Any]
629 """The intermediary class handled by this
630 :class:`.AssociationProxyInstance`.
631
632 Intercepted append/set/assignment events will result
633 in the generation of new instances of this class.
634
635 """
636
637 @classmethod
638 def for_proxy(
639 cls,
640 parent: AssociationProxy[_T],
641 owning_class: Type[Any],
642 parent_instance: Any,
643 ) -> AssociationProxyInstance[_T]:
644 target_collection = parent.target_collection
645 value_attr = parent.value_attr
646 prop = cast(
647 "orm.RelationshipProperty[_T]",
648 orm.class_mapper(owning_class).get_property(target_collection),
649 )
650
651 # this was never asserted before but this should be made clear.
652 if not isinstance(prop, orm.RelationshipProperty):
653 raise NotImplementedError(
654 "association proxy to a non-relationship "
655 "intermediary is not supported"
656 ) from None
657
658 target_class = prop.mapper.class_
659
660 try:
661 target_assoc = cast(
662 "AssociationProxyInstance[_T]",
663 cls._cls_unwrap_target_assoc_proxy(target_class, value_attr),
664 )
665 except AttributeError:
666 # the proxied attribute doesn't exist on the target class;
667 # return an "ambiguous" instance that will work on a per-object
668 # basis
669 return AmbiguousAssociationProxyInstance(
670 parent, owning_class, target_class, value_attr
671 )
672 except Exception as err:
673 raise exc.InvalidRequestError(
674 f"Association proxy received an unexpected error when "
675 f"trying to retrieve attribute "
676 f'"{target_class.__name__}.{parent.value_attr}" from '
677 f'class "{target_class.__name__}": {err}'
678 ) from err
679 else:
680 return cls._construct_for_assoc(
681 target_assoc, parent, owning_class, target_class, value_attr
682 )
683
684 @classmethod
685 def _construct_for_assoc(
686 cls,
687 target_assoc: Optional[AssociationProxyInstance[_T]],
688 parent: _AssociationProxyProtocol[_T],
689 owning_class: Type[Any],
690 target_class: Type[Any],
691 value_attr: str,
692 ) -> AssociationProxyInstance[_T]:
693 if target_assoc is not None:
694 return ObjectAssociationProxyInstance(
695 parent, owning_class, target_class, value_attr
696 )
697
698 attr = getattr(target_class, value_attr)
699 if not hasattr(attr, "_is_internal_proxy"):
700 return AmbiguousAssociationProxyInstance(
701 parent, owning_class, target_class, value_attr
702 )
703 is_object = attr._impl_uses_objects
704 if is_object:
705 return ObjectAssociationProxyInstance(
706 parent, owning_class, target_class, value_attr
707 )
708 else:
709 return ColumnAssociationProxyInstance(
710 parent, owning_class, target_class, value_attr
711 )
712
713 def _get_property(self) -> MapperProperty[Any]:
714 return orm.class_mapper(self.owning_class).get_property(
715 self.target_collection
716 )
717
718 @property
719 def _comparator(self) -> PropComparator[Any]:
720 return getattr( # type: ignore
721 self.owning_class, self.target_collection
722 ).comparator
723
724 def __clause_element__(self) -> NoReturn:
725 raise NotImplementedError(
726 "The association proxy can't be used as a plain column "
727 "expression; it only works inside of a comparison expression"
728 )
729
730 @classmethod
731 def _cls_unwrap_target_assoc_proxy(
732 cls, target_class: Any, value_attr: str
733 ) -> Optional[AssociationProxyInstance[_T]]:
734 attr = getattr(target_class, value_attr)
735 assert not isinstance(attr, AssociationProxy)
736 if isinstance(attr, AssociationProxyInstance):
737 return attr
738 return None
739
740 @util.memoized_property
741 def _unwrap_target_assoc_proxy(
742 self,
743 ) -> Optional[AssociationProxyInstance[_T]]:
744 return self._cls_unwrap_target_assoc_proxy(
745 self.target_class, self.value_attr
746 )
747
748 @property
749 def remote_attr(self) -> SQLORMOperations[_T]:
750 """The 'remote' class attribute referenced by this
751 :class:`.AssociationProxyInstance`.
752
753 .. seealso::
754
755 :attr:`.AssociationProxyInstance.attr`
756
757 :attr:`.AssociationProxyInstance.local_attr`
758
759 """
760 return cast(
761 "SQLORMOperations[_T]", getattr(self.target_class, self.value_attr)
762 )
763
764 @property
765 def local_attr(self) -> SQLORMOperations[Any]:
766 """The 'local' class attribute referenced by this
767 :class:`.AssociationProxyInstance`.
768
769 .. seealso::
770
771 :attr:`.AssociationProxyInstance.attr`
772
773 :attr:`.AssociationProxyInstance.remote_attr`
774
775 """
776 return cast(
777 "SQLORMOperations[Any]",
778 getattr(self.owning_class, self.target_collection),
779 )
780
781 @property
782 def attr(self) -> Tuple[SQLORMOperations[Any], SQLORMOperations[_T]]:
783 """Return a tuple of ``(local_attr, remote_attr)``.
784
785 This attribute was originally intended to facilitate using the
786 :meth:`_query.Query.join` method to join across the two relationships
787 at once, however this makes use of a deprecated calling style.
788
789 To use :meth:`_sql.select.join` or :meth:`_orm.Query.join` with
790 an association proxy, the current method is to make use of the
791 :attr:`.AssociationProxyInstance.local_attr` and
792 :attr:`.AssociationProxyInstance.remote_attr` attributes separately::
793
794 stmt = (
795 select(Parent)
796 .join(Parent.proxied.local_attr)
797 .join(Parent.proxied.remote_attr)
798 )
799
800 A future release may seek to provide a more succinct join pattern
801 for association proxy attributes.
802
803 .. seealso::
804
805 :attr:`.AssociationProxyInstance.local_attr`
806
807 :attr:`.AssociationProxyInstance.remote_attr`
808
809 """
810 return (self.local_attr, self.remote_attr)
811
812 @util.memoized_property
813 def scalar(self) -> bool:
814 """Return ``True`` if this :class:`.AssociationProxyInstance`
815 proxies a scalar relationship on the local side."""
816
817 scalar = not self._get_property().uselist
818 if scalar:
819 self._initialize_scalar_accessors()
820 return scalar
821
822 @util.memoized_property
823 def _value_is_scalar(self) -> bool:
824 return (
825 not self._get_property()
826 .mapper.get_property(self.value_attr)
827 .uselist
828 )
829
830 @property
831 def _target_is_object(self) -> bool:
832 raise NotImplementedError()
833
834 _scalar_get: _GetterProtocol[_T]
835 _scalar_set: _PlainSetterProtocol[_T]
836
837 def _initialize_scalar_accessors(self) -> None:
838 if self.parent.getset_factory:
839 get, set_ = self.parent.getset_factory(None, self)
840 else:
841 get, set_ = self.parent._default_getset(None)
842 self._scalar_get, self._scalar_set = get, cast(
843 "_PlainSetterProtocol[_T]", set_
844 )
845
846 def _default_getset(
847 self, collection_class: Any
848 ) -> Tuple[_GetterProtocol[Any], _SetterProtocol]:
849 attr = self.value_attr
850 _getter = operator.attrgetter(attr)
851
852 def getter(instance: Any) -> Optional[_T]:
853 return _getter(instance) if instance is not None else None
854
855 if collection_class is dict:
856
857 def dict_setter(instance: Any, k: Any, value: _T) -> None:
858 setattr(instance, attr, value)
859
860 return getter, dict_setter
861 else:
862
863 def plain_setter(o: Any, v: _T) -> None:
864 setattr(o, attr, v)
865
866 return getter, plain_setter
867
868 @util.ro_non_memoized_property
869 def info(self) -> _InfoType:
870 return self.parent.info
871
872 @overload
873 def get(self: _Self, obj: Literal[None]) -> _Self: ...
874
875 @overload
876 def get(self, obj: Any) -> _T: ...
877
878 def get(
879 self, obj: Any
880 ) -> Union[Optional[_T], AssociationProxyInstance[_T]]:
881 if obj is None:
882 return self
883
884 proxy: _T
885
886 if self.scalar:
887 target = getattr(obj, self.target_collection)
888 return self._scalar_get(target)
889 else:
890 try:
891 # If the owning instance is reborn (orm session resurrect,
892 # etc.), refresh the proxy cache.
893 creator_id, self_id, proxy = cast(
894 "Tuple[int, int, _T]", getattr(obj, self.key)
895 )
896 except AttributeError:
897 pass
898 else:
899 if id(obj) == creator_id and id(self) == self_id:
900 assert self.collection_class is not None
901 return proxy
902
903 self.collection_class, proxy = self._new(
904 _lazy_collection(obj, self.target_collection)
905 )
906 setattr(obj, self.key, (id(obj), id(self), proxy))
907 return proxy
908
909 def set(self, obj: Any, values: _T) -> None:
910 if self.scalar:
911 creator = cast(
912 "_PlainCreatorProtocol[_T]",
913 (
914 self.parent.creator
915 if self.parent.creator
916 else self.target_class
917 ),
918 )
919 target = getattr(obj, self.target_collection)
920 if target is None:
921 if (
922 values is None
923 and not self.parent.create_on_none_assignment
924 ):
925 return
926 setattr(obj, self.target_collection, creator(values))
927 else:
928 self._scalar_set(target, values)
929 if values is None and self.parent.cascade_scalar_deletes:
930 setattr(obj, self.target_collection, None)
931 else:
932 proxy = self.get(obj)
933 assert self.collection_class is not None
934 if proxy is not values:
935 proxy._bulk_replace(self, values)
936
937 def delete(self, obj: Any) -> None:
938 if self.owning_class is None:
939 self._calc_owner(obj, None)
940
941 if self.scalar:
942 target = getattr(obj, self.target_collection)
943 if target is not None:
944 delattr(target, self.value_attr)
945 delattr(obj, self.target_collection)
946
947 def _new(
948 self, lazy_collection: _LazyCollectionProtocol[_T]
949 ) -> Tuple[Type[Any], _T]:
950 creator = (
951 self.parent.creator
952 if self.parent.creator is not None
953 else cast("_CreatorProtocol", self.target_class)
954 )
955 collection_class = util.duck_type_collection(lazy_collection())
956
957 if collection_class is None:
958 raise exc.InvalidRequestError(
959 f"lazy collection factory did not return a "
960 f"valid collection type, got {collection_class}"
961 )
962 if self.parent.proxy_factory:
963 return (
964 collection_class,
965 self.parent.proxy_factory(
966 lazy_collection, creator, self.value_attr, self
967 ),
968 )
969
970 if self.parent.getset_factory:
971 getter, setter = self.parent.getset_factory(collection_class, self)
972 else:
973 getter, setter = self.parent._default_getset(collection_class)
974
975 if collection_class is list:
976 return (
977 collection_class,
978 cast(
979 _T,
980 _AssociationList(
981 lazy_collection, creator, getter, setter, self
982 ),
983 ),
984 )
985 elif collection_class is dict:
986 return (
987 collection_class,
988 cast(
989 _T,
990 _AssociationDict(
991 lazy_collection, creator, getter, setter, self
992 ),
993 ),
994 )
995 elif collection_class is set:
996 return (
997 collection_class,
998 cast(
999 _T,
1000 _AssociationSet(
1001 lazy_collection, creator, getter, setter, self
1002 ),
1003 ),
1004 )
1005 else:
1006 raise exc.ArgumentError(
1007 "could not guess which interface to use for "
1008 'collection_class "%s" backing "%s"; specify a '
1009 "proxy_factory and proxy_bulk_set manually"
1010 % (self.collection_class, self.target_collection)
1011 )
1012
1013 def _set(
1014 self, proxy: _AssociationCollection[Any], values: Iterable[Any]
1015 ) -> None:
1016 if self.parent.proxy_bulk_set:
1017 self.parent.proxy_bulk_set(proxy, values)
1018 elif self.collection_class is list:
1019 cast("_AssociationList[Any]", proxy).extend(values)
1020 elif self.collection_class is dict:
1021 cast("_AssociationDict[Any, Any]", proxy).update(values)
1022 elif self.collection_class is set:
1023 cast("_AssociationSet[Any]", proxy).update(values)
1024 else:
1025 raise exc.ArgumentError(
1026 "no proxy_bulk_set supplied for custom "
1027 "collection_class implementation"
1028 )
1029
1030 def _inflate(self, proxy: _AssociationCollection[Any]) -> None:
1031 creator = (
1032 self.parent.creator
1033 and self.parent.creator
1034 or cast(_CreatorProtocol, self.target_class)
1035 )
1036
1037 if self.parent.getset_factory:
1038 getter, setter = self.parent.getset_factory(
1039 self.collection_class, self
1040 )
1041 else:
1042 getter, setter = self.parent._default_getset(self.collection_class)
1043
1044 proxy.creator = creator
1045 proxy.getter = getter
1046 proxy.setter = setter
1047
1048 def _criterion_exists(
1049 self,
1050 criterion: Optional[_ColumnExpressionArgument[bool]] = None,
1051 **kwargs: Any,
1052 ) -> ColumnElement[bool]:
1053 is_has = kwargs.pop("is_has", None)
1054
1055 target_assoc = self._unwrap_target_assoc_proxy
1056 if target_assoc is not None:
1057 inner = target_assoc._criterion_exists(
1058 criterion=criterion, **kwargs
1059 )
1060 return self._comparator._criterion_exists(inner)
1061
1062 if self._target_is_object:
1063 attr = getattr(self.target_class, self.value_attr)
1064 value_expr = attr.comparator._criterion_exists(criterion, **kwargs)
1065 else:
1066 if kwargs:
1067 raise exc.ArgumentError(
1068 "Can't apply keyword arguments to column-targeted "
1069 "association proxy; use =="
1070 )
1071 elif is_has and criterion is not None:
1072 raise exc.ArgumentError(
1073 "Non-empty has() not allowed for "
1074 "column-targeted association proxy; use =="
1075 )
1076
1077 value_expr = criterion
1078
1079 return self._comparator._criterion_exists(value_expr)
1080
1081 def any(
1082 self,
1083 criterion: Optional[_ColumnExpressionArgument[bool]] = None,
1084 **kwargs: Any,
1085 ) -> ColumnElement[bool]:
1086 """Produce a proxied 'any' expression using EXISTS.
1087
1088 This expression will be a composed product
1089 using the :meth:`.Relationship.Comparator.any`
1090 and/or :meth:`.Relationship.Comparator.has`
1091 operators of the underlying proxied attributes.
1092
1093 """
1094 if self._unwrap_target_assoc_proxy is None and (
1095 self.scalar
1096 and (not self._target_is_object or self._value_is_scalar)
1097 ):
1098 raise exc.InvalidRequestError(
1099 "'any()' not implemented for scalar attributes. Use has()."
1100 )
1101 return self._criterion_exists(
1102 criterion=criterion, is_has=False, **kwargs
1103 )
1104
1105 def has(
1106 self,
1107 criterion: Optional[_ColumnExpressionArgument[bool]] = None,
1108 **kwargs: Any,
1109 ) -> ColumnElement[bool]:
1110 """Produce a proxied 'has' expression using EXISTS.
1111
1112 This expression will be a composed product
1113 using the :meth:`.Relationship.Comparator.any`
1114 and/or :meth:`.Relationship.Comparator.has`
1115 operators of the underlying proxied attributes.
1116
1117 """
1118 if self._unwrap_target_assoc_proxy is None and (
1119 not self.scalar
1120 or (self._target_is_object and not self._value_is_scalar)
1121 ):
1122 raise exc.InvalidRequestError(
1123 "'has()' not implemented for collections. Use any()."
1124 )
1125 return self._criterion_exists(
1126 criterion=criterion, is_has=True, **kwargs
1127 )
1128
1129 def __repr__(self) -> str:
1130 return "%s(%r)" % (self.__class__.__name__, self.parent)
1131
1132
1133class AmbiguousAssociationProxyInstance(AssociationProxyInstance[_T]):
1134 """an :class:`.AssociationProxyInstance` where we cannot determine
1135 the type of target object.
1136 """
1137
1138 _is_canonical = False
1139
1140 def _ambiguous(self) -> NoReturn:
1141 raise AttributeError(
1142 "Association proxy %s.%s refers to an attribute '%s' that is not "
1143 "directly mapped on class %s; therefore this operation cannot "
1144 "proceed since we don't know what type of object is referred "
1145 "towards"
1146 % (
1147 self.owning_class.__name__,
1148 self.target_collection,
1149 self.value_attr,
1150 self.target_class,
1151 )
1152 )
1153
1154 def get(self, obj: Any) -> Any:
1155 if obj is None:
1156 return self
1157 else:
1158 return super().get(obj)
1159
1160 def __eq__(self, obj: object) -> NoReturn:
1161 self._ambiguous()
1162
1163 def __ne__(self, obj: object) -> NoReturn:
1164 self._ambiguous()
1165
1166 def any(
1167 self,
1168 criterion: Optional[_ColumnExpressionArgument[bool]] = None,
1169 **kwargs: Any,
1170 ) -> NoReturn:
1171 self._ambiguous()
1172
1173 def has(
1174 self,
1175 criterion: Optional[_ColumnExpressionArgument[bool]] = None,
1176 **kwargs: Any,
1177 ) -> NoReturn:
1178 self._ambiguous()
1179
1180 @util.memoized_property
1181 def _lookup_cache(self) -> Dict[Type[Any], AssociationProxyInstance[_T]]:
1182 # mapping of <subclass>->AssociationProxyInstance.
1183 # e.g. proxy is A-> A.b -> B -> B.b_attr, but B.b_attr doesn't exist;
1184 # only B1(B) and B2(B) have "b_attr", keys in here would be B1, B2
1185 return {}
1186
1187 def _non_canonical_get_for_object(
1188 self, parent_instance: Any
1189 ) -> AssociationProxyInstance[_T]:
1190 if parent_instance is not None:
1191 actual_obj = getattr(parent_instance, self.target_collection)
1192 if actual_obj is not None:
1193 try:
1194 insp = inspect(actual_obj)
1195 except exc.NoInspectionAvailable:
1196 pass
1197 else:
1198 mapper = insp.mapper
1199 instance_class = mapper.class_
1200 if instance_class not in self._lookup_cache:
