descriptor_props.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. # orm/descriptor_props.py
  2. # Copyright (C) 2005-2017 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 .interfaces import MapperProperty, PropComparator
  12. from .util import _none_set
  13. from . import attributes
  14. from .. import util, sql, exc as sa_exc, event, schema
  15. from ..sql import expression
  16. from . import properties
  17. from . import query
  18. class DescriptorProperty(MapperProperty):
  19. """:class:`.MapperProperty` which proxies access to a
  20. user-defined descriptor."""
  21. doc = None
  22. def instrument_class(self, mapper):
  23. prop = self
  24. class _ProxyImpl(object):
  25. accepts_scalar_loader = False
  26. expire_missing = True
  27. collection = False
  28. def __init__(self, key):
  29. self.key = key
  30. if hasattr(prop, 'get_history'):
  31. def get_history(self, state, dict_,
  32. passive=attributes.PASSIVE_OFF):
  33. return prop.get_history(state, dict_, passive)
  34. if self.descriptor is None:
  35. desc = getattr(mapper.class_, self.key, None)
  36. if mapper._is_userland_descriptor(desc):
  37. self.descriptor = desc
  38. if self.descriptor is None:
  39. def fset(obj, value):
  40. setattr(obj, self.name, value)
  41. def fdel(obj):
  42. delattr(obj, self.name)
  43. def fget(obj):
  44. return getattr(obj, self.name)
  45. self.descriptor = property(
  46. fget=fget,
  47. fset=fset,
  48. fdel=fdel,
  49. )
  50. proxy_attr = attributes.create_proxied_attribute(
  51. self.descriptor)(
  52. self.parent.class_,
  53. self.key,
  54. self.descriptor,
  55. lambda: self._comparator_factory(mapper),
  56. doc=self.doc,
  57. original_property=self
  58. )
  59. proxy_attr.impl = _ProxyImpl(self.key)
  60. mapper.class_manager.instrument_attribute(self.key, proxy_attr)
  61. @util.langhelpers.dependency_for("sqlalchemy.orm.properties")
  62. class CompositeProperty(DescriptorProperty):
  63. """Defines a "composite" mapped attribute, representing a collection
  64. of columns as one attribute.
  65. :class:`.CompositeProperty` is constructed using the :func:`.composite`
  66. function.
  67. .. seealso::
  68. :ref:`mapper_composite`
  69. """
  70. def __init__(self, class_, *attrs, **kwargs):
  71. r"""Return a composite column-based property for use with a Mapper.
  72. See the mapping documentation section :ref:`mapper_composite` for a
  73. full usage example.
  74. The :class:`.MapperProperty` returned by :func:`.composite`
  75. is the :class:`.CompositeProperty`.
  76. :param class\_:
  77. The "composite type" class.
  78. :param \*cols:
  79. List of Column objects to be mapped.
  80. :param active_history=False:
  81. When ``True``, indicates that the "previous" value for a
  82. scalar attribute should be loaded when replaced, if not
  83. already loaded. See the same flag on :func:`.column_property`.
  84. .. versionchanged:: 0.7
  85. This flag specifically becomes meaningful
  86. - previously it was a placeholder.
  87. :param group:
  88. A group name for this property when marked as deferred.
  89. :param deferred:
  90. When True, the column property is "deferred", meaning that it does
  91. not load immediately, and is instead loaded when the attribute is
  92. first accessed on an instance. See also
  93. :func:`~sqlalchemy.orm.deferred`.
  94. :param comparator_factory: a class which extends
  95. :class:`.CompositeProperty.Comparator` which provides custom SQL
  96. clause generation for comparison operations.
  97. :param doc:
  98. optional string that will be applied as the doc on the
  99. class-bound descriptor.
  100. :param info: Optional data dictionary which will be populated into the
  101. :attr:`.MapperProperty.info` attribute of this object.
  102. .. versionadded:: 0.8
  103. :param extension:
  104. an :class:`.AttributeExtension` instance,
  105. or list of extensions, which will be prepended to the list of
  106. attribute listeners for the resulting descriptor placed on the
  107. class. **Deprecated.** Please see :class:`.AttributeEvents`.
  108. """
  109. super(CompositeProperty, self).__init__()
  110. self.attrs = attrs
  111. self.composite_class = class_
  112. self.active_history = kwargs.get('active_history', False)
  113. self.deferred = kwargs.get('deferred', False)
  114. self.group = kwargs.get('group', None)
  115. self.comparator_factory = kwargs.pop('comparator_factory',
  116. self.__class__.Comparator)
  117. if 'info' in kwargs:
  118. self.info = kwargs.pop('info')
  119. util.set_creation_order(self)
  120. self._create_descriptor()
  121. def instrument_class(self, mapper):
  122. super(CompositeProperty, self).instrument_class(mapper)
  123. self._setup_event_handlers()
  124. def do_init(self):
  125. """Initialization which occurs after the :class:`.CompositeProperty`
  126. has been associated with its parent mapper.
  127. """
  128. self._setup_arguments_on_columns()
  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)
  142. for key in self._attribute_keys
  143. ]
  144. # current expected behavior here is that the composite is
  145. # created on access if the object is persistent or if
  146. # col attributes have non-None. This would be better
  147. # if the composite were created unconditionally,
  148. # but that would be a behavioral change.
  149. if self.key not in dict_ and (
  150. state.key is not None or
  151. not _none_set.issuperset(values)
  152. ):
  153. dict_[self.key] = self.composite_class(*values)
  154. state.manager.dispatch.refresh(state, None, [self.key])
  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,
  170. value.__composite_values__()):
  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 [
  184. getattr(self.parent.class_, prop.key)
  185. for prop in self.props
  186. ]
  187. @util.memoized_property
  188. def props(self):
  189. props = []
  190. for attr in self.attrs:
  191. if isinstance(attr, str):
  192. prop = self.parent.get_property(
  193. attr, _configure_mappers=False)
  194. elif isinstance(attr, schema.Column):
  195. prop = self.parent._columntoproperty[attr]
  196. elif isinstance(attr, attributes.InstrumentedAttribute):
  197. prop = attr.property
  198. else:
  199. raise sa_exc.ArgumentError(
  200. "Composite expects Column objects or mapped "
  201. "attributes/attribute names as arguments, got: %r"
  202. % (attr,))
  203. props.append(prop)
  204. return props
  205. @property
  206. def columns(self):
  207. return [a for a in self.attrs if isinstance(a, schema.Column)]
  208. def _setup_arguments_on_columns(self):
  209. """Propagate configuration arguments made on this composite
  210. to the target columns, for those that apply.
  211. """
  212. for prop in self.props:
  213. prop.active_history = self.active_history
  214. if self.deferred:
  215. prop.deferred = self.deferred
  216. prop.strategy_key = (
  217. ("deferred", True),
  218. ("instrument", True))
  219. prop.group = self.group
  220. def _setup_event_handlers(self):
  221. """Establish events that populate/expire the composite attribute."""
  222. def load_handler(state, *args):
  223. dict_ = state.dict
  224. if self.key in dict_:
  225. return
  226. # if column elements aren't loaded, skip.
  227. # __get__() will initiate a load for those
  228. # columns
  229. for k in self._attribute_keys:
  230. if k not in dict_:
  231. return
  232. # assert self.key not in dict_
  233. dict_[self.key] = self.composite_class(
  234. *[state.dict[key] for key in
  235. self._attribute_keys]
  236. )
  237. def expire_handler(state, keys):
  238. if keys is None or set(self._attribute_keys).intersection(keys):
  239. state.dict.pop(self.key, None)
  240. def insert_update_handler(mapper, connection, state):
  241. """After an insert or update, some columns may be expired due
  242. to server side defaults, or re-populated due to client side
  243. defaults. Pop out the composite value here so that it
  244. recreates.
  245. """
  246. state.dict.pop(self.key, None)
  247. event.listen(self.parent, 'after_insert',
  248. insert_update_handler, raw=True)
  249. event.listen(self.parent, 'after_update',
  250. insert_update_handler, raw=True)
  251. event.listen(self.parent, 'load',
  252. load_handler, raw=True, propagate=True)
  253. event.listen(self.parent, 'refresh',
  254. load_handler, raw=True, propagate=True)
  255. event.listen(self.parent, 'expire',
  256. expire_handler, raw=True, propagate=True)
  257. # TODO: need a deserialize hook here
  258. @util.memoized_property
  259. def _attribute_keys(self):
  260. return [
  261. prop.key for prop in self.props
  262. ]
  263. def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
  264. """Provided for userland code that uses attributes.get_history()."""
  265. added = []
  266. deleted = []
  267. has_history = False
  268. for prop in self.props:
  269. key = prop.key
  270. hist = state.manager[key].impl.get_history(state, dict_)
  271. if hist.has_changes():
  272. has_history = True
  273. non_deleted = hist.non_deleted()
  274. if non_deleted:
  275. added.extend(non_deleted)
  276. else:
  277. added.append(None)
  278. if hist.deleted:
  279. deleted.extend(hist.deleted)
  280. else:
  281. deleted.append(None)
  282. if has_history:
  283. return attributes.History(
  284. [self.composite_class(*added)],
  285. (),
  286. [self.composite_class(*deleted)]
  287. )
  288. else:
  289. return attributes.History(
  290. (), [self.composite_class(*added)], ()
  291. )
  292. def _comparator_factory(self, mapper):
  293. return self.comparator_factory(self, mapper)
  294. class CompositeBundle(query.Bundle):
  295. def __init__(self, property, expr):
  296. self.property = property
  297. super(CompositeProperty.CompositeBundle, self).__init__(
  298. property.key, *expr)
  299. def create_row_processor(self, query, procs, labels):
  300. def proc(row):
  301. return self.property.composite_class(
  302. *[proc(row) for proc in procs])
  303. return proc
  304. class Comparator(PropComparator):
  305. """Produce boolean, comparison, and other operators for
  306. :class:`.CompositeProperty` attributes.
  307. See the example in :ref:`composite_operations` for an overview
  308. of usage , as well as the documentation for :class:`.PropComparator`.
  309. See also:
  310. :class:`.PropComparator`
  311. :class:`.ColumnOperators`
  312. :ref:`types_operators`
  313. :attr:`.TypeEngine.comparator_factory`
  314. """
  315. __hash__ = None
  316. @property
  317. def clauses(self):
  318. return self.__clause_element__()
  319. def __clause_element__(self):
  320. return expression.ClauseList(
  321. group=False, *self._comparable_elements)
  322. def _query_clause_element(self):
  323. return CompositeProperty.CompositeBundle(
  324. self.prop, self.__clause_element__())
  325. @util.memoized_property
  326. def _comparable_elements(self):
  327. if self._adapt_to_entity:
  328. return [
  329. getattr(
  330. self._adapt_to_entity.entity,
  331. prop.key
  332. ) for prop in self.prop._comparable_elements
  333. ]
  334. else:
  335. return self.prop._comparable_elements
  336. def __eq__(self, other):
  337. if other is None:
  338. values = [None] * len(self.prop._comparable_elements)
  339. else:
  340. values = other.__composite_values__()
  341. comparisons = [
  342. a == b
  343. for a, b in zip(self.prop._comparable_elements, values)
  344. ]
  345. if self._adapt_to_entity:
  346. comparisons = [self.adapter(x) for x in comparisons]
  347. return sql.and_(*comparisons)
  348. def __ne__(self, other):
  349. return sql.not_(self.__eq__(other))
  350. def __str__(self):
  351. return str(self.parent.class_.__name__) + "." + self.key
  352. @util.langhelpers.dependency_for("sqlalchemy.orm.properties")
  353. class ConcreteInheritedProperty(DescriptorProperty):
  354. """A 'do nothing' :class:`.MapperProperty` that disables
  355. an attribute on a concrete subclass that is only present
  356. on the inherited mapper, not the concrete classes' mapper.
  357. Cases where this occurs include:
  358. * When the superclass mapper is mapped against a
  359. "polymorphic union", which includes all attributes from
  360. all subclasses.
  361. * When a relationship() is configured on an inherited mapper,
  362. but not on the subclass mapper. Concrete mappers require
  363. that relationship() is configured explicitly on each
  364. subclass.
  365. """
  366. def _comparator_factory(self, mapper):
  367. comparator_callable = None
  368. for m in self.parent.iterate_to_root():
  369. p = m._props[self.key]
  370. if not isinstance(p, ConcreteInheritedProperty):
  371. comparator_callable = p.comparator_factory
  372. break
  373. return comparator_callable
  374. def __init__(self):
  375. super(ConcreteInheritedProperty, self).__init__()
  376. def warn():
  377. raise AttributeError("Concrete %s does not implement "
  378. "attribute %r at the instance level. Add "
  379. "this property explicitly to %s." %
  380. (self.parent, self.key, self.parent))
  381. class NoninheritedConcreteProp(object):
  382. def __set__(s, obj, value):
  383. warn()
  384. def __delete__(s, obj):
  385. warn()
  386. def __get__(s, obj, owner):
  387. if obj is None:
  388. return self.descriptor
  389. warn()
  390. self.descriptor = NoninheritedConcreteProp()
  391. @util.langhelpers.dependency_for("sqlalchemy.orm.properties")
  392. class SynonymProperty(DescriptorProperty):
  393. def __init__(self, name, map_column=None,
  394. descriptor=None, comparator_factory=None,
  395. doc=None, info=None):
  396. """Denote an attribute name as a synonym to a mapped property,
  397. in that the attribute will mirror the value and expression behavior
  398. of another attribute.
  399. :param name: the name of the existing mapped property. This
  400. can refer to the string name of any :class:`.MapperProperty`
  401. configured on the class, including column-bound attributes
  402. and relationships.
  403. :param descriptor: a Python :term:`descriptor` that will be used
  404. as a getter (and potentially a setter) when this attribute is
  405. accessed at the instance level.
  406. :param map_column: if ``True``, the :func:`.synonym` construct will
  407. locate the existing named :class:`.MapperProperty` based on the
  408. attribute name of this :func:`.synonym`, and assign it to a new
  409. attribute linked to the name of this :func:`.synonym`.
  410. That is, given a mapping like::
  411. class MyClass(Base):
  412. __tablename__ = 'my_table'
  413. id = Column(Integer, primary_key=True)
  414. job_status = Column(String(50))
  415. job_status = synonym("_job_status", map_column=True)
  416. The above class ``MyClass`` will now have the ``job_status``
  417. :class:`.Column` object mapped to the attribute named
  418. ``_job_status``, and the attribute named ``job_status`` will refer
  419. to the synonym itself. This feature is typically used in
  420. conjunction with the ``descriptor`` argument in order to link a
  421. user-defined descriptor as a "wrapper" for an existing column.
  422. :param info: Optional data dictionary which will be populated into the
  423. :attr:`.InspectionAttr.info` attribute of this object.
  424. .. versionadded:: 1.0.0
  425. :param comparator_factory: A subclass of :class:`.PropComparator`
  426. that will provide custom comparison behavior at the SQL expression
  427. level.
  428. .. note::
  429. For the use case of providing an attribute which redefines both
  430. Python-level and SQL-expression level behavior of an attribute,
  431. please refer to the Hybrid attribute introduced at
  432. :ref:`mapper_hybrids` for a more effective technique.
  433. .. seealso::
  434. :ref:`synonyms` - examples of functionality.
  435. :ref:`mapper_hybrids` - Hybrids provide a better approach for
  436. more complicated attribute-wrapping schemes than synonyms.
  437. """
  438. super(SynonymProperty, self).__init__()
  439. self.name = name
  440. self.map_column = map_column
  441. self.descriptor = descriptor
  442. self.comparator_factory = comparator_factory
  443. self.doc = doc or (descriptor and descriptor.__doc__) or None
  444. if info:
  445. self.info = info
  446. util.set_creation_order(self)
  447. # TODO: when initialized, check _proxied_property,
  448. # emit a warning if its not a column-based property
  449. @util.memoized_property
  450. def _proxied_property(self):
  451. return getattr(self.parent.class_, self.name).property
  452. def _comparator_factory(self, mapper):
  453. prop = self._proxied_property
  454. if self.comparator_factory:
  455. comp = self.comparator_factory(prop, mapper)
  456. else:
  457. comp = prop.comparator_factory(prop, mapper)
  458. return comp
  459. def set_parent(self, parent, init):
  460. if self.map_column:
  461. # implement the 'map_column' option.
  462. if self.key not in parent.mapped_table.c:
  463. raise sa_exc.ArgumentError(
  464. "Can't compile synonym '%s': no column on table "
  465. "'%s' named '%s'"
  466. % (self.name, parent.mapped_table.description, self.key))
  467. elif parent.mapped_table.c[self.key] in \
  468. parent._columntoproperty and \
  469. parent._columntoproperty[
  470. parent.mapped_table.c[self.key]
  471. ].key == self.name:
  472. raise sa_exc.ArgumentError(
  473. "Can't call map_column=True for synonym %r=%r, "
  474. "a ColumnProperty already exists keyed to the name "
  475. "%r for column %r" %
  476. (self.key, self.name, self.name, self.key)
  477. )
  478. p = properties.ColumnProperty(parent.mapped_table.c[self.key])
  479. parent._configure_property(
  480. self.name, p,
  481. init=init,
  482. setparent=True)
  483. p._mapped_by_synonym = self.key
  484. self.parent = parent
  485. @util.langhelpers.dependency_for("sqlalchemy.orm.properties")
  486. class ComparableProperty(DescriptorProperty):
  487. """Instruments a Python property for use in query expressions."""
  488. def __init__(
  489. self, comparator_factory, descriptor=None, doc=None, info=None):
  490. """Provides a method of applying a :class:`.PropComparator`
  491. to any Python descriptor attribute.
  492. .. versionchanged:: 0.7
  493. :func:`.comparable_property` is superseded by
  494. the :mod:`~sqlalchemy.ext.hybrid` extension. See the example
  495. at :ref:`hybrid_custom_comparators`.
  496. Allows any Python descriptor to behave like a SQL-enabled
  497. attribute when used at the class level in queries, allowing
  498. redefinition of expression operator behavior.
  499. In the example below we redefine :meth:`.PropComparator.operate`
  500. to wrap both sides of an expression in ``func.lower()`` to produce
  501. case-insensitive comparison::
  502. from sqlalchemy.orm import comparable_property
  503. from sqlalchemy.orm.interfaces import PropComparator
  504. from sqlalchemy.sql import func
  505. from sqlalchemy import Integer, String, Column
  506. from sqlalchemy.ext.declarative import declarative_base
  507. class CaseInsensitiveComparator(PropComparator):
  508. def __clause_element__(self):
  509. return self.prop
  510. def operate(self, op, other):
  511. return op(
  512. func.lower(self.__clause_element__()),
  513. func.lower(other)
  514. )
  515. Base = declarative_base()
  516. class SearchWord(Base):
  517. __tablename__ = 'search_word'
  518. id = Column(Integer, primary_key=True)
  519. word = Column(String)
  520. word_insensitive = comparable_property(lambda prop, mapper:
  521. CaseInsensitiveComparator(
  522. mapper.c.word, mapper)
  523. )
  524. A mapping like the above allows the ``word_insensitive`` attribute
  525. to render an expression like::
  526. >>> print SearchWord.word_insensitive == "Trucks"
  527. lower(search_word.word) = lower(:lower_1)
  528. :param comparator_factory:
  529. A PropComparator subclass or factory that defines operator behavior
  530. for this property.
  531. :param descriptor:
  532. Optional when used in a ``properties={}`` declaration. The Python
  533. descriptor or property to layer comparison behavior on top of.
  534. The like-named descriptor will be automatically retrieved from the
  535. mapped class if left blank in a ``properties`` declaration.
  536. :param info: Optional data dictionary which will be populated into the
  537. :attr:`.InspectionAttr.info` attribute of this object.
  538. .. versionadded:: 1.0.0
  539. """
  540. super(ComparableProperty, self).__init__()
  541. self.descriptor = descriptor
  542. self.comparator_factory = comparator_factory
  543. self.doc = doc or (descriptor and descriptor.__doc__) or None
  544. if info:
  545. self.info = info
  546. util.set_creation_order(self)
  547. def _comparator_factory(self, mapper):
  548. return self.comparator_factory(self, mapper)