Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/descriptor_props.py: 32%
286 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1# orm/descriptor_props.py
2# Copyright (C) 2005-2023 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
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 util as orm_util
16from .interfaces import MapperProperty
17from .interfaces import PropComparator
18from .util import _none_set
19from .. import event
20from .. import exc as sa_exc
21from .. import schema
22from .. import sql
23from .. import util
24from ..sql import expression
25from ..sql import operators
28class DescriptorProperty(MapperProperty):
29 """:class:`.MapperProperty` which proxies access to a
30 user-defined descriptor."""
32 doc = None
34 uses_objects = False
35 _links_to_entity = False
37 def instrument_class(self, mapper):
38 prop = self
40 class _ProxyImpl(object):
41 accepts_scalar_loader = False
42 load_on_unexpire = True
43 collection = False
45 @property
46 def uses_objects(self):
47 return prop.uses_objects
49 def __init__(self, key):
50 self.key = key
52 if hasattr(prop, "get_history"):
54 def get_history(
55 self, state, dict_, passive=attributes.PASSIVE_OFF
56 ):
57 return prop.get_history(state, dict_, passive)
59 if self.descriptor is None:
60 desc = getattr(mapper.class_, self.key, None)
61 if mapper._is_userland_descriptor(self.key, desc):
62 self.descriptor = desc
64 if self.descriptor is None:
66 def fset(obj, value):
67 setattr(obj, self.name, value)
69 def fdel(obj):
70 delattr(obj, self.name)
72 def fget(obj):
73 return getattr(obj, self.name)
75 self.descriptor = property(fget=fget, fset=fset, fdel=fdel)
77 proxy_attr = attributes.create_proxied_attribute(self.descriptor)(
78 self.parent.class_,
79 self.key,
80 self.descriptor,
81 lambda: self._comparator_factory(mapper),
82 doc=self.doc,
83 original_property=self,
84 )
85 proxy_attr.impl = _ProxyImpl(self.key)
86 mapper.class_manager.instrument_attribute(self.key, proxy_attr)
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 def __init__(self, class_, *attrs, **kwargs):
103 r"""Return a composite column-based property for use with a Mapper.
105 See the mapping documentation section :ref:`mapper_composite` for a
106 full usage example.
108 The :class:`.MapperProperty` returned by :func:`.composite`
109 is the :class:`.CompositeProperty`.
111 :param class\_:
112 The "composite type" class, or any classmethod or callable which
113 will produce a new instance of the composite object given the
114 column values in order.
116 :param \*cols:
117 List of Column objects to be mapped.
119 :param active_history=False:
120 When ``True``, indicates that the "previous" value for a
121 scalar attribute should be loaded when replaced, if not
122 already loaded. See the same flag on :func:`.column_property`.
124 :param group:
125 A group name for this property when marked as deferred.
127 :param deferred:
128 When True, the column property is "deferred", meaning that it does
129 not load immediately, and is instead loaded when the attribute is
130 first accessed on an instance. See also
131 :func:`~sqlalchemy.orm.deferred`.
133 :param comparator_factory: a class which extends
134 :class:`.CompositeProperty.Comparator` which provides custom SQL
135 clause generation for comparison operations.
137 :param doc:
138 optional string that will be applied as the doc on the
139 class-bound descriptor.
141 :param info: Optional data dictionary which will be populated into the
142 :attr:`.MapperProperty.info` attribute of this object.
144 """
145 super(CompositeProperty, self).__init__()
147 self.attrs = attrs
148 self.composite_class = class_
149 self.active_history = kwargs.get("active_history", False)
150 self.deferred = kwargs.get("deferred", False)
151 self.group = kwargs.get("group", None)
152 self.comparator_factory = kwargs.pop(
153 "comparator_factory", self.__class__.Comparator
154 )
155 if "info" in kwargs:
156 self.info = kwargs.pop("info")
158 util.set_creation_order(self)
159 self._create_descriptor()
161 def instrument_class(self, mapper):
162 super(CompositeProperty, self).instrument_class(mapper)
163 self._setup_event_handlers()
165 def do_init(self):
166 """Initialization which occurs after the :class:`.CompositeProperty`
167 has been associated with its parent mapper.
169 """
170 self._setup_arguments_on_columns()
172 _COMPOSITE_FGET = object()
174 def _create_descriptor(self):
175 """Create the Python descriptor that will serve as
176 the access point on instances of the mapped class.
178 """
180 def fget(instance):
181 dict_ = attributes.instance_dict(instance)
182 state = attributes.instance_state(instance)
184 if self.key not in dict_:
185 # key not present. Iterate through related
186 # attributes, retrieve their values. This
187 # ensures they all load.
188 values = [
189 getattr(instance, key) for key in self._attribute_keys
190 ]
192 # current expected behavior here is that the composite is
193 # created on access if the object is persistent or if
194 # col attributes have non-None. This would be better
195 # if the composite were created unconditionally,
196 # but that would be a behavioral change.
197 if self.key not in dict_ and (
198 state.key is not None or not _none_set.issuperset(values)
199 ):
200 dict_[self.key] = self.composite_class(*values)
201 state.manager.dispatch.refresh(
202 state, self._COMPOSITE_FGET, [self.key]
203 )
205 return dict_.get(self.key, None)
207 def fset(instance, value):
208 dict_ = attributes.instance_dict(instance)
209 state = attributes.instance_state(instance)
210 attr = state.manager[self.key]
211 previous = dict_.get(self.key, attributes.NO_VALUE)
212 for fn in attr.dispatch.set:
213 value = fn(state, value, previous, attr.impl)
214 dict_[self.key] = value
215 if value is None:
216 for key in self._attribute_keys:
217 setattr(instance, key, None)
218 else:
219 for key, value in zip(
220 self._attribute_keys, value.__composite_values__()
221 ):
222 setattr(instance, key, value)
224 def fdel(instance):
225 state = attributes.instance_state(instance)
226 dict_ = attributes.instance_dict(instance)
227 previous = dict_.pop(self.key, attributes.NO_VALUE)
228 attr = state.manager[self.key]
229 attr.dispatch.remove(state, previous, attr.impl)
230 for key in self._attribute_keys:
231 setattr(instance, key, None)
233 self.descriptor = property(fget, fset, fdel)
235 @util.memoized_property
236 def _comparable_elements(self):
237 return [getattr(self.parent.class_, prop.key) for prop in self.props]
239 @util.memoized_property
240 def props(self):
241 props = []
242 for attr in self.attrs:
243 if isinstance(attr, str):
244 prop = self.parent.get_property(attr, _configure_mappers=False)
245 elif isinstance(attr, schema.Column):
246 prop = self.parent._columntoproperty[attr]
247 elif isinstance(attr, attributes.InstrumentedAttribute):
248 prop = attr.property
249 else:
250 raise sa_exc.ArgumentError(
251 "Composite expects Column objects or mapped "
252 "attributes/attribute names as arguments, got: %r"
253 % (attr,)
254 )
255 props.append(prop)
256 return props
258 @property
259 def columns(self):
260 return [a for a in self.attrs if isinstance(a, schema.Column)]
262 def _setup_arguments_on_columns(self):
263 """Propagate configuration arguments made on this composite
264 to the target columns, for those that apply.
266 """
267 for prop in self.props:
268 prop.active_history = self.active_history
269 if self.deferred:
270 prop.deferred = self.deferred
271 prop.strategy_key = (("deferred", True), ("instrument", True))
272 prop.group = self.group
274 def _setup_event_handlers(self):
275 """Establish events that populate/expire the composite attribute."""
277 def load_handler(state, context):
278 _load_refresh_handler(state, context, None, is_refresh=False)
280 def refresh_handler(state, context, to_load):
281 # note this corresponds to sqlalchemy.ext.mutable load_attrs()
283 if not to_load or (
284 {self.key}.union(self._attribute_keys)
285 ).intersection(to_load):
286 _load_refresh_handler(state, context, to_load, is_refresh=True)
288 def _load_refresh_handler(state, context, to_load, is_refresh):
289 dict_ = state.dict
291 # if context indicates we are coming from the
292 # fget() handler, this already set the value; skip the
293 # handler here. (other handlers like mutablecomposite will still
294 # want to catch it)
295 # there's an insufficiency here in that the fget() handler
296 # really should not be using the refresh event and there should
297 # be some other event that mutablecomposite can subscribe
298 # towards for this.
300 if (
301 not is_refresh or context is self._COMPOSITE_FGET
302 ) and self.key in dict_:
303 return
305 # if column elements aren't loaded, skip.
306 # __get__() will initiate a load for those
307 # columns
308 for k in self._attribute_keys:
309 if k not in dict_:
310 return
312 dict_[self.key] = self.composite_class(
313 *[state.dict[key] for key in self._attribute_keys]
314 )
316 def expire_handler(state, keys):
317 if keys is None or set(self._attribute_keys).intersection(keys):
318 state.dict.pop(self.key, None)
320 def insert_update_handler(mapper, connection, state):
321 """After an insert or update, some columns may be expired due
322 to server side defaults, or re-populated due to client side
323 defaults. Pop out the composite value here so that it
324 recreates.
326 """
328 state.dict.pop(self.key, None)
330 event.listen(
331 self.parent, "after_insert", insert_update_handler, raw=True
332 )
333 event.listen(
334 self.parent, "after_update", insert_update_handler, raw=True
335 )
336 event.listen(
337 self.parent, "load", load_handler, raw=True, propagate=True
338 )
339 event.listen(
340 self.parent, "refresh", refresh_handler, raw=True, propagate=True
341 )
342 event.listen(
343 self.parent, "expire", expire_handler, raw=True, propagate=True
344 )
346 # TODO: need a deserialize hook here
348 @util.memoized_property
349 def _attribute_keys(self):
350 return [prop.key for prop in self.props]
352 def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
353 """Provided for userland code that uses attributes.get_history()."""
355 added = []
356 deleted = []
358 has_history = False
359 for prop in self.props:
360 key = prop.key
361 hist = state.manager[key].impl.get_history(state, dict_)
362 if hist.has_changes():
363 has_history = True
365 non_deleted = hist.non_deleted()
366 if non_deleted:
367 added.extend(non_deleted)
368 else:
369 added.append(None)
370 if hist.deleted:
371 deleted.extend(hist.deleted)
372 else:
373 deleted.append(None)
375 if has_history:
376 return attributes.History(
377 [self.composite_class(*added)],
378 (),
379 [self.composite_class(*deleted)],
380 )
381 else:
382 return attributes.History((), [self.composite_class(*added)], ())
384 def _comparator_factory(self, mapper):
385 return self.comparator_factory(self, mapper)
387 class CompositeBundle(orm_util.Bundle):
388 def __init__(self, property_, expr):
389 self.property = property_
390 super(CompositeProperty.CompositeBundle, self).__init__(
391 property_.key, *expr
392 )
394 def create_row_processor(self, query, procs, labels):
395 def proc(row):
396 return self.property.composite_class(
397 *[proc(row) for proc in procs]
398 )
400 return proc
402 class Comparator(PropComparator):
403 """Produce boolean, comparison, and other operators for
404 :class:`.CompositeProperty` attributes.
406 See the example in :ref:`composite_operations` for an overview
407 of usage , as well as the documentation for :class:`.PropComparator`.
409 .. seealso::
411 :class:`.PropComparator`
413 :class:`.ColumnOperators`
415 :ref:`types_operators`
417 :attr:`.TypeEngine.comparator_factory`
419 """
421 __hash__ = None
423 @util.memoized_property
424 def clauses(self):
425 return expression.ClauseList(
426 group=False, *self._comparable_elements
427 )
429 def __clause_element__(self):
430 return self.expression
432 @util.memoized_property
433 def expression(self):
434 clauses = self.clauses._annotate(
435 {
436 "parententity": self._parententity,
437 "parentmapper": self._parententity,
438 "proxy_key": self.prop.key,
439 }
440 )
441 return CompositeProperty.CompositeBundle(self.prop, clauses)
443 def _bulk_update_tuples(self, value):
444 if isinstance(value, sql.elements.BindParameter):
445 value = value.value
447 if value is None:
448 values = [None for key in self.prop._attribute_keys]
449 elif isinstance(value, self.prop.composite_class):
450 values = value.__composite_values__()
451 else:
452 raise sa_exc.ArgumentError(
453 "Can't UPDATE composite attribute %s to %r"
454 % (self.prop, value)
455 )
457 return zip(self._comparable_elements, values)
459 @util.memoized_property
460 def _comparable_elements(self):
461 if self._adapt_to_entity:
462 return [
463 getattr(self._adapt_to_entity.entity, prop.key)
464 for prop in self.prop._comparable_elements
465 ]
466 else:
467 return self.prop._comparable_elements
469 def __eq__(self, other):
470 if other is None:
471 values = [None] * len(self.prop._comparable_elements)
472 else:
473 values = other.__composite_values__()
474 comparisons = [
475 a == b for a, b in zip(self.prop._comparable_elements, values)
476 ]
477 if self._adapt_to_entity:
478 comparisons = [self.adapter(x) for x in comparisons]
479 return sql.and_(*comparisons)
481 def __ne__(self, other):
482 return sql.not_(self.__eq__(other))
484 def __str__(self):
485 return str(self.parent.class_.__name__) + "." + self.key
488class ConcreteInheritedProperty(DescriptorProperty):
489 """A 'do nothing' :class:`.MapperProperty` that disables
490 an attribute on a concrete subclass that is only present
491 on the inherited mapper, not the concrete classes' mapper.
493 Cases where this occurs include:
495 * When the superclass mapper is mapped against a
496 "polymorphic union", which includes all attributes from
497 all subclasses.
498 * When a relationship() is configured on an inherited mapper,
499 but not on the subclass mapper. Concrete mappers require
500 that relationship() is configured explicitly on each
501 subclass.
503 """
505 def _comparator_factory(self, mapper):
506 comparator_callable = None
508 for m in self.parent.iterate_to_root():
509 p = m._props[self.key]
510 if not isinstance(p, ConcreteInheritedProperty):
511 comparator_callable = p.comparator_factory
512 break
513 return comparator_callable
515 def __init__(self):
516 super(ConcreteInheritedProperty, self).__init__()
518 def warn():
519 raise AttributeError(
520 "Concrete %s does not implement "
521 "attribute %r at the instance level. Add "
522 "this property explicitly to %s."
523 % (self.parent, self.key, self.parent)
524 )
526 class NoninheritedConcreteProp(object):
527 def __set__(s, obj, value):
528 warn()
530 def __delete__(s, obj):
531 warn()
533 def __get__(s, obj, owner):
534 if obj is None:
535 return self.descriptor
536 warn()
538 self.descriptor = NoninheritedConcreteProp()
541class SynonymProperty(DescriptorProperty):
542 def __init__(
543 self,
544 name,
545 map_column=None,
546 descriptor=None,
547 comparator_factory=None,
548 doc=None,
549 info=None,
550 ):
551 """Denote an attribute name as a synonym to a mapped property,
552 in that the attribute will mirror the value and expression behavior
553 of another attribute.
555 e.g.::
557 class MyClass(Base):
558 __tablename__ = 'my_table'
560 id = Column(Integer, primary_key=True)
561 job_status = Column(String(50))
563 status = synonym("job_status")
566 :param name: the name of the existing mapped property. This
567 can refer to the string name ORM-mapped attribute
568 configured on the class, including column-bound attributes
569 and relationships.
571 :param descriptor: a Python :term:`descriptor` that will be used
572 as a getter (and potentially a setter) when this attribute is
573 accessed at the instance level.
575 :param map_column: **For classical mappings and mappings against
576 an existing Table object only**. if ``True``, the :func:`.synonym`
577 construct will locate the :class:`_schema.Column`
578 object upon the mapped
579 table that would normally be associated with the attribute name of
580 this synonym, and produce a new :class:`.ColumnProperty` that instead
581 maps this :class:`_schema.Column`
582 to the alternate name given as the "name"
583 argument of the synonym; in this way, the usual step of redefining
584 the mapping of the :class:`_schema.Column`
585 to be under a different name is
586 unnecessary. This is usually intended to be used when a
587 :class:`_schema.Column`
588 is to be replaced with an attribute that also uses a
589 descriptor, that is, in conjunction with the
590 :paramref:`.synonym.descriptor` parameter::
592 my_table = Table(
593 "my_table", metadata,
594 Column('id', Integer, primary_key=True),
595 Column('job_status', String(50))
596 )
598 class MyClass(object):
599 @property
600 def _job_status_descriptor(self):
601 return "Status: %s" % self._job_status
604 mapper(
605 MyClass, my_table, properties={
606 "job_status": synonym(
607 "_job_status", map_column=True,
608 descriptor=MyClass._job_status_descriptor)
609 }
610 )
612 Above, the attribute named ``_job_status`` is automatically
613 mapped to the ``job_status`` column::
615 >>> j1 = MyClass()
616 >>> j1._job_status = "employed"
617 >>> j1.job_status
618 Status: employed
620 When using Declarative, in order to provide a descriptor in
621 conjunction with a synonym, use the
622 :func:`sqlalchemy.ext.declarative.synonym_for` helper. However,
623 note that the :ref:`hybrid properties <mapper_hybrids>` feature
624 should usually be preferred, particularly when redefining attribute
625 behavior.
627 :param info: Optional data dictionary which will be populated into the
628 :attr:`.InspectionAttr.info` attribute of this object.
630 .. versionadded:: 1.0.0
632 :param comparator_factory: A subclass of :class:`.PropComparator`
633 that will provide custom comparison behavior at the SQL expression
634 level.
636 .. note::
638 For the use case of providing an attribute which redefines both
639 Python-level and SQL-expression level behavior of an attribute,
640 please refer to the Hybrid attribute introduced at
641 :ref:`mapper_hybrids` for a more effective technique.
643 .. seealso::
645 :ref:`synonyms` - Overview of synonyms
647 :func:`.synonym_for` - a helper oriented towards Declarative
649 :ref:`mapper_hybrids` - The Hybrid Attribute extension provides an
650 updated approach to augmenting attribute behavior more flexibly
651 than can be achieved with synonyms.
653 """
654 super(SynonymProperty, self).__init__()
656 self.name = name
657 self.map_column = map_column
658 self.descriptor = descriptor
659 self.comparator_factory = comparator_factory
660 self.doc = doc or (descriptor and descriptor.__doc__) or None
661 if info:
662 self.info = info
664 util.set_creation_order(self)
666 @property
667 def uses_objects(self):
668 return getattr(self.parent.class_, self.name).impl.uses_objects
670 # TODO: when initialized, check _proxied_object,
671 # emit a warning if its not a column-based property
673 @util.memoized_property
674 def _proxied_object(self):
675 attr = getattr(self.parent.class_, self.name)
676 if not hasattr(attr, "property") or not isinstance(
677 attr.property, MapperProperty
678 ):
679 # attribute is a non-MapperProprerty proxy such as
680 # hybrid or association proxy
681 if isinstance(attr, attributes.QueryableAttribute):
682 return attr.comparator
683 elif isinstance(attr, operators.ColumnOperators):
684 return attr
686 raise sa_exc.InvalidRequestError(
687 """synonym() attribute "%s.%s" only supports """
688 """ORM mapped attributes, got %r"""
689 % (self.parent.class_.__name__, self.name, attr)
690 )
691 return attr.property
693 def _comparator_factory(self, mapper):
694 prop = self._proxied_object
696 if isinstance(prop, MapperProperty):
697 if self.comparator_factory:
698 comp = self.comparator_factory(prop, mapper)
699 else:
700 comp = prop.comparator_factory(prop, mapper)
701 return comp
702 else:
703 return prop
705 def get_history(self, *arg, **kw):
706 attr = getattr(self.parent.class_, self.name)
707 return attr.impl.get_history(*arg, **kw)
709 @util.preload_module("sqlalchemy.orm.properties")
710 def set_parent(self, parent, init):
711 properties = util.preloaded.orm_properties
713 if self.map_column:
714 # implement the 'map_column' option.
715 if self.key not in parent.persist_selectable.c:
716 raise sa_exc.ArgumentError(
717 "Can't compile synonym '%s': no column on table "
718 "'%s' named '%s'"
719 % (
720 self.name,
721 parent.persist_selectable.description,
722 self.key,
723 )
724 )
725 elif (
726 parent.persist_selectable.c[self.key]
727 in parent._columntoproperty
728 and parent._columntoproperty[
729 parent.persist_selectable.c[self.key]
730 ].key
731 == self.name
732 ):
733 raise sa_exc.ArgumentError(
734 "Can't call map_column=True for synonym %r=%r, "
735 "a ColumnProperty already exists keyed to the name "
736 "%r for column %r"
737 % (self.key, self.name, self.name, self.key)
738 )
739 p = properties.ColumnProperty(
740 parent.persist_selectable.c[self.key]
741 )
742 parent._configure_property(self.name, p, init=init, setparent=True)
743 p._mapped_by_synonym = self.key
745 self.parent = parent