Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py: 31%
836 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
1# orm/attributes.py
2# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
8"""Defines instrumentation for class attributes and their interaction
9with instances.
11This module is usually not directly visible to user applications, but
12defines a large part of the ORM's interactivity.
15"""
17import operator
19from . import collections
20from . import exc as orm_exc
21from . import interfaces
22from .base import ATTR_EMPTY
23from .base import ATTR_WAS_SET
24from .base import CALLABLES_OK
25from .base import DEFERRED_HISTORY_LOAD
26from .base import INIT_OK
27from .base import instance_dict
28from .base import instance_state
29from .base import instance_str
30from .base import LOAD_AGAINST_COMMITTED
31from .base import manager_of_class
32from .base import NEVER_SET # noqa
33from .base import NO_AUTOFLUSH
34from .base import NO_CHANGE # noqa
35from .base import NO_RAISE
36from .base import NO_VALUE
37from .base import NON_PERSISTENT_OK # noqa
38from .base import PASSIVE_CLASS_MISMATCH # noqa
39from .base import PASSIVE_NO_FETCH
40from .base import PASSIVE_NO_FETCH_RELATED # noqa
41from .base import PASSIVE_NO_INITIALIZE
42from .base import PASSIVE_NO_RESULT
43from .base import PASSIVE_OFF
44from .base import PASSIVE_ONLY_PERSISTENT
45from .base import PASSIVE_RETURN_NO_VALUE
46from .base import RELATED_OBJECT_OK # noqa
47from .base import SQL_OK # noqa
48from .base import state_str
49from .. import event
50from .. import exc
51from .. import inspection
52from .. import util
53from ..sql import base as sql_base
54from ..sql import roles
55from ..sql import traversals
56from ..sql import visitors
59class NoKey(str):
60 pass
63NO_KEY = NoKey("no name")
66@inspection._self_inspects
67class QueryableAttribute(
68 interfaces._MappedAttribute,
69 interfaces.InspectionAttr,
70 interfaces.PropComparator,
71 traversals.HasCopyInternals,
72 roles.JoinTargetRole,
73 roles.OnClauseRole,
74 sql_base.Immutable,
75 sql_base.MemoizedHasCacheKey,
76):
77 """Base class for :term:`descriptor` objects that intercept
78 attribute events on behalf of a :class:`.MapperProperty`
79 object. The actual :class:`.MapperProperty` is accessible
80 via the :attr:`.QueryableAttribute.property`
81 attribute.
84 .. seealso::
86 :class:`.InstrumentedAttribute`
88 :class:`.MapperProperty`
90 :attr:`_orm.Mapper.all_orm_descriptors`
92 :attr:`_orm.Mapper.attrs`
93 """
95 is_attribute = True
97 # PropComparator has a __visit_name__ to participate within
98 # traversals. Disambiguate the attribute vs. a comparator.
99 __visit_name__ = "orm_instrumented_attribute"
101 def __init__(
102 self,
103 class_,
104 key,
105 parententity,
106 impl=None,
107 comparator=None,
108 of_type=None,
109 extra_criteria=(),
110 ):
111 self.class_ = class_
112 self.key = key
113 self._parententity = parententity
114 self.impl = impl
115 self.comparator = comparator
116 self._of_type = of_type
117 self._extra_criteria = extra_criteria
119 manager = manager_of_class(class_)
120 # manager is None in the case of AliasedClass
121 if manager:
122 # propagate existing event listeners from
123 # immediate superclass
124 for base in manager._bases:
125 if key in base:
126 self.dispatch._update(base[key].dispatch)
127 if base[key].dispatch._active_history:
128 self.dispatch._active_history = True
130 _cache_key_traversal = [
131 ("key", visitors.ExtendedInternalTraversal.dp_string),
132 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
133 ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
134 ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
135 ]
137 def __reduce__(self):
138 # this method is only used in terms of the
139 # sqlalchemy.ext.serializer extension
140 return (
141 _queryable_attribute_unreduce,
142 (
143 self.key,
144 self._parententity.mapper.class_,
145 self._parententity,
146 self._parententity.entity,
147 ),
148 )
150 @util.memoized_property
151 def _supports_population(self):
152 return self.impl.supports_population
154 @property
155 def _impl_uses_objects(self):
156 return self.impl.uses_objects
158 def get_history(self, instance, passive=PASSIVE_OFF):
159 return self.impl.get_history(
160 instance_state(instance), instance_dict(instance), passive
161 )
163 @util.memoized_property
164 def info(self):
165 """Return the 'info' dictionary for the underlying SQL element.
167 The behavior here is as follows:
169 * If the attribute is a column-mapped property, i.e.
170 :class:`.ColumnProperty`, which is mapped directly
171 to a schema-level :class:`_schema.Column` object, this attribute
172 will return the :attr:`.SchemaItem.info` dictionary associated
173 with the core-level :class:`_schema.Column` object.
175 * If the attribute is a :class:`.ColumnProperty` but is mapped to
176 any other kind of SQL expression other than a
177 :class:`_schema.Column`,
178 the attribute will refer to the :attr:`.MapperProperty.info`
179 dictionary associated directly with the :class:`.ColumnProperty`,
180 assuming the SQL expression itself does not have its own ``.info``
181 attribute (which should be the case, unless a user-defined SQL
182 construct has defined one).
184 * If the attribute refers to any other kind of
185 :class:`.MapperProperty`, including :class:`.RelationshipProperty`,
186 the attribute will refer to the :attr:`.MapperProperty.info`
187 dictionary associated with that :class:`.MapperProperty`.
189 * To access the :attr:`.MapperProperty.info` dictionary of the
190 :class:`.MapperProperty` unconditionally, including for a
191 :class:`.ColumnProperty` that's associated directly with a
192 :class:`_schema.Column`, the attribute can be referred to using
193 :attr:`.QueryableAttribute.property` attribute, as
194 ``MyClass.someattribute.property.info``.
196 .. seealso::
198 :attr:`.SchemaItem.info`
200 :attr:`.MapperProperty.info`
202 """
203 return self.comparator.info
205 @util.memoized_property
206 def parent(self):
207 """Return an inspection instance representing the parent.
209 This will be either an instance of :class:`_orm.Mapper`
210 or :class:`.AliasedInsp`, depending upon the nature
211 of the parent entity which this attribute is associated
212 with.
214 """
215 return inspection.inspect(self._parententity)
217 @util.memoized_property
218 def expression(self):
219 """The SQL expression object represented by this
220 :class:`.QueryableAttribute`.
222 This will typically be an instance of a :class:`_sql.ColumnElement`
223 subclass representing a column expression.
225 """
226 if self.key is NO_KEY:
227 annotations = {"entity_namespace": self._entity_namespace}
228 else:
229 annotations = {
230 "proxy_key": self.key,
231 "proxy_owner": self._parententity,
232 "entity_namespace": self._entity_namespace,
233 }
235 ce = self.comparator.__clause_element__()
236 try:
237 anno = ce._annotate
238 except AttributeError as ae:
239 util.raise_(
240 exc.InvalidRequestError(
241 'When interpreting attribute "%s" as a SQL expression, '
242 "expected __clause_element__() to return "
243 "a ClauseElement object, got: %r" % (self, ce)
244 ),
245 from_=ae,
246 )
247 else:
248 return anno(annotations)
250 @property
251 def _entity_namespace(self):
252 return self._parententity
254 @property
255 def _annotations(self):
256 return self.__clause_element__()._annotations
258 def __clause_element__(self):
259 return self.expression
261 @property
262 def _from_objects(self):
263 return self.expression._from_objects
265 def _bulk_update_tuples(self, value):
266 """Return setter tuples for a bulk UPDATE."""
268 return self.comparator._bulk_update_tuples(value)
270 def adapt_to_entity(self, adapt_to_entity):
271 assert not self._of_type
272 return self.__class__(
273 adapt_to_entity.entity,
274 self.key,
275 impl=self.impl,
276 comparator=self.comparator.adapt_to_entity(adapt_to_entity),
277 parententity=adapt_to_entity,
278 )
280 def of_type(self, entity):
281 return QueryableAttribute(
282 self.class_,
283 self.key,
284 self._parententity,
285 impl=self.impl,
286 comparator=self.comparator.of_type(entity),
287 of_type=inspection.inspect(entity),
288 extra_criteria=self._extra_criteria,
289 )
291 def and_(self, *other):
292 return QueryableAttribute(
293 self.class_,
294 self.key,
295 self._parententity,
296 impl=self.impl,
297 comparator=self.comparator.and_(*other),
298 of_type=self._of_type,
299 extra_criteria=self._extra_criteria + other,
300 )
302 def _clone(self, **kw):
303 return QueryableAttribute(
304 self.class_,
305 self.key,
306 self._parententity,
307 impl=self.impl,
308 comparator=self.comparator,
309 of_type=self._of_type,
310 extra_criteria=self._extra_criteria,
311 )
313 def label(self, name):
314 return self.__clause_element__().label(name)
316 def operate(self, op, *other, **kwargs):
317 return op(self.comparator, *other, **kwargs)
319 def reverse_operate(self, op, other, **kwargs):
320 return op(other, self.comparator, **kwargs)
322 def hasparent(self, state, optimistic=False):
323 return self.impl.hasparent(state, optimistic=optimistic) is not False
325 def __getattr__(self, key):
326 try:
327 return getattr(self.comparator, key)
328 except AttributeError as err:
329 util.raise_(
330 AttributeError(
331 "Neither %r object nor %r object associated with %s "
332 "has an attribute %r"
333 % (
334 type(self).__name__,
335 type(self.comparator).__name__,
336 self,
337 key,
338 )
339 ),
340 replace_context=err,
341 )
343 def __str__(self):
344 return "%s.%s" % (self.class_.__name__, self.key)
346 @util.memoized_property
347 def property(self):
348 """Return the :class:`.MapperProperty` associated with this
349 :class:`.QueryableAttribute`.
352 Return values here will commonly be instances of
353 :class:`.ColumnProperty` or :class:`.RelationshipProperty`.
356 """
357 return self.comparator.property
360def _queryable_attribute_unreduce(key, mapped_class, parententity, entity):
361 # this method is only used in terms of the
362 # sqlalchemy.ext.serializer extension
363 if parententity.is_aliased_class:
364 return entity._get_from_serialized(key, mapped_class, parententity)
365 else:
366 return getattr(entity, key)
369if util.py3k:
370 from typing import TypeVar, Generic
372 _T = TypeVar("_T")
373 _Generic_T = Generic[_T]
374else:
375 _Generic_T = type("_Generic_T", (), {})
378class Mapped(QueryableAttribute, _Generic_T):
379 """Represent an ORM mapped :term:`descriptor` attribute for typing
380 purposes.
382 This class represents the complete descriptor interface for any class
383 attribute that will have been :term:`instrumented` by the ORM
384 :class:`_orm.Mapper` class. When used with typing stubs, it is the final
385 type that would be used by a type checker such as mypy to provide the full
386 behavioral contract for the attribute.
388 .. tip::
390 The :class:`_orm.Mapped` class represents attributes that are handled
391 directly by the :class:`_orm.Mapper` class. It does not include other
392 Python descriptor classes that are provided as extensions, including
393 :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`.
394 While these systems still make use of ORM-specific superclasses
395 and structures, they are not :term:`instrumented` by the
396 :class:`_orm.Mapper` and instead provide their own functionality
397 when they are accessed on a class.
399 When using the :ref:`SQLAlchemy Mypy plugin <mypy_toplevel>`, the
400 :class:`_orm.Mapped` construct is used in typing annotations to indicate to
401 the plugin those attributes that are expected to be mapped; the plugin also
402 applies :class:`_orm.Mapped` as an annotation automatically when it scans
403 through declarative mappings in :ref:`orm_declarative_table` style. For
404 more indirect mapping styles such as
405 :ref:`imperative table <orm_imperative_table_configuration>` it is
406 typically applied explicitly to class level attributes that expect
407 to be mapped based on a given :class:`_schema.Table` configuration.
409 :class:`_orm.Mapped` is defined in the
410 `sqlalchemy2-stubs <https://pypi.org/project/sqlalchemy2-stubs>`_ project
411 as a :pep:`484` generic class which may subscribe to any arbitrary Python
412 type, which represents the Python type handled by the attribute::
414 class MyMappedClass(Base):
415 __table_ = Table(
416 "some_table", Base.metadata,
417 Column("id", Integer, primary_key=True),
418 Column("data", String(50)),
419 Column("created_at", DateTime)
420 )
422 id : Mapped[int]
423 data: Mapped[str]
424 created_at: Mapped[datetime]
426 For complete background on how to use :class:`_orm.Mapped` with
427 pep-484 tools like Mypy, see the link below for background on SQLAlchemy's
428 Mypy plugin.
430 .. versionadded:: 1.4
432 .. seealso::
434 :ref:`mypy_toplevel` - complete background on Mypy integration
436 """
438 def __get__(self, instance, owner):
439 raise NotImplementedError()
441 def __set__(self, instance, value):
442 raise NotImplementedError()
444 def __delete__(self, instance):
445 raise NotImplementedError()
448class InstrumentedAttribute(Mapped):
449 """Class bound instrumented attribute which adds basic
450 :term:`descriptor` methods.
452 See :class:`.QueryableAttribute` for a description of most features.
455 """
457 inherit_cache = True
459 def __set__(self, instance, value):
460 self.impl.set(
461 instance_state(instance), instance_dict(instance), value, None
462 )
464 def __delete__(self, instance):
465 self.impl.delete(instance_state(instance), instance_dict(instance))
467 def __get__(self, instance, owner):
468 if instance is None:
469 return self
471 dict_ = instance_dict(instance)
472 if self._supports_population and self.key in dict_:
473 return dict_[self.key]
474 else:
475 try:
476 state = instance_state(instance)
477 except AttributeError as err:
478 util.raise_(
479 orm_exc.UnmappedInstanceError(instance),
480 replace_context=err,
481 )
482 return self.impl.get(state, dict_)
485HasEntityNamespace = util.namedtuple(
486 "HasEntityNamespace", ["entity_namespace"]
487)
488HasEntityNamespace.is_mapper = HasEntityNamespace.is_aliased_class = False
491def create_proxied_attribute(descriptor):
492 """Create an QueryableAttribute / user descriptor hybrid.
494 Returns a new QueryableAttribute type that delegates descriptor
495 behavior and getattr() to the given descriptor.
496 """
498 # TODO: can move this to descriptor_props if the need for this
499 # function is removed from ext/hybrid.py
501 class Proxy(QueryableAttribute):
502 """Presents the :class:`.QueryableAttribute` interface as a
503 proxy on top of a Python descriptor / :class:`.PropComparator`
504 combination.
506 """
508 _extra_criteria = ()
510 def __init__(
511 self,
512 class_,
513 key,
514 descriptor,
515 comparator,
516 adapt_to_entity=None,
517 doc=None,
518 original_property=None,
519 ):
520 self.class_ = class_
521 self.key = key
522 self.descriptor = descriptor
523 self.original_property = original_property
524 self._comparator = comparator
525 self._adapt_to_entity = adapt_to_entity
526 self.__doc__ = doc
528 _is_internal_proxy = True
530 _cache_key_traversal = [
531 ("key", visitors.ExtendedInternalTraversal.dp_string),
532 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
533 ]
535 @property
536 def _impl_uses_objects(self):
537 return (
538 self.original_property is not None
539 and getattr(self.class_, self.key).impl.uses_objects
540 )
542 @property
543 def _parententity(self):
544 return inspection.inspect(self.class_, raiseerr=False)
546 @property
547 def _entity_namespace(self):
548 if hasattr(self._comparator, "_parententity"):
549 return self._comparator._parententity
550 else:
551 # used by hybrid attributes which try to remain
552 # agnostic of any ORM concepts like mappers
553 return HasEntityNamespace(self.class_)
555 @property
556 def property(self):
557 return self.comparator.property
559 @util.memoized_property
560 def comparator(self):
561 if callable(self._comparator):
562 self._comparator = self._comparator()
563 if self._adapt_to_entity:
564 self._comparator = self._comparator.adapt_to_entity(
565 self._adapt_to_entity
566 )
567 return self._comparator
569 def adapt_to_entity(self, adapt_to_entity):
570 return self.__class__(
571 adapt_to_entity.entity,
572 self.key,
573 self.descriptor,
574 self._comparator,
575 adapt_to_entity,
576 )
578 def _clone(self, **kw):
579 return self.__class__(
580 self.class_,
581 self.key,
582 self.descriptor,
583 self._comparator,
584 adapt_to_entity=self._adapt_to_entity,
585 original_property=self.original_property,
586 )
588 def __get__(self, instance, owner):
589 retval = self.descriptor.__get__(instance, owner)
590 # detect if this is a plain Python @property, which just returns
591 # itself for class level access. If so, then return us.
592 # Otherwise, return the object returned by the descriptor.
593 if retval is self.descriptor and instance is None:
594 return self
595 else:
596 return retval
598 def __str__(self):
599 return "%s.%s" % (self.class_.__name__, self.key)
601 def __getattr__(self, attribute):
602 """Delegate __getattr__ to the original descriptor and/or
603 comparator."""
604 try:
605 return getattr(descriptor, attribute)
606 except AttributeError as err:
607 if attribute == "comparator":
608 util.raise_(
609 AttributeError("comparator"), replace_context=err
610 )
611 try:
612 # comparator itself might be unreachable
613 comparator = self.comparator
614 except AttributeError as err2:
615 util.raise_(
616 AttributeError(
617 "Neither %r object nor unconfigured comparator "
618 "object associated with %s has an attribute %r"
619 % (type(descriptor).__name__, self, attribute)
620 ),
621 replace_context=err2,
622 )
623 else:
624 try:
625 return getattr(comparator, attribute)
626 except AttributeError as err3:
627 util.raise_(
628 AttributeError(
629 "Neither %r object nor %r object "
630 "associated with %s has an attribute %r"
631 % (
632 type(descriptor).__name__,
633 type(comparator).__name__,
634 self,
635 attribute,
636 )
637 ),
638 replace_context=err3,
639 )
641 Proxy.__name__ = type(descriptor).__name__ + "Proxy"
643 util.monkeypatch_proxied_specials(
644 Proxy, type(descriptor), name="descriptor", from_instance=descriptor
645 )
646 return Proxy
649OP_REMOVE = util.symbol("REMOVE")
650OP_APPEND = util.symbol("APPEND")
651OP_REPLACE = util.symbol("REPLACE")
652OP_BULK_REPLACE = util.symbol("BULK_REPLACE")
653OP_MODIFIED = util.symbol("MODIFIED")
656class AttributeEvent(object):
657 """A token propagated throughout the course of a chain of attribute
658 events.
660 Serves as an indicator of the source of the event and also provides
661 a means of controlling propagation across a chain of attribute
662 operations.
664 The :class:`.Event` object is sent as the ``initiator`` argument
665 when dealing with events such as :meth:`.AttributeEvents.append`,
666 :meth:`.AttributeEvents.set`,
667 and :meth:`.AttributeEvents.remove`.
669 The :class:`.Event` object is currently interpreted by the backref
670 event handlers, and is used to control the propagation of operations
671 across two mutually-dependent attributes.
673 .. versionadded:: 0.9.0
675 :attribute impl: The :class:`.AttributeImpl` which is the current event
676 initiator.
678 :attribute op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`,
679 :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_REPLACE`, indicating the
680 source operation.
682 """
684 __slots__ = "impl", "op", "parent_token"
686 def __init__(self, attribute_impl, op):
687 self.impl = attribute_impl
688 self.op = op
689 self.parent_token = self.impl.parent_token
691 def __eq__(self, other):
692 return (
693 isinstance(other, AttributeEvent)
694 and other.impl is self.impl
695 and other.op == self.op
696 )
698 @property
699 def key(self):
700 return self.impl.key
702 def hasparent(self, state):
703 return self.impl.hasparent(state)
706Event = AttributeEvent
709class AttributeImpl(object):
710 """internal implementation for instrumented attributes."""
712 def __init__(
713 self,
714 class_,
715 key,
716 callable_,
717 dispatch,
718 trackparent=False,
719 compare_function=None,
720 active_history=False,
721 parent_token=None,
722 load_on_unexpire=True,
723 send_modified_events=True,
724 accepts_scalar_loader=None,
725 **kwargs
726 ):
727 r"""Construct an AttributeImpl.
729 :param \class_: associated class
731 :param key: string name of the attribute
733 :param \callable_:
734 optional function which generates a callable based on a parent
735 instance, which produces the "default" values for a scalar or
736 collection attribute when it's first accessed, if not present
737 already.
739 :param trackparent:
740 if True, attempt to track if an instance has a parent attached
741 to it via this attribute.
743 :param compare_function:
744 a function that compares two values which are normally
745 assignable to this attribute.
747 :param active_history:
748 indicates that get_history() should always return the "old" value,
749 even if it means executing a lazy callable upon attribute change.
751 :param parent_token:
752 Usually references the MapperProperty, used as a key for
753 the hasparent() function to identify an "owning" attribute.
754 Allows multiple AttributeImpls to all match a single
755 owner attribute.
757 :param load_on_unexpire:
758 if False, don't include this attribute in a load-on-expired
759 operation, i.e. the "expired_attribute_loader" process.
760 The attribute can still be in the "expired" list and be
761 considered to be "expired". Previously, this flag was called
762 "expire_missing" and is only used by a deferred column
763 attribute.
765 :param send_modified_events:
766 if False, the InstanceState._modified_event method will have no
767 effect; this means the attribute will never show up as changed in a
768 history entry.
770 """
771 self.class_ = class_
772 self.key = key
773 self.callable_ = callable_
774 self.dispatch = dispatch
775 self.trackparent = trackparent
776 self.parent_token = parent_token or self
777 self.send_modified_events = send_modified_events
778 if compare_function is None:
779 self.is_equal = operator.eq
780 else:
781 self.is_equal = compare_function
783 if accepts_scalar_loader is not None:
784 self.accepts_scalar_loader = accepts_scalar_loader
785 else:
786 self.accepts_scalar_loader = self.default_accepts_scalar_loader
788 _deferred_history = kwargs.pop("_deferred_history", False)
789 self._deferred_history = _deferred_history
791 if active_history:
792 self.dispatch._active_history = True
794 self.load_on_unexpire = load_on_unexpire
795 self._modified_token = Event(self, OP_MODIFIED)
797 __slots__ = (
798 "class_",
799 "key",
800 "callable_",
801 "dispatch",
802 "trackparent",
803 "parent_token",
804 "send_modified_events",
805 "is_equal",
806 "load_on_unexpire",
807 "_modified_token",
808 "accepts_scalar_loader",
809 "_deferred_history",
810 )
812 def __str__(self):
813 return "%s.%s" % (self.class_.__name__, self.key)
815 def _get_active_history(self):
816 """Backwards compat for impl.active_history"""
818 return self.dispatch._active_history
820 def _set_active_history(self, value):
821 self.dispatch._active_history = value
823 active_history = property(_get_active_history, _set_active_history)
825 def hasparent(self, state, optimistic=False):
826 """Return the boolean value of a `hasparent` flag attached to
827 the given state.
829 The `optimistic` flag determines what the default return value
830 should be if no `hasparent` flag can be located.
832 As this function is used to determine if an instance is an
833 *orphan*, instances that were loaded from storage should be
834 assumed to not be orphans, until a True/False value for this
835 flag is set.
837 An instance attribute that is loaded by a callable function
838 will also not have a `hasparent` flag.
840 """
841 msg = "This AttributeImpl is not configured to track parents."
842 assert self.trackparent, msg
844 return (
845 state.parents.get(id(self.parent_token), optimistic) is not False
846 )
848 def sethasparent(self, state, parent_state, value):
849 """Set a boolean flag on the given item corresponding to
850 whether or not it is attached to a parent object via the
851 attribute represented by this ``InstrumentedAttribute``.
853 """
854 msg = "This AttributeImpl is not configured to track parents."
855 assert self.trackparent, msg
857 id_ = id(self.parent_token)
858 if value:
859 state.parents[id_] = parent_state
860 else:
861 if id_ in state.parents:
862 last_parent = state.parents[id_]
864 if (
865 last_parent is not False
866 and last_parent.key != parent_state.key
867 ):
869 if last_parent.obj() is None:
870 raise orm_exc.StaleDataError(
871 "Removing state %s from parent "
872 "state %s along attribute '%s', "
873 "but the parent record "
874 "has gone stale, can't be sure this "
875 "is the most recent parent."
876 % (
877 state_str(state),
878 state_str(parent_state),
879 self.key,
880 )
881 )
883 return
885 state.parents[id_] = False
887 def get_history(self, state, dict_, passive=PASSIVE_OFF):
888 raise NotImplementedError()
890 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
891 """Return a list of tuples of (state, obj)
892 for all objects in this attribute's current state
893 + history.
895 Only applies to object-based attributes.
897 This is an inlining of existing functionality
898 which roughly corresponds to:
900 get_state_history(
901 state,
902 key,
903 passive=PASSIVE_NO_INITIALIZE).sum()
905 """
906 raise NotImplementedError()
908 def _default_value(self, state, dict_):
909 """Produce an empty value for an uninitialized scalar attribute."""
911 assert self.key not in dict_, (
912 "_default_value should only be invoked for an "
913 "uninitialized or expired attribute"
914 )
916 value = None
917 for fn in self.dispatch.init_scalar:
918 ret = fn(state, value, dict_)
919 if ret is not ATTR_EMPTY:
920 value = ret
922 return value
924 def get(self, state, dict_, passive=PASSIVE_OFF):
925 """Retrieve a value from the given object.
926 If a callable is assembled on this object's attribute, and
927 passive is False, the callable will be executed and the
928 resulting value will be set as the new value for this attribute.
929 """
930 if self.key in dict_:
931 return dict_[self.key]
932 else:
933 # if history present, don't load
934 key = self.key
935 if (
936 key not in state.committed_state
937 or state.committed_state[key] is NO_VALUE
938 ):
939 if not passive & CALLABLES_OK:
940 return PASSIVE_NO_RESULT
942 value = self._fire_loader_callables(state, key, passive)
944 if value is PASSIVE_NO_RESULT or value is NO_VALUE:
945 return value
946 elif value is ATTR_WAS_SET:
947 try:
948 return dict_[key]
949 except KeyError as err:
950 # TODO: no test coverage here.
951 util.raise_(
952 KeyError(
953 "Deferred loader for attribute "
954 "%r failed to populate "
955 "correctly" % key
956 ),
957 replace_context=err,
958 )
959 elif value is not ATTR_EMPTY:
960 return self.set_committed_value(state, dict_, value)
962 if not passive & INIT_OK:
963 return NO_VALUE
964 else:
965 return self._default_value(state, dict_)
967 def _fire_loader_callables(self, state, key, passive):
968 if (
969 self.accepts_scalar_loader
970 and self.load_on_unexpire
971 and key in state.expired_attributes
972 ):
973 return state._load_expired(state, passive)
974 elif key in state.callables:
975 callable_ = state.callables[key]
976 return callable_(state, passive)
977 elif self.callable_:
978 return self.callable_(state, passive)
979 else:
980 return ATTR_EMPTY
982 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
983 self.set(state, dict_, value, initiator, passive=passive)
985 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
986 self.set(
987 state, dict_, None, initiator, passive=passive, check_old=value
988 )
990 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
991 self.set(
992 state,
993 dict_,
994 None,
995 initiator,
996 passive=passive,
997 check_old=value,
998 pop=True,
999 )
1001 def set(
1002 self,
1003 state,
1004 dict_,
1005 value,
1006 initiator,
1007 passive=PASSIVE_OFF,
1008 check_old=None,
1009 pop=False,
1010 ):
1011 raise NotImplementedError()
1013 def get_committed_value(self, state, dict_, passive=PASSIVE_OFF):
1014 """return the unchanged value of this attribute"""
1016 if self.key in state.committed_state:
1017 value = state.committed_state[self.key]
1018 if value is NO_VALUE:
1019 return None
1020 else:
1021 return value
1022 else:
1023 return self.get(state, dict_, passive=passive)
1025 def set_committed_value(self, state, dict_, value):
1026 """set an attribute value on the given instance and 'commit' it."""
1028 dict_[self.key] = value
1029 state._commit(dict_, [self.key])
1030 return value
1033class ScalarAttributeImpl(AttributeImpl):
1034 """represents a scalar value-holding InstrumentedAttribute."""
1036 default_accepts_scalar_loader = True
1037 uses_objects = False
1038 supports_population = True
1039 collection = False
1040 dynamic = False
1042 __slots__ = "_replace_token", "_append_token", "_remove_token"
1044 def __init__(self, *arg, **kw):
1045 super(ScalarAttributeImpl, self).__init__(*arg, **kw)
1046 self._replace_token = self._append_token = Event(self, OP_REPLACE)
1047 self._remove_token = Event(self, OP_REMOVE)
1049 def delete(self, state, dict_):
1050 if self.dispatch._active_history:
1051 old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
1052 else:
1053 old = dict_.get(self.key, NO_VALUE)
1055 if self.dispatch.remove:
1056 self.fire_remove_event(state, dict_, old, self._remove_token)
1057 state._modified_event(dict_, self, old)
1059 existing = dict_.pop(self.key, NO_VALUE)
1060 if (
1061 existing is NO_VALUE
1062 and old is NO_VALUE
1063 and not state.expired
1064 and self.key not in state.expired_attributes
1065 ):
1066 raise AttributeError("%s object does not have a value" % self)
1068 def get_history(self, state, dict_, passive=PASSIVE_OFF):
1069 if self.key in dict_:
1070 return History.from_scalar_attribute(self, state, dict_[self.key])
1071 elif self.key in state.committed_state:
1072 return History.from_scalar_attribute(self, state, NO_VALUE)
1073 else:
1074 if passive & INIT_OK:
1075 passive ^= INIT_OK
1076 current = self.get(state, dict_, passive=passive)
1077 if current is PASSIVE_NO_RESULT:
1078 return HISTORY_BLANK
1079 else:
1080 return History.from_scalar_attribute(self, state, current)
1082 def set(
1083 self,
1084 state,
1085 dict_,
1086 value,
1087 initiator,
1088 passive=PASSIVE_OFF,
1089 check_old=None,
1090 pop=False,
1091 ):
1092 if self.dispatch._active_history:
1093 old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
1094 else:
1095 old = dict_.get(self.key, NO_VALUE)
1097 if self.dispatch.set:
1098 value = self.fire_replace_event(
1099 state, dict_, value, old, initiator
1100 )
1101 state._modified_event(dict_, self, old)
1102 dict_[self.key] = value
1104 def fire_replace_event(self, state, dict_, value, previous, initiator):
1105 for fn in self.dispatch.set:
1106 value = fn(
1107 state, value, previous, initiator or self._replace_token
1108 )
1109 return value
1111 def fire_remove_event(self, state, dict_, value, initiator):
1112 for fn in self.dispatch.remove:
1113 fn(state, value, initiator or self._remove_token)
1115 @property
1116 def type(self):
1117 self.property.columns[0].type
1120class ScalarObjectAttributeImpl(ScalarAttributeImpl):
1121 """represents a scalar-holding InstrumentedAttribute,
1122 where the target object is also instrumented.
1124 Adds events to delete/set operations.
1126 """
1128 default_accepts_scalar_loader = False
1129 uses_objects = True
1130 supports_population = True
1131 collection = False
1133 __slots__ = ()
1135 def delete(self, state, dict_):
1136 if self.dispatch._active_history:
1137 old = self.get(
1138 state,
1139 dict_,
1140 passive=PASSIVE_ONLY_PERSISTENT
1141 | NO_AUTOFLUSH
1142 | LOAD_AGAINST_COMMITTED,
1143 )
1144 else:
1145 old = self.get(
1146 state,
1147 dict_,
1148 passive=PASSIVE_NO_FETCH ^ INIT_OK
1149 | LOAD_AGAINST_COMMITTED
1150 | NO_RAISE,
1151 )
1153 self.fire_remove_event(state, dict_, old, self._remove_token)
1155 existing = dict_.pop(self.key, NO_VALUE)
1157 # if the attribute is expired, we currently have no way to tell
1158 # that an object-attribute was expired vs. not loaded. So
1159 # for this test, we look to see if the object has a DB identity.
1160 if (
1161 existing is NO_VALUE
1162 and old is not PASSIVE_NO_RESULT
1163 and state.key is None
1164 ):
1165 raise AttributeError("%s object does not have a value" % self)
1167 def get_history(self, state, dict_, passive=PASSIVE_OFF):
1168 if self.key in dict_:
1169 current = dict_[self.key]
1170 else:
1171 if passive & INIT_OK:
1172 passive ^= INIT_OK
1173 current = self.get(state, dict_, passive=passive)
1174 if current is PASSIVE_NO_RESULT:
1175 return HISTORY_BLANK
1177 if not self._deferred_history:
1178 return History.from_object_attribute(self, state, current)
1179 else:
1180 original = state.committed_state.get(self.key, _NO_HISTORY)
1181 if original is PASSIVE_NO_RESULT:
1183 loader_passive = passive | (
1184 PASSIVE_ONLY_PERSISTENT
1185 | NO_AUTOFLUSH
1186 | LOAD_AGAINST_COMMITTED
1187 | NO_RAISE
1188 | DEFERRED_HISTORY_LOAD
1189 )
1190 original = self._fire_loader_callables(
1191 state, self.key, loader_passive
1192 )
1193 return History.from_object_attribute(
1194 self, state, current, original=original
1195 )
1197 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
1198 if self.key in dict_:
1199 current = dict_[self.key]
1200 elif passive & CALLABLES_OK:
1201 current = self.get(state, dict_, passive=passive)
1202 else:
1203 return []
1205 # can't use __hash__(), can't use __eq__() here
1206 if (
1207 current is not None
1208 and current is not PASSIVE_NO_RESULT
1209 and current is not NO_VALUE
1210 ):
1211 ret = [(instance_state(current), current)]
1212 else:
1213 ret = [(None, None)]
1215 if self.key in state.committed_state:
1216 original = state.committed_state[self.key]
1217 if (
1218 original is not None
1219 and original is not PASSIVE_NO_RESULT
1220 and original is not NO_VALUE
1221 and original is not current
1222 ):
1224 ret.append((instance_state(original), original))
1225 return ret
1227 def set(
1228 self,
1229 state,
1230 dict_,
1231 value,
1232 initiator,
1233 passive=PASSIVE_OFF,
1234 check_old=None,
1235 pop=False,
1236 ):
1237 """Set a value on the given InstanceState."""
1239 if self.dispatch._active_history:
1240 old = self.get(
1241 state,
1242 dict_,
1243 passive=PASSIVE_ONLY_PERSISTENT
1244 | NO_AUTOFLUSH
1245 | LOAD_AGAINST_COMMITTED,
1246 )
1247 else:
1248 old = self.get(
1249 state,
1250 dict_,
1251 passive=PASSIVE_NO_FETCH ^ INIT_OK
1252 | LOAD_AGAINST_COMMITTED
1253 | NO_RAISE,
1254 )
1256 if (
1257 check_old is not None
1258 and old is not PASSIVE_NO_RESULT
1259 and check_old is not old
1260 ):
1261 if pop:
1262 return
1263 else:
1264 raise ValueError(
1265 "Object %s not associated with %s on attribute '%s'"
1266 % (instance_str(check_old), state_str(state), self.key)
1267 )
1269 value = self.fire_replace_event(state, dict_, value, old, initiator)
1270 dict_[self.key] = value
1272 def fire_remove_event(self, state, dict_, value, initiator):
1273 if self.trackparent and value not in (
1274 None,
1275 PASSIVE_NO_RESULT,
1276 NO_VALUE,
1277 ):
1278 self.sethasparent(instance_state(value), state, False)
1280 for fn in self.dispatch.remove:
1281 fn(state, value, initiator or self._remove_token)
1283 state._modified_event(dict_, self, value)
1285 def fire_replace_event(self, state, dict_, value, previous, initiator):
1286 if self.trackparent:
1287 if previous is not value and previous not in (
1288 None,
1289 PASSIVE_NO_RESULT,
1290 NO_VALUE,
1291 ):
1292 self.sethasparent(instance_state(previous), state, False)
1294 for fn in self.dispatch.set:
1295 value = fn(
1296 state, value, previous, initiator or self._replace_token
1297 )
1299 state._modified_event(dict_, self, previous)
1301 if self.trackparent:
1302 if value is not None:
1303 self.sethasparent(instance_state(value), state, True)
1305 return value
1308class CollectionAttributeImpl(AttributeImpl):
1309 """A collection-holding attribute that instruments changes in membership.
1311 Only handles collections of instrumented objects.
1313 InstrumentedCollectionAttribute holds an arbitrary, user-specified
1314 container object (defaulting to a list) and brokers access to the
1315 CollectionAdapter, a "view" onto that object that presents consistent bag
1316 semantics to the orm layer independent of the user data implementation.
1318 """
1320 default_accepts_scalar_loader = False
1321 uses_objects = True
1322 supports_population = True
1323 collection = True
1324 dynamic = False
1326 __slots__ = (
1327 "copy",
1328 "collection_factory",
1329 "_append_token",
1330 "_remove_token",
1331 "_bulk_replace_token",
1332 "_duck_typed_as",
1333 )
1335 def __init__(
1336 self,
1337 class_,
1338 key,
1339 callable_,
1340 dispatch,
1341 typecallable=None,
1342 trackparent=False,
1343 copy_function=None,
1344 compare_function=None,
1345 **kwargs
1346 ):
1347 super(CollectionAttributeImpl, self).__init__(
1348 class_,
1349 key,
1350 callable_,
1351 dispatch,
1352 trackparent=trackparent,
1353 compare_function=compare_function,
1354 **kwargs
1355 )
1357 if copy_function is None:
1358 copy_function = self.__copy
1359 self.copy = copy_function
1360 self.collection_factory = typecallable
1361 self._append_token = Event(self, OP_APPEND)
1362 self._remove_token = Event(self, OP_REMOVE)
1363 self._bulk_replace_token = Event(self, OP_BULK_REPLACE)
1364 self._duck_typed_as = util.duck_type_collection(
1365 self.collection_factory()
1366 )
1368 if getattr(self.collection_factory, "_sa_linker", None):
1370 @event.listens_for(self, "init_collection")
1371 def link(target, collection, collection_adapter):
1372 collection._sa_linker(collection_adapter)
1374 @event.listens_for(self, "dispose_collection")
1375 def unlink(target, collection, collection_adapter):
1376 collection._sa_linker(None)
1378 def __copy(self, item):
1379 return [y for y in collections.collection_adapter(item)]
1381 def get_history(self, state, dict_, passive=PASSIVE_OFF):
1382 current = self.get(state, dict_, passive=passive)
1383 if current is PASSIVE_NO_RESULT:
1384 return HISTORY_BLANK
1385 else:
1386 return History.from_collection(self, state, current)
1388 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
1389 # NOTE: passive is ignored here at the moment
1391 if self.key not in dict_:
1392 return []
1394 current = dict_[self.key]
1395 current = getattr(current, "_sa_adapter")
1397 if self.key in state.committed_state:
1398 original = state.committed_state[self.key]
1399 if original is not NO_VALUE:
1400 current_states = [
1401 ((c is not None) and instance_state(c) or None, c)
1402 for c in current
1403 ]
1404 original_states = [
1405 ((c is not None) and instance_state(c) or None, c)
1406 for c in original
1407 ]
1409 current_set = dict(current_states)
1410 original_set = dict(original_states)
1412 return (
1413 [
1414 (s, o)
1415 for s, o in current_states
1416 if s not in original_set
1417 ]
1418 + [(s, o) for s, o in current_states if s in original_set]
1419 + [
1420 (s, o)
1421 for s, o in original_states
1422 if s not in current_set
1423 ]
1424 )
1426 return [(instance_state(o), o) for o in current]
1428 def fire_append_event(self, state, dict_, value, initiator):
1429 for fn in self.dispatch.append:
1430 value = fn(state, value, initiator or self._append_token)
1432 state._modified_event(dict_, self, NO_VALUE, True)
1434 if self.trackparent and value is not None:
1435 self.sethasparent(instance_state(value), state, True)
1437 return value
1439 def fire_append_wo_mutation_event(self, state, dict_, value, initiator):
1440 for fn in self.dispatch.append_wo_mutation:
1441 value = fn(state, value, initiator or self._append_token)
1443 return value
1445 def fire_pre_remove_event(self, state, dict_, initiator):
1446 """A special event used for pop() operations.
1448 The "remove" event needs to have the item to be removed passed to
1449 it, which in the case of pop from a set, we don't have a way to access
1450 the item before the operation. the event is used for all pop()
1451 operations (even though set.pop is the one where it is really needed).
1453 """
1454 state._modified_event(dict_, self, NO_VALUE, True)
1456 def fire_remove_event(self, state, dict_, value, initiator):
1457 if self.trackparent and value is not None:
1458 self.sethasparent(instance_state(value), state, False)
1460 for fn in self.dispatch.remove:
1461 fn(state, value, initiator or self._remove_token)
1463 state._modified_event(dict_, self, NO_VALUE, True)
1465 def delete(self, state, dict_):
1466 if self.key not in dict_:
1467 return
1469 state._modified_event(dict_, self, NO_VALUE, True)
1471 collection = self.get_collection(state, state.dict)
1472 collection.clear_with_event()
1474 # key is always present because we checked above. e.g.
1475 # del is a no-op if collection not present.
1476 del dict_[self.key]
1478 def _default_value(self, state, dict_):
1479 """Produce an empty collection for an un-initialized attribute"""
1481 assert self.key not in dict_, (
1482 "_default_value should only be invoked for an "
1483 "uninitialized or expired attribute"
1484 )
1486 if self.key in state._empty_collections:
1487 return state._empty_collections[self.key]
1489 adapter, user_data = self._initialize_collection(state)
1490 adapter._set_empty(user_data)
1491 return user_data
1493 def _initialize_collection(self, state):
1495 adapter, collection = state.manager.initialize_collection(
1496 self.key, state, self.collection_factory
1497 )
1499 self.dispatch.init_collection(state, collection, adapter)
1501 return adapter, collection
1503 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1504 collection = self.get_collection(state, dict_, passive=passive)
1505 if collection is PASSIVE_NO_RESULT:
1506 value = self.fire_append_event(state, dict_, value, initiator)
1507 assert (
1508 self.key not in dict_
1509 ), "Collection was loaded during event handling."
1510 state._get_pending_mutation(self.key).append(value)
1511 else:
1512 collection.append_with_event(value, initiator)
1514 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1515 collection = self.get_collection(state, state.dict, passive=passive)
1516 if collection is PASSIVE_NO_RESULT:
1517 self.fire_remove_event(state, dict_, value, initiator)
1518 assert (
1519 self.key not in dict_
1520 ), "Collection was loaded during event handling."
1521 state._get_pending_mutation(self.key).remove(value)
1522 else:
1523 collection.remove_with_event(value, initiator)
1525 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1526 try:
1527 # TODO: better solution here would be to add
1528 # a "popper" role to collections.py to complement
1529 # "remover".
1530 self.remove(state, dict_, value, initiator, passive=passive)
1531 except (ValueError, KeyError, IndexError):
1532 pass
1534 def set(
1535 self,
1536 state,
1537 dict_,
1538 value,
1539 initiator=None,
1540 passive=PASSIVE_OFF,
1541 check_old=None,
1542 pop=False,
1543 _adapt=True,
1544 ):
1545 iterable = orig_iterable = value
1547 # pulling a new collection first so that an adaptation exception does
1548 # not trigger a lazy load of the old collection.
1549 new_collection, user_data = self._initialize_collection(state)
1550 if _adapt:
1551 if new_collection._converter is not None:
1552 iterable = new_collection._converter(iterable)
1553 else:
1554 setting_type = util.duck_type_collection(iterable)
1555 receiving_type = self._duck_typed_as
1557 if setting_type is not receiving_type:
1558 given = (
1559 iterable is None
1560 and "None"
1561 or iterable.__class__.__name__
1562 )
1563 wanted = self._duck_typed_as.__name__
1564 raise TypeError(
1565 "Incompatible collection type: %s is not %s-like"
1566 % (given, wanted)
1567 )
1569 # If the object is an adapted collection, return the (iterable)
1570 # adapter.
1571 if hasattr(iterable, "_sa_iterator"):
1572 iterable = iterable._sa_iterator()
1573 elif setting_type is dict:
1574 if util.py3k:
1575 iterable = iterable.values()
1576 else:
1577 iterable = getattr(
1578 iterable, "itervalues", iterable.values
1579 )()
1580 else:
1581 iterable = iter(iterable)
1582 new_values = list(iterable)
1584 evt = self._bulk_replace_token
1586 self.dispatch.bulk_replace(state, new_values, evt)
1588 # propagate NO_RAISE in passive through to the get() for the
1589 # existing object (ticket #8862)
1590 old = self.get(
1591 state,
1592 dict_,
1593 passive=PASSIVE_ONLY_PERSISTENT ^ (passive & NO_RAISE),
1594 )
1595 if old is PASSIVE_NO_RESULT:
1596 old = self._default_value(state, dict_)
1597 elif old is orig_iterable:
1598 # ignore re-assignment of the current collection, as happens
1599 # implicitly with in-place operators (foo.collection |= other)
1600 return
1602 # place a copy of "old" in state.committed_state
1603 state._modified_event(dict_, self, old, True)
1605 old_collection = old._sa_adapter
1607 dict_[self.key] = user_data
1609 collections.bulk_replace(
1610 new_values, old_collection, new_collection, initiator=evt
1611 )
1613 self._dispose_previous_collection(state, old, old_collection, True)
1615 def _dispose_previous_collection(
1616 self, state, collection, adapter, fire_event
1617 ):
1618 del collection._sa_adapter
1620 # discarding old collection make sure it is not referenced in empty
1621 # collections.
1622 state._empty_collections.pop(self.key, None)
1623 if fire_event:
1624 self.dispatch.dispose_collection(state, collection, adapter)
1626 def _invalidate_collection(self, collection):
1627 adapter = getattr(collection, "_sa_adapter")
1628 adapter.invalidated = True
1630 def set_committed_value(self, state, dict_, value):
1631 """Set an attribute value on the given instance and 'commit' it."""
1633 collection, user_data = self._initialize_collection(state)
1635 if value:
1636 collection.append_multiple_without_event(value)
1638 state.dict[self.key] = user_data
1640 state._commit(dict_, [self.key])
1642 if self.key in state._pending_mutations:
1643 # pending items exist. issue a modified event,
1644 # add/remove new items.
1645 state._modified_event(dict_, self, user_data, True)
1647 pending = state._pending_mutations.pop(self.key)
1648 added = pending.added_items
1649 removed = pending.deleted_items
1650 for item in added:
1651 collection.append_without_event(item)
1652 for item in removed:
1653 collection.remove_without_event(item)
1655 return user_data
1657 def get_collection(
1658 self, state, dict_, user_data=None, passive=PASSIVE_OFF
1659 ):
1660 """Retrieve the CollectionAdapter associated with the given state.
1662 if user_data is None, retrieves it from the state using normal
1663 "get()" rules, which will fire lazy callables or return the "empty"
1664 collection value.
1666 """
1667 if user_data is None:
1668 user_data = self.get(state, dict_, passive=passive)
1669 if user_data is PASSIVE_NO_RESULT:
1670 return user_data
1672 return user_data._sa_adapter
1675def backref_listeners(attribute, key, uselist):
1676 """Apply listeners to synchronize a two-way relationship."""
1678 # use easily recognizable names for stack traces.
1680 # in the sections marked "tokens to test for a recursive loop",
1681 # this is somewhat brittle and very performance-sensitive logic
1682 # that is specific to how we might arrive at each event. a marker
1683 # that can target us directly to arguments being invoked against
1684 # the impl might be simpler, but could interfere with other systems.
1686 parent_token = attribute.impl.parent_token
1687 parent_impl = attribute.impl
1689 def _acceptable_key_err(child_state, initiator, child_impl):
1690 raise ValueError(
1691 "Bidirectional attribute conflict detected: "
1692 'Passing object %s to attribute "%s" '
1693 'triggers a modify event on attribute "%s" '
1694 'via the backref "%s".'
1695 % (
1696 state_str(child_state),
1697 initiator.parent_token,
1698 child_impl.parent_token,
1699 attribute.impl.parent_token,
1700 )
1701 )
1703 def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
1704 if oldchild is child:
1705 return child
1706 if (
1707 oldchild is not None
1708 and oldchild is not PASSIVE_NO_RESULT
1709 and oldchild is not NO_VALUE
1710 ):
1711 # With lazy=None, there's no guarantee that the full collection is
1712 # present when updating via a backref.
1713 old_state, old_dict = (
1714 instance_state(oldchild),
1715 instance_dict(oldchild),
1716 )
1717 impl = old_state.manager[key].impl
1719 # tokens to test for a recursive loop.
1720 if not impl.collection and not impl.dynamic:
1721 check_recursive_token = impl._replace_token
1722 else:
1723 check_recursive_token = impl._remove_token
1725 if initiator is not check_recursive_token:
1726 impl.pop(
1727 old_state,
1728 old_dict,
1729 state.obj(),
1730 parent_impl._append_token,
1731 passive=PASSIVE_NO_FETCH,
1732 )
1734 if child is not None:
1735 child_state, child_dict = (
1736 instance_state(child),
1737 instance_dict(child),
1738 )
1739 child_impl = child_state.manager[key].impl
1741 if (
1742 initiator.parent_token is not parent_token
1743 and initiator.parent_token is not child_impl.parent_token
1744 ):
1745 _acceptable_key_err(state, initiator, child_impl)
1747 # tokens to test for a recursive loop.
1748 check_append_token = child_impl._append_token
1749 check_bulk_replace_token = (
1750 child_impl._bulk_replace_token
1751 if child_impl.collection
1752 else None
1753 )
1755 if (
1756 initiator is not check_append_token
1757 and initiator is not check_bulk_replace_token
1758 ):
1759 child_impl.append(
1760 child_state,
1761 child_dict,
1762 state.obj(),
1763 initiator,
1764 passive=PASSIVE_NO_FETCH,
1765 )
1766 return child
1768 def emit_backref_from_collection_append_event(state, child, initiator):
1769 if child is None:
1770 return
1772 child_state, child_dict = instance_state(child), instance_dict(child)
1773 child_impl = child_state.manager[key].impl
1775 if (
1776 initiator.parent_token is not parent_token
1777 and initiator.parent_token is not child_impl.parent_token
1778 ):
1779 _acceptable_key_err(state, initiator, child_impl)
1781 # tokens to test for a recursive loop.
1782 check_append_token = child_impl._append_token
1783 check_bulk_replace_token = (
1784 child_impl._bulk_replace_token if child_impl.collection else None
1785 )
1787 if (
1788 initiator is not check_append_token
1789 and initiator is not check_bulk_replace_token
1790 ):
1791 child_impl.append(
1792 child_state,
1793 child_dict,
1794 state.obj(),
1795 initiator,
1796 passive=PASSIVE_NO_FETCH,
1797 )
1798 return child
1800 def emit_backref_from_collection_remove_event(state, child, initiator):
1801 if (
1802 child is not None
1803 and child is not PASSIVE_NO_RESULT
1804 and child is not NO_VALUE
1805 ):
1806 child_state, child_dict = (
1807 instance_state(child),
1808 instance_dict(child),
1809 )
1810 child_impl = child_state.manager[key].impl
1812 # tokens to test for a recursive loop.
1813 if not child_impl.collection and not child_impl.dynamic:
1814 check_remove_token = child_impl._remove_token
1815 check_replace_token = child_impl._replace_token
1816 check_for_dupes_on_remove = uselist and not parent_impl.dynamic
1817 else:
1818 check_remove_token = child_impl._remove_token
1819 check_replace_token = (
1820 child_impl._bulk_replace_token
1821 if child_impl.collection
1822 else None
1823 )
1824 check_for_dupes_on_remove = False
1826 if (
1827 initiator is not check_remove_token
1828 and initiator is not check_replace_token
1829 ):
1831 if not check_for_dupes_on_remove or not util.has_dupes(
1832 # when this event is called, the item is usually
1833 # present in the list, except for a pop() operation.
1834 state.dict[parent_impl.key],
1835 child,
1836 ):
1837 child_impl.pop(
1838 child_state,
1839 child_dict,
1840 state.obj(),
1841 initiator,
1842 passive=PASSIVE_NO_FETCH,
1843 )
1845 if uselist:
1846 event.listen(
1847 attribute,
1848 "append",
1849 emit_backref_from_collection_append_event,
1850 retval=True,
1851 raw=True,
1852 )
1853 else:
1854 event.listen(
1855 attribute,
1856 "set",
1857 emit_backref_from_scalar_set_event,
1858 retval=True,
1859 raw=True,
1860 )
1861 # TODO: need coverage in test/orm/ of remove event
1862 event.listen(
1863 attribute,
1864 "remove",
1865 emit_backref_from_collection_remove_event,
1866 retval=True,
1867 raw=True,
1868 )
1871_NO_HISTORY = util.symbol("NO_HISTORY")
1872_NO_STATE_SYMBOLS = frozenset([id(PASSIVE_NO_RESULT), id(NO_VALUE)])
1875class History(util.namedtuple("History", ["added", "unchanged", "deleted"])):
1876 """A 3-tuple of added, unchanged and deleted values,
1877 representing the changes which have occurred on an instrumented
1878 attribute.
1880 The easiest way to get a :class:`.History` object for a particular
1881 attribute on an object is to use the :func:`_sa.inspect` function::
1883 from sqlalchemy import inspect
1885 hist = inspect(myobject).attrs.myattribute.history
1887 Each tuple member is an iterable sequence:
1889 * ``added`` - the collection of items added to the attribute (the first
1890 tuple element).
1892 * ``unchanged`` - the collection of items that have not changed on the
1893 attribute (the second tuple element).
1895 * ``deleted`` - the collection of items that have been removed from the
1896 attribute (the third tuple element).
1898 """
1900 def __bool__(self):
1901 return self != HISTORY_BLANK
1903 __nonzero__ = __bool__
1905 def empty(self):
1906 """Return True if this :class:`.History` has no changes
1907 and no existing, unchanged state.
1909 """
1911 return not bool((self.added or self.deleted) or self.unchanged)
1913 def sum(self):
1914 """Return a collection of added + unchanged + deleted."""
1916 return (
1917 (self.added or []) + (self.unchanged or []) + (self.deleted or [])
1918 )
1920 def non_deleted(self):
1921 """Return a collection of added + unchanged."""
1923 return (self.added or []) + (self.unchanged or [])
1925 def non_added(self):
1926 """Return a collection of unchanged + deleted."""
1928 return (self.unchanged or []) + (self.deleted or [])
1930 def has_changes(self):
1931 """Return True if this :class:`.History` has changes."""
1933 return bool(self.added or self.deleted)
1935 def as_state(self):
1936 return History(
1937 [
1938 (c is not None) and instance_state(c) or None
1939 for c in self.added
1940 ],
1941 [
1942 (c is not None) and instance_state(c) or None
1943 for c in self.unchanged
1944 ],
1945 [
1946 (c is not None) and instance_state(c) or None
1947 for c in self.deleted
1948 ],
1949 )
1951 @classmethod
1952 def from_scalar_attribute(cls, attribute, state, current):
1953 original = state.committed_state.get(attribute.key, _NO_HISTORY)
1955 if original is _NO_HISTORY:
1956 if current is NO_VALUE:
1957 return cls((), (), ())
1958 else:
1959 return cls((), [current], ())
1960 # don't let ClauseElement expressions here trip things up
1961 elif (
1962 current is not NO_VALUE
1963 and attribute.is_equal(current, original) is True
1964 ):
1965 return cls((), [current], ())
1966 else:
1967 # current convention on native scalars is to not
1968 # include information
1969 # about missing previous value in "deleted", but
1970 # we do include None, which helps in some primary
1971 # key situations
1972 if id(original) in _NO_STATE_SYMBOLS:
1973 deleted = ()
1974 # indicate a "del" operation occurred when we don't have
1975 # the previous value as: ([None], (), ())
1976 if id(current) in _NO_STATE_SYMBOLS:
1977 current = None
1978 else:
1979 deleted = [original]
1980 if current is NO_VALUE:
1981 return cls((), (), deleted)
1982 else:
1983 return cls([current], (), deleted)
1985 @classmethod
1986 def from_object_attribute(
1987 cls, attribute, state, current, original=_NO_HISTORY
1988 ):
1989 if original is _NO_HISTORY:
1990 original = state.committed_state.get(attribute.key, _NO_HISTORY)
1992 if original is _NO_HISTORY:
1993 if current is NO_VALUE:
1994 return cls((), (), ())
1995 else:
1996 return cls((), [current], ())
1997 elif current is original and current is not NO_VALUE:
1998 return cls((), [current], ())
1999 else:
2000 # current convention on related objects is to not
2001 # include information
2002 # about missing previous value in "deleted", and
2003 # to also not include None - the dependency.py rules
2004 # ignore the None in any case.
2005 if id(original) in _NO_STATE_SYMBOLS or original is None:
2006 deleted = ()
2007 # indicate a "del" operation occurred when we don't have
2008 # the previous value as: ([None], (), ())
2009 if id(current) in _NO_STATE_SYMBOLS:
2010 current = None
2011 else:
2012 deleted = [original]
2013 if current is NO_VALUE:
2014 return cls((), (), deleted)
2015 else:
2016 return cls([current], (), deleted)
2018 @classmethod
2019 def from_collection(cls, attribute, state, current):
2020 original = state.committed_state.get(attribute.key, _NO_HISTORY)
2021 if current is NO_VALUE:
2022 return cls((), (), ())
2024 current = getattr(current, "_sa_adapter")
2025 if original is NO_VALUE:
2026 return cls(list(current), (), ())
2027 elif original is _NO_HISTORY:
2028 return cls((), list(current), ())
2029 else:
2031 current_states = [
2032 ((c is not None) and instance_state(c) or None, c)
2033 for c in current
2034 ]
2035 original_states = [
2036 ((c is not None) and instance_state(c) or None, c)
2037 for c in original
2038 ]
2040 current_set = dict(current_states)
2041 original_set = dict(original_states)
2043 return cls(
2044 [o for s, o in current_states if s not in original_set],
2045 [o for s, o in current_states if s in original_set],
2046 [o for s, o in original_states if s not in current_set],
2047 )
2050HISTORY_BLANK = History(None, None, None)
2053def get_history(obj, key, passive=PASSIVE_OFF):
2054 """Return a :class:`.History` record for the given object
2055 and attribute key.
2057 This is the **pre-flush** history for a given attribute, which is
2058 reset each time the :class:`.Session` flushes changes to the
2059 current database transaction.
2061 .. note::
2063 Prefer to use the :attr:`.AttributeState.history` and
2064 :meth:`.AttributeState.load_history` accessors to retrieve the
2065 :class:`.History` for instance attributes.
2068 :param obj: an object whose class is instrumented by the
2069 attributes package.
2071 :param key: string attribute name.
2073 :param passive: indicates loading behavior for the attribute
2074 if the value is not already present. This is a
2075 bitflag attribute, which defaults to the symbol
2076 :attr:`.PASSIVE_OFF` indicating all necessary SQL
2077 should be emitted.
2079 .. seealso::
2081 :attr:`.AttributeState.history`
2083 :meth:`.AttributeState.load_history` - retrieve history
2084 using loader callables if the value is not locally present.
2086 """
2088 return get_state_history(instance_state(obj), key, passive)
2091def get_state_history(state, key, passive=PASSIVE_OFF):
2092 return state.get_history(key, passive)
2095def has_parent(cls, obj, key, optimistic=False):
2096 """TODO"""
2097 manager = manager_of_class(cls)
2098 state = instance_state(obj)
2099 return manager.has_parent(state, key, optimistic)
2102def register_attribute(class_, key, **kw):
2103 comparator = kw.pop("comparator", None)
2104 parententity = kw.pop("parententity", None)
2105 doc = kw.pop("doc", None)
2106 desc = register_descriptor(class_, key, comparator, parententity, doc=doc)
2107 register_attribute_impl(class_, key, **kw)
2108 return desc
2111def register_attribute_impl(
2112 class_,
2113 key,
2114 uselist=False,
2115 callable_=None,
2116 useobject=False,
2117 impl_class=None,
2118 backref=None,
2119 **kw
2120):
2122 manager = manager_of_class(class_)
2123 if uselist:
2124 factory = kw.pop("typecallable", None)
2125 typecallable = manager.instrument_collection_class(
2126 key, factory or list
2127 )
2128 else:
2129 typecallable = kw.pop("typecallable", None)
2131 dispatch = manager[key].dispatch
2133 if impl_class:
2134 impl = impl_class(class_, key, typecallable, dispatch, **kw)
2135 elif uselist:
2136 impl = CollectionAttributeImpl(
2137 class_, key, callable_, dispatch, typecallable=typecallable, **kw
2138 )
2139 elif useobject:
2140 impl = ScalarObjectAttributeImpl(
2141 class_, key, callable_, dispatch, **kw
2142 )
2143 else:
2144 impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
2146 manager[key].impl = impl
2148 if backref:
2149 backref_listeners(manager[key], backref, uselist)
2151 manager.post_configure_attribute(key)
2152 return manager[key]
2155def register_descriptor(
2156 class_, key, comparator=None, parententity=None, doc=None
2157):
2158 manager = manager_of_class(class_)
2160 descriptor = InstrumentedAttribute(
2161 class_, key, comparator=comparator, parententity=parententity
2162 )
2164 descriptor.__doc__ = doc
2166 manager.instrument_attribute(key, descriptor)
2167 return descriptor
2170def unregister_attribute(class_, key):
2171 manager_of_class(class_).uninstrument_attribute(key)
2174def init_collection(obj, key):
2175 """Initialize a collection attribute and return the collection adapter.
2177 This function is used to provide direct access to collection internals
2178 for a previously unloaded attribute. e.g.::
2180 collection_adapter = init_collection(someobject, 'elements')
2181 for elem in values:
2182 collection_adapter.append_without_event(elem)
2184 For an easier way to do the above, see
2185 :func:`~sqlalchemy.orm.attributes.set_committed_value`.
2187 :param obj: a mapped object
2189 :param key: string attribute name where the collection is located.
2191 """
2192 state = instance_state(obj)
2193 dict_ = state.dict
2194 return init_state_collection(state, dict_, key)
2197def init_state_collection(state, dict_, key):
2198 """Initialize a collection attribute and return the collection adapter.
2200 Discards any existing collection which may be there.
2202 """
2203 attr = state.manager[key].impl
2205 old = dict_.pop(key, None) # discard old collection
2206 if old is not None:
2207 old_collection = old._sa_adapter
2208 attr._dispose_previous_collection(state, old, old_collection, False)
2210 user_data = attr._default_value(state, dict_)
2211 adapter = attr.get_collection(state, dict_, user_data)
2212 adapter._reset_empty()
2214 return adapter
2217def set_committed_value(instance, key, value):
2218 """Set the value of an attribute with no history events.
2220 Cancels any previous history present. The value should be
2221 a scalar value for scalar-holding attributes, or
2222 an iterable for any collection-holding attribute.
2224 This is the same underlying method used when a lazy loader
2225 fires off and loads additional data from the database.
2226 In particular, this method can be used by application code
2227 which has loaded additional attributes or collections through
2228 separate queries, which can then be attached to an instance
2229 as though it were part of its original loaded state.
2231 """
2232 state, dict_ = instance_state(instance), instance_dict(instance)
2233 state.manager[key].impl.set_committed_value(state, dict_, value)
2236def set_attribute(instance, key, value, initiator=None):
2237 """Set the value of an attribute, firing history events.
2239 This function may be used regardless of instrumentation
2240 applied directly to the class, i.e. no descriptors are required.
2241 Custom attribute management schemes will need to make usage
2242 of this method to establish attribute state as understood
2243 by SQLAlchemy.
2245 :param instance: the object that will be modified
2247 :param key: string name of the attribute
2249 :param value: value to assign
2251 :param initiator: an instance of :class:`.Event` that would have
2252 been propagated from a previous event listener. This argument
2253 is used when the :func:`.set_attribute` function is being used within
2254 an existing event listening function where an :class:`.Event` object
2255 is being supplied; the object may be used to track the origin of the
2256 chain of events.
2258 .. versionadded:: 1.2.3
2260 """
2261 state, dict_ = instance_state(instance), instance_dict(instance)
2262 state.manager[key].impl.set(state, dict_, value, initiator)
2265def get_attribute(instance, key):
2266 """Get the value of an attribute, firing any callables required.
2268 This function may be used regardless of instrumentation
2269 applied directly to the class, i.e. no descriptors are required.
2270 Custom attribute management schemes will need to make usage
2271 of this method to make usage of attribute state as understood
2272 by SQLAlchemy.
2274 """
2275 state, dict_ = instance_state(instance), instance_dict(instance)
2276 return state.manager[key].impl.get(state, dict_)
2279def del_attribute(instance, key):
2280 """Delete the value of an attribute, firing history events.
2282 This function may be used regardless of instrumentation
2283 applied directly to the class, i.e. no descriptors are required.
2284 Custom attribute management schemes will need to make usage
2285 of this method to establish attribute state as understood
2286 by SQLAlchemy.
2288 """
2289 state, dict_ = instance_state(instance), instance_dict(instance)
2290 state.manager[key].impl.delete(state, dict_)
2293def flag_modified(instance, key):
2294 """Mark an attribute on an instance as 'modified'.
2296 This sets the 'modified' flag on the instance and
2297 establishes an unconditional change event for the given attribute.
2298 The attribute must have a value present, else an
2299 :class:`.InvalidRequestError` is raised.
2301 To mark an object "dirty" without referring to any specific attribute
2302 so that it is considered within a flush, use the
2303 :func:`.attributes.flag_dirty` call.
2305 .. seealso::
2307 :func:`.attributes.flag_dirty`
2309 """
2310 state, dict_ = instance_state(instance), instance_dict(instance)
2311 impl = state.manager[key].impl
2312 impl.dispatch.modified(state, impl._modified_token)
2313 state._modified_event(dict_, impl, NO_VALUE, is_userland=True)
2316def flag_dirty(instance):
2317 """Mark an instance as 'dirty' without any specific attribute mentioned.
2319 This is a special operation that will allow the object to travel through
2320 the flush process for interception by events such as
2321 :meth:`.SessionEvents.before_flush`. Note that no SQL will be emitted in
2322 the flush process for an object that has no changes, even if marked dirty
2323 via this method. However, a :meth:`.SessionEvents.before_flush` handler
2324 will be able to see the object in the :attr:`.Session.dirty` collection and
2325 may establish changes on it, which will then be included in the SQL
2326 emitted.
2328 .. versionadded:: 1.2
2330 .. seealso::
2332 :func:`.attributes.flag_modified`
2334 """
2336 state, dict_ = instance_state(instance), instance_dict(instance)
2337 state._modified_event(dict_, None, NO_VALUE, is_userland=True)