Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/sqlalchemy/orm/descriptor_props.py : 22%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# orm/descriptor_props.py
2# Copyright (C) 2005-2020 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
8"""Descriptor properties are more "auxiliary" properties
9that exist as configurational elements, but don't participate
10as actively in the load/persist ORM loop.
12"""
14from . import attributes
15from . import properties
16from . import query
17from .interfaces import MapperProperty
18from .interfaces import PropComparator
19from .util import _none_set
20from .. import event
21from .. import exc as sa_exc
22from .. import schema
23from .. import sql
24from .. import util
25from ..sql import expression
28class DescriptorProperty(MapperProperty):
29 """:class:`.MapperProperty` which proxies access to a
30 user-defined descriptor."""
32 doc = None
34 uses_objects = False
36 def instrument_class(self, mapper):
37 prop = self
39 class _ProxyImpl(object):
40 accepts_scalar_loader = False
41 expire_missing = True
42 collection = False
44 @property
45 def uses_objects(self):
46 return prop.uses_objects
48 def __init__(self, key):
49 self.key = key
51 if hasattr(prop, "get_history"):
53 def get_history(
54 self, state, dict_, passive=attributes.PASSIVE_OFF
55 ):
56 return prop.get_history(state, dict_, passive)
58 if self.descriptor is None:
59 desc = getattr(mapper.class_, self.key, None)
60 if mapper._is_userland_descriptor(desc):
61 self.descriptor = desc
63 if self.descriptor is None:
65 def fset(obj, value):
66 setattr(obj, self.name, value)
68 def fdel(obj):
69 delattr(obj, self.name)
71 def fget(obj):
72 return getattr(obj, self.name)
74 self.descriptor = property(fget=fget, fset=fset, fdel=fdel)
76 proxy_attr = attributes.create_proxied_attribute(self.descriptor)(
77 self.parent.class_,
78 self.key,
79 self.descriptor,
80 lambda: self._comparator_factory(mapper),
81 doc=self.doc,
82 original_property=self,
83 )
84 proxy_attr.impl = _ProxyImpl(self.key)
85 mapper.class_manager.instrument_attribute(self.key, proxy_attr)
88@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True)
89class CompositeProperty(DescriptorProperty):
90 """Defines a "composite" mapped attribute, representing a collection
91 of columns as one attribute.
93 :class:`.CompositeProperty` is constructed using the :func:`.composite`
94 function.
96 .. seealso::
98 :ref:`mapper_composite`
100 """
102 @util.deprecated_params(
103 extension=(
104 "0.7",
105 ":class:`.AttributeExtension` is deprecated in favor of the "
106 ":class:`.AttributeEvents` listener interface. The "
107 ":paramref:`.composite.extension` parameter will be "
108 "removed in a future release.",
109 )
110 )
111 def __init__(self, class_, *attrs, **kwargs):
112 r"""Return a composite column-based property for use with a Mapper.
114 See the mapping documentation section :ref:`mapper_composite` for a
115 full usage example.
117 The :class:`.MapperProperty` returned by :func:`.composite`
118 is the :class:`.CompositeProperty`.
120 :param class\_:
121 The "composite type" class, or any classmethod or callable which
122 will produce a new instance of the composite object given the
123 column values in order.
125 :param \*cols:
126 List of Column objects to be mapped.
128 :param active_history=False:
129 When ``True``, indicates that the "previous" value for a
130 scalar attribute should be loaded when replaced, if not
131 already loaded. See the same flag on :func:`.column_property`.
133 :param group:
134 A group name for this property when marked as deferred.
136 :param deferred:
137 When True, the column property is "deferred", meaning that it does
138 not load immediately, and is instead loaded when the attribute is
139 first accessed on an instance. See also
140 :func:`~sqlalchemy.orm.deferred`.
142 :param comparator_factory: a class which extends
143 :class:`.CompositeProperty.Comparator` which provides custom SQL
144 clause generation for comparison operations.
146 :param doc:
147 optional string that will be applied as the doc on the
148 class-bound descriptor.
150 :param info: Optional data dictionary which will be populated into the
151 :attr:`.MapperProperty.info` attribute of this object.
153 :param extension:
154 an :class:`.AttributeExtension` instance,
155 or list of extensions, which will be prepended to the list of
156 attribute listeners for the resulting descriptor placed on the
157 class.
159 """
160 super(CompositeProperty, self).__init__()
162 self.attrs = attrs
163 self.composite_class = class_
164 self.active_history = kwargs.get("active_history", False)
165 self.deferred = kwargs.get("deferred", False)
166 self.group = kwargs.get("group", None)
167 self.comparator_factory = kwargs.pop(
168 "comparator_factory", self.__class__.Comparator
169 )
170 if "info" in kwargs:
171 self.info = kwargs.pop("info")
173 util.set_creation_order(self)
174 self._create_descriptor()
176 def instrument_class(self, mapper):
177 super(CompositeProperty, self).instrument_class(mapper)
178 self._setup_event_handlers()
180 def do_init(self):
181 """Initialization which occurs after the :class:`.CompositeProperty`
182 has been associated with its parent mapper.
184 """
185 self._setup_arguments_on_columns()
187 def _create_descriptor(self):
188 """Create the Python descriptor that will serve as
189 the access point on instances of the mapped class.
191 """
193 def fget(instance):
194 dict_ = attributes.instance_dict(instance)
195 state = attributes.instance_state(instance)
197 if self.key not in dict_:
198 # key not present. Iterate through related
199 # attributes, retrieve their values. This
200 # ensures they all load.
201 values = [
202 getattr(instance, key) for key in self._attribute_keys
203 ]
205 # current expected behavior here is that the composite is
206 # created on access if the object is persistent or if
207 # col attributes have non-None. This would be better
208 # if the composite were created unconditionally,
209 # but that would be a behavioral change.
210 if self.key not in dict_ and (
211 state.key is not None or not _none_set.issuperset(values)
212 ):
213 dict_[self.key] = self.composite_class(*values)
214 state.manager.dispatch.refresh(state, None, [self.key])
216 return dict_.get(self.key, None)
218 def fset(instance, value):
219 dict_ = attributes.instance_dict(instance)
220 state = attributes.instance_state(instance)
221 attr = state.manager[self.key]
222 previous = dict_.get(self.key, attributes.NO_VALUE)
223 for fn in attr.dispatch.set:
224 value = fn(state, value, previous, attr.impl)
225 dict_[self.key] = value
226 if value is None:
227 for key in self._attribute_keys:
228 setattr(instance, key, None)
229 else:
230 for key, value in zip(
231 self._attribute_keys, value.__composite_values__()
232 ):
233 setattr(instance, key, value)
235 def fdel(instance):
236 state = attributes.instance_state(instance)
237 dict_ = attributes.instance_dict(instance)
238 previous = dict_.pop(self.key, attributes.NO_VALUE)
239 attr = state.manager[self.key]
240 attr.dispatch.remove(state, previous, attr.impl)
241 for key in self._attribute_keys:
242 setattr(instance, key, None)
244 self.descriptor = property(fget, fset, fdel)
246 @util.memoized_property
247 def _comparable_elements(self):
248 return [getattr(self.parent.class_, prop.key) for prop in self.props]
250 @util.memoized_property
251 def props(self):
252 props = []
253 for attr in self.attrs:
254 if isinstance(attr, str):
255 prop = self.parent.get_property(attr, _configure_mappers=False)
256 elif isinstance(attr, schema.Column):
257 prop = self.parent._columntoproperty[attr]
258 elif isinstance(attr, attributes.InstrumentedAttribute):
259 prop = attr.property
260 else:
261 raise sa_exc.ArgumentError(
262 "Composite expects Column objects or mapped "
263 "attributes/attribute names as arguments, got: %r"
264 % (attr,)
265 )
266 props.append(prop)
267 return props
269 @property
270 def columns(self):
271 return [a for a in self.attrs if isinstance(a, schema.Column)]
273 def _setup_arguments_on_columns(self):
274 """Propagate configuration arguments made on this composite
275 to the target columns, for those that apply.
277 """
278 for prop in self.props:
279 prop.active_history = self.active_history
280 if self.deferred:
281 prop.deferred = self.deferred
282 prop.strategy_key = (("deferred", True), ("instrument", True))
283 prop.group = self.group
285 def _setup_event_handlers(self):
286 """Establish events that populate/expire the composite attribute."""
288 def load_handler(state, *args):
289 _load_refresh_handler(state, args, is_refresh=False)
291 def refresh_handler(state, *args):
292 _load_refresh_handler(state, args, is_refresh=True)
294 def _load_refresh_handler(state, args, is_refresh):
295 dict_ = state.dict
297 if not is_refresh and self.key in dict_:
298 return
300 # if column elements aren't loaded, skip.
301 # __get__() will initiate a load for those
302 # columns
303 for k in self._attribute_keys:
304 if k not in dict_:
305 return
307 dict_[self.key] = self.composite_class(
308 *[state.dict[key] for key in self._attribute_keys]
309 )
311 def expire_handler(state, keys):
312 if keys is None or set(self._attribute_keys).intersection(keys):
313 state.dict.pop(self.key, None)
315 def insert_update_handler(mapper, connection, state):
316 """After an insert or update, some columns may be expired due
317 to server side defaults, or re-populated due to client side
318 defaults. Pop out the composite value here so that it
319 recreates.
321 """
323 state.dict.pop(self.key, None)
325 event.listen(
326 self.parent, "after_insert", insert_update_handler, raw=True
327 )
328 event.listen(
329 self.parent, "after_update", insert_update_handler, raw=True
330 )
331 event.listen(
332 self.parent, "load", load_handler, raw=True, propagate=True
333 )
334 event.listen(
335 self.parent, "refresh", refresh_handler, raw=True, propagate=True
336 )
337 event.listen(
338 self.parent, "expire", expire_handler, raw=True, propagate=True
339 )
341 # TODO: need a deserialize hook here
343 @util.memoized_property
344 def _attribute_keys(self):
345 return [prop.key for prop in self.props]
347 def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
348 """Provided for userland code that uses attributes.get_history()."""
350 added = []
351 deleted = []
353 has_history = False
354 for prop in self.props:
355 key = prop.key
356 hist = state.manager[key].impl.get_history(state, dict_)
357 if hist.has_changes():
358 has_history = True
360 non_deleted = hist.non_deleted()
361 if non_deleted:
362 added.extend(non_deleted)
363 else:
364 added.append(None)
365 if hist.deleted:
366 deleted.extend(hist.deleted)
367 else:
368 deleted.append(None)
370 if has_history:
371 return attributes.History(
372 [self.composite_class(*added)],
373 (),
374 [self.composite_class(*deleted)],
375 )
376 else:
377 return attributes.History((), [self.composite_class(*added)], ())
379 def _comparator_factory(self, mapper):
380 return self.comparator_factory(self, mapper)
382 class CompositeBundle(query.Bundle):
383 def __init__(self, property_, expr):
384 self.property = property_
385 super(CompositeProperty.CompositeBundle, self).__init__(
386 property_.key, *expr
387 )
389 def create_row_processor(self, query, procs, labels):
390 def proc(row):
391 return self.property.composite_class(
392 *[proc(row) for proc in procs]
393 )
395 return proc
397 class Comparator(PropComparator):
398 """Produce boolean, comparison, and other operators for
399 :class:`.CompositeProperty` attributes.
401 See the example in :ref:`composite_operations` for an overview
402 of usage , as well as the documentation for :class:`.PropComparator`.
404 .. seealso::
406 :class:`.PropComparator`
408 :class:`.ColumnOperators`
410 :ref:`types_operators`
412 :attr:`.TypeEngine.comparator_factory`
414 """
416 __hash__ = None
418 @property
419 def clauses(self):
420 return self.__clause_element__()
422 def __clause_element__(self):
423 return expression.ClauseList(
424 group=False, *self._comparable_elements
425 )
427 def _query_clause_element(self):
428 return CompositeProperty.CompositeBundle(
429 self.prop, self.__clause_element__()
430 )
432 def _bulk_update_tuples(self, value):
433 if value is None:
434 values = [None for key in self.prop._attribute_keys]
435 elif isinstance(value, self.prop.composite_class):
436 values = value.__composite_values__()
437 else:
438 raise sa_exc.ArgumentError(
439 "Can't UPDATE composite attribute %s to %r"
440 % (self.prop, value)
441 )
443 return zip(self._comparable_elements, values)
445 @util.memoized_property
446 def _comparable_elements(self):
447 if self._adapt_to_entity:
448 return [
449 getattr(self._adapt_to_entity.entity, prop.key)
450 for prop in self.prop._comparable_elements
451 ]
452 else:
453 return self.prop._comparable_elements
455 def __eq__(self, other):
456 if other is None:
457 values = [None] * len(self.prop._comparable_elements)
458 else:
459 values = other.__composite_values__()
460 comparisons = [
461 a == b for a, b in zip(self.prop._comparable_elements, values)
462 ]
463 if self._adapt_to_entity:
464 comparisons = [self.adapter(x) for x in comparisons]
465 return sql.and_(*comparisons)
467 def __ne__(self, other):
468 return sql.not_(self.__eq__(other))
470 def __str__(self):
471 return str(self.parent.class_.__name__) + "." + self.key
474@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True)
475class ConcreteInheritedProperty(DescriptorProperty):
476 """A 'do nothing' :class:`.MapperProperty` that disables
477 an attribute on a concrete subclass that is only present
478 on the inherited mapper, not the concrete classes' mapper.
480 Cases where this occurs include:
482 * When the superclass mapper is mapped against a
483 "polymorphic union", which includes all attributes from
484 all subclasses.
485 * When a relationship() is configured on an inherited mapper,
486 but not on the subclass mapper. Concrete mappers require
487 that relationship() is configured explicitly on each
488 subclass.
490 """
492 def _comparator_factory(self, mapper):
493 comparator_callable = None
495 for m in self.parent.iterate_to_root():
496 p = m._props[self.key]
497 if not isinstance(p, ConcreteInheritedProperty):
498 comparator_callable = p.comparator_factory
499 break
500 return comparator_callable
502 def __init__(self):
503 super(ConcreteInheritedProperty, self).__init__()
505 def warn():
506 raise AttributeError(
507 "Concrete %s does not implement "
508 "attribute %r at the instance level. Add "
509 "this property explicitly to %s."
510 % (self.parent, self.key, self.parent)
511 )
513 class NoninheritedConcreteProp(object):
514 def __set__(s, obj, value):
515 warn()
517 def __delete__(s, obj):
518 warn()
520 def __get__(s, obj, owner):
521 if obj is None:
522 return self.descriptor
523 warn()
525 self.descriptor = NoninheritedConcreteProp()
528@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True)
529class SynonymProperty(DescriptorProperty):
530 def __init__(
531 self,
532 name,
533 map_column=None,
534 descriptor=None,
535 comparator_factory=None,
536 doc=None,
537 info=None,
538 ):
539 """Denote an attribute name as a synonym to a mapped property,
540 in that the attribute will mirror the value and expression behavior
541 of another attribute.
543 e.g.::
545 class MyClass(Base):
546 __tablename__ = 'my_table'
548 id = Column(Integer, primary_key=True)
549 job_status = Column(String(50))
551 status = synonym("job_status")
554 :param name: the name of the existing mapped property. This
555 can refer to the string name ORM-mapped attribute
556 configured on the class, including column-bound attributes
557 and relationships.
559 :param descriptor: a Python :term:`descriptor` that will be used
560 as a getter (and potentially a setter) when this attribute is
561 accessed at the instance level.
563 :param map_column: **For classical mappings and mappings against
564 an existing Table object only**. if ``True``, the :func:`.synonym`
565 construct will locate the :class:`_schema.Column`
566 object upon the mapped
567 table that would normally be associated with the attribute name of
568 this synonym, and produce a new :class:`.ColumnProperty` that instead
569 maps this :class:`_schema.Column`
570 to the alternate name given as the "name"
571 argument of the synonym; in this way, the usual step of redefining
572 the mapping of the :class:`_schema.Column`
573 to be under a different name is
574 unnecessary. This is usually intended to be used when a
575 :class:`_schema.Column`
576 is to be replaced with an attribute that also uses a
577 descriptor, that is, in conjunction with the
578 :paramref:`.synonym.descriptor` parameter::
580 my_table = Table(
581 "my_table", metadata,
582 Column('id', Integer, primary_key=True),
583 Column('job_status', String(50))
584 )
586 class MyClass(object):
587 @property
588 def _job_status_descriptor(self):
589 return "Status: %s" % self._job_status
592 mapper(
593 MyClass, my_table, properties={
594 "job_status": synonym(
595 "_job_status", map_column=True,
596 descriptor=MyClass._job_status_descriptor)
597 }
598 )
600 Above, the attribute named ``_job_status`` is automatically
601 mapped to the ``job_status`` column::
603 >>> j1 = MyClass()
604 >>> j1._job_status = "employed"
605 >>> j1.job_status
606 Status: employed
608 When using Declarative, in order to provide a descriptor in
609 conjunction with a synonym, use the
610 :func:`sqlalchemy.ext.declarative.synonym_for` helper. However,
611 note that the :ref:`hybrid properties <mapper_hybrids>` feature
612 should usually be preferred, particularly when redefining attribute
613 behavior.
615 :param info: Optional data dictionary which will be populated into the
616 :attr:`.InspectionAttr.info` attribute of this object.
618 .. versionadded:: 1.0.0
620 :param comparator_factory: A subclass of :class:`.PropComparator`
621 that will provide custom comparison behavior at the SQL expression
622 level.
624 .. note::
626 For the use case of providing an attribute which redefines both
627 Python-level and SQL-expression level behavior of an attribute,
628 please refer to the Hybrid attribute introduced at
629 :ref:`mapper_hybrids` for a more effective technique.
631 .. seealso::
633 :ref:`synonyms` - Overview of synonyms
635 :func:`.synonym_for` - a helper oriented towards Declarative
637 :ref:`mapper_hybrids` - The Hybrid Attribute extension provides an
638 updated approach to augmenting attribute behavior more flexibly
639 than can be achieved with synonyms.
641 """
642 super(SynonymProperty, self).__init__()
644 self.name = name
645 self.map_column = map_column
646 self.descriptor = descriptor
647 self.comparator_factory = comparator_factory
648 self.doc = doc or (descriptor and descriptor.__doc__) or None
649 if info:
650 self.info = info
652 util.set_creation_order(self)
654 @property
655 def uses_objects(self):
656 return getattr(self.parent.class_, self.name).impl.uses_objects
658 # TODO: when initialized, check _proxied_property,
659 # emit a warning if its not a column-based property
661 @util.memoized_property
662 def _proxied_property(self):
663 attr = getattr(self.parent.class_, self.name)
664 if not hasattr(attr, "property") or not isinstance(
665 attr.property, MapperProperty
666 ):
667 raise sa_exc.InvalidRequestError(
668 """synonym() attribute "%s.%s" only supports """
669 """ORM mapped attributes, got %r"""
670 % (self.parent.class_.__name__, self.name, attr)
671 )
672 return attr.property
674 def _comparator_factory(self, mapper):
675 prop = self._proxied_property
677 if self.comparator_factory:
678 comp = self.comparator_factory(prop, mapper)
679 else:
680 comp = prop.comparator_factory(prop, mapper)
681 return comp
683 def get_history(self, *arg, **kw):
684 attr = getattr(self.parent.class_, self.name)
685 return attr.impl.get_history(*arg, **kw)
687 def set_parent(self, parent, init):
688 if self.map_column:
689 # implement the 'map_column' option.
690 if self.key not in parent.persist_selectable.c:
691 raise sa_exc.ArgumentError(
692 "Can't compile synonym '%s': no column on table "
693 "'%s' named '%s'"
694 % (
695 self.name,
696 parent.persist_selectable.description,
697 self.key,
698 )
699 )
700 elif (
701 parent.persist_selectable.c[self.key]
702 in parent._columntoproperty
703 and parent._columntoproperty[
704 parent.persist_selectable.c[self.key]
705 ].key
706 == self.name
707 ):
708 raise sa_exc.ArgumentError(
709 "Can't call map_column=True for synonym %r=%r, "
710 "a ColumnProperty already exists keyed to the name "
711 "%r for column %r"
712 % (self.key, self.name, self.name, self.key)
713 )
714 p = properties.ColumnProperty(
715 parent.persist_selectable.c[self.key]
716 )
717 parent._configure_property(self.name, p, init=init, setparent=True)
718 p._mapped_by_synonym = self.key
720 self.parent = parent
723@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True)
724@util.deprecated_cls(
725 "0.7",
726 ":func:`.comparable_property` is deprecated and will be removed in a "
727 "future release. Please refer to the :mod:`~sqlalchemy.ext.hybrid` "
728 "extension.",
729)
730class ComparableProperty(DescriptorProperty):
731 """Instruments a Python property for use in query expressions."""
733 def __init__(
734 self, comparator_factory, descriptor=None, doc=None, info=None
735 ):
736 """Provides a method of applying a :class:`.PropComparator`
737 to any Python descriptor attribute.
740 Allows any Python descriptor to behave like a SQL-enabled
741 attribute when used at the class level in queries, allowing
742 redefinition of expression operator behavior.
744 In the example below we redefine :meth:`.PropComparator.operate`
745 to wrap both sides of an expression in ``func.lower()`` to produce
746 case-insensitive comparison::
748 from sqlalchemy.orm import comparable_property
749 from sqlalchemy.orm.interfaces import PropComparator
750 from sqlalchemy.sql import func
751 from sqlalchemy import Integer, String, Column
752 from sqlalchemy.ext.declarative import declarative_base
754 class CaseInsensitiveComparator(PropComparator):
755 def __clause_element__(self):
756 return self.prop
758 def operate(self, op, other):
759 return op(
760 func.lower(self.__clause_element__()),
761 func.lower(other)
762 )
764 Base = declarative_base()
766 class SearchWord(Base):
767 __tablename__ = 'search_word'
768 id = Column(Integer, primary_key=True)
769 word = Column(String)
770 word_insensitive = comparable_property(lambda prop, mapper:
771 CaseInsensitiveComparator(
772 mapper.c.word, mapper)
773 )
776 A mapping like the above allows the ``word_insensitive`` attribute
777 to render an expression like::
779 >>> print(SearchWord.word_insensitive == "Trucks")
780 lower(search_word.word) = lower(:lower_1)
782 :param comparator_factory:
783 A PropComparator subclass or factory that defines operator behavior
784 for this property.
786 :param descriptor:
787 Optional when used in a ``properties={}`` declaration. The Python
788 descriptor or property to layer comparison behavior on top of.
790 The like-named descriptor will be automatically retrieved from the
791 mapped class if left blank in a ``properties`` declaration.
793 :param info: Optional data dictionary which will be populated into the
794 :attr:`.InspectionAttr.info` attribute of this object.
796 .. versionadded:: 1.0.0
798 """
799 super(ComparableProperty, self).__init__()
800 self.descriptor = descriptor
801 self.comparator_factory = comparator_factory
802 self.doc = doc or (descriptor and descriptor.__doc__) or None
803 if info:
804 self.info = info
805 util.set_creation_order(self)
807 def _comparator_factory(self, mapper):
808 return self.comparator_factory(self, mapper)