descriptor_props.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. # orm/descriptor_props.py
  2. # Copyright (C) 2005-2022 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 . import attributes
  12. from . import util as orm_util
  13. from .interfaces import MapperProperty
  14. from .interfaces import PropComparator
  15. from .util import _none_set
  16. from .. import event
  17. from .. import exc as sa_exc
  18. from .. import schema
  19. from .. import sql
  20. from .. import util
  21. from ..sql import expression
  22. from ..sql import operators
  23. class DescriptorProperty(MapperProperty):
  24. """:class:`.MapperProperty` which proxies access to a
  25. user-defined descriptor."""
  26. doc = None
  27. uses_objects = False
  28. _links_to_entity = False
  29. def instrument_class(self, mapper):
  30. prop = self
  31. class _ProxyImpl(object):
  32. accepts_scalar_loader = False
  33. load_on_unexpire = True
  34. collection = False
  35. @property
  36. def uses_objects(self):
  37. return prop.uses_objects
  38. def __init__(self, key):
  39. self.key = key
  40. if hasattr(prop, "get_history"):
  41. def get_history(
  42. self, state, dict_, passive=attributes.PASSIVE_OFF
  43. ):
  44. return prop.get_history(state, dict_, passive)
  45. if self.descriptor is None:
  46. desc = getattr(mapper.class_, self.key, None)
  47. if mapper._is_userland_descriptor(self.key, desc):
  48. self.descriptor = desc
  49. if self.descriptor is None:
  50. def fset(obj, value):
  51. setattr(obj, self.name, value)
  52. def fdel(obj):
  53. delattr(obj, self.name)
  54. def fget(obj):
  55. return getattr(obj, self.name)
  56. self.descriptor = property(fget=fget, fset=fset, fdel=fdel)
  57. proxy_attr = attributes.create_proxied_attribute(self.descriptor)(
  58. self.parent.class_,
  59. self.key,
  60. self.descriptor,
  61. lambda: self._comparator_factory(mapper),
  62. doc=self.doc,
  63. original_property=self,
  64. )
  65. proxy_attr.impl = _ProxyImpl(self.key)
  66. mapper.class_manager.instrument_attribute(self.key, proxy_attr)
  67. class CompositeProperty(DescriptorProperty):
  68. """Defines a "composite" mapped attribute, representing a collection
  69. of columns as one attribute.
  70. :class:`.CompositeProperty` is constructed using the :func:`.composite`
  71. function.
  72. .. seealso::
  73. :ref:`mapper_composite`
  74. """
  75. def __init__(self, class_, *attrs, **kwargs):
  76. r"""Return a composite column-based property for use with a Mapper.
  77. See the mapping documentation section :ref:`mapper_composite` for a
  78. full usage example.
  79. The :class:`.MapperProperty` returned by :func:`.composite`
  80. is the :class:`.CompositeProperty`.
  81. :param class\_:
  82. The "composite type" class, or any classmethod or callable which
  83. will produce a new instance of the composite object given the
  84. column values in order.
  85. :param \*cols:
  86. List of Column objects to be mapped.
  87. :param active_history=False:
  88. When ``True``, indicates that the "previous" value for a
  89. scalar attribute should be loaded when replaced, if not
  90. already loaded. See the same flag on :func:`.column_property`.
  91. :param group:
  92. A group name for this property when marked as deferred.
  93. :param deferred:
  94. When True, the column property is "deferred", meaning that it does
  95. not load immediately, and is instead loaded when the attribute is
  96. first accessed on an instance. See also
  97. :func:`~sqlalchemy.orm.deferred`.
  98. :param comparator_factory: a class which extends
  99. :class:`.CompositeProperty.Comparator` which provides custom SQL
  100. clause generation for comparison operations.
  101. :param doc:
  102. optional string that will be applied as the doc on the
  103. class-bound descriptor.
  104. :param info: Optional data dictionary which will be populated into the
  105. :attr:`.MapperProperty.info` attribute of this object.
  106. """
  107. super(CompositeProperty, self).__init__()
  108. self.attrs = attrs
  109. self.composite_class = class_
  110. self.active_history = kwargs.get("active_history", False)
  111. self.deferred = kwargs.get("deferred", False)
  112. self.group = kwargs.get("group", None)
  113. self.comparator_factory = kwargs.pop(
  114. "comparator_factory", self.__class__.Comparator
  115. )
  116. if "info" in kwargs:
  117. self.info = kwargs.pop("info")
  118. util.set_creation_order(self)
  119. self._create_descriptor()
  120. def instrument_class(self, mapper):
  121. super(CompositeProperty, self).instrument_class(mapper)
  122. self._setup_event_handlers()
  123. def do_init(self):
  124. """Initialization which occurs after the :class:`.CompositeProperty`
  125. has been associated with its parent mapper.
  126. """
  127. self._setup_arguments_on_columns()
  128. _COMPOSITE_FGET = object()
  129. def _create_descriptor(self):
  130. """Create the Python descriptor that will serve as
  131. the access point on instances of the mapped class.
  132. """
  133. def fget(instance):
  134. dict_ = attributes.instance_dict(instance)
  135. state = attributes.instance_state(instance)
  136. if self.key not in dict_:
  137. # key not present. Iterate through related
  138. # attributes, retrieve their values. This
  139. # ensures they all load.
  140. values = [
  141. getattr(instance, key) for key in self._attribute_keys
  142. ]
  143. # current expected behavior here is that the composite is
  144. # created on access if the object is persistent or if
  145. # col attributes have non-None. This would be better
  146. # if the composite were created unconditionally,
  147. # but that would be a behavioral change.
  148. if self.key not in dict_ and (
  149. state.key is not None or not _none_set.issuperset(values)
  150. ):
  151. dict_[self.key] = self.composite_class(*values)
  152. state.manager.dispatch.refresh(
  153. state, self._COMPOSITE_FGET, [self.key]
  154. )
  155. return dict_.get(self.key, None)
  156. def fset(instance, value):
  157. dict_ = attributes.instance_dict(instance)
  158. state = attributes.instance_state(instance)
  159. attr = state.manager[self.key]
  160. previous = dict_.get(self.key, attributes.NO_VALUE)
  161. for fn in attr.dispatch.set:
  162. value = fn(state, value, previous, attr.impl)
  163. dict_[self.key] = value
  164. if value is None:
  165. for key in self._attribute_keys:
  166. setattr(instance, key, None)
  167. else:
  168. for key, value in zip(
  169. self._attribute_keys, value.__composite_values__()
  170. ):
  171. setattr(instance, key, value)
  172. def fdel(instance):
  173. state = attributes.instance_state(instance)
  174. dict_ = attributes.instance_dict(instance)
  175. previous = dict_.pop(self.key, attributes.NO_VALUE)
  176. attr = state.manager[self.key]
  177. attr.dispatch.remove(state, previous, attr.impl)
  178. for key in self._attribute_keys:
  179. setattr(instance, key, None)
  180. self.descriptor = property(fget, fset, fdel)
  181. @util.memoized_property
  182. def _comparable_elements(self):
  183. return [getattr(self.parent.class_, prop.key) for prop in self.props]
  184. @util.memoized_property
  185. def props(self):
  186. props = []
  187. for attr in self.attrs:
  188. if isinstance(attr, str):
  189. prop = self.parent.get_property(attr, _configure_mappers=False)
  190. elif isinstance(attr, schema.Column):
  191. prop = self.parent._columntoproperty[attr]
  192. elif isinstance(attr, attributes.InstrumentedAttribute):
  193. prop = attr.property
  194. else:
  195. raise sa_exc.ArgumentError(
  196. "Composite expects Column objects or mapped "
  197. "attributes/attribute names as arguments, got: %r"
  198. % (attr,)
  199. )
  200. props.append(prop)
  201. return props
  202. @property
  203. def columns(self):
  204. return [a for a in self.attrs if isinstance(a, schema.Column)]
  205. def _setup_arguments_on_columns(self):
  206. """Propagate configuration arguments made on this composite
  207. to the target columns, for those that apply.
  208. """
  209. for prop in self.props:
  210. prop.active_history = self.active_history
  211. if self.deferred:
  212. prop.deferred = self.deferred
  213. prop.strategy_key = (("deferred", True), ("instrument", True))
  214. prop.group = self.group
  215. def _setup_event_handlers(self):
  216. """Establish events that populate/expire the composite attribute."""
  217. def load_handler(state, context):
  218. _load_refresh_handler(state, context, None, is_refresh=False)
  219. def refresh_handler(state, context, to_load):
  220. # note this corresponds to sqlalchemy.ext.mutable load_attrs()
  221. if not to_load or (
  222. {self.key}.union(self._attribute_keys)
  223. ).intersection(to_load):
  224. _load_refresh_handler(state, context, to_load, is_refresh=True)
  225. def _load_refresh_handler(state, context, to_load, is_refresh):
  226. dict_ = state.dict
  227. # if context indicates we are coming from the
  228. # fget() handler, this already set the value; skip the
  229. # handler here. (other handlers like mutablecomposite will still
  230. # want to catch it)
  231. # there's an insufficiency here in that the fget() handler
  232. # really should not be using the refresh event and there should
  233. # be some other event that mutablecomposite can subscribe
  234. # towards for this.
  235. if (
  236. not is_refresh or context is self._COMPOSITE_FGET
  237. ) and self.key in dict_:
  238. return
  239. # if column elements aren't loaded, skip.
  240. # __get__() will initiate a load for those
  241. # columns
  242. for k in self._attribute_keys:
  243. if k not in dict_:
  244. return
  245. dict_[self.key] = self.composite_class(
  246. *[state.dict[key] for key in self._attribute_keys]
  247. )
  248. def expire_handler(state, keys):
  249. if keys is None or set(self._attribute_keys).intersection(keys):
  250. state.dict.pop(self.key, None)
  251. def insert_update_handler(mapper, connection, state):
  252. """After an insert or update, some columns may be expired due
  253. to server side defaults, or re-populated due to client side
  254. defaults. Pop out the composite value here so that it
  255. recreates.
  256. """
  257. state.dict.pop(self.key, None)
  258. event.listen(
  259. self.parent, "after_insert", insert_update_handler, raw=True
  260. )
  261. event.listen(
  262. self.parent, "after_update", insert_update_handler, raw=True
  263. )
  264. event.listen(
  265. self.parent, "load", load_handler, raw=True, propagate=True
  266. )
  267. event.listen(
  268. self.parent, "refresh", refresh_handler, raw=True, propagate=True
  269. )
  270. event.listen(
  271. self.parent, "expire", expire_handler, raw=True, propagate=True
  272. )
  273. # TODO: need a deserialize hook here
  274. @util.memoized_property
  275. def _attribute_keys(self):
  276. return [prop.key for prop in self.props]
  277. def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
  278. """Provided for userland code that uses attributes.get_history()."""
  279. added = []
  280. deleted = []
  281. has_history = False
  282. for prop in self.props:
  283. key = prop.key
  284. hist = state.manager[key].impl.get_history(state, dict_)
  285. if hist.has_changes():
  286. has_history = True
  287. non_deleted = hist.non_deleted()
  288. if non_deleted:
  289. added.extend(non_deleted)
  290. else:
  291. added.append(None)
  292. if hist.deleted:
  293. deleted.extend(hist.deleted)
  294. else:
  295. deleted.append(None)
  296. if has_history:
  297. return attributes.History(
  298. [self.composite_class(*added)],
  299. (),
  300. [self.composite_class(*deleted)],
  301. )
  302. else:
  303. return attributes.History((), [self.composite_class(*added)], ())
  304. def _comparator_factory(self, mapper):
  305. return self.comparator_factory(self, mapper)
  306. class CompositeBundle(orm_util.Bundle):
  307. def __init__(self, property_, expr):
  308. self.property = property_
  309. super(CompositeProperty.CompositeBundle, self).__init__(
  310. property_.key, *expr
  311. )
  312. def create_row_processor(self, query, procs, labels):
  313. def proc(row):
  314. return self.property.composite_class(
  315. *[proc(row) for proc in procs]
  316. )
  317. return proc
  318. class Comparator(PropComparator):
  319. """Produce boolean, comparison, and other operators for
  320. :class:`.CompositeProperty` attributes.
  321. See the example in :ref:`composite_operations` for an overview
  322. of usage , as well as the documentation for :class:`.PropComparator`.
  323. .. seealso::
  324. :class:`.PropComparator`
  325. :class:`.ColumnOperators`
  326. :ref:`types_operators`
  327. :attr:`.TypeEngine.comparator_factory`
  328. """
  329. __hash__ = None
  330. @util.memoized_property
  331. def clauses(self):
  332. return expression.ClauseList(
  333. group=False, *self._comparable_elements
  334. )
  335. def __clause_element__(self):
  336. return self.expression
  337. @util.memoized_property
  338. def expression(self):
  339. clauses = self.clauses._annotate(
  340. {
  341. "parententity": self._parententity,
  342. "parentmapper": self._parententity,
  343. "proxy_key": self.prop.key,
  344. }
  345. )
  346. return CompositeProperty.CompositeBundle(self.prop, clauses)
  347. def _bulk_update_tuples(self, value):
  348. if isinstance(value, sql.elements.BindParameter):
  349. value = value.value
  350. if value is None:
  351. values = [None for key in self.prop._attribute_keys]
  352. elif isinstance(value, self.prop.composite_class):
  353. values = value.__composite_values__()
  354. else:
  355. raise sa_exc.ArgumentError(
  356. "Can't UPDATE composite attribute %s to %r"
  357. % (self.prop, value)
  358. )
  359. return zip(self._comparable_elements, values)
  360. @util.memoized_property
  361. def _comparable_elements(self):
  362. if self._adapt_to_entity:
  363. return [
  364. getattr(self._adapt_to_entity.entity, prop.key)
  365. for prop in self.prop._comparable_elements
  366. ]
  367. else:
  368. return self.prop._comparable_elements
  369. def __eq__(self, other):
  370. if other is None:
  371. values = [None] * len(self.prop._comparable_elements)
  372. else:
  373. values = other.__composite_values__()
  374. comparisons = [
  375. a == b for a, b in zip(self.prop._comparable_elements, values)
  376. ]
  377. if self._adapt_to_entity:
  378. comparisons = [self.adapter(x) for x in comparisons]
  379. return sql.and_(*comparisons)
  380. def __ne__(self, other):
  381. return sql.not_(self.__eq__(other))
  382. def __str__(self):
  383. return str(self.parent.class_.__name__) + "." + self.key
  384. class ConcreteInheritedProperty(DescriptorProperty):
  385. """A 'do nothing' :class:`.MapperProperty` that disables
  386. an attribute on a concrete subclass that is only present
  387. on the inherited mapper, not the concrete classes' mapper.
  388. Cases where this occurs include:
  389. * When the superclass mapper is mapped against a
  390. "polymorphic union", which includes all attributes from
  391. all subclasses.
  392. * When a relationship() is configured on an inherited mapper,
  393. but not on the subclass mapper. Concrete mappers require
  394. that relationship() is configured explicitly on each
  395. subclass.
  396. """
  397. def _comparator_factory(self, mapper):
  398. comparator_callable = None
  399. for m in self.parent.iterate_to_root():
  400. p = m._props[self.key]
  401. if not isinstance(p, ConcreteInheritedProperty):
  402. comparator_callable = p.comparator_factory
  403. break
  404. return comparator_callable
  405. def __init__(self):
  406. super(ConcreteInheritedProperty, self).__init__()
  407. def warn():
  408. raise AttributeError(
  409. "Concrete %s does not implement "
  410. "attribute %r at the instance level. Add "
  411. "this property explicitly to %s."
  412. % (self.parent, self.key, self.parent)
  413. )
  414. class NoninheritedConcreteProp(object):
  415. def __set__(s, obj, value):
  416. warn()
  417. def __delete__(s, obj):
  418. warn()
  419. def __get__(s, obj, owner):
  420. if obj is None:
  421. return self.descriptor
  422. warn()
  423. self.descriptor = NoninheritedConcreteProp()
  424. class SynonymProperty(DescriptorProperty):
  425. def __init__(
  426. self,
  427. name,
  428. map_column=None,
  429. descriptor=None,
  430. comparator_factory=None,
  431. doc=None,
  432. info=None,
  433. ):
  434. """Denote an attribute name as a synonym to a mapped property,
  435. in that the attribute will mirror the value and expression behavior
  436. of another attribute.
  437. e.g.::
  438. class MyClass(Base):
  439. __tablename__ = 'my_table'
  440. id = Column(Integer, primary_key=True)
  441. job_status = Column(String(50))
  442. status = synonym("job_status")
  443. :param name: the name of the existing mapped property. This
  444. can refer to the string name ORM-mapped attribute
  445. configured on the class, including column-bound attributes
  446. and relationships.
  447. :param descriptor: a Python :term:`descriptor` that will be used
  448. as a getter (and potentially a setter) when this attribute is
  449. accessed at the instance level.
  450. :param map_column: **For classical mappings and mappings against
  451. an existing Table object only**. if ``True``, the :func:`.synonym`
  452. construct will locate the :class:`_schema.Column`
  453. object upon the mapped
  454. table that would normally be associated with the attribute name of
  455. this synonym, and produce a new :class:`.ColumnProperty` that instead
  456. maps this :class:`_schema.Column`
  457. to the alternate name given as the "name"
  458. argument of the synonym; in this way, the usual step of redefining
  459. the mapping of the :class:`_schema.Column`
  460. to be under a different name is
  461. unnecessary. This is usually intended to be used when a
  462. :class:`_schema.Column`
  463. is to be replaced with an attribute that also uses a
  464. descriptor, that is, in conjunction with the
  465. :paramref:`.synonym.descriptor` parameter::
  466. my_table = Table(
  467. "my_table", metadata,
  468. Column('id', Integer, primary_key=True),
  469. Column('job_status', String(50))
  470. )
  471. class MyClass(object):
  472. @property
  473. def _job_status_descriptor(self):
  474. return "Status: %s" % self._job_status
  475. mapper(
  476. MyClass, my_table, properties={
  477. "job_status": synonym(
  478. "_job_status", map_column=True,
  479. descriptor=MyClass._job_status_descriptor)
  480. }
  481. )
  482. Above, the attribute named ``_job_status`` is automatically
  483. mapped to the ``job_status`` column::
  484. >>> j1 = MyClass()
  485. >>> j1._job_status = "employed"
  486. >>> j1.job_status
  487. Status: employed
  488. When using Declarative, in order to provide a descriptor in
  489. conjunction with a synonym, use the
  490. :func:`sqlalchemy.ext.declarative.synonym_for` helper. However,
  491. note that the :ref:`hybrid properties <mapper_hybrids>` feature
  492. should usually be preferred, particularly when redefining attribute
  493. behavior.
  494. :param info: Optional data dictionary which will be populated into the
  495. :attr:`.InspectionAttr.info` attribute of this object.
  496. .. versionadded:: 1.0.0
  497. :param comparator_factory: A subclass of :class:`.PropComparator`
  498. that will provide custom comparison behavior at the SQL expression
  499. level.
  500. .. note::
  501. For the use case of providing an attribute which redefines both
  502. Python-level and SQL-expression level behavior of an attribute,
  503. please refer to the Hybrid attribute introduced at
  504. :ref:`mapper_hybrids` for a more effective technique.
  505. .. seealso::
  506. :ref:`synonyms` - Overview of synonyms
  507. :func:`.synonym_for` - a helper oriented towards Declarative
  508. :ref:`mapper_hybrids` - The Hybrid Attribute extension provides an
  509. updated approach to augmenting attribute behavior more flexibly
  510. than can be achieved with synonyms.
  511. """
  512. super(SynonymProperty, self).__init__()
  513. self.name = name
  514. self.map_column = map_column
  515. self.descriptor = descriptor
  516. self.comparator_factory = comparator_factory
  517. self.doc = doc or (descriptor and descriptor.__doc__) or None
  518. if info:
  519. self.info = info
  520. util.set_creation_order(self)
  521. @property
  522. def uses_objects(self):
  523. return getattr(self.parent.class_, self.name).impl.uses_objects
  524. # TODO: when initialized, check _proxied_object,
  525. # emit a warning if its not a column-based property
  526. @util.memoized_property
  527. def _proxied_object(self):
  528. attr = getattr(self.parent.class_, self.name)
  529. if not hasattr(attr, "property") or not isinstance(
  530. attr.property, MapperProperty
  531. ):
  532. # attribute is a non-MapperProprerty proxy such as
  533. # hybrid or association proxy
  534. if isinstance(attr, attributes.QueryableAttribute):
  535. return attr.comparator
  536. elif isinstance(attr, operators.ColumnOperators):
  537. return attr
  538. raise sa_exc.InvalidRequestError(
  539. """synonym() attribute "%s.%s" only supports """
  540. """ORM mapped attributes, got %r"""
  541. % (self.parent.class_.__name__, self.name, attr)
  542. )
  543. return attr.property
  544. def _comparator_factory(self, mapper):
  545. prop = self._proxied_object
  546. if isinstance(prop, MapperProperty):
  547. if self.comparator_factory:
  548. comp = self.comparator_factory(prop, mapper)
  549. else:
  550. comp = prop.comparator_factory(prop, mapper)
  551. return comp
  552. else:
  553. return prop
  554. def get_history(self, *arg, **kw):
  555. attr = getattr(self.parent.class_, self.name)
  556. return attr.impl.get_history(*arg, **kw)
  557. @util.preload_module("sqlalchemy.orm.properties")
  558. def set_parent(self, parent, init):
  559. properties = util.preloaded.orm_properties
  560. if self.map_column:
  561. # implement the 'map_column' option.
  562. if self.key not in parent.persist_selectable.c:
  563. raise sa_exc.ArgumentError(
  564. "Can't compile synonym '%s': no column on table "
  565. "'%s' named '%s'"
  566. % (
  567. self.name,
  568. parent.persist_selectable.description,
  569. self.key,
  570. )
  571. )
  572. elif (
  573. parent.persist_selectable.c[self.key]
  574. in parent._columntoproperty
  575. and parent._columntoproperty[
  576. parent.persist_selectable.c[self.key]
  577. ].key
  578. == self.name
  579. ):
  580. raise sa_exc.ArgumentError(
  581. "Can't call map_column=True for synonym %r=%r, "
  582. "a ColumnProperty already exists keyed to the name "
  583. "%r for column %r"
  584. % (self.key, self.name, self.name, self.key)
  585. )
  586. p = properties.ColumnProperty(
  587. parent.persist_selectable.c[self.key]
  588. )
  589. parent._configure_property(self.name, p, init=init, setparent=True)
  590. p._mapped_by_synonym = self.key
  591. self.parent = parent