descriptor_props.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  1. # orm/descriptor_props.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. """Descriptor properties are more "auxiliary" properties
  8. that exist as configurational elements, but don't participate
  9. as actively in the load/persist ORM loop.
  10. """
  11. from __future__ import annotations
  12. from dataclasses import is_dataclass
  13. import inspect
  14. import itertools
  15. import operator
  16. import typing
  17. from typing import Any
  18. from typing import Callable
  19. from typing import Dict
  20. from typing import List
  21. from typing import NoReturn
  22. from typing import Optional
  23. from typing import Sequence
  24. from typing import Tuple
  25. from typing import Type
  26. from typing import TYPE_CHECKING
  27. from typing import TypeVar
  28. from typing import Union
  29. import weakref
  30. from . import attributes
  31. from . import util as orm_util
  32. from .base import _DeclarativeMapped
  33. from .base import LoaderCallableStatus
  34. from .base import Mapped
  35. from .base import PassiveFlag
  36. from .base import SQLORMOperations
  37. from .interfaces import _AttributeOptions
  38. from .interfaces import _IntrospectsAnnotations
  39. from .interfaces import _MapsColumns
  40. from .interfaces import MapperProperty
  41. from .interfaces import PropComparator
  42. from .util import _none_set
  43. from .util import de_stringify_annotation
  44. from .. import event
  45. from .. import exc as sa_exc
  46. from .. import schema
  47. from .. import sql
  48. from .. import util
  49. from ..sql import expression
  50. from ..sql import operators
  51. from ..sql.elements import BindParameter
  52. from ..util.typing import get_args
  53. from ..util.typing import is_fwd_ref
  54. from ..util.typing import is_pep593
  55. if typing.TYPE_CHECKING:
  56. from ._typing import _InstanceDict
  57. from ._typing import _RegistryType
  58. from .attributes import History
  59. from .attributes import InstrumentedAttribute
  60. from .attributes import QueryableAttribute
  61. from .context import ORMCompileState
  62. from .decl_base import _ClassScanMapperConfig
  63. from .mapper import Mapper
  64. from .properties import ColumnProperty
  65. from .properties import MappedColumn
  66. from .state import InstanceState
  67. from ..engine.base import Connection
  68. from ..engine.row import Row
  69. from ..sql._typing import _DMLColumnArgument
  70. from ..sql._typing import _InfoType
  71. from ..sql.elements import ClauseList
  72. from ..sql.elements import ColumnElement
  73. from ..sql.operators import OperatorType
  74. from ..sql.schema import Column
  75. from ..sql.selectable import Select
  76. from ..util.typing import _AnnotationScanType
  77. from ..util.typing import CallableReference
  78. from ..util.typing import DescriptorReference
  79. from ..util.typing import RODescriptorReference
  80. _T = TypeVar("_T", bound=Any)
  81. _PT = TypeVar("_PT", bound=Any)
  82. class DescriptorProperty(MapperProperty[_T]):
  83. """:class:`.MapperProperty` which proxies access to a
  84. user-defined descriptor."""
  85. doc: Optional[str] = None
  86. uses_objects = False
  87. _links_to_entity = False
  88. descriptor: DescriptorReference[Any]
  89. def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
  90. raise NotImplementedError(
  91. "This MapperProperty does not implement column loader strategies"
  92. )
  93. def get_history(
  94. self,
  95. state: InstanceState[Any],
  96. dict_: _InstanceDict,
  97. passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
  98. ) -> History:
  99. raise NotImplementedError()
  100. def instrument_class(self, mapper: Mapper[Any]) -> None:
  101. prop = self
  102. class _ProxyImpl(attributes.AttributeImpl):
  103. accepts_scalar_loader = False
  104. load_on_unexpire = True
  105. collection = False
  106. @property
  107. def uses_objects(self) -> bool: # type: ignore
  108. return prop.uses_objects
  109. def __init__(self, key: str):
  110. self.key = key
  111. def get_history(
  112. self,
  113. state: InstanceState[Any],
  114. dict_: _InstanceDict,
  115. passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
  116. ) -> History:
  117. return prop.get_history(state, dict_, passive)
  118. if self.descriptor is None:
  119. desc = getattr(mapper.class_, self.key, None)
  120. if mapper._is_userland_descriptor(self.key, desc):
  121. self.descriptor = desc
  122. if self.descriptor is None:
  123. def fset(obj: Any, value: Any) -> None:
  124. setattr(obj, self.name, value)
  125. def fdel(obj: Any) -> None:
  126. delattr(obj, self.name)
  127. def fget(obj: Any) -> Any:
  128. return getattr(obj, self.name)
  129. self.descriptor = property(fget=fget, fset=fset, fdel=fdel)
  130. proxy_attr = attributes.create_proxied_attribute(self.descriptor)(
  131. self.parent.class_,
  132. self.key,
  133. self.descriptor,
  134. lambda: self._comparator_factory(mapper),
  135. doc=self.doc,
  136. original_property=self,
  137. )
  138. proxy_attr.impl = _ProxyImpl(self.key)
  139. mapper.class_manager.instrument_attribute(self.key, proxy_attr)
  140. _CompositeAttrType = Union[
  141. str,
  142. "Column[_T]",
  143. "MappedColumn[_T]",
  144. "InstrumentedAttribute[_T]",
  145. "Mapped[_T]",
  146. ]
  147. _CC = TypeVar("_CC", bound=Any)
  148. _composite_getters: weakref.WeakKeyDictionary[
  149. Type[Any], Callable[[Any], Tuple[Any, ...]]
  150. ] = weakref.WeakKeyDictionary()
  151. class CompositeProperty(
  152. _MapsColumns[_CC], _IntrospectsAnnotations, DescriptorProperty[_CC]
  153. ):
  154. """Defines a "composite" mapped attribute, representing a collection
  155. of columns as one attribute.
  156. :class:`.CompositeProperty` is constructed using the :func:`.composite`
  157. function.
  158. .. seealso::
  159. :ref:`mapper_composite`
  160. """
  161. composite_class: Union[Type[_CC], Callable[..., _CC]]
  162. attrs: Tuple[_CompositeAttrType[Any], ...]
  163. _generated_composite_accessor: CallableReference[
  164. Optional[Callable[[_CC], Tuple[Any, ...]]]
  165. ]
  166. comparator_factory: Type[Comparator[_CC]]
  167. def __init__(
  168. self,
  169. _class_or_attr: Union[
  170. None, Type[_CC], Callable[..., _CC], _CompositeAttrType[Any]
  171. ] = None,
  172. *attrs: _CompositeAttrType[Any],
  173. attribute_options: Optional[_AttributeOptions] = None,
  174. active_history: bool = False,
  175. deferred: bool = False,
  176. group: Optional[str] = None,
  177. comparator_factory: Optional[Type[Comparator[_CC]]] = None,
  178. info: Optional[_InfoType] = None,
  179. **kwargs: Any,
  180. ):
  181. super().__init__(attribute_options=attribute_options)
  182. if isinstance(_class_or_attr, (Mapped, str, sql.ColumnElement)):
  183. self.attrs = (_class_or_attr,) + attrs
  184. # will initialize within declarative_scan
  185. self.composite_class = None # type: ignore
  186. else:
  187. self.composite_class = _class_or_attr # type: ignore
  188. self.attrs = attrs
  189. self.active_history = active_history
  190. self.deferred = deferred
  191. self.group = group
  192. self.comparator_factory = (
  193. comparator_factory
  194. if comparator_factory is not None
  195. else self.__class__.Comparator
  196. )
  197. self._generated_composite_accessor = None
  198. if info is not None:
  199. self.info.update(info)
  200. util.set_creation_order(self)
  201. self._create_descriptor()
  202. self._init_accessor()
  203. def instrument_class(self, mapper: Mapper[Any]) -> None:
  204. super().instrument_class(mapper)
  205. self._setup_event_handlers()
  206. def _composite_values_from_instance(self, value: _CC) -> Tuple[Any, ...]:
  207. if self._generated_composite_accessor:
  208. return self._generated_composite_accessor(value)
  209. else:
  210. try:
  211. accessor = value.__composite_values__
  212. except AttributeError as ae:
  213. raise sa_exc.InvalidRequestError(
  214. f"Composite class {self.composite_class.__name__} is not "
  215. f"a dataclass and does not define a __composite_values__()"
  216. " method; can't get state"
  217. ) from ae
  218. else:
  219. return accessor() # type: ignore
  220. def do_init(self) -> None:
  221. """Initialization which occurs after the :class:`.Composite`
  222. has been associated with its parent mapper.
  223. """
  224. self._setup_arguments_on_columns()
  225. _COMPOSITE_FGET = object()
  226. def _create_descriptor(self) -> None:
  227. """Create the Python descriptor that will serve as
  228. the access point on instances of the mapped class.
  229. """
  230. def fget(instance: Any) -> Any:
  231. dict_ = attributes.instance_dict(instance)
  232. state = attributes.instance_state(instance)
  233. if self.key not in dict_:
  234. # key not present. Iterate through related
  235. # attributes, retrieve their values. This
  236. # ensures they all load.
  237. values = [
  238. getattr(instance, key) for key in self._attribute_keys
  239. ]
  240. # current expected behavior here is that the composite is
  241. # created on access if the object is persistent or if
  242. # col attributes have non-None. This would be better
  243. # if the composite were created unconditionally,
  244. # but that would be a behavioral change.
  245. if self.key not in dict_ and (
  246. state.key is not None or not _none_set.issuperset(values)
  247. ):
  248. dict_[self.key] = self.composite_class(*values)
  249. state.manager.dispatch.refresh(
  250. state, self._COMPOSITE_FGET, [self.key]
  251. )
  252. return dict_.get(self.key, None)
  253. def fset(instance: Any, value: Any) -> None:
  254. dict_ = attributes.instance_dict(instance)
  255. state = attributes.instance_state(instance)
  256. attr = state.manager[self.key]
  257. if attr.dispatch._active_history:
  258. previous = fget(instance)
  259. else:
  260. previous = dict_.get(self.key, LoaderCallableStatus.NO_VALUE)
  261. for fn in attr.dispatch.set:
  262. value = fn(state, value, previous, attr.impl)
  263. dict_[self.key] = value
  264. if value is None:
  265. for key in self._attribute_keys:
  266. setattr(instance, key, None)
  267. else:
  268. for key, value in zip(
  269. self._attribute_keys,
  270. self._composite_values_from_instance(value),
  271. ):
  272. setattr(instance, key, value)
  273. def fdel(instance: Any) -> None:
  274. state = attributes.instance_state(instance)
  275. dict_ = attributes.instance_dict(instance)
  276. attr = state.manager[self.key]
  277. if attr.dispatch._active_history:
  278. previous = fget(instance)
  279. dict_.pop(self.key, None)
  280. else:
  281. previous = dict_.pop(self.key, LoaderCallableStatus.NO_VALUE)
  282. attr = state.manager[self.key]
  283. attr.dispatch.remove(state, previous, attr.impl)
  284. for key in self._attribute_keys:
  285. setattr(instance, key, None)
  286. self.descriptor = property(fget, fset, fdel)
  287. @util.preload_module("sqlalchemy.orm.properties")
  288. def declarative_scan(
  289. self,
  290. decl_scan: _ClassScanMapperConfig,
  291. registry: _RegistryType,
  292. cls: Type[Any],
  293. originating_module: Optional[str],
  294. key: str,
  295. mapped_container: Optional[Type[Mapped[Any]]],
  296. annotation: Optional[_AnnotationScanType],
  297. extracted_mapped_annotation: Optional[_AnnotationScanType],
  298. is_dataclass_field: bool,
  299. ) -> None:
  300. MappedColumn = util.preloaded.orm_properties.MappedColumn
  301. if (
  302. self.composite_class is None
  303. and extracted_mapped_annotation is None
  304. ):
  305. self._raise_for_required(key, cls)
  306. argument = extracted_mapped_annotation
  307. if is_pep593(argument):
  308. argument = get_args(argument)[0]
  309. if argument and self.composite_class is None:
  310. if isinstance(argument, str) or is_fwd_ref(
  311. argument, check_generic=True
  312. ):
  313. if originating_module is None:
  314. str_arg = (
  315. argument.__forward_arg__
  316. if hasattr(argument, "__forward_arg__")
  317. else str(argument)
  318. )
  319. raise sa_exc.ArgumentError(
  320. f"Can't use forward ref {argument} for composite "
  321. f"class argument; set up the type as Mapped[{str_arg}]"
  322. )
  323. argument = de_stringify_annotation(
  324. cls, argument, originating_module, include_generic=True
  325. )
  326. self.composite_class = argument
  327. if is_dataclass(self.composite_class):
  328. self._setup_for_dataclass(
  329. decl_scan, registry, cls, originating_module, key
  330. )
  331. else:
  332. for attr in self.attrs:
  333. if (
  334. isinstance(attr, (MappedColumn, schema.Column))
  335. and attr.name is None
  336. ):
  337. raise sa_exc.ArgumentError(
  338. "Composite class column arguments must be named "
  339. "unless a dataclass is used"
  340. )
  341. self._init_accessor()
  342. def _init_accessor(self) -> None:
  343. if is_dataclass(self.composite_class) and not hasattr(
  344. self.composite_class, "__composite_values__"
  345. ):
  346. insp = inspect.signature(self.composite_class)
  347. getter = operator.attrgetter(
  348. *[p.name for p in insp.parameters.values()]
  349. )
  350. if len(insp.parameters) == 1:
  351. self._generated_composite_accessor = lambda obj: (getter(obj),)
  352. else:
  353. self._generated_composite_accessor = getter
  354. if (
  355. self.composite_class is not None
  356. and isinstance(self.composite_class, type)
  357. and self.composite_class not in _composite_getters
  358. ):
  359. if self._generated_composite_accessor is not None:
  360. _composite_getters[self.composite_class] = (
  361. self._generated_composite_accessor
  362. )
  363. elif hasattr(self.composite_class, "__composite_values__"):
  364. _composite_getters[self.composite_class] = (
  365. lambda obj: obj.__composite_values__()
  366. )
  367. @util.preload_module("sqlalchemy.orm.properties")
  368. @util.preload_module("sqlalchemy.orm.decl_base")
  369. def _setup_for_dataclass(
  370. self,
  371. decl_scan: _ClassScanMapperConfig,
  372. registry: _RegistryType,
  373. cls: Type[Any],
  374. originating_module: Optional[str],
  375. key: str,
  376. ) -> None:
  377. MappedColumn = util.preloaded.orm_properties.MappedColumn
  378. decl_base = util.preloaded.orm_decl_base
  379. insp = inspect.signature(self.composite_class)
  380. for param, attr in itertools.zip_longest(
  381. insp.parameters.values(), self.attrs
  382. ):
  383. if param is None:
  384. raise sa_exc.ArgumentError(
  385. f"number of composite attributes "
  386. f"{len(self.attrs)} exceeds "
  387. f"that of the number of attributes in class "
  388. f"{self.composite_class.__name__} {len(insp.parameters)}"
  389. )
  390. if attr is None:
  391. # fill in missing attr spots with empty MappedColumn
  392. attr = MappedColumn()
  393. self.attrs += (attr,)
  394. if isinstance(attr, MappedColumn):
  395. attr.declarative_scan_for_composite(
  396. decl_scan,
  397. registry,
  398. cls,
  399. originating_module,
  400. key,
  401. param.name,
  402. param.annotation,
  403. )
  404. elif isinstance(attr, schema.Column):
  405. decl_base._undefer_column_name(param.name, attr)
  406. @util.memoized_property
  407. def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
  408. return [getattr(self.parent.class_, prop.key) for prop in self.props]
  409. @util.memoized_property
  410. @util.preload_module("orm.properties")
  411. def props(self) -> Sequence[MapperProperty[Any]]:
  412. props = []
  413. MappedColumn = util.preloaded.orm_properties.MappedColumn
  414. for attr in self.attrs:
  415. if isinstance(attr, str):
  416. prop = self.parent.get_property(attr, _configure_mappers=False)
  417. elif isinstance(attr, schema.Column):
  418. prop = self.parent._columntoproperty[attr]
  419. elif isinstance(attr, MappedColumn):
  420. prop = self.parent._columntoproperty[attr.column]
  421. elif isinstance(attr, attributes.InstrumentedAttribute):
  422. prop = attr.property
  423. else:
  424. prop = None
  425. if not isinstance(prop, MapperProperty):
  426. raise sa_exc.ArgumentError(
  427. "Composite expects Column objects or mapped "
  428. f"attributes/attribute names as arguments, got: {attr!r}"
  429. )
  430. props.append(prop)
  431. return props
  432. def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
  433. return self._comparable_elements
  434. @util.non_memoized_property
  435. @util.preload_module("orm.properties")
  436. def columns(self) -> Sequence[Column[Any]]:
  437. MappedColumn = util.preloaded.orm_properties.MappedColumn
  438. return [
  439. a.column if isinstance(a, MappedColumn) else a
  440. for a in self.attrs
  441. if isinstance(a, (schema.Column, MappedColumn))
  442. ]
  443. @property
  444. def mapper_property_to_assign(self) -> Optional[MapperProperty[_CC]]:
  445. return self
  446. @property
  447. def columns_to_assign(self) -> List[Tuple[schema.Column[Any], int]]:
  448. return [(c, 0) for c in self.columns if c.table is None]
  449. @util.preload_module("orm.properties")
  450. def _setup_arguments_on_columns(self) -> None:
  451. """Propagate configuration arguments made on this composite
  452. to the target columns, for those that apply.
  453. """
  454. ColumnProperty = util.preloaded.orm_properties.ColumnProperty
  455. for prop in self.props:
  456. if not isinstance(prop, ColumnProperty):
  457. continue
  458. else:
  459. cprop = prop
  460. cprop.active_history = self.active_history
  461. if self.deferred:
  462. cprop.deferred = self.deferred
  463. cprop.strategy_key = (("deferred", True), ("instrument", True))
  464. cprop.group = self.group
  465. def _setup_event_handlers(self) -> None:
  466. """Establish events that populate/expire the composite attribute."""
  467. def load_handler(
  468. state: InstanceState[Any], context: ORMCompileState
  469. ) -> None:
  470. _load_refresh_handler(state, context, None, is_refresh=False)
  471. def refresh_handler(
  472. state: InstanceState[Any],
  473. context: ORMCompileState,
  474. to_load: Optional[Sequence[str]],
  475. ) -> None:
  476. # note this corresponds to sqlalchemy.ext.mutable load_attrs()
  477. if not to_load or (
  478. {self.key}.union(self._attribute_keys)
  479. ).intersection(to_load):
  480. _load_refresh_handler(state, context, to_load, is_refresh=True)
  481. def _load_refresh_handler(
  482. state: InstanceState[Any],
  483. context: ORMCompileState,
  484. to_load: Optional[Sequence[str]],
  485. is_refresh: bool,
  486. ) -> None:
  487. dict_ = state.dict
  488. # if context indicates we are coming from the
  489. # fget() handler, this already set the value; skip the
  490. # handler here. (other handlers like mutablecomposite will still
  491. # want to catch it)
  492. # there's an insufficiency here in that the fget() handler
  493. # really should not be using the refresh event and there should
  494. # be some other event that mutablecomposite can subscribe
  495. # towards for this.
  496. if (
  497. not is_refresh or context is self._COMPOSITE_FGET
  498. ) and self.key in dict_:
  499. return
  500. # if column elements aren't loaded, skip.
  501. # __get__() will initiate a load for those
  502. # columns
  503. for k in self._attribute_keys:
  504. if k not in dict_:
  505. return
  506. dict_[self.key] = self.composite_class(
  507. *[state.dict[key] for key in self._attribute_keys]
  508. )
  509. def expire_handler(
  510. state: InstanceState[Any], keys: Optional[Sequence[str]]
  511. ) -> None:
  512. if keys is None or set(self._attribute_keys).intersection(keys):
  513. state.dict.pop(self.key, None)
  514. def insert_update_handler(
  515. mapper: Mapper[Any],
  516. connection: Connection,
  517. state: InstanceState[Any],
  518. ) -> None:
  519. """After an insert or update, some columns may be expired due
  520. to server side defaults, or re-populated due to client side
  521. defaults. Pop out the composite value here so that it
  522. recreates.
  523. """
  524. state.dict.pop(self.key, None)
  525. event.listen(
  526. self.parent, "after_insert", insert_update_handler, raw=True
  527. )
  528. event.listen(
  529. self.parent, "after_update", insert_update_handler, raw=True
  530. )
  531. event.listen(
  532. self.parent, "load", load_handler, raw=True, propagate=True
  533. )
  534. event.listen(
  535. self.parent, "refresh", refresh_handler, raw=True, propagate=True
  536. )
  537. event.listen(
  538. self.parent, "expire", expire_handler, raw=True, propagate=True
  539. )
  540. proxy_attr = self.parent.class_manager[self.key]
  541. proxy_attr.impl.dispatch = proxy_attr.dispatch # type: ignore
  542. proxy_attr.impl.dispatch._active_history = self.active_history
  543. # TODO: need a deserialize hook here
  544. @util.memoized_property
  545. def _attribute_keys(self) -> Sequence[str]:
  546. return [prop.key for prop in self.props]
  547. def _populate_composite_bulk_save_mappings_fn(
  548. self,
  549. ) -> Callable[[Dict[str, Any]], None]:
  550. if self._generated_composite_accessor:
  551. get_values = self._generated_composite_accessor
  552. else:
  553. def get_values(val: Any) -> Tuple[Any]:
  554. return val.__composite_values__() # type: ignore
  555. attrs = [prop.key for prop in self.props]
  556. def populate(dest_dict: Dict[str, Any]) -> None:
  557. dest_dict.update(
  558. {
  559. key: val
  560. for key, val in zip(
  561. attrs, get_values(dest_dict.pop(self.key))
  562. )
  563. }
  564. )
  565. return populate
  566. def get_history(
  567. self,
  568. state: InstanceState[Any],
  569. dict_: _InstanceDict,
  570. passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
  571. ) -> History:
  572. """Provided for userland code that uses attributes.get_history()."""
  573. added: List[Any] = []
  574. deleted: List[Any] = []
  575. has_history = False
  576. for prop in self.props:
  577. key = prop.key
  578. hist = state.manager[key].impl.get_history(state, dict_)
  579. if hist.has_changes():
  580. has_history = True
  581. non_deleted = hist.non_deleted()
  582. if non_deleted:
  583. added.extend(non_deleted)
  584. else:
  585. added.append(None)
  586. if hist.deleted:
  587. deleted.extend(hist.deleted)
  588. else:
  589. deleted.append(None)
  590. if has_history:
  591. return attributes.History(
  592. [self.composite_class(*added)],
  593. (),
  594. [self.composite_class(*deleted)],
  595. )
  596. else:
  597. return attributes.History((), [self.composite_class(*added)], ())
  598. def _comparator_factory(
  599. self, mapper: Mapper[Any]
  600. ) -> Composite.Comparator[_CC]:
  601. return self.comparator_factory(self, mapper)
  602. class CompositeBundle(orm_util.Bundle[_T]):
  603. def __init__(
  604. self,
  605. property_: Composite[_T],
  606. expr: ClauseList,
  607. ):
  608. self.property = property_
  609. super().__init__(property_.key, *expr)
  610. def create_row_processor(
  611. self,
  612. query: Select[Any],
  613. procs: Sequence[Callable[[Row[Any]], Any]],
  614. labels: Sequence[str],
  615. ) -> Callable[[Row[Any]], Any]:
  616. def proc(row: Row[Any]) -> Any:
  617. return self.property.composite_class(
  618. *[proc(row) for proc in procs]
  619. )
  620. return proc
  621. class Comparator(PropComparator[_PT]):
  622. """Produce boolean, comparison, and other operators for
  623. :class:`.Composite` attributes.
  624. See the example in :ref:`composite_operations` for an overview
  625. of usage , as well as the documentation for :class:`.PropComparator`.
  626. .. seealso::
  627. :class:`.PropComparator`
  628. :class:`.ColumnOperators`
  629. :ref:`types_operators`
  630. :attr:`.TypeEngine.comparator_factory`
  631. """
  632. # https://github.com/python/mypy/issues/4266
  633. __hash__ = None # type: ignore
  634. prop: RODescriptorReference[Composite[_PT]]
  635. @util.memoized_property
  636. def clauses(self) -> ClauseList:
  637. return expression.ClauseList(
  638. group=False, *self._comparable_elements
  639. )
  640. def __clause_element__(self) -> CompositeProperty.CompositeBundle[_PT]:
  641. return self.expression
  642. @util.memoized_property
  643. def expression(self) -> CompositeProperty.CompositeBundle[_PT]:
  644. clauses = self.clauses._annotate(
  645. {
  646. "parententity": self._parententity,
  647. "parentmapper": self._parententity,
  648. "proxy_key": self.prop.key,
  649. }
  650. )
  651. return CompositeProperty.CompositeBundle(self.prop, clauses)
  652. def _bulk_update_tuples(
  653. self, value: Any
  654. ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
  655. if isinstance(value, BindParameter):
  656. value = value.value
  657. values: Sequence[Any]
  658. if value is None:
  659. values = [None for key in self.prop._attribute_keys]
  660. elif isinstance(self.prop.composite_class, type) and isinstance(
  661. value, self.prop.composite_class
  662. ):
  663. values = self.prop._composite_values_from_instance(
  664. value # type: ignore[arg-type]
  665. )
  666. else:
  667. raise sa_exc.ArgumentError(
  668. "Can't UPDATE composite attribute %s to %r"
  669. % (self.prop, value)
  670. )
  671. return list(zip(self._comparable_elements, values))
  672. @util.memoized_property
  673. def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
  674. if self._adapt_to_entity:
  675. return [
  676. getattr(self._adapt_to_entity.entity, prop.key)
  677. for prop in self.prop._comparable_elements
  678. ]
  679. else:
  680. return self.prop._comparable_elements
  681. def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
  682. return self._compare(operators.eq, other)
  683. def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
  684. return self._compare(operators.ne, other)
  685. def __lt__(self, other: Any) -> ColumnElement[bool]:
  686. return self._compare(operators.lt, other)
  687. def __gt__(self, other: Any) -> ColumnElement[bool]:
  688. return self._compare(operators.gt, other)
  689. def __le__(self, other: Any) -> ColumnElement[bool]:
  690. return self._compare(operators.le, other)
  691. def __ge__(self, other: Any) -> ColumnElement[bool]:
  692. return self._compare(operators.ge, other)
  693. # what might be interesting would be if we create
  694. # an instance of the composite class itself with
  695. # the columns as data members, then use "hybrid style" comparison
  696. # to create these comparisons. then your Point.__eq__() method could
  697. # be where comparison behavior is defined for SQL also. Likely
  698. # not a good choice for default behavior though, not clear how it would
  699. # work w/ dataclasses, etc. also no demand for any of this anyway.
  700. def _compare(
  701. self, operator: OperatorType, other: Any
  702. ) -> ColumnElement[bool]:
  703. values: Sequence[Any]
  704. if other is None:
  705. values = [None] * len(self.prop._comparable_elements)
  706. else:
  707. values = self.prop._composite_values_from_instance(other)
  708. comparisons = [
  709. operator(a, b)
  710. for a, b in zip(self.prop._comparable_elements, values)
  711. ]
  712. if self._adapt_to_entity:
  713. assert self.adapter is not None
  714. comparisons = [self.adapter(x) for x in comparisons]
  715. return sql.and_(*comparisons)
  716. def __str__(self) -> str:
  717. return str(self.parent.class_.__name__) + "." + self.key
  718. class Composite(CompositeProperty[_T], _DeclarativeMapped[_T]):
  719. """Declarative-compatible front-end for the :class:`.CompositeProperty`
  720. class.
  721. Public constructor is the :func:`_orm.composite` function.
  722. .. versionchanged:: 2.0 Added :class:`_orm.Composite` as a Declarative
  723. compatible subclass of :class:`_orm.CompositeProperty`.
  724. .. seealso::
  725. :ref:`mapper_composite`
  726. """
  727. inherit_cache = True
  728. """:meta private:"""
  729. class ConcreteInheritedProperty(DescriptorProperty[_T]):
  730. """A 'do nothing' :class:`.MapperProperty` that disables
  731. an attribute on a concrete subclass that is only present
  732. on the inherited mapper, not the concrete classes' mapper.
  733. Cases where this occurs include:
  734. * When the superclass mapper is mapped against a
  735. "polymorphic union", which includes all attributes from
  736. all subclasses.
  737. * When a relationship() is configured on an inherited mapper,
  738. but not on the subclass mapper. Concrete mappers require
  739. that relationship() is configured explicitly on each
  740. subclass.
  741. """
  742. def _comparator_factory(
  743. self, mapper: Mapper[Any]
  744. ) -> Type[PropComparator[_T]]:
  745. comparator_callable = None
  746. for m in self.parent.iterate_to_root():
  747. p = m._props[self.key]
  748. if getattr(p, "comparator_factory", None) is not None:
  749. comparator_callable = p.comparator_factory
  750. break
  751. assert comparator_callable is not None
  752. return comparator_callable(p, mapper) # type: ignore
  753. def __init__(self) -> None:
  754. super().__init__()
  755. def warn() -> NoReturn:
  756. raise AttributeError(
  757. "Concrete %s does not implement "
  758. "attribute %r at the instance level. Add "
  759. "this property explicitly to %s."
  760. % (self.parent, self.key, self.parent)
  761. )
  762. class NoninheritedConcreteProp:
  763. def __set__(s: Any, obj: Any, value: Any) -> NoReturn:
  764. warn()
  765. def __delete__(s: Any, obj: Any) -> NoReturn:
  766. warn()
  767. def __get__(s: Any, obj: Any, owner: Any) -> Any:
  768. if obj is None:
  769. return self.descriptor
  770. warn()
  771. self.descriptor = NoninheritedConcreteProp()
  772. class SynonymProperty(DescriptorProperty[_T]):
  773. """Denote an attribute name as a synonym to a mapped property,
  774. in that the attribute will mirror the value and expression behavior
  775. of another attribute.
  776. :class:`.Synonym` is constructed using the :func:`_orm.synonym`
  777. function.
  778. .. seealso::
  779. :ref:`synonyms` - Overview of synonyms
  780. """
  781. comparator_factory: Optional[Type[PropComparator[_T]]]
  782. def __init__(
  783. self,
  784. name: str,
  785. map_column: Optional[bool] = None,
  786. descriptor: Optional[Any] = None,
  787. comparator_factory: Optional[Type[PropComparator[_T]]] = None,
  788. attribute_options: Optional[_AttributeOptions] = None,
  789. info: Optional[_InfoType] = None,
  790. doc: Optional[str] = None,
  791. ):
  792. super().__init__(attribute_options=attribute_options)
  793. self.name = name
  794. self.map_column = map_column
  795. self.descriptor = descriptor
  796. self.comparator_factory = comparator_factory
  797. if doc:
  798. self.doc = doc
  799. elif descriptor and descriptor.__doc__:
  800. self.doc = descriptor.__doc__
  801. else:
  802. self.doc = None
  803. if info:
  804. self.info.update(info)
  805. util.set_creation_order(self)
  806. if not TYPE_CHECKING:
  807. @property
  808. def uses_objects(self) -> bool:
  809. return getattr(self.parent.class_, self.name).impl.uses_objects
  810. # TODO: when initialized, check _proxied_object,
  811. # emit a warning if its not a column-based property
  812. @util.memoized_property
  813. def _proxied_object(
  814. self,
  815. ) -> Union[MapperProperty[_T], SQLORMOperations[_T]]:
  816. attr = getattr(self.parent.class_, self.name)
  817. if not hasattr(attr, "property") or not isinstance(
  818. attr.property, MapperProperty
  819. ):
  820. # attribute is a non-MapperProprerty proxy such as
  821. # hybrid or association proxy
  822. if isinstance(attr, attributes.QueryableAttribute):
  823. return attr.comparator
  824. elif isinstance(attr, SQLORMOperations):
  825. # assocaition proxy comes here
  826. return attr
  827. raise sa_exc.InvalidRequestError(
  828. """synonym() attribute "%s.%s" only supports """
  829. """ORM mapped attributes, got %r"""
  830. % (self.parent.class_.__name__, self.name, attr)
  831. )
  832. return attr.property
  833. def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
  834. return (getattr(self.parent.class_, self.name),)
  835. def _comparator_factory(self, mapper: Mapper[Any]) -> SQLORMOperations[_T]:
  836. prop = self._proxied_object
  837. if isinstance(prop, MapperProperty):
  838. if self.comparator_factory:
  839. comp = self.comparator_factory(prop, mapper)
  840. else:
  841. comp = prop.comparator_factory(prop, mapper)
  842. return comp
  843. else:
  844. return prop
  845. def get_history(
  846. self,
  847. state: InstanceState[Any],
  848. dict_: _InstanceDict,
  849. passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
  850. ) -> History:
  851. attr: QueryableAttribute[Any] = getattr(self.parent.class_, self.name)
  852. return attr.impl.get_history(state, dict_, passive=passive)
  853. @util.preload_module("sqlalchemy.orm.properties")
  854. def set_parent(self, parent: Mapper[Any], init: bool) -> None:
  855. properties = util.preloaded.orm_properties
  856. if self.map_column:
  857. # implement the 'map_column' option.
  858. if self.key not in parent.persist_selectable.c:
  859. raise sa_exc.ArgumentError(
  860. "Can't compile synonym '%s': no column on table "
  861. "'%s' named '%s'"
  862. % (
  863. self.name,
  864. parent.persist_selectable.description,
  865. self.key,
  866. )
  867. )
  868. elif (
  869. parent.persist_selectable.c[self.key]
  870. in parent._columntoproperty
  871. and parent._columntoproperty[
  872. parent.persist_selectable.c[self.key]
  873. ].key
  874. == self.name
  875. ):
  876. raise sa_exc.ArgumentError(
  877. "Can't call map_column=True for synonym %r=%r, "
  878. "a ColumnProperty already exists keyed to the name "
  879. "%r for column %r"
  880. % (self.key, self.name, self.name, self.key)
  881. )
  882. p: ColumnProperty[Any] = properties.ColumnProperty(
  883. parent.persist_selectable.c[self.key]
  884. )
  885. parent._configure_property(self.name, p, init=init, setparent=True)
  886. p._mapped_by_synonym = self.key
  887. self.parent = parent
  888. class Synonym(SynonymProperty[_T], _DeclarativeMapped[_T]):
  889. """Declarative front-end for the :class:`.SynonymProperty` class.
  890. Public constructor is the :func:`_orm.synonym` function.
  891. .. versionchanged:: 2.0 Added :class:`_orm.Synonym` as a Declarative
  892. compatible subclass for :class:`_orm.SynonymProperty`
  893. .. seealso::
  894. :ref:`synonyms` - Overview of synonyms
  895. """
  896. inherit_cache = True
  897. """:meta private:"""