mapped_collection.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. # orm/mapped_collection.py
  2. # Copyright (C) 2005-2025 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. from __future__ import annotations
  8. import operator
  9. from typing import Any
  10. from typing import Callable
  11. from typing import Dict
  12. from typing import Generic
  13. from typing import List
  14. from typing import Optional
  15. from typing import Sequence
  16. from typing import Tuple
  17. from typing import Type
  18. from typing import TYPE_CHECKING
  19. from typing import TypeVar
  20. from typing import Union
  21. from . import base
  22. from .collections import collection
  23. from .collections import collection_adapter
  24. from .. import exc as sa_exc
  25. from .. import util
  26. from ..sql import coercions
  27. from ..sql import expression
  28. from ..sql import roles
  29. from ..util.langhelpers import Missing
  30. from ..util.langhelpers import MissingOr
  31. from ..util.typing import Literal
  32. if TYPE_CHECKING:
  33. from . import AttributeEventToken
  34. from . import Mapper
  35. from .collections import CollectionAdapter
  36. from ..sql.elements import ColumnElement
  37. _KT = TypeVar("_KT", bound=Any)
  38. _VT = TypeVar("_VT", bound=Any)
  39. class _PlainColumnGetter(Generic[_KT]):
  40. """Plain column getter, stores collection of Column objects
  41. directly.
  42. Serializes to a :class:`._SerializableColumnGetterV2`
  43. which has more expensive __call__() performance
  44. and some rare caveats.
  45. """
  46. __slots__ = ("cols", "composite")
  47. def __init__(self, cols: Sequence[ColumnElement[_KT]]) -> None:
  48. self.cols = cols
  49. self.composite = len(cols) > 1
  50. def __reduce__(
  51. self,
  52. ) -> Tuple[
  53. Type[_SerializableColumnGetterV2[_KT]],
  54. Tuple[Sequence[Tuple[Optional[str], Optional[str]]]],
  55. ]:
  56. return _SerializableColumnGetterV2._reduce_from_cols(self.cols)
  57. def _cols(self, mapper: Mapper[_KT]) -> Sequence[ColumnElement[_KT]]:
  58. return self.cols
  59. def __call__(self, value: _KT) -> MissingOr[Union[_KT, Tuple[_KT, ...]]]:
  60. state = base.instance_state(value)
  61. m = base._state_mapper(state)
  62. key: List[_KT] = [
  63. m._get_state_attr_by_column(state, state.dict, col)
  64. for col in self._cols(m)
  65. ]
  66. if self.composite:
  67. return tuple(key)
  68. else:
  69. obj = key[0]
  70. if obj is None:
  71. return Missing
  72. else:
  73. return obj
  74. class _SerializableColumnGetterV2(_PlainColumnGetter[_KT]):
  75. """Updated serializable getter which deals with
  76. multi-table mapped classes.
  77. Two extremely unusual cases are not supported.
  78. Mappings which have tables across multiple metadata
  79. objects, or which are mapped to non-Table selectables
  80. linked across inheriting mappers may fail to function
  81. here.
  82. """
  83. __slots__ = ("colkeys",)
  84. def __init__(
  85. self, colkeys: Sequence[Tuple[Optional[str], Optional[str]]]
  86. ) -> None:
  87. self.colkeys = colkeys
  88. self.composite = len(colkeys) > 1
  89. def __reduce__(
  90. self,
  91. ) -> Tuple[
  92. Type[_SerializableColumnGetterV2[_KT]],
  93. Tuple[Sequence[Tuple[Optional[str], Optional[str]]]],
  94. ]:
  95. return self.__class__, (self.colkeys,)
  96. @classmethod
  97. def _reduce_from_cols(cls, cols: Sequence[ColumnElement[_KT]]) -> Tuple[
  98. Type[_SerializableColumnGetterV2[_KT]],
  99. Tuple[Sequence[Tuple[Optional[str], Optional[str]]]],
  100. ]:
  101. def _table_key(c: ColumnElement[_KT]) -> Optional[str]:
  102. if not isinstance(c.table, expression.TableClause):
  103. return None
  104. else:
  105. return c.table.key # type: ignore
  106. colkeys = [(c.key, _table_key(c)) for c in cols]
  107. return _SerializableColumnGetterV2, (colkeys,)
  108. def _cols(self, mapper: Mapper[_KT]) -> Sequence[ColumnElement[_KT]]:
  109. cols: List[ColumnElement[_KT]] = []
  110. metadata = getattr(mapper.local_table, "metadata", None)
  111. for ckey, tkey in self.colkeys:
  112. if tkey is None or metadata is None or tkey not in metadata:
  113. cols.append(mapper.local_table.c[ckey]) # type: ignore
  114. else:
  115. cols.append(metadata.tables[tkey].c[ckey])
  116. return cols
  117. def column_keyed_dict(
  118. mapping_spec: Union[Type[_KT], Callable[[_KT], _VT]],
  119. *,
  120. ignore_unpopulated_attribute: bool = False,
  121. ) -> Type[KeyFuncDict[_KT, _KT]]:
  122. """A dictionary-based collection type with column-based keying.
  123. .. versionchanged:: 2.0 Renamed :data:`.column_mapped_collection` to
  124. :class:`.column_keyed_dict`.
  125. Returns a :class:`.KeyFuncDict` factory which will produce new
  126. dictionary keys based on the value of a particular :class:`.Column`-mapped
  127. attribute on ORM mapped instances to be added to the dictionary.
  128. .. note:: the value of the target attribute must be assigned with its
  129. value at the time that the object is being added to the
  130. dictionary collection. Additionally, changes to the key attribute
  131. are **not tracked**, which means the key in the dictionary is not
  132. automatically synchronized with the key value on the target object
  133. itself. See :ref:`key_collections_mutations` for further details.
  134. .. seealso::
  135. :ref:`orm_dictionary_collection` - background on use
  136. :param mapping_spec: a :class:`_schema.Column` object that is expected
  137. to be mapped by the target mapper to a particular attribute on the
  138. mapped class, the value of which on a particular instance is to be used
  139. as the key for a new dictionary entry for that instance.
  140. :param ignore_unpopulated_attribute: if True, and the mapped attribute
  141. indicated by the given :class:`_schema.Column` target attribute
  142. on an object is not populated at all, the operation will be silently
  143. skipped. By default, an error is raised.
  144. .. versionadded:: 2.0 an error is raised by default if the attribute
  145. being used for the dictionary key is determined that it was never
  146. populated with any value. The
  147. :paramref:`_orm.column_keyed_dict.ignore_unpopulated_attribute`
  148. parameter may be set which will instead indicate that this condition
  149. should be ignored, and the append operation silently skipped.
  150. This is in contrast to the behavior of the 1.x series which would
  151. erroneously populate the value in the dictionary with an arbitrary key
  152. value of ``None``.
  153. """
  154. cols = [
  155. coercions.expect(roles.ColumnArgumentRole, q, argname="mapping_spec")
  156. for q in util.to_list(mapping_spec)
  157. ]
  158. keyfunc = _PlainColumnGetter(cols)
  159. return _mapped_collection_cls(
  160. keyfunc,
  161. ignore_unpopulated_attribute=ignore_unpopulated_attribute,
  162. )
  163. class _AttrGetter:
  164. __slots__ = ("attr_name", "getter")
  165. def __init__(self, attr_name: str):
  166. self.attr_name = attr_name
  167. self.getter = operator.attrgetter(attr_name)
  168. def __call__(self, mapped_object: Any) -> Any:
  169. obj = self.getter(mapped_object)
  170. if obj is None:
  171. state = base.instance_state(mapped_object)
  172. mp = state.mapper
  173. if self.attr_name in mp.attrs:
  174. dict_ = state.dict
  175. obj = dict_.get(self.attr_name, base.NO_VALUE)
  176. if obj is None:
  177. return Missing
  178. else:
  179. return Missing
  180. return obj
  181. def __reduce__(self) -> Tuple[Type[_AttrGetter], Tuple[str]]:
  182. return _AttrGetter, (self.attr_name,)
  183. def attribute_keyed_dict(
  184. attr_name: str, *, ignore_unpopulated_attribute: bool = False
  185. ) -> Type[KeyFuncDict[Any, Any]]:
  186. """A dictionary-based collection type with attribute-based keying.
  187. .. versionchanged:: 2.0 Renamed :data:`.attribute_mapped_collection` to
  188. :func:`.attribute_keyed_dict`.
  189. Returns a :class:`.KeyFuncDict` factory which will produce new
  190. dictionary keys based on the value of a particular named attribute on
  191. ORM mapped instances to be added to the dictionary.
  192. .. note:: the value of the target attribute must be assigned with its
  193. value at the time that the object is being added to the
  194. dictionary collection. Additionally, changes to the key attribute
  195. are **not tracked**, which means the key in the dictionary is not
  196. automatically synchronized with the key value on the target object
  197. itself. See :ref:`key_collections_mutations` for further details.
  198. .. seealso::
  199. :ref:`orm_dictionary_collection` - background on use
  200. :param attr_name: string name of an ORM-mapped attribute
  201. on the mapped class, the value of which on a particular instance
  202. is to be used as the key for a new dictionary entry for that instance.
  203. :param ignore_unpopulated_attribute: if True, and the target attribute
  204. on an object is not populated at all, the operation will be silently
  205. skipped. By default, an error is raised.
  206. .. versionadded:: 2.0 an error is raised by default if the attribute
  207. being used for the dictionary key is determined that it was never
  208. populated with any value. The
  209. :paramref:`_orm.attribute_keyed_dict.ignore_unpopulated_attribute`
  210. parameter may be set which will instead indicate that this condition
  211. should be ignored, and the append operation silently skipped.
  212. This is in contrast to the behavior of the 1.x series which would
  213. erroneously populate the value in the dictionary with an arbitrary key
  214. value of ``None``.
  215. """
  216. return _mapped_collection_cls(
  217. _AttrGetter(attr_name),
  218. ignore_unpopulated_attribute=ignore_unpopulated_attribute,
  219. )
  220. def keyfunc_mapping(
  221. keyfunc: Callable[[Any], Any],
  222. *,
  223. ignore_unpopulated_attribute: bool = False,
  224. ) -> Type[KeyFuncDict[_KT, Any]]:
  225. """A dictionary-based collection type with arbitrary keying.
  226. .. versionchanged:: 2.0 Renamed :data:`.mapped_collection` to
  227. :func:`.keyfunc_mapping`.
  228. Returns a :class:`.KeyFuncDict` factory with a keying function
  229. generated from keyfunc, a callable that takes an entity and returns a
  230. key value.
  231. .. note:: the given keyfunc is called only once at the time that the
  232. target object is being added to the collection. Changes to the
  233. effective value returned by the function are not tracked.
  234. .. seealso::
  235. :ref:`orm_dictionary_collection` - background on use
  236. :param keyfunc: a callable that will be passed the ORM-mapped instance
  237. which should then generate a new key to use in the dictionary.
  238. If the value returned is :attr:`.LoaderCallableStatus.NO_VALUE`, an error
  239. is raised.
  240. :param ignore_unpopulated_attribute: if True, and the callable returns
  241. :attr:`.LoaderCallableStatus.NO_VALUE` for a particular instance, the
  242. operation will be silently skipped. By default, an error is raised.
  243. .. versionadded:: 2.0 an error is raised by default if the callable
  244. being used for the dictionary key returns
  245. :attr:`.LoaderCallableStatus.NO_VALUE`, which in an ORM attribute
  246. context indicates an attribute that was never populated with any value.
  247. The :paramref:`_orm.mapped_collection.ignore_unpopulated_attribute`
  248. parameter may be set which will instead indicate that this condition
  249. should be ignored, and the append operation silently skipped. This is
  250. in contrast to the behavior of the 1.x series which would erroneously
  251. populate the value in the dictionary with an arbitrary key value of
  252. ``None``.
  253. """
  254. return _mapped_collection_cls(
  255. keyfunc, ignore_unpopulated_attribute=ignore_unpopulated_attribute
  256. )
  257. class KeyFuncDict(Dict[_KT, _VT]):
  258. """Base for ORM mapped dictionary classes.
  259. Extends the ``dict`` type with additional methods needed by SQLAlchemy ORM
  260. collection classes. Use of :class:`_orm.KeyFuncDict` is most directly
  261. by using the :func:`.attribute_keyed_dict` or
  262. :func:`.column_keyed_dict` class factories.
  263. :class:`_orm.KeyFuncDict` may also serve as the base for user-defined
  264. custom dictionary classes.
  265. .. versionchanged:: 2.0 Renamed :class:`.MappedCollection` to
  266. :class:`.KeyFuncDict`.
  267. .. seealso::
  268. :func:`_orm.attribute_keyed_dict`
  269. :func:`_orm.column_keyed_dict`
  270. :ref:`orm_dictionary_collection`
  271. :ref:`orm_custom_collection`
  272. """
  273. def __init__(
  274. self,
  275. keyfunc: Callable[[Any], Any],
  276. *dict_args: Any,
  277. ignore_unpopulated_attribute: bool = False,
  278. ) -> None:
  279. """Create a new collection with keying provided by keyfunc.
  280. keyfunc may be any callable that takes an object and returns an object
  281. for use as a dictionary key.
  282. The keyfunc will be called every time the ORM needs to add a member by
  283. value-only (such as when loading instances from the database) or
  284. remove a member. The usual cautions about dictionary keying apply-
  285. ``keyfunc(object)`` should return the same output for the life of the
  286. collection. Keying based on mutable properties can result in
  287. unreachable instances "lost" in the collection.
  288. """
  289. self.keyfunc = keyfunc
  290. self.ignore_unpopulated_attribute = ignore_unpopulated_attribute
  291. super().__init__(*dict_args)
  292. @classmethod
  293. def _unreduce(
  294. cls,
  295. keyfunc: Callable[[Any], Any],
  296. values: Dict[_KT, _KT],
  297. adapter: Optional[CollectionAdapter] = None,
  298. ) -> "KeyFuncDict[_KT, _KT]":
  299. mp: KeyFuncDict[_KT, _KT] = KeyFuncDict(keyfunc)
  300. mp.update(values)
  301. # note that the adapter sets itself up onto this collection
  302. # when its `__setstate__` method is called
  303. return mp
  304. def __reduce__(
  305. self,
  306. ) -> Tuple[
  307. Callable[[_KT, _KT], KeyFuncDict[_KT, _KT]],
  308. Tuple[Any, Union[Dict[_KT, _KT], Dict[_KT, _KT]], CollectionAdapter],
  309. ]:
  310. return (
  311. KeyFuncDict._unreduce,
  312. (
  313. self.keyfunc,
  314. dict(self),
  315. collection_adapter(self),
  316. ),
  317. )
  318. @util.preload_module("sqlalchemy.orm.attributes")
  319. def _raise_for_unpopulated(
  320. self,
  321. value: _KT,
  322. initiator: Union[AttributeEventToken, Literal[None, False]] = None,
  323. *,
  324. warn_only: bool,
  325. ) -> None:
  326. mapper = base.instance_state(value).mapper
  327. attributes = util.preloaded.orm_attributes
  328. if not isinstance(initiator, attributes.AttributeEventToken):
  329. relationship = "unknown relationship"
  330. elif initiator.key in mapper.attrs:
  331. relationship = f"{mapper.attrs[initiator.key]}"
  332. else:
  333. relationship = initiator.key
  334. if warn_only:
  335. util.warn(
  336. f"Attribute keyed dictionary value for "
  337. f"attribute '{relationship}' was None; this will raise "
  338. "in a future release. "
  339. f"To skip this assignment entirely, "
  340. f'Set the "ignore_unpopulated_attribute=True" '
  341. f"parameter on the mapped collection factory."
  342. )
  343. else:
  344. raise sa_exc.InvalidRequestError(
  345. "In event triggered from population of "
  346. f"attribute '{relationship}' "
  347. "(potentially from a backref), "
  348. f"can't populate value in KeyFuncDict; "
  349. "dictionary key "
  350. f"derived from {base.instance_str(value)} is not "
  351. f"populated. Ensure appropriate state is set up on "
  352. f"the {base.instance_str(value)} object "
  353. f"before assigning to the {relationship} attribute. "
  354. f"To skip this assignment entirely, "
  355. f'Set the "ignore_unpopulated_attribute=True" '
  356. f"parameter on the mapped collection factory."
  357. )
  358. @collection.appender # type: ignore[misc]
  359. @collection.internally_instrumented # type: ignore[misc]
  360. def set(
  361. self,
  362. value: _KT,
  363. _sa_initiator: Union[AttributeEventToken, Literal[None, False]] = None,
  364. ) -> None:
  365. """Add an item by value, consulting the keyfunc for the key."""
  366. key = self.keyfunc(value)
  367. if key is base.NO_VALUE:
  368. if not self.ignore_unpopulated_attribute:
  369. self._raise_for_unpopulated(
  370. value, _sa_initiator, warn_only=False
  371. )
  372. else:
  373. return
  374. elif key is Missing:
  375. if not self.ignore_unpopulated_attribute:
  376. self._raise_for_unpopulated(
  377. value, _sa_initiator, warn_only=True
  378. )
  379. key = None
  380. else:
  381. return
  382. self.__setitem__(key, value, _sa_initiator) # type: ignore[call-arg]
  383. @collection.remover # type: ignore[misc]
  384. @collection.internally_instrumented # type: ignore[misc]
  385. def remove(
  386. self,
  387. value: _KT,
  388. _sa_initiator: Union[AttributeEventToken, Literal[None, False]] = None,
  389. ) -> None:
  390. """Remove an item by value, consulting the keyfunc for the key."""
  391. key = self.keyfunc(value)
  392. if key is base.NO_VALUE:
  393. if not self.ignore_unpopulated_attribute:
  394. self._raise_for_unpopulated(
  395. value, _sa_initiator, warn_only=False
  396. )
  397. return
  398. elif key is Missing:
  399. if not self.ignore_unpopulated_attribute:
  400. self._raise_for_unpopulated(
  401. value, _sa_initiator, warn_only=True
  402. )
  403. key = None
  404. else:
  405. return
  406. # Let self[key] raise if key is not in this collection
  407. # testlib.pragma exempt:__ne__
  408. if self[key] != value:
  409. raise sa_exc.InvalidRequestError(
  410. "Can not remove '%s': collection holds '%s' for key '%s'. "
  411. "Possible cause: is the KeyFuncDict key function "
  412. "based on mutable properties or properties that only obtain "
  413. "values after flush?" % (value, self[key], key)
  414. )
  415. self.__delitem__(key, _sa_initiator) # type: ignore[call-arg]
  416. def _mapped_collection_cls(
  417. keyfunc: Callable[[Any], Any], ignore_unpopulated_attribute: bool
  418. ) -> Type[KeyFuncDict[_KT, _KT]]:
  419. class _MKeyfuncMapped(KeyFuncDict[_KT, _KT]):
  420. def __init__(self, *dict_args: Any) -> None:
  421. super().__init__(
  422. keyfunc,
  423. *dict_args,
  424. ignore_unpopulated_attribute=ignore_unpopulated_attribute,
  425. )
  426. return _MKeyfuncMapped
  427. MappedCollection = KeyFuncDict
  428. """A synonym for :class:`.KeyFuncDict`.
  429. .. versionchanged:: 2.0 Renamed :class:`.MappedCollection` to
  430. :class:`.KeyFuncDict`.
  431. """
  432. mapped_collection = keyfunc_mapping
  433. """A synonym for :func:`_orm.keyfunc_mapping`.
  434. .. versionchanged:: 2.0 Renamed :data:`.mapped_collection` to
  435. :func:`_orm.keyfunc_mapping`
  436. """
  437. attribute_mapped_collection = attribute_keyed_dict
  438. """A synonym for :func:`_orm.attribute_keyed_dict`.
  439. .. versionchanged:: 2.0 Renamed :data:`.attribute_mapped_collection` to
  440. :func:`_orm.attribute_keyed_dict`
  441. """
  442. column_mapped_collection = column_keyed_dict
  443. """A synonym for :func:`_orm.column_keyed_dict.
  444. .. versionchanged:: 2.0 Renamed :func:`.column_mapped_collection` to
  445. :func:`_orm.column_keyed_dict`
  446. """