Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py: 32%
848 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/attributes.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"""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
57from ..sql.traversals import HasCacheKey
58from ..sql.visitors import InternalTraversal
61class NoKey(str):
62 pass
65NO_KEY = NoKey("no name")
68@inspection._self_inspects
69class QueryableAttribute(
70 interfaces._MappedAttribute,
71 interfaces.InspectionAttr,
72 interfaces.PropComparator,
73 traversals.HasCopyInternals,
74 roles.JoinTargetRole,
75 roles.OnClauseRole,
76 sql_base.Immutable,
77 sql_base.MemoizedHasCacheKey,
78):
79 """Base class for :term:`descriptor` objects that intercept
80 attribute events on behalf of a :class:`.MapperProperty`
81 object. The actual :class:`.MapperProperty` is accessible
82 via the :attr:`.QueryableAttribute.property`
83 attribute.
86 .. seealso::
88 :class:`.InstrumentedAttribute`
90 :class:`.MapperProperty`
92 :attr:`_orm.Mapper.all_orm_descriptors`
94 :attr:`_orm.Mapper.attrs`
95 """
97 is_attribute = True
99 # PropComparator has a __visit_name__ to participate within
100 # traversals. Disambiguate the attribute vs. a comparator.
101 __visit_name__ = "orm_instrumented_attribute"
103 def __init__(
104 self,
105 class_,
106 key,
107 parententity,
108 impl=None,
109 comparator=None,
110 of_type=None,
111 extra_criteria=(),
112 ):
113 self.class_ = class_
114 self.key = key
115 self._parententity = parententity
116 self.impl = impl
117 self.comparator = comparator
118 self._of_type = of_type
119 self._extra_criteria = extra_criteria
121 manager = manager_of_class(class_)
122 # manager is None in the case of AliasedClass
123 if manager:
124 # propagate existing event listeners from
125 # immediate superclass
126 for base in manager._bases:
127 if key in base:
128 self.dispatch._update(base[key].dispatch)
129 if base[key].dispatch._active_history:
130 self.dispatch._active_history = True
132 _cache_key_traversal = [
133 ("key", visitors.ExtendedInternalTraversal.dp_string),
134 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
135 ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
136 ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
137 ]
139 def __reduce__(self):
140 # this method is only used in terms of the
141 # sqlalchemy.ext.serializer extension
142 return (
143 _queryable_attribute_unreduce,
144 (
145 self.key,
146 self._parententity.mapper.class_,
147 self._parententity,
148 self._parententity.entity,
149 ),
150 )
152 @util.memoized_property
153 def _supports_population(self):
154 return self.impl.supports_population
156 @property
157 def _impl_uses_objects(self):
158 return self.impl.uses_objects
160 def get_history(self, instance, passive=PASSIVE_OFF):
161 return self.impl.get_history(
162 instance_state(instance), instance_dict(instance), passive
163 )
165 @util.memoized_property
166 def info(self):
167 """Return the 'info' dictionary for the underlying SQL element.
169 The behavior here is as follows:
171 * If the attribute is a column-mapped property, i.e.
172 :class:`.ColumnProperty`, which is mapped directly
173 to a schema-level :class:`_schema.Column` object, this attribute
174 will return the :attr:`.SchemaItem.info` dictionary associated
175 with the core-level :class:`_schema.Column` object.
177 * If the attribute is a :class:`.ColumnProperty` but is mapped to
178 any other kind of SQL expression other than a
179 :class:`_schema.Column`,
180 the attribute will refer to the :attr:`.MapperProperty.info`
181 dictionary associated directly with the :class:`.ColumnProperty`,
182 assuming the SQL expression itself does not have its own ``.info``
183 attribute (which should be the case, unless a user-defined SQL
184 construct has defined one).
186 * If the attribute refers to any other kind of
187 :class:`.MapperProperty`, including :class:`.RelationshipProperty`,
188 the attribute will refer to the :attr:`.MapperProperty.info`
189 dictionary associated with that :class:`.MapperProperty`.
191 * To access the :attr:`.MapperProperty.info` dictionary of the
192 :class:`.MapperProperty` unconditionally, including for a
193 :class:`.ColumnProperty` that's associated directly with a
194 :class:`_schema.Column`, the attribute can be referred to using
195 :attr:`.QueryableAttribute.property` attribute, as
196 ``MyClass.someattribute.property.info``.
198 .. seealso::
200 :attr:`.SchemaItem.info`
202 :attr:`.MapperProperty.info`
204 """
205 return self.comparator.info
207 @util.memoized_property
208 def parent(self):
209 """Return an inspection instance representing the parent.
211 This will be either an instance of :class:`_orm.Mapper`
212 or :class:`.AliasedInsp`, depending upon the nature
213 of the parent entity which this attribute is associated
214 with.
216 """
217 return inspection.inspect(self._parententity)
219 @util.memoized_property
220 def expression(self):
221 """The SQL expression object represented by this
222 :class:`.QueryableAttribute`.
224 This will typically be an instance of a :class:`_sql.ColumnElement`
225 subclass representing a column expression.
227 """
228 entity_namespace = self._entity_namespace
229 assert isinstance(entity_namespace, HasCacheKey)
231 if self.key is NO_KEY:
232 annotations = {"entity_namespace": entity_namespace}
233 else:
234 annotations = {
235 "proxy_key": self.key,
236 "proxy_owner": self._parententity,
237 "entity_namespace": entity_namespace,
238 }
240 ce = self.comparator.__clause_element__()
241 try:
242 anno = ce._annotate
243 except AttributeError as ae:
244 util.raise_(
245 exc.InvalidRequestError(
246 'When interpreting attribute "%s" as a SQL expression, '
247 "expected __clause_element__() to return "
248 "a ClauseElement object, got: %r" % (self, ce)
249 ),
250 from_=ae,
251 )
252 else:
253 return anno(annotations)
255 @property
256 def _entity_namespace(self):
257 return self._parententity
259 @property
260 def _annotations(self):
261 return self.__clause_element__()._annotations
263 def __clause_element__(self):
264 return self.expression
266 @property
267 def _from_objects(self):
268 return self.expression._from_objects
270 def _bulk_update_tuples(self, value):
271 """Return setter tuples for a bulk UPDATE."""
273 return self.comparator._bulk_update_tuples(value)
275 def adapt_to_entity(self, adapt_to_entity):
276 assert not self._of_type
277 return self.__class__(
278 adapt_to_entity.entity,
279 self.key,
280 impl=self.impl,
281 comparator=self.comparator.adapt_to_entity(adapt_to_entity),
282 parententity=adapt_to_entity,
283 )
285 def of_type(self, entity):
286 return QueryableAttribute(
287 self.class_,
288 self.key,
289 self._parententity,
290 impl=self.impl,
291 comparator=self.comparator.of_type(entity),
292 of_type=inspection.inspect(entity),
293 extra_criteria=self._extra_criteria,
294 )
296 def and_(self, *other):
297 return QueryableAttribute(
298 self.class_,
299 self.key,
300 self._parententity,
301 impl=self.impl,
302 comparator=self.comparator.and_(*other),
303 of_type=self._of_type,
304 extra_criteria=self._extra_criteria + other,
305 )
307 def _clone(self, **kw):
308 return QueryableAttribute(
309 self.class_,
310 self.key,
311 self._parententity,
312 impl=self.impl,
313 comparator=self.comparator,
314 of_type=self._of_type,
315 extra_criteria=self._extra_criteria,
316 )
318 def label(self, name):
319 return self.__clause_element__().label(name)
321 def operate(self, op, *other, **kwargs):
322 return op(self.comparator, *other, **kwargs)
324 def reverse_operate(self, op, other, **kwargs):
325 return op(other, self.comparator, **kwargs)
327 def hasparent(self, state, optimistic=False):
328 return self.impl.hasparent(state, optimistic=optimistic) is not False
330 def __getattr__(self, key):
331 try:
332 return getattr(self.comparator, key)
333 except AttributeError as err:
334 util.raise_(
335 AttributeError(
336 "Neither %r object nor %r object associated with %s "
337 "has an attribute %r"
338 % (
339 type(self).__name__,
340 type(self.comparator).__name__,
341 self,
342 key,
343 )
344 ),
345 replace_context=err,
346 )
348 def __str__(self):
349 return "%s.%s" % (self.class_.__name__, self.key)
351 @util.memoized_property
352 def property(self):
353 """Return the :class:`.MapperProperty` associated with this
354 :class:`.QueryableAttribute`.
357 Return values here will commonly be instances of
358 :class:`.ColumnProperty` or :class:`.RelationshipProperty`.
361 """
362 return self.comparator.property
365def _queryable_attribute_unreduce(key, mapped_class, parententity, entity):
366 # this method is only used in terms of the
367 # sqlalchemy.ext.serializer extension
368 if parententity.is_aliased_class:
369 return entity._get_from_serialized(key, mapped_class, parententity)
370 else:
371 return getattr(entity, key)
374if util.py3k:
375 from typing import TypeVar, Generic
377 _T = TypeVar("_T")
378 _Generic_T = Generic[_T]
379else:
380 _Generic_T = type("_Generic_T", (), {})
383class Mapped(QueryableAttribute, _Generic_T):
384 """Represent an ORM mapped :term:`descriptor` attribute for typing
385 purposes.
387 This class represents the complete descriptor interface for any class
388 attribute that will have been :term:`instrumented` by the ORM
389 :class:`_orm.Mapper` class. When used with typing stubs, it is the final
390 type that would be used by a type checker such as mypy to provide the full
391 behavioral contract for the attribute.
393 .. tip::
395 The :class:`_orm.Mapped` class represents attributes that are handled
396 directly by the :class:`_orm.Mapper` class. It does not include other
397 Python descriptor classes that are provided as extensions, including
398 :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`.
399 While these systems still make use of ORM-specific superclasses
400 and structures, they are not :term:`instrumented` by the
401 :class:`_orm.Mapper` and instead provide their own functionality
402 when they are accessed on a class.
404 When using the :ref:`SQLAlchemy Mypy plugin <mypy_toplevel>`, the
405 :class:`_orm.Mapped` construct is used in typing annotations to indicate to
406 the plugin those attributes that are expected to be mapped; the plugin also
407 applies :class:`_orm.Mapped` as an annotation automatically when it scans
408 through declarative mappings in :ref:`orm_declarative_table` style. For
409 more indirect mapping styles such as
410 :ref:`imperative table <orm_imperative_table_configuration>` it is
411 typically applied explicitly to class level attributes that expect
412 to be mapped based on a given :class:`_schema.Table` configuration.
414 :class:`_orm.Mapped` is defined in the
415 `sqlalchemy2-stubs <https://pypi.org/project/sqlalchemy2-stubs>`_ project
416 as a :pep:`484` generic class which may subscribe to any arbitrary Python
417 type, which represents the Python type handled by the attribute::
419 class MyMappedClass(Base):
420 __table_ = Table(
421 "some_table", Base.metadata,
422 Column("id", Integer, primary_key=True),
423 Column("data", String(50)),
424 Column("created_at", DateTime)
425 )
427 id : Mapped[int]
428 data: Mapped[str]
429 created_at: Mapped[datetime]
431 For complete background on how to use :class:`_orm.Mapped` with
432 pep-484 tools like Mypy, see the link below for background on SQLAlchemy's
433 Mypy plugin.
435 .. versionadded:: 1.4
437 .. seealso::
439 :ref:`mypy_toplevel` - complete background on Mypy integration
441 """
443 def __get__(self, instance, owner):
444 raise NotImplementedError()
446 def __set__(self, instance, value):
447 raise NotImplementedError()
449 def __delete__(self, instance):
450 raise NotImplementedError()
453class InstrumentedAttribute(Mapped):
454 """Class bound instrumented attribute which adds basic
455 :term:`descriptor` methods.
457 See :class:`.QueryableAttribute` for a description of most features.
460 """
462 inherit_cache = True
464 def __set__(self, instance, value):
465 self.impl.set(
466 instance_state(instance), instance_dict(instance), value, None
467 )
469 def __delete__(self, instance):
470 self.impl.delete(instance_state(instance), instance_dict(instance))
472 def __get__(self, instance, owner):
473 if instance is None:
474 return self
476 dict_ = instance_dict(instance)
477 if self._supports_population and self.key in dict_:
478 return dict_[self.key]
479 else:
480 try:
481 state = instance_state(instance)
482 except AttributeError as err:
483 util.raise_(
484 orm_exc.UnmappedInstanceError(instance),
485 replace_context=err,
486 )
487 return self.impl.get(state, dict_)
490class HasEntityNamespace(HasCacheKey):
491 __slots__ = ("_entity_namespace",)
493 is_mapper = False
494 is_aliased_class = False
496 _traverse_internals = [
497 ("_entity_namespace", InternalTraversal.dp_has_cache_key),
498 ]
500 def __init__(self, ent):
501 self._entity_namespace = ent
503 @property
504 def entity_namespace(self):
505 return self._entity_namespace.entity_namespace
508def create_proxied_attribute(descriptor):
509 """Create an QueryableAttribute / user descriptor hybrid.
511 Returns a new QueryableAttribute type that delegates descriptor
512 behavior and getattr() to the given descriptor.
513 """
515 # TODO: can move this to descriptor_props if the need for this
516 # function is removed from ext/hybrid.py
518 class Proxy(QueryableAttribute):
519 """Presents the :class:`.QueryableAttribute` interface as a
520 proxy on top of a Python descriptor / :class:`.PropComparator`
521 combination.
523 """
525 _extra_criteria = ()
527 def __init__(
528 self,
529 class_,
530 key,
531 descriptor,
532 comparator,
533 adapt_to_entity=None,
534 doc=None,
535 original_property=None,
536 ):
537 self.class_ = class_
538 self.key = key
539 self.descriptor = descriptor
540 self.original_property = original_property
541 self._comparator = comparator
542 self._adapt_to_entity = adapt_to_entity
543 self.__doc__ = doc
545 _is_internal_proxy = True
547 _cache_key_traversal = [
548 ("key", visitors.ExtendedInternalTraversal.dp_string),
549 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
550 ]
552 @property
553 def _impl_uses_objects(self):
554 return (
555 self.original_property is not None
556 and getattr(self.class_, self.key).impl.uses_objects
557 )
559 @property
560 def _parententity(self):
561 return inspection.inspect(self.class_, raiseerr=False)
563 @property
564 def _entity_namespace(self):
565 if hasattr(self._comparator, "_parententity"):
566 return self._comparator._parententity
567 else:
568 # used by hybrid attributes which try to remain
569 # agnostic of any ORM concepts like mappers
570 return HasEntityNamespace(self._parententity)
572 @property
573 def property(self):
574 return self.comparator.property
576 @util.memoized_property
577 def comparator(self):
578 if callable(self._comparator):
579 self._comparator = self._comparator()
580 if self._adapt_to_entity:
581 self._comparator = self._comparator.adapt_to_entity(
582 self._adapt_to_entity
583 )
584 return self._comparator
586 def adapt_to_entity(self, adapt_to_entity):
587 return self.__class__(
588 adapt_to_entity.entity,
589 self.key,
590 self.descriptor,
591 self._comparator,
592 adapt_to_entity,
593 )
595 def _clone(self, **kw):
596 return self.__class__(
597 self.class_,
598 self.key,
599 self.descriptor,
600 self._comparator,
601 adapt_to_entity=self._adapt_to_entity,
602 original_property=self.original_property,
603 )
605 def __get__(self, instance, owner):
606 retval = self.descriptor.__get__(instance, owner)
607 # detect if this is a plain Python @property, which just returns
608 # itself for class level access. If so, then return us.
609 # Otherwise, return the object returned by the descriptor.
610 if retval is self.descriptor and instance is None:
611 return self
612 else:
613 return retval
615 def __str__(self):
616 return "%s.%s" % (self.class_.__name__, self.key)
618 def __getattr__(self, attribute):
619 """Delegate __getattr__ to the original descriptor and/or
620 comparator."""
621 try:
622 return getattr(descriptor, attribute)
623 except AttributeError as err:
624 if attribute == "comparator":
625 util.raise_(
626 AttributeError("comparator"), replace_context=err
627 )
628 try:
629 # comparator itself might be unreachable
630 comparator = self.comparator
631 except AttributeError as err2:
632 util.raise_(
633 AttributeError(
634 "Neither %r object nor unconfigured comparator "
635 "object associated with %s has an attribute %r"
636 % (type(descriptor).__name__, self, attribute)
637 ),
638 replace_context=err2,
639 )
640 else:
641 try:
642 return getattr(comparator, attribute)
643 except AttributeError as err3:
644 util.raise_(
645 AttributeError(
646 "Neither %r object nor %r object "
647 "associated with %s has an attribute %r"
648 % (
649 type(descriptor).__name__,
650 type(comparator).__name__,
651 self,
652 attribute,
653 )
654 ),
655 replace_context=err3,
656 )
658 Proxy.__name__ = type(descriptor).__name__ + "Proxy"
660 util.monkeypatch_proxied_specials(
661 Proxy, type(descriptor), name="descriptor", from_instance=descriptor
662 )
663 return Proxy
666OP_REMOVE = util.symbol("REMOVE")
667OP_APPEND = util.symbol("APPEND")
668OP_REPLACE = util.symbol("REPLACE")
669OP_BULK_REPLACE = util.symbol("BULK_REPLACE")
670OP_MODIFIED = util.symbol("MODIFIED")
673class AttributeEvent(object):
674 """A token propagated throughout the course of a chain of attribute
675 events.
677 Serves as an indicator of the source of the event and also provides
678 a means of controlling propagation across a chain of attribute
679 operations.
681 The :class:`.Event` object is sent as the ``initiator`` argument
682 when dealing with events such as :meth:`.AttributeEvents.append`,
683 :meth:`.AttributeEvents.set`,
684 and :meth:`.AttributeEvents.remove`.
686 The :class:`.Event` object is currently interpreted by the backref
687 event handlers, and is used to control the propagation of operations
688 across two mutually-dependent attributes.
690 .. versionadded:: 0.9.0
692 :attribute impl: The :class:`.AttributeImpl` which is the current event
693 initiator.
695 :attribute op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`,
696 :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_REPLACE`, indicating the
697 source operation.
699 """
701 __slots__ = "impl", "op", "parent_token"
703 def __init__(self, attribute_impl, op):
704 self.impl = attribute_impl
705 self.op = op
706 self.parent_token = self.impl.parent_token
708 def __eq__(self, other):
709 return (
710 isinstance(other, AttributeEvent)
711 and other.impl is self.impl
712 and other.op == self.op
713 )
715 @property
716 def key(self):
717 return self.impl.key
719 def hasparent(self, state):
720 return self.impl.hasparent(state)
723Event = AttributeEvent
726class AttributeImpl(object):
727 """internal implementation for instrumented attributes."""
729 def __init__(
730 self,
731 class_,
732 key,
733 callable_,
734 dispatch,
735 trackparent=False,
736 compare_function=None,
737 active_history=False,
738 parent_token=None,
739 load_on_unexpire=True,
740 send_modified_events=True,
741 accepts_scalar_loader=None,
742 **kwargs
743 ):
744 r"""Construct an AttributeImpl.
746 :param \class_: associated class
748 :param key: string name of the attribute
750 :param \callable_:
751 optional function which generates a callable based on a parent
752 instance, which produces the "default" values for a scalar or
753 collection attribute when it's first accessed, if not present
754 already.
756 :param trackparent:
757 if True, attempt to track if an instance has a parent attached
758 to it via this attribute.
760 :param compare_function:
761 a function that compares two values which are normally
762 assignable to this attribute.
764 :param active_history:
765 indicates that get_history() should always return the "old" value,
766 even if it means executing a lazy callable upon attribute change.
768 :param parent_token:
769 Usually references the MapperProperty, used as a key for
770 the hasparent() function to identify an "owning" attribute.
771 Allows multiple AttributeImpls to all match a single
772 owner attribute.
774 :param load_on_unexpire:
775 if False, don't include this attribute in a load-on-expired
776 operation, i.e. the "expired_attribute_loader" process.
777 The attribute can still be in the "expired" list and be
778 considered to be "expired". Previously, this flag was called
779 "expire_missing" and is only used by a deferred column
780 attribute.
782 :param send_modified_events:
783 if False, the InstanceState._modified_event method will have no
784 effect; this means the attribute will never show up as changed in a
785 history entry.
787 """
788 self.class_ = class_
789 self.key = key
790 self.callable_ = callable_
791 self.dispatch = dispatch
792 self.trackparent = trackparent
793 self.parent_token = parent_token or self
794 self.send_modified_events = send_modified_events
795 if compare_function is None:
796 self.is_equal = operator.eq
797 else:
798 self.is_equal = compare_function
800 if accepts_scalar_loader is not None:
801 self.accepts_scalar_loader = accepts_scalar_loader
802 else:
803 self.accepts_scalar_loader = self.default_accepts_scalar_loader
805 _deferred_history = kwargs.pop("_deferred_history", False)
806 self._deferred_history = _deferred_history
808 if active_history:
809 self.dispatch._active_history = True
811 self.load_on_unexpire = load_on_unexpire
812 self._modified_token = Event(self, OP_MODIFIED)
814 __slots__ = (
815 "class_",
816 "key",
817 "callable_",
818 "dispatch",
819 "trackparent",
820 "parent_token",
821 "send_modified_events",
822 "is_equal",
823 "load_on_unexpire",
824 "_modified_token",
825 "accepts_scalar_loader",
826 "_deferred_history",
827 )
829 def __str__(self):
830 return "%s.%s" % (self.class_.__name__, self.key)
832 def _get_active_history(self):
833 """Backwards compat for impl.active_history"""
835 return self.dispatch._active_history
837 def _set_active_history(self, value):
838 self.dispatch._active_history = value
840 active_history = property(_get_active_history, _set_active_history)
842 def hasparent(self, state, optimistic=False):
843 """Return the boolean value of a `hasparent` flag attached to
844 the given state.
846 The `optimistic` flag determines what the default return value
847 should be if no `hasparent` flag can be located.
849 As this function is used to determine if an instance is an
850 *orphan*, instances that were loaded from storage should be
851 assumed to not be orphans, until a True/False value for this
852 flag is set.
854 An instance attribute that is loaded by a callable function
855 will also not have a `hasparent` flag.
857 """
858 msg = "This AttributeImpl is not configured to track parents."
859 assert self.trackparent, msg
861 return (
862 state.parents.get(id(self.parent_token), optimistic) is not False
863 )
865 def sethasparent(self, state, parent_state, value):
866 """Set a boolean flag on the given item corresponding to
867 whether or not it is attached to a parent object via the
868 attribute represented by this ``InstrumentedAttribute``.
870 """
871 msg = "This AttributeImpl is not configured to track parents."
872 assert self.trackparent, msg
874 id_ = id(self.parent_token)
875 if value:
876 state.parents[id_] = parent_state
877 else:
878 if id_ in state.parents:
879 last_parent = state.parents[id_]
881 if (
882 last_parent is not False
883 and last_parent.key != parent_state.key
884 ):
886 if last_parent.obj() is None:
887 raise orm_exc.StaleDataError(
888 "Removing state %s from parent "
889 "state %s along attribute '%s', "
890 "but the parent record "
891 "has gone stale, can't be sure this "
892 "is the most recent parent."
893 % (
894 state_str(state),
895 state_str(parent_state),
896 self.key,
897 )
898 )
900 return
902 state.parents[id_] = False
904 def get_history(self, state, dict_, passive=PASSIVE_OFF):
905 raise NotImplementedError()
907 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
908 """Return a list of tuples of (state, obj)
909 for all objects in this attribute's current state
910 + history.
912 Only applies to object-based attributes.
914 This is an inlining of existing functionality
915 which roughly corresponds to:
917 get_state_history(
918 state,
919 key,
920 passive=PASSIVE_NO_INITIALIZE).sum()
922 """
923 raise NotImplementedError()
925 def _default_value(self, state, dict_):
926 """Produce an empty value for an uninitialized scalar attribute."""
928 assert self.key not in dict_, (
929 "_default_value should only be invoked for an "
930 "uninitialized or expired attribute"
931 )
933 value = None
934 for fn in self.dispatch.init_scalar:
935 ret = fn(state, value, dict_)
936 if ret is not ATTR_EMPTY:
937 value = ret
939 return value
941 def get(self, state, dict_, passive=PASSIVE_OFF):
942 """Retrieve a value from the given object.
943 If a callable is assembled on this object's attribute, and
944 passive is False, the callable will be executed and the
945 resulting value will be set as the new value for this attribute.
946 """
947 if self.key in dict_:
948 return dict_[self.key]
949 else:
950 # if history present, don't load
951 key = self.key
952 if (
953 key not in state.committed_state
954 or state.committed_state[key] is NO_VALUE
955 ):
956 if not passive & CALLABLES_OK:
957 return PASSIVE_NO_RESULT
959 value = self._fire_loader_callables(state, key, passive)
961 if value is PASSIVE_NO_RESULT or value is NO_VALUE:
962 return value
963 elif value is ATTR_WAS_SET:
964 try:
965 return dict_[key]
966 except KeyError as err:
967 # TODO: no test coverage here.
968 util.raise_(
969 KeyError(
970 "Deferred loader for attribute "
971 "%r failed to populate "
972 "correctly" % key
973 ),
974 replace_context=err,
975 )
976 elif value is not ATTR_EMPTY:
977 return self.set_committed_value(state, dict_, value)
979 if not passive & INIT_OK:
980 return NO_VALUE
981 else:
982 return self._default_value(state, dict_)
984 def _fire_loader_callables(self, state, key, passive):
985 if (
986 self.accepts_scalar_loader
987 and self.load_on_unexpire
988 and key in state.expired_attributes
989 ):
990 return state._load_expired(state, passive)
991 elif key in state.callables:
992 callable_ = state.callables[key]
993 return callable_(state, passive)
994 elif self.callable_:
995 return self.callable_(state, passive)
996 else:
997 return ATTR_EMPTY
999 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1000 self.set(state, dict_, value, initiator, passive=passive)
1002 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1003 self.set(
1004 state, dict_, None, initiator, passive=passive, check_old=value
1005 )
1007 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1008 self.set(
1009 state,
1010 dict_,
1011 None,
1012 initiator,
1013 passive=passive,
1014 check_old=value,
1015 pop=True,
1016 )
1018 def set(
1019 self,
1020 state,
1021 dict_,
1022 value,
1023 initiator,
1024 passive=PASSIVE_OFF,
1025 check_old=None,
1026 pop=False,
1027 ):
1028 raise NotImplementedError()
1030 def get_committed_value(self, state, dict_, passive=PASSIVE_OFF):
1031 """return the unchanged value of this attribute"""
1033 if self.key in state.committed_state:
1034 value = state.committed_state[self.key]
1035 if value is NO_VALUE:
1036 return None
1037 else:
1038 return value
1039 else:
1040 return self.get(state, dict_, passive=passive)
1042 def set_committed_value(self, state, dict_, value):
1043 """set an attribute value on the given instance and 'commit' it."""
1045 dict_[self.key] = value
1046 state._commit(dict_, [self.key])
1047 return value
1050class ScalarAttributeImpl(AttributeImpl):
1051 """represents a scalar value-holding InstrumentedAttribute."""
1053 default_accepts_scalar_loader = True
1054 uses_objects = False
1055 supports_population = True
1056 collection = False
1057 dynamic = False
1059 __slots__ = "_replace_token", "_append_token", "_remove_token"
1061 def __init__(self, *arg, **kw):
1062 super(ScalarAttributeImpl, self).__init__(*arg, **kw)
1063 self._replace_token = self._append_token = Event(self, OP_REPLACE)
1064 self._remove_token = Event(self, OP_REMOVE)
1066 def delete(self, state, dict_):
1067 if self.dispatch._active_history:
1068 old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
1069 else:
1070 old = dict_.get(self.key, NO_VALUE)
1072 if self.dispatch.remove:
1073 self.fire_remove_event(state, dict_, old, self._remove_token)
1074 state._modified_event(dict_, self, old)
1076 existing = dict_.pop(self.key, NO_VALUE)
1077 if (
1078 existing is NO_VALUE
1079 and old is NO_VALUE
1080 and not state.expired
1081 and self.key not in state.expired_attributes
1082 ):
1083 raise AttributeError("%s object does not have a value" % self)
1085 def get_history(self, state, dict_, passive=PASSIVE_OFF):
1086 if self.key in dict_:
1087 return History.from_scalar_attribute(self, state, dict_[self.key])
1088 elif self.key in state.committed_state:
1089 return History.from_scalar_attribute(self, state, NO_VALUE)
1090 else:
1091 if passive & INIT_OK:
1092 passive ^= INIT_OK
1093 current = self.get(state, dict_, passive=passive)
1094 if current is PASSIVE_NO_RESULT:
1095 return HISTORY_BLANK
1096 else:
1097 return History.from_scalar_attribute(self, state, current)
1099 def set(
1100 self,
1101 state,
1102 dict_,
1103 value,
1104 initiator,
1105 passive=PASSIVE_OFF,
1106 check_old=None,
1107 pop=False,
1108 ):
1109 if self.dispatch._active_history:
1110 old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
1111 else:
1112 old = dict_.get(self.key, NO_VALUE)
1114 if self.dispatch.set:
1115 value = self.fire_replace_event(
1116 state, dict_, value, old, initiator
1117 )
1118 state._modified_event(dict_, self, old)
1119 dict_[self.key] = value
1121 def fire_replace_event(self, state, dict_, value, previous, initiator):
1122 for fn in self.dispatch.set:
1123 value = fn(
1124 state, value, previous, initiator or self._replace_token
1125 )
1126 return value
1128 def fire_remove_event(self, state, dict_, value, initiator):
1129 for fn in self.dispatch.remove:
1130 fn(state, value, initiator or self._remove_token)
1132 @property
1133 def type(self):
1134 self.property.columns[0].type
1137class ScalarObjectAttributeImpl(ScalarAttributeImpl):
1138 """represents a scalar-holding InstrumentedAttribute,
1139 where the target object is also instrumented.
1141 Adds events to delete/set operations.
1143 """
1145 default_accepts_scalar_loader = False
1146 uses_objects = True
1147 supports_population = True
1148 collection = False
1150 __slots__ = ()
1152 def delete(self, state, dict_):
1153 if self.dispatch._active_history:
1154 old = self.get(
1155 state,
1156 dict_,
1157 passive=PASSIVE_ONLY_PERSISTENT
1158 | NO_AUTOFLUSH
1159 | LOAD_AGAINST_COMMITTED,
1160 )
1161 else:
1162 old = self.get(
1163 state,
1164 dict_,
1165 passive=PASSIVE_NO_FETCH ^ INIT_OK
1166 | LOAD_AGAINST_COMMITTED
1167 | NO_RAISE,
1168 )
1170 self.fire_remove_event(state, dict_, old, self._remove_token)
1172 existing = dict_.pop(self.key, NO_VALUE)
1174 # if the attribute is expired, we currently have no way to tell
1175 # that an object-attribute was expired vs. not loaded. So
1176 # for this test, we look to see if the object has a DB identity.
1177 if (
1178 existing is NO_VALUE
1179 and old is not PASSIVE_NO_RESULT
1180 and state.key is None
1181 ):
1182 raise AttributeError("%s object does not have a value" % self)
1184 def get_history(self, state, dict_, passive=PASSIVE_OFF):
1185 if self.key in dict_:
1186 current = dict_[self.key]
1187 else:
1188 if passive & INIT_OK:
1189 passive ^= INIT_OK
1190 current = self.get(state, dict_, passive=passive)
1191 if current is PASSIVE_NO_RESULT:
1192 return HISTORY_BLANK
1194 if not self._deferred_history:
1195 return History.from_object_attribute(self, state, current)
1196 else:
1197 original = state.committed_state.get(self.key, _NO_HISTORY)
1198 if original is PASSIVE_NO_RESULT:
1200 loader_passive = passive | (
1201 PASSIVE_ONLY_PERSISTENT
1202 | NO_AUTOFLUSH
1203 | LOAD_AGAINST_COMMITTED
1204 | NO_RAISE
1205 | DEFERRED_HISTORY_LOAD
1206 )
1207 original = self._fire_loader_callables(
1208 state, self.key, loader_passive
1209 )
1210 return History.from_object_attribute(
1211 self, state, current, original=original
1212 )
1214 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
1215 if self.key in dict_:
1216 current = dict_[self.key]
1217 elif passive & CALLABLES_OK:
1218 current = self.get(state, dict_, passive=passive)
1219 else:
1220 return []
1222 # can't use __hash__(), can't use __eq__() here
1223 if (
1224 current is not None
1225 and current is not PASSIVE_NO_RESULT
1226 and current is not NO_VALUE
1227 ):
1228 ret = [(instance_state(current), current)]
1229 else:
1230 ret = [(None, None)]
1232 if self.key in state.committed_state:
1233 original = state.committed_state[self.key]
1234 if (
1235 original is not None
1236 and original is not PASSIVE_NO_RESULT
1237 and original is not NO_VALUE
1238 and original is not current
1239 ):
1241 ret.append((instance_state(original), original))
1242 return ret
1244 def set(
1245 self,
1246 state,
1247 dict_,
1248 value,
1249 initiator,
1250 passive=PASSIVE_OFF,
1251 check_old=None,
1252 pop=False,
1253 ):
1254 """Set a value on the given InstanceState."""
1256 if self.dispatch._active_history:
1257 old = self.get(
1258 state,
1259 dict_,
1260 passive=PASSIVE_ONLY_PERSISTENT
1261 | NO_AUTOFLUSH
1262 | LOAD_AGAINST_COMMITTED,
1263 )
1264 else:
1265 old = self.get(
1266 state,
1267 dict_,
1268 passive=PASSIVE_NO_FETCH ^ INIT_OK
1269 | LOAD_AGAINST_COMMITTED
1270 | NO_RAISE,
1271 )
1273 if (
1274 check_old is not None
1275 and old is not PASSIVE_NO_RESULT
1276 and check_old is not old
1277 ):
1278 if pop:
1279 return
1280 else:
1281 raise ValueError(
1282 "Object %s not associated with %s on attribute '%s'"
1283 % (instance_str(check_old), state_str(state), self.key)
1284 )
1286 value = self.fire_replace_event(state, dict_, value, old, initiator)
1287 dict_[self.key] = value
1289 def fire_remove_event(self, state, dict_, value, initiator):
1290 if self.trackparent and value not in (
1291 None,
1292 PASSIVE_NO_RESULT,
1293 NO_VALUE,
1294 ):
1295 self.sethasparent(instance_state(value), state, False)
1297 for fn in self.dispatch.remove:
1298 fn(state, value, initiator or self._remove_token)
1300 state._modified_event(dict_, self, value)
1302 def fire_replace_event(self, state, dict_, value, previous, initiator):
1303 if self.trackparent:
1304 if previous is not value and previous not in (
1305 None,
1306 PASSIVE_NO_RESULT,
1307 NO_VALUE,
1308 ):
1309 self.sethasparent(instance_state(previous), state, False)
1311 for fn in self.dispatch.set:
1312 value = fn(
1313 state, value, previous, initiator or self._replace_token
1314 )
1316 state._modified_event(dict_, self, previous)
1318 if self.trackparent:
1319 if value is not None:
1320 self.sethasparent(instance_state(value), state, True)
1322 return value
1325class CollectionAttributeImpl(AttributeImpl):
1326 """A collection-holding attribute that instruments changes in membership.
1328 Only handles collections of instrumented objects.
1330 InstrumentedCollectionAttribute holds an arbitrary, user-specified
1331 container object (defaulting to a list) and brokers access to the
1332 CollectionAdapter, a "view" onto that object that presents consistent bag
1333 semantics to the orm layer independent of the user data implementation.
1335 """
1337 default_accepts_scalar_loader = False
1338 uses_objects = True
1339 supports_population = True
1340 collection = True
1341 dynamic = False
1343 __slots__ = (
1344 "copy",
1345 "collection_factory",
1346 "_append_token",
1347 "_remove_token",
1348 "_bulk_replace_token",
1349 "_duck_typed_as",
1350 )
1352 def __init__(
1353 self,
1354 class_,
1355 key,
1356 callable_,
1357 dispatch,
1358 typecallable=None,
1359 trackparent=False,
1360 copy_function=None,
1361 compare_function=None,
1362 **kwargs
1363 ):
1364 super(CollectionAttributeImpl, self).__init__(
1365 class_,
1366 key,
1367 callable_,
1368 dispatch,
1369 trackparent=trackparent,
1370 compare_function=compare_function,
1371 **kwargs
1372 )
1374 if copy_function is None:
1375 copy_function = self.__copy
1376 self.copy = copy_function
1377 self.collection_factory = typecallable
1378 self._append_token = Event(self, OP_APPEND)
1379 self._remove_token = Event(self, OP_REMOVE)
1380 self._bulk_replace_token = Event(self, OP_BULK_REPLACE)
1381 self._duck_typed_as = util.duck_type_collection(
1382 self.collection_factory()
1383 )
1385 if getattr(self.collection_factory, "_sa_linker", None):
1387 @event.listens_for(self, "init_collection")
1388 def link(target, collection, collection_adapter):
1389 collection._sa_linker(collection_adapter)
1391 @event.listens_for(self, "dispose_collection")
1392 def unlink(target, collection, collection_adapter):
1393 collection._sa_linker(None)
1395 def __copy(self, item):
1396 return [y for y in collections.collection_adapter(item)]
1398 def get_history(self, state, dict_, passive=PASSIVE_OFF):
1399 current = self.get(state, dict_, passive=passive)
1400 if current is PASSIVE_NO_RESULT:
1401 return HISTORY_BLANK
1402 else:
1403 return History.from_collection(self, state, current)
1405 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
1406 # NOTE: passive is ignored here at the moment
1408 if self.key not in dict_:
1409 return []
1411 current = dict_[self.key]
1412 current = getattr(current, "_sa_adapter")
1414 if self.key in state.committed_state:
1415 original = state.committed_state[self.key]
1416 if original is not NO_VALUE:
1417 current_states = [
1418 ((c is not None) and instance_state(c) or None, c)
1419 for c in current
1420 ]
1421 original_states = [
1422 ((c is not None) and instance_state(c) or None, c)
1423 for c in original
1424 ]
1426 current_set = dict(current_states)
1427 original_set = dict(original_states)
1429 return (
1430 [
1431 (s, o)
1432 for s, o in current_states
1433 if s not in original_set
1434 ]
1435 + [(s, o) for s, o in current_states if s in original_set]
1436 + [
1437 (s, o)
1438 for s, o in original_states
1439 if s not in current_set
1440 ]
1441 )
1443 return [(instance_state(o), o) for o in current]
1445 def fire_append_event(self, state, dict_, value, initiator):
1446 for fn in self.dispatch.append:
1447 value = fn(state, value, initiator or self._append_token)
1449 state._modified_event(dict_, self, NO_VALUE, True)
1451 if self.trackparent and value is not None:
1452 self.sethasparent(instance_state(value), state, True)
1454 return value
1456 def fire_append_wo_mutation_event(self, state, dict_, value, initiator):
1457 for fn in self.dispatch.append_wo_mutation:
1458 value = fn(state, value, initiator or self._append_token)
1460 return value
1462 def fire_pre_remove_event(self, state, dict_, initiator):
1463 """A special event used for pop() operations.
1465 The "remove" event needs to have the item to be removed passed to
1466 it, which in the case of pop from a set, we don't have a way to access
1467 the item before the operation. the event is used for all pop()
1468 operations (even though set.pop is the one where it is really needed).
1470 """
1471 state._modified_event(dict_, self, NO_VALUE, True)
1473 def fire_remove_event(self, state, dict_, value, initiator):
1474 if self.trackparent and value is not None:
1475 self.sethasparent(instance_state(value), state, False)
1477 for fn in self.dispatch.remove:
1478 fn(state, value, initiator or self._remove_token)
1480 state._modified_event(dict_, self, NO_VALUE, True)
1482 def delete(self, state, dict_):
1483 if self.key not in dict_:
1484 return
1486 state._modified_event(dict_, self, NO_VALUE, True)
1488 collection = self.get_collection(state, state.dict)
1489 collection.clear_with_event()
1491 # key is always present because we checked above. e.g.
1492 # del is a no-op if collection not present.
1493 del dict_[self.key]
1495 def _default_value(self, state, dict_):
1496 """Produce an empty collection for an un-initialized attribute"""
1498 assert self.key not in dict_, (
1499 "_default_value should only be invoked for an "
1500 "uninitialized or expired attribute"
1501 )
1503 if self.key in state._empty_collections:
1504 return state._empty_collections[self.key]
1506 adapter, user_data = self._initialize_collection(state)
1507 adapter._set_empty(user_data)
1508 return user_data
1510 def _initialize_collection(self, state):
1512 adapter, collection = state.manager.initialize_collection(
1513 self.key, state, self.collection_factory
1514 )
1516 self.dispatch.init_collection(state, collection, adapter)
1518 return adapter, collection
1520 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1521 collection = self.get_collection(state, dict_, passive=passive)
1522 if collection is PASSIVE_NO_RESULT:
1523 value = self.fire_append_event(state, dict_, value, initiator)
1524 assert (
1525 self.key not in dict_
1526 ), "Collection was loaded during event handling."
1527 state._get_pending_mutation(self.key).append(value)
1528 else:
1529 collection.append_with_event(value, initiator)
1531 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1532 collection = self.get_collection(state, state.dict, passive=passive)
1533 if collection is PASSIVE_NO_RESULT:
1534 self.fire_remove_event(state, dict_, value, initiator)
1535 assert (
1536 self.key not in dict_
1537 ), "Collection was loaded during event handling."
1538 state._get_pending_mutation(self.key).remove(value)
1539 else:
1540 collection.remove_with_event(value, initiator)
1542 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1543 try:
1544 # TODO: better solution here would be to add
1545 # a "popper" role to collections.py to complement
1546 # "remover".
1547 self.remove(state, dict_, value, initiator, passive=passive)
1548 except (ValueError, KeyError, IndexError):
1549 pass
1551 def set(
1552 self,
1553 state,
1554 dict_,
1555 value,
1556 initiator=None,
1557 passive=PASSIVE_OFF,
1558 check_old=None,
1559 pop=False,
1560 _adapt=True,
1561 ):
1562 iterable = orig_iterable = value
1564 # pulling a new collection first so that an adaptation exception does
1565 # not trigger a lazy load of the old collection.
1566 new_collection, user_data = self._initialize_collection(state)
1567 if _adapt:
1568 if new_collection._converter is not None:
1569 iterable = new_collection._converter(iterable)
1570 else:
1571 setting_type = util.duck_type_collection(iterable)
1572 receiving_type = self._duck_typed_as
1574 if setting_type is not receiving_type:
1575 given = (
1576 iterable is None
1577 and "None"
1578 or iterable.__class__.__name__
1579 )
1580 wanted = self._duck_typed_as.__name__
1581 raise TypeError(
1582 "Incompatible collection type: %s is not %s-like"
1583 % (given, wanted)
1584 )
1586 # If the object is an adapted collection, return the (iterable)
1587 # adapter.
1588 if hasattr(iterable, "_sa_iterator"):
1589 iterable = iterable._sa_iterator()
1590 elif setting_type is dict:
1591 if util.py3k:
1592 iterable = iterable.values()
1593 else:
1594 iterable = getattr(
1595 iterable, "itervalues", iterable.values
1596 )()
1597 else:
1598 iterable = iter(iterable)
1599 new_values = list(iterable)
1601 evt = self._bulk_replace_token
1603 self.dispatch.bulk_replace(state, new_values, evt)
1605 # propagate NO_RAISE in passive through to the get() for the
1606 # existing object (ticket #8862)
1607 old = self.get(
1608 state,
1609 dict_,
1610 passive=PASSIVE_ONLY_PERSISTENT ^ (passive & NO_RAISE),
1611 )
1612 if old is PASSIVE_NO_RESULT:
1613 old = self._default_value(state, dict_)
1614 elif old is orig_iterable:
1615 # ignore re-assignment of the current collection, as happens
1616 # implicitly with in-place operators (foo.collection |= other)
1617 return
1619 # place a copy of "old" in state.committed_state
1620 state._modified_event(dict_, self, old, True)
1622 old_collection = old._sa_adapter
1624 dict_[self.key] = user_data
1626 collections.bulk_replace(
1627 new_values, old_collection, new_collection, initiator=evt
1628 )
1630 self._dispose_previous_collection(state, old, old_collection, True)
1632 def _dispose_previous_collection(
1633 self, state, collection, adapter, fire_event
1634 ):
1635 del collection._sa_adapter
1637 # discarding old collection make sure it is not referenced in empty
1638 # collections.
1639 state._empty_collections.pop(self.key, None)
1640 if fire_event:
1641 self.dispatch.dispose_collection(state, collection, adapter)
1643 def _invalidate_collection(self, collection):
1644 adapter = getattr(collection, "_sa_adapter")
1645 adapter.invalidated = True
1647 def set_committed_value(self, state, dict_, value):
1648 """Set an attribute value on the given instance and 'commit' it."""
1650 collection, user_data = self._initialize_collection(state)
1652 if value:
1653 collection.append_multiple_without_event(value)
1655 state.dict[self.key] = user_data
1657 state._commit(dict_, [self.key])
1659 if self.key in state._pending_mutations:
1660 # pending items exist. issue a modified event,
1661 # add/remove new items.
1662 state._modified_event(dict_, self, user_data, True)
1664 pending = state._pending_mutations.pop(self.key)
1665 added = pending.added_items
1666 removed = pending.deleted_items
1667 for item in added:
1668 collection.append_without_event(item)
1669 for item in removed:
1670 collection.remove_without_event(item)
1672 return user_data
1674 def get_collection(
1675 self, state, dict_, user_data=None, passive=PASSIVE_OFF
1676 ):
1677 """Retrieve the CollectionAdapter associated with the given state.
1679 if user_data is None, retrieves it from the state using normal
1680 "get()" rules, which will fire lazy callables or return the "empty"
1681 collection value.
1683 """
1684 if user_data is None:
1685 user_data = self.get(state, dict_, passive=passive)
1686 if user_data is PASSIVE_NO_RESULT:
1687 return user_data
1689 return user_data._sa_adapter
1692def backref_listeners(attribute, key, uselist):
1693 """Apply listeners to synchronize a two-way relationship."""
1695 # use easily recognizable names for stack traces.
1697 # in the sections marked "tokens to test for a recursive loop",
1698 # this is somewhat brittle and very performance-sensitive logic
1699 # that is specific to how we might arrive at each event. a marker
1700 # that can target us directly to arguments being invoked against
1701 # the impl might be simpler, but could interfere with other systems.
1703 parent_token = attribute.impl.parent_token
1704 parent_impl = attribute.impl
1706 def _acceptable_key_err(child_state, initiator, child_impl):
1707 raise ValueError(
1708 "Bidirectional attribute conflict detected: "
1709 'Passing object %s to attribute "%s" '
1710 'triggers a modify event on attribute "%s" '
1711 'via the backref "%s".'
1712 % (
1713 state_str(child_state),
1714 initiator.parent_token,
1715 child_impl.parent_token,
1716 attribute.impl.parent_token,
1717 )
1718 )
1720 def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
1721 if oldchild is child:
1722 return child
1723 if (
1724 oldchild is not None
1725 and oldchild is not PASSIVE_NO_RESULT
1726 and oldchild is not NO_VALUE
1727 ):
1728 # With lazy=None, there's no guarantee that the full collection is
1729 # present when updating via a backref.
1730 old_state, old_dict = (
1731 instance_state(oldchild),
1732 instance_dict(oldchild),
1733 )
1734 impl = old_state.manager[key].impl
1736 # tokens to test for a recursive loop.
1737 if not impl.collection and not impl.dynamic:
1738 check_recursive_token = impl._replace_token
1739 else:
1740 check_recursive_token = impl._remove_token
1742 if initiator is not check_recursive_token:
1743 impl.pop(
1744 old_state,
1745 old_dict,
1746 state.obj(),
1747 parent_impl._append_token,
1748 passive=PASSIVE_NO_FETCH,
1749 )
1751 if child is not None:
1752 child_state, child_dict = (
1753 instance_state(child),
1754 instance_dict(child),
1755 )
1756 child_impl = child_state.manager[key].impl
1758 if (
1759 initiator.parent_token is not parent_token
1760 and initiator.parent_token is not child_impl.parent_token
1761 ):
1762 _acceptable_key_err(state, initiator, child_impl)
1764 # tokens to test for a recursive loop.
1765 check_append_token = child_impl._append_token
1766 check_bulk_replace_token = (
1767 child_impl._bulk_replace_token
1768 if child_impl.collection
1769 else None
1770 )
1772 if (
1773 initiator is not check_append_token
1774 and initiator is not check_bulk_replace_token
1775 ):
1776 child_impl.append(
1777 child_state,
1778 child_dict,
1779 state.obj(),
1780 initiator,
1781 passive=PASSIVE_NO_FETCH,
1782 )
1783 return child
1785 def emit_backref_from_collection_append_event(state, child, initiator):
1786 if child is None:
1787 return
1789 child_state, child_dict = instance_state(child), instance_dict(child)
1790 child_impl = child_state.manager[key].impl
1792 if (
1793 initiator.parent_token is not parent_token
1794 and initiator.parent_token is not child_impl.parent_token
1795 ):
1796 _acceptable_key_err(state, initiator, child_impl)
1798 # tokens to test for a recursive loop.
1799 check_append_token = child_impl._append_token
1800 check_bulk_replace_token = (
1801 child_impl._bulk_replace_token if child_impl.collection else None
1802 )
1804 if (
1805 initiator is not check_append_token
1806 and initiator is not check_bulk_replace_token
1807 ):
1808 child_impl.append(
1809 child_state,
1810 child_dict,
1811 state.obj(),
1812 initiator,
1813 passive=PASSIVE_NO_FETCH,
1814 )
1815 return child
1817 def emit_backref_from_collection_remove_event(state, child, initiator):
1818 if (
1819 child is not None
1820 and child is not PASSIVE_NO_RESULT
1821 and child is not NO_VALUE
1822 ):
1823 child_state, child_dict = (
1824 instance_state(child),
1825 instance_dict(child),
1826 )
1827 child_impl = child_state.manager[key].impl
1829 # tokens to test for a recursive loop.
1830 if not child_impl.collection and not child_impl.dynamic:
1831 check_remove_token = child_impl._remove_token
1832 check_replace_token = child_impl._replace_token
1833 check_for_dupes_on_remove = uselist and not parent_impl.dynamic
1834 else:
1835 check_remove_token = child_impl._remove_token
1836 check_replace_token = (
1837 child_impl._bulk_replace_token
1838 if child_impl.collection
1839 else None
1840 )
1841 check_for_dupes_on_remove = False
1843 if (
1844 initiator is not check_remove_token
1845 and initiator is not check_replace_token
1846 ):
1848 if not check_for_dupes_on_remove or not util.has_dupes(
1849 # when this event is called, the item is usually
1850 # present in the list, except for a pop() operation.
1851 state.dict[parent_impl.key],
1852 child,
1853 ):
1854 child_impl.pop(
1855 child_state,
1856 child_dict,
1857 state.obj(),
1858 initiator,
1859 passive=PASSIVE_NO_FETCH,
1860 )
1862 if uselist:
1863 event.listen(
1864 attribute,
1865 "append",
1866 emit_backref_from_collection_append_event,
1867 retval=True,
1868 raw=True,
1869 )
1870 else:
1871 event.listen(
1872 attribute,
1873 "set",
1874 emit_backref_from_scalar_set_event,
1875 retval=True,
1876 raw=True,
1877 )
1878 # TODO: need coverage in test/orm/ of remove event
1879 event.listen(
1880 attribute,
1881 "remove",
1882 emit_backref_from_collection_remove_event,
1883 retval=True,
1884 raw=True,
1885 )
1888_NO_HISTORY = util.symbol("NO_HISTORY")
1889_NO_STATE_SYMBOLS = frozenset([id(PASSIVE_NO_RESULT), id(NO_VALUE)])
1892class History(util.namedtuple("History", ["added", "unchanged", "deleted"])):
1893 """A 3-tuple of added, unchanged and deleted values,
1894 representing the changes which have occurred on an instrumented
1895 attribute.
1897 The easiest way to get a :class:`.History` object for a particular
1898 attribute on an object is to use the :func:`_sa.inspect` function::
1900 from sqlalchemy import inspect
1902 hist = inspect(myobject).attrs.myattribute.history
1904 Each tuple member is an iterable sequence:
1906 * ``added`` - the collection of items added to the attribute (the first
1907 tuple element).
1909 * ``unchanged`` - the collection of items that have not changed on the
1910 attribute (the second tuple element).
1912 * ``deleted`` - the collection of items that have been removed from the
1913 attribute (the third tuple element).
1915 """
1917 def __bool__(self):
1918 return self != HISTORY_BLANK
1920 __nonzero__ = __bool__
1922 def empty(self):
1923 """Return True if this :class:`.History` has no changes
1924 and no existing, unchanged state.
1926 """
1928 return not bool((self.added or self.deleted) or self.unchanged)
1930 def sum(self):
1931 """Return a collection of added + unchanged + deleted."""
1933 return (
1934 (self.added or []) + (self.unchanged or []) + (self.deleted or [])
1935 )
1937 def non_deleted(self):
1938 """Return a collection of added + unchanged."""
1940 return (self.added or []) + (self.unchanged or [])
1942 def non_added(self):
1943 """Return a collection of unchanged + deleted."""
1945 return (self.unchanged or []) + (self.deleted or [])
1947 def has_changes(self):
1948 """Return True if this :class:`.History` has changes."""
1950 return bool(self.added or self.deleted)
1952 def as_state(self):
1953 return History(
1954 [
1955 (c is not None) and instance_state(c) or None
1956 for c in self.added
1957 ],
1958 [
1959 (c is not None) and instance_state(c) or None
1960 for c in self.unchanged
1961 ],
1962 [
1963 (c is not None) and instance_state(c) or None
1964 for c in self.deleted
1965 ],
1966 )
1968 @classmethod
1969 def from_scalar_attribute(cls, attribute, state, current):
1970 original = state.committed_state.get(attribute.key, _NO_HISTORY)
1972 if original is _NO_HISTORY:
1973 if current is NO_VALUE:
1974 return cls((), (), ())
1975 else:
1976 return cls((), [current], ())
1977 # don't let ClauseElement expressions here trip things up
1978 elif (
1979 current is not NO_VALUE
1980 and attribute.is_equal(current, original) is True
1981 ):
1982 return cls((), [current], ())
1983 else:
1984 # current convention on native scalars is to not
1985 # include information
1986 # about missing previous value in "deleted", but
1987 # we do include None, which helps in some primary
1988 # key situations
1989 if id(original) in _NO_STATE_SYMBOLS:
1990 deleted = ()
1991 # indicate a "del" operation occurred when we don't have
1992 # the previous value as: ([None], (), ())
1993 if id(current) in _NO_STATE_SYMBOLS:
1994 current = None
1995 else:
1996 deleted = [original]
1997 if current is NO_VALUE:
1998 return cls((), (), deleted)
1999 else:
2000 return cls([current], (), deleted)
2002 @classmethod
2003 def from_object_attribute(
2004 cls, attribute, state, current, original=_NO_HISTORY
2005 ):
2006 if original is _NO_HISTORY:
2007 original = state.committed_state.get(attribute.key, _NO_HISTORY)
2009 if original is _NO_HISTORY:
2010 if current is NO_VALUE:
2011 return cls((), (), ())
2012 else:
2013 return cls((), [current], ())
2014 elif current is original and current is not NO_VALUE:
2015 return cls((), [current], ())
2016 else:
2017 # current convention on related objects is to not
2018 # include information
2019 # about missing previous value in "deleted", and
2020 # to also not include None - the dependency.py rules
2021 # ignore the None in any case.
2022 if id(original) in _NO_STATE_SYMBOLS or original is None:
2023 deleted = ()
2024 # indicate a "del" operation occurred when we don't have
2025 # the previous value as: ([None], (), ())
2026 if id(current) in _NO_STATE_SYMBOLS:
2027 current = None
2028 else:
2029 deleted = [original]
2030 if current is NO_VALUE:
2031 return cls((), (), deleted)
2032 else:
2033 return cls([current], (), deleted)
2035 @classmethod
2036 def from_collection(cls, attribute, state, current):
2037 original = state.committed_state.get(attribute.key, _NO_HISTORY)
2038 if current is NO_VALUE:
2039 return cls((), (), ())
2041 current = getattr(current, "_sa_adapter")
2042 if original is NO_VALUE:
2043 return cls(list(current), (), ())
2044 elif original is _NO_HISTORY:
2045 return cls((), list(current), ())
2046 else:
2048 current_states = [
2049 ((c is not None) and instance_state(c) or None, c)
2050 for c in current
2051 ]
2052 original_states = [
2053 ((c is not None) and instance_state(c) or None, c)
2054 for c in original
2055 ]
2057 current_set = dict(current_states)
2058 original_set = dict(original_states)
2060 return cls(
2061 [o for s, o in current_states if s not in original_set],
2062 [o for s, o in current_states if s in original_set],
2063 [o for s, o in original_states if s not in current_set],
2064 )
2067HISTORY_BLANK = History(None, None, None)
2070def get_history(obj, key, passive=PASSIVE_OFF):
2071 """Return a :class:`.History` record for the given object
2072 and attribute key.
2074 This is the **pre-flush** history for a given attribute, which is
2075 reset each time the :class:`.Session` flushes changes to the
2076 current database transaction.
2078 .. note::
2080 Prefer to use the :attr:`.AttributeState.history` and
2081 :meth:`.AttributeState.load_history` accessors to retrieve the
2082 :class:`.History` for instance attributes.
2085 :param obj: an object whose class is instrumented by the
2086 attributes package.
2088 :param key: string attribute name.
2090 :param passive: indicates loading behavior for the attribute
2091 if the value is not already present. This is a
2092 bitflag attribute, which defaults to the symbol
2093 :attr:`.PASSIVE_OFF` indicating all necessary SQL
2094 should be emitted.
2096 .. seealso::
2098 :attr:`.AttributeState.history`
2100 :meth:`.AttributeState.load_history` - retrieve history
2101 using loader callables if the value is not locally present.
2103 """
2105 return get_state_history(instance_state(obj), key, passive)
2108def get_state_history(state, key, passive=PASSIVE_OFF):
2109 return state.get_history(key, passive)
2112def has_parent(cls, obj, key, optimistic=False):
2113 """TODO"""
2114 manager = manager_of_class(cls)
2115 state = instance_state(obj)
2116 return manager.has_parent(state, key, optimistic)
2119def register_attribute(class_, key, **kw):
2120 comparator = kw.pop("comparator", None)
2121 parententity = kw.pop("parententity", None)
2122 doc = kw.pop("doc", None)
2123 desc = register_descriptor(class_, key, comparator, parententity, doc=doc)
2124 register_attribute_impl(class_, key, **kw)
2125 return desc
2128def register_attribute_impl(
2129 class_,
2130 key,
2131 uselist=False,
2132 callable_=None,
2133 useobject=False,
2134 impl_class=None,
2135 backref=None,
2136 **kw
2137):
2139 manager = manager_of_class(class_)
2140 if uselist:
2141 factory = kw.pop("typecallable", None)
2142 typecallable = manager.instrument_collection_class(
2143 key, factory or list
2144 )
2145 else:
2146 typecallable = kw.pop("typecallable", None)
2148 dispatch = manager[key].dispatch
2150 if impl_class:
2151 impl = impl_class(class_, key, typecallable, dispatch, **kw)
2152 elif uselist:
2153 impl = CollectionAttributeImpl(
2154 class_, key, callable_, dispatch, typecallable=typecallable, **kw
2155 )
2156 elif useobject:
2157 impl = ScalarObjectAttributeImpl(
2158 class_, key, callable_, dispatch, **kw
2159 )
2160 else:
2161 impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
2163 manager[key].impl = impl
2165 if backref:
2166 backref_listeners(manager[key], backref, uselist)
2168 manager.post_configure_attribute(key)
2169 return manager[key]
2172def register_descriptor(
2173 class_, key, comparator=None, parententity=None, doc=None
2174):
2175 manager = manager_of_class(class_)
2177 descriptor = InstrumentedAttribute(
2178 class_, key, comparator=comparator, parententity=parententity
2179 )
2181 descriptor.__doc__ = doc
2183 manager.instrument_attribute(key, descriptor)
2184 return descriptor
2187def unregister_attribute(class_, key):
2188 manager_of_class(class_).uninstrument_attribute(key)
2191def init_collection(obj, key):
2192 """Initialize a collection attribute and return the collection adapter.
2194 This function is used to provide direct access to collection internals
2195 for a previously unloaded attribute. e.g.::
2197 collection_adapter = init_collection(someobject, 'elements')
2198 for elem in values:
2199 collection_adapter.append_without_event(elem)
2201 For an easier way to do the above, see
2202 :func:`~sqlalchemy.orm.attributes.set_committed_value`.
2204 :param obj: a mapped object
2206 :param key: string attribute name where the collection is located.
2208 """
2209 state = instance_state(obj)
2210 dict_ = state.dict
2211 return init_state_collection(state, dict_, key)
2214def init_state_collection(state, dict_, key):
2215 """Initialize a collection attribute and return the collection adapter.
2217 Discards any existing collection which may be there.
2219 """
2220 attr = state.manager[key].impl
2222 old = dict_.pop(key, None) # discard old collection
2223 if old is not None:
2224 old_collection = old._sa_adapter
2225 attr._dispose_previous_collection(state, old, old_collection, False)
2227 user_data = attr._default_value(state, dict_)
2228 adapter = attr.get_collection(state, dict_, user_data)
2229 adapter._reset_empty()
2231 return adapter
2234def set_committed_value(instance, key, value):
2235 """Set the value of an attribute with no history events.
2237 Cancels any previous history present. The value should be
2238 a scalar value for scalar-holding attributes, or
2239 an iterable for any collection-holding attribute.
2241 This is the same underlying method used when a lazy loader
2242 fires off and loads additional data from the database.
2243 In particular, this method can be used by application code
2244 which has loaded additional attributes or collections through
2245 separate queries, which can then be attached to an instance
2246 as though it were part of its original loaded state.
2248 """
2249 state, dict_ = instance_state(instance), instance_dict(instance)
2250 state.manager[key].impl.set_committed_value(state, dict_, value)
2253def set_attribute(instance, key, value, initiator=None):
2254 """Set the value of an attribute, firing history events.
2256 This function may be used regardless of instrumentation
2257 applied directly to the class, i.e. no descriptors are required.
2258 Custom attribute management schemes will need to make usage
2259 of this method to establish attribute state as understood
2260 by SQLAlchemy.
2262 :param instance: the object that will be modified
2264 :param key: string name of the attribute
2266 :param value: value to assign
2268 :param initiator: an instance of :class:`.Event` that would have
2269 been propagated from a previous event listener. This argument
2270 is used when the :func:`.set_attribute` function is being used within
2271 an existing event listening function where an :class:`.Event` object
2272 is being supplied; the object may be used to track the origin of the
2273 chain of events.
2275 .. versionadded:: 1.2.3
2277 """
2278 state, dict_ = instance_state(instance), instance_dict(instance)
2279 state.manager[key].impl.set(state, dict_, value, initiator)
2282def get_attribute(instance, key):
2283 """Get the value of an attribute, firing any callables required.
2285 This function may be used regardless of instrumentation
2286 applied directly to the class, i.e. no descriptors are required.
2287 Custom attribute management schemes will need to make usage
2288 of this method to make usage of attribute state as understood
2289 by SQLAlchemy.
2291 """
2292 state, dict_ = instance_state(instance), instance_dict(instance)
2293 return state.manager[key].impl.get(state, dict_)
2296def del_attribute(instance, key):
2297 """Delete the value of an attribute, firing history events.
2299 This function may be used regardless of instrumentation
2300 applied directly to the class, i.e. no descriptors are required.
2301 Custom attribute management schemes will need to make usage
2302 of this method to establish attribute state as understood
2303 by SQLAlchemy.
2305 """
2306 state, dict_ = instance_state(instance), instance_dict(instance)
2307 state.manager[key].impl.delete(state, dict_)
2310def flag_modified(instance, key):
2311 """Mark an attribute on an instance as 'modified'.
2313 This sets the 'modified' flag on the instance and
2314 establishes an unconditional change event for the given attribute.
2315 The attribute must have a value present, else an
2316 :class:`.InvalidRequestError` is raised.
2318 To mark an object "dirty" without referring to any specific attribute
2319 so that it is considered within a flush, use the
2320 :func:`.attributes.flag_dirty` call.
2322 .. seealso::
2324 :func:`.attributes.flag_dirty`
2326 """
2327 state, dict_ = instance_state(instance), instance_dict(instance)
2328 impl = state.manager[key].impl
2329 impl.dispatch.modified(state, impl._modified_token)
2330 state._modified_event(dict_, impl, NO_VALUE, is_userland=True)
2333def flag_dirty(instance):
2334 """Mark an instance as 'dirty' without any specific attribute mentioned.
2336 This is a special operation that will allow the object to travel through
2337 the flush process for interception by events such as
2338 :meth:`.SessionEvents.before_flush`. Note that no SQL will be emitted in
2339 the flush process for an object that has no changes, even if marked dirty
2340 via this method. However, a :meth:`.SessionEvents.before_flush` handler
2341 will be able to see the object in the :attr:`.Session.dirty` collection and
2342 may establish changes on it, which will then be included in the SQL
2343 emitted.
2345 .. versionadded:: 1.2
2347 .. seealso::
2349 :func:`.attributes.flag_modified`
2351 """
2353 state, dict_ = instance_state(instance), instance_dict(instance)
2354 state._modified_event(dict_, None, NO_VALUE, is_userland=True)