You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

745 lines
25KB

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