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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# orm/attributes.py
2# Copyright (C) 2005-2020 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: http://www.opensource.org/licenses/mit-license.php
8"""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 INIT_OK
26from .base import instance_dict
27from .base import instance_state
28from .base import instance_str
29from .base import LOAD_AGAINST_COMMITTED
30from .base import manager_of_class
31from .base import NEVER_SET
32from .base import NO_AUTOFLUSH
33from .base import NO_CHANGE # noqa
34from .base import NO_RAISE
35from .base import NO_VALUE
36from .base import NON_PERSISTENT_OK # noqa
37from .base import PASSIVE_CLASS_MISMATCH # noqa
38from .base import PASSIVE_NO_FETCH
39from .base import PASSIVE_NO_FETCH_RELATED # noqa
40from .base import PASSIVE_NO_INITIALIZE
41from .base import PASSIVE_NO_RESULT
42from .base import PASSIVE_OFF
43from .base import PASSIVE_ONLY_PERSISTENT
44from .base import PASSIVE_RETURN_NEVER_SET
45from .base import RELATED_OBJECT_OK # noqa
46from .base import SQL_OK # noqa
47from .base import state_str
48from .. import event
49from .. import inspection
50from .. import util
53@inspection._self_inspects
54class QueryableAttribute(
55 interfaces._MappedAttribute,
56 interfaces.InspectionAttr,
57 interfaces.PropComparator,
58):
59 """Base class for :term:`descriptor` objects that intercept
60 attribute events on behalf of a :class:`.MapperProperty`
61 object. The actual :class:`.MapperProperty` is accessible
62 via the :attr:`.QueryableAttribute.property`
63 attribute.
66 .. seealso::
68 :class:`.InstrumentedAttribute`
70 :class:`.MapperProperty`
72 :attr:`_orm.Mapper.all_orm_descriptors`
74 :attr:`_orm.Mapper.attrs`
75 """
77 is_attribute = True
79 def __init__(
80 self,
81 class_,
82 key,
83 impl=None,
84 comparator=None,
85 parententity=None,
86 of_type=None,
87 ):
88 self.class_ = class_
89 self.key = key
90 self.impl = impl
91 self.comparator = comparator
92 self._parententity = parententity
93 self._of_type = of_type
95 manager = manager_of_class(class_)
96 # manager is None in the case of AliasedClass
97 if manager:
98 # propagate existing event listeners from
99 # immediate superclass
100 for base in manager._bases:
101 if key in base:
102 self.dispatch._update(base[key].dispatch)
103 if base[key].dispatch._active_history:
104 self.dispatch._active_history = True
106 @util.memoized_property
107 def _supports_population(self):
108 return self.impl.supports_population
110 @property
111 def _impl_uses_objects(self):
112 return self.impl.uses_objects
114 def get_history(self, instance, passive=PASSIVE_OFF):
115 return self.impl.get_history(
116 instance_state(instance), instance_dict(instance), passive
117 )
119 def __selectable__(self):
120 # TODO: conditionally attach this method based on clause_element ?
121 return self
123 @util.memoized_property
124 def info(self):
125 """Return the 'info' dictionary for the underlying SQL element.
127 The behavior here is as follows:
129 * If the attribute is a column-mapped property, i.e.
130 :class:`.ColumnProperty`, which is mapped directly
131 to a schema-level :class:`_schema.Column` object, this attribute
132 will return the :attr:`.SchemaItem.info` dictionary associated
133 with the core-level :class:`_schema.Column` object.
135 * If the attribute is a :class:`.ColumnProperty` but is mapped to
136 any other kind of SQL expression other than a
137 :class:`_schema.Column`,
138 the attribute will refer to the :attr:`.MapperProperty.info`
139 dictionary associated directly with the :class:`.ColumnProperty`,
140 assuming the SQL expression itself does not have its own ``.info``
141 attribute (which should be the case, unless a user-defined SQL
142 construct has defined one).
144 * If the attribute refers to any other kind of
145 :class:`.MapperProperty`, including :class:`.RelationshipProperty`,
146 the attribute will refer to the :attr:`.MapperProperty.info`
147 dictionary associated with that :class:`.MapperProperty`.
149 * To access the :attr:`.MapperProperty.info` dictionary of the
150 :class:`.MapperProperty` unconditionally, including for a
151 :class:`.ColumnProperty` that's associated directly with a
152 :class:`_schema.Column`, the attribute can be referred to using
153 :attr:`.QueryableAttribute.property` attribute, as
154 ``MyClass.someattribute.property.info``.
156 .. seealso::
158 :attr:`.SchemaItem.info`
160 :attr:`.MapperProperty.info`
162 """
163 return self.comparator.info
165 @util.memoized_property
166 def parent(self):
167 """Return an inspection instance representing the parent.
169 This will be either an instance of :class:`_orm.Mapper`
170 or :class:`.AliasedInsp`, depending upon the nature
171 of the parent entity which this attribute is associated
172 with.
174 """
175 return inspection.inspect(self._parententity)
177 @property
178 def expression(self):
179 return self.comparator.__clause_element__()
181 def __clause_element__(self):
182 return self.comparator.__clause_element__()
184 def _query_clause_element(self):
185 """like __clause_element__(), but called specifically
186 by :class:`_query.Query` to allow special behavior."""
188 return self.comparator._query_clause_element()
190 def _bulk_update_tuples(self, value):
191 """Return setter tuples for a bulk UPDATE."""
193 return self.comparator._bulk_update_tuples(value)
195 def adapt_to_entity(self, adapt_to_entity):
196 assert not self._of_type
197 return self.__class__(
198 adapt_to_entity.entity,
199 self.key,
200 impl=self.impl,
201 comparator=self.comparator.adapt_to_entity(adapt_to_entity),
202 parententity=adapt_to_entity,
203 )
205 def of_type(self, cls):
206 return QueryableAttribute(
207 self.class_,
208 self.key,
209 self.impl,
210 self.comparator.of_type(cls),
211 self._parententity,
212 of_type=cls,
213 )
215 def label(self, name):
216 return self._query_clause_element().label(name)
218 def operate(self, op, *other, **kwargs):
219 return op(self.comparator, *other, **kwargs)
221 def reverse_operate(self, op, other, **kwargs):
222 return op(other, self.comparator, **kwargs)
224 def hasparent(self, state, optimistic=False):
225 return self.impl.hasparent(state, optimistic=optimistic) is not False
227 def __getattr__(self, key):
228 try:
229 return getattr(self.comparator, key)
230 except AttributeError as err:
231 util.raise_(
232 AttributeError(
233 "Neither %r object nor %r object associated with %s "
234 "has an attribute %r"
235 % (
236 type(self).__name__,
237 type(self.comparator).__name__,
238 self,
239 key,
240 )
241 ),
242 replace_context=err,
243 )
245 def __str__(self):
246 return "%s.%s" % (self.class_.__name__, self.key)
248 @util.memoized_property
249 def property(self):
250 """Return the :class:`.MapperProperty` associated with this
251 :class:`.QueryableAttribute`.
254 Return values here will commonly be instances of
255 :class:`.ColumnProperty` or :class:`.RelationshipProperty`.
258 """
259 return self.comparator.property
262class InstrumentedAttribute(QueryableAttribute):
263 """Class bound instrumented attribute which adds basic
264 :term:`descriptor` methods.
266 See :class:`.QueryableAttribute` for a description of most features.
269 """
271 def __set__(self, instance, value):
272 self.impl.set(
273 instance_state(instance), instance_dict(instance), value, None
274 )
276 def __delete__(self, instance):
277 self.impl.delete(instance_state(instance), instance_dict(instance))
279 def __get__(self, instance, owner):
280 if instance is None:
281 return self
283 dict_ = instance_dict(instance)
284 if self._supports_population and self.key in dict_:
285 return dict_[self.key]
286 else:
287 return self.impl.get(instance_state(instance), dict_)
290def create_proxied_attribute(descriptor):
291 """Create an QueryableAttribute / user descriptor hybrid.
293 Returns a new QueryableAttribute type that delegates descriptor
294 behavior and getattr() to the given descriptor.
295 """
297 # TODO: can move this to descriptor_props if the need for this
298 # function is removed from ext/hybrid.py
300 class Proxy(QueryableAttribute):
301 """Presents the :class:`.QueryableAttribute` interface as a
302 proxy on top of a Python descriptor / :class:`.PropComparator`
303 combination.
305 """
307 def __init__(
308 self,
309 class_,
310 key,
311 descriptor,
312 comparator,
313 adapt_to_entity=None,
314 doc=None,
315 original_property=None,
316 ):
317 self.class_ = class_
318 self.key = key
319 self.descriptor = descriptor
320 self.original_property = original_property
321 self._comparator = comparator
322 self._adapt_to_entity = adapt_to_entity
323 self.__doc__ = doc
325 _is_internal_proxy = True
327 @property
328 def _impl_uses_objects(self):
329 return (
330 self.original_property is not None
331 and getattr(self.class_, self.key).impl.uses_objects
332 )
334 @property
335 def property(self):
336 return self.comparator.property
338 @util.memoized_property
339 def comparator(self):
340 if util.callable(self._comparator):
341 self._comparator = self._comparator()
342 if self._adapt_to_entity:
343 self._comparator = self._comparator.adapt_to_entity(
344 self._adapt_to_entity
345 )
346 return self._comparator
348 def adapt_to_entity(self, adapt_to_entity):
349 return self.__class__(
350 adapt_to_entity.entity,
351 self.key,
352 self.descriptor,
353 self._comparator,
354 adapt_to_entity,
355 )
357 def __get__(self, instance, owner):
358 retval = self.descriptor.__get__(instance, owner)
359 # detect if this is a plain Python @property, which just returns
360 # itself for class level access. If so, then return us.
361 # Otherwise, return the object returned by the descriptor.
362 if retval is self.descriptor and instance is None:
363 return self
364 else:
365 return retval
367 def __str__(self):
368 return "%s.%s" % (self.class_.__name__, self.key)
370 def __getattr__(self, attribute):
371 """Delegate __getattr__ to the original descriptor and/or
372 comparator."""
373 try:
374 return getattr(descriptor, attribute)
375 except AttributeError as err:
376 if attribute == "comparator":
377 util.raise_(
378 AttributeError("comparator"), replace_context=err
379 )
380 try:
381 # comparator itself might be unreachable
382 comparator = self.comparator
383 except AttributeError as err2:
384 util.raise_(
385 AttributeError(
386 "Neither %r object nor unconfigured comparator "
387 "object associated with %s has an attribute %r"
388 % (type(descriptor).__name__, self, attribute)
389 ),
390 replace_context=err2,
391 )
392 else:
393 try:
394 return getattr(comparator, attribute)
395 except AttributeError as err3:
396 util.raise_(
397 AttributeError(
398 "Neither %r object nor %r object "
399 "associated with %s has an attribute %r"
400 % (
401 type(descriptor).__name__,
402 type(comparator).__name__,
403 self,
404 attribute,
405 )
406 ),
407 replace_context=err3,
408 )
410 Proxy.__name__ = type(descriptor).__name__ + "Proxy"
412 util.monkeypatch_proxied_specials(
413 Proxy, type(descriptor), name="descriptor", from_instance=descriptor
414 )
415 return Proxy
418OP_REMOVE = util.symbol("REMOVE")
419OP_APPEND = util.symbol("APPEND")
420OP_REPLACE = util.symbol("REPLACE")
421OP_BULK_REPLACE = util.symbol("BULK_REPLACE")
422OP_MODIFIED = util.symbol("MODIFIED")
425class Event(object):
426 """A token propagated throughout the course of a chain of attribute
427 events.
429 Serves as an indicator of the source of the event and also provides
430 a means of controlling propagation across a chain of attribute
431 operations.
433 The :class:`.Event` object is sent as the ``initiator`` argument
434 when dealing with events such as :meth:`.AttributeEvents.append`,
435 :meth:`.AttributeEvents.set`,
436 and :meth:`.AttributeEvents.remove`.
438 The :class:`.Event` object is currently interpreted by the backref
439 event handlers, and is used to control the propagation of operations
440 across two mutually-dependent attributes.
442 .. versionadded:: 0.9.0
444 :attribute impl: The :class:`.AttributeImpl` which is the current event
445 initiator.
447 :attribute op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`,
448 :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_REPLACE`, indicating the
449 source operation.
451 """
453 __slots__ = "impl", "op", "parent_token"
455 def __init__(self, attribute_impl, op):
456 self.impl = attribute_impl
457 self.op = op
458 self.parent_token = self.impl.parent_token
460 def __eq__(self, other):
461 return (
462 isinstance(other, Event)
463 and other.impl is self.impl
464 and other.op == self.op
465 )
467 @property
468 def key(self):
469 return self.impl.key
471 def hasparent(self, state):
472 return self.impl.hasparent(state)
475class AttributeImpl(object):
476 """internal implementation for instrumented attributes."""
478 def __init__(
479 self,
480 class_,
481 key,
482 callable_,
483 dispatch,
484 trackparent=False,
485 extension=None,
486 compare_function=None,
487 active_history=False,
488 parent_token=None,
489 expire_missing=True,
490 send_modified_events=True,
491 accepts_scalar_loader=None,
492 **kwargs
493 ):
494 r"""Construct an AttributeImpl.
496 :param \class_: associated class
498 :param key: string name of the attribute
500 :param \callable_:
501 optional function which generates a callable based on a parent
502 instance, which produces the "default" values for a scalar or
503 collection attribute when it's first accessed, if not present
504 already.
506 :param trackparent:
507 if True, attempt to track if an instance has a parent attached
508 to it via this attribute.
510 :param extension:
511 a single or list of AttributeExtension object(s) which will
512 receive set/delete/append/remove/etc. events.
513 The event package is now used.
515 .. deprecated:: 1.3
517 The :paramref:`.AttributeImpl.extension` parameter is deprecated
518 and will be removed in a future release, corresponding to the
519 "extension" parameter on the :class:`.MapperProprty` classes
520 like :func:`.column_property` and :func:`_orm.relationship` The
521 events system is now used.
523 :param compare_function:
524 a function that compares two values which are normally
525 assignable to this attribute.
527 :param active_history:
528 indicates that get_history() should always return the "old" value,
529 even if it means executing a lazy callable upon attribute change.
531 :param parent_token:
532 Usually references the MapperProperty, used as a key for
533 the hasparent() function to identify an "owning" attribute.
534 Allows multiple AttributeImpls to all match a single
535 owner attribute.
537 :param expire_missing:
538 if False, don't add an "expiry" callable to this attribute
539 during state.expire_attributes(None), if no value is present
540 for this key.
542 :param send_modified_events:
543 if False, the InstanceState._modified_event method will have no
544 effect; this means the attribute will never show up as changed in a
545 history entry.
547 """
548 self.class_ = class_
549 self.key = key
550 self.callable_ = callable_
551 self.dispatch = dispatch
552 self.trackparent = trackparent
553 self.parent_token = parent_token or self
554 self.send_modified_events = send_modified_events
555 if compare_function is None:
556 self.is_equal = operator.eq
557 else:
558 self.is_equal = compare_function
560 if accepts_scalar_loader is not None:
561 self.accepts_scalar_loader = accepts_scalar_loader
562 else:
563 self.accepts_scalar_loader = self.default_accepts_scalar_loader
565 # TODO: pass in the manager here
566 # instead of doing a lookup
567 attr = manager_of_class(class_)[key]
569 for ext in util.to_list(extension or []):
570 ext._adapt_listener(attr, ext)
572 if active_history:
573 self.dispatch._active_history = True
575 self.expire_missing = expire_missing
576 self._modified_token = Event(self, OP_MODIFIED)
578 __slots__ = (
579 "class_",
580 "key",
581 "callable_",
582 "dispatch",
583 "trackparent",
584 "parent_token",
585 "send_modified_events",
586 "is_equal",
587 "expire_missing",
588 "_modified_token",
589 "accepts_scalar_loader",
590 )
592 def __str__(self):
593 return "%s.%s" % (self.class_.__name__, self.key)
595 def _get_active_history(self):
596 """Backwards compat for impl.active_history"""
598 return self.dispatch._active_history
600 def _set_active_history(self, value):
601 self.dispatch._active_history = value
603 active_history = property(_get_active_history, _set_active_history)
605 def hasparent(self, state, optimistic=False):
606 """Return the boolean value of a `hasparent` flag attached to
607 the given state.
609 The `optimistic` flag determines what the default return value
610 should be if no `hasparent` flag can be located.
612 As this function is used to determine if an instance is an
613 *orphan*, instances that were loaded from storage should be
614 assumed to not be orphans, until a True/False value for this
615 flag is set.
617 An instance attribute that is loaded by a callable function
618 will also not have a `hasparent` flag.
620 """
621 msg = "This AttributeImpl is not configured to track parents."
622 assert self.trackparent, msg
624 return (
625 state.parents.get(id(self.parent_token), optimistic) is not False
626 )
628 def sethasparent(self, state, parent_state, value):
629 """Set a boolean flag on the given item corresponding to
630 whether or not it is attached to a parent object via the
631 attribute represented by this ``InstrumentedAttribute``.
633 """
634 msg = "This AttributeImpl is not configured to track parents."
635 assert self.trackparent, msg
637 id_ = id(self.parent_token)
638 if value:
639 state.parents[id_] = parent_state
640 else:
641 if id_ in state.parents:
642 last_parent = state.parents[id_]
644 if (
645 last_parent is not False
646 and last_parent.key != parent_state.key
647 ):
649 if last_parent.obj() is None:
650 raise orm_exc.StaleDataError(
651 "Removing state %s from parent "
652 "state %s along attribute '%s', "
653 "but the parent record "
654 "has gone stale, can't be sure this "
655 "is the most recent parent."
656 % (
657 state_str(state),
658 state_str(parent_state),
659 self.key,
660 )
661 )
663 return
665 state.parents[id_] = False
667 def get_history(self, state, dict_, passive=PASSIVE_OFF):
668 raise NotImplementedError()
670 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
671 """Return a list of tuples of (state, obj)
672 for all objects in this attribute's current state
673 + history.
675 Only applies to object-based attributes.
677 This is an inlining of existing functionality
678 which roughly corresponds to:
680 get_state_history(
681 state,
682 key,
683 passive=PASSIVE_NO_INITIALIZE).sum()
685 """
686 raise NotImplementedError()
688 def initialize(self, state, dict_):
689 """Initialize the given state's attribute with an empty value."""
691 value = None
692 for fn in self.dispatch.init_scalar:
693 ret = fn(state, value, dict_)
694 if ret is not ATTR_EMPTY:
695 value = ret
697 return value
699 def get(self, state, dict_, passive=PASSIVE_OFF):
700 """Retrieve a value from the given object.
701 If a callable is assembled on this object's attribute, and
702 passive is False, the callable will be executed and the
703 resulting value will be set as the new value for this attribute.
704 """
705 if self.key in dict_:
706 return dict_[self.key]
707 else:
708 # if history present, don't load
709 key = self.key
710 if (
711 key not in state.committed_state
712 or state.committed_state[key] is NEVER_SET
713 ):
714 if not passive & CALLABLES_OK:
715 return PASSIVE_NO_RESULT
717 if key in state.expired_attributes:
718 value = state._load_expired(state, passive)
719 elif key in state.callables:
720 callable_ = state.callables[key]
721 value = callable_(state, passive)
722 elif self.callable_:
723 value = self.callable_(state, passive)
724 else:
725 value = ATTR_EMPTY
727 if value is PASSIVE_NO_RESULT or value is NEVER_SET:
728 return value
729 elif value is ATTR_WAS_SET:
730 try:
731 return dict_[key]
732 except KeyError as err:
733 # TODO: no test coverage here.
734 util.raise_(
735 KeyError(
736 "Deferred loader for attribute "
737 "%r failed to populate "
738 "correctly" % key
739 ),
740 replace_context=err,
741 )
742 elif value is not ATTR_EMPTY:
743 return self.set_committed_value(state, dict_, value)
745 if not passive & INIT_OK:
746 return NEVER_SET
747 else:
748 # Return a new, empty value
749 return self.initialize(state, dict_)
751 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
752 self.set(state, dict_, value, initiator, passive=passive)
754 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
755 self.set(
756 state, dict_, None, initiator, passive=passive, check_old=value
757 )
759 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
760 self.set(
761 state,
762 dict_,
763 None,
764 initiator,
765 passive=passive,
766 check_old=value,
767 pop=True,
768 )
770 def set(
771 self,
772 state,
773 dict_,
774 value,
775 initiator,
776 passive=PASSIVE_OFF,
777 check_old=None,
778 pop=False,
779 ):
780 raise NotImplementedError()
782 def get_committed_value(self, state, dict_, passive=PASSIVE_OFF):
783 """return the unchanged value of this attribute"""
785 if self.key in state.committed_state:
786 value = state.committed_state[self.key]
787 if value in (NO_VALUE, NEVER_SET):
788 return None
789 else:
790 return value
791 else:
792 return self.get(state, dict_, passive=passive)
794 def set_committed_value(self, state, dict_, value):
795 """set an attribute value on the given instance and 'commit' it."""
797 dict_[self.key] = value
798 state._commit(dict_, [self.key])
799 return value
802class ScalarAttributeImpl(AttributeImpl):
803 """represents a scalar value-holding InstrumentedAttribute."""
805 default_accepts_scalar_loader = True
806 uses_objects = False
807 supports_population = True
808 collection = False
809 dynamic = False
811 __slots__ = "_replace_token", "_append_token", "_remove_token"
813 def __init__(self, *arg, **kw):
814 super(ScalarAttributeImpl, self).__init__(*arg, **kw)
815 self._replace_token = self._append_token = Event(self, OP_REPLACE)
816 self._remove_token = Event(self, OP_REMOVE)
818 def delete(self, state, dict_):
819 if self.dispatch._active_history:
820 old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
821 else:
822 old = dict_.get(self.key, NO_VALUE)
824 if self.dispatch.remove:
825 self.fire_remove_event(state, dict_, old, self._remove_token)
826 state._modified_event(dict_, self, old)
828 existing = dict_.pop(self.key, NO_VALUE)
829 if (
830 existing is NO_VALUE
831 and old is NO_VALUE
832 and not state.expired
833 and self.key not in state.expired_attributes
834 ):
835 raise AttributeError("%s object does not have a value" % self)
837 def get_history(self, state, dict_, passive=PASSIVE_OFF):
838 if self.key in dict_:
839 return History.from_scalar_attribute(self, state, dict_[self.key])
840 else:
841 if passive & INIT_OK:
842 passive ^= INIT_OK
843 current = self.get(state, dict_, passive=passive)
844 if current is PASSIVE_NO_RESULT:
845 return HISTORY_BLANK
846 else:
847 return History.from_scalar_attribute(self, state, current)
849 def set(
850 self,
851 state,
852 dict_,
853 value,
854 initiator,
855 passive=PASSIVE_OFF,
856 check_old=None,
857 pop=False,
858 ):
859 if self.dispatch._active_history:
860 old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
861 else:
862 old = dict_.get(self.key, NO_VALUE)
864 if self.dispatch.set:
865 value = self.fire_replace_event(
866 state, dict_, value, old, initiator
867 )
868 state._modified_event(dict_, self, old)
869 dict_[self.key] = value
871 def fire_replace_event(self, state, dict_, value, previous, initiator):
872 for fn in self.dispatch.set:
873 value = fn(
874 state, value, previous, initiator or self._replace_token
875 )
876 return value
878 def fire_remove_event(self, state, dict_, value, initiator):
879 for fn in self.dispatch.remove:
880 fn(state, value, initiator or self._remove_token)
882 @property
883 def type(self):
884 self.property.columns[0].type
887class ScalarObjectAttributeImpl(ScalarAttributeImpl):
888 """represents a scalar-holding InstrumentedAttribute,
889 where the target object is also instrumented.
891 Adds events to delete/set operations.
893 """
895 default_accepts_scalar_loader = False
896 uses_objects = True
897 supports_population = True
898 collection = False
900 __slots__ = ()
902 def delete(self, state, dict_):
903 if self.dispatch._active_history:
904 old = self.get(
905 state,
906 dict_,
907 passive=PASSIVE_ONLY_PERSISTENT
908 | NO_AUTOFLUSH
909 | LOAD_AGAINST_COMMITTED,
910 )
911 else:
912 old = self.get(
913 state,
914 dict_,
915 passive=PASSIVE_NO_FETCH ^ INIT_OK
916 | LOAD_AGAINST_COMMITTED
917 | NO_RAISE,
918 )
920 self.fire_remove_event(state, dict_, old, self._remove_token)
922 existing = dict_.pop(self.key, NO_VALUE)
924 # if the attribute is expired, we currently have no way to tell
925 # that an object-attribute was expired vs. not loaded. So
926 # for this test, we look to see if the object has a DB identity.
927 if (
928 existing is NO_VALUE
929 and old is not PASSIVE_NO_RESULT
930 and state.key is None
931 ):
932 raise AttributeError("%s object does not have a value" % self)
934 def get_history(self, state, dict_, passive=PASSIVE_OFF):
935 if self.key in dict_:
936 return History.from_object_attribute(self, state, dict_[self.key])
937 else:
938 if passive & INIT_OK:
939 passive ^= INIT_OK
940 current = self.get(state, dict_, passive=passive)
941 if current is PASSIVE_NO_RESULT:
942 return HISTORY_BLANK
943 else:
944 return History.from_object_attribute(self, state, current)
946 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
947 if self.key in dict_:
948 current = dict_[self.key]
949 elif passive & CALLABLES_OK:
950 current = self.get(state, dict_, passive=passive)
951 else:
952 return []
954 # can't use __hash__(), can't use __eq__() here
955 if (
956 current is not None
957 and current is not PASSIVE_NO_RESULT
958 and current is not NEVER_SET
959 ):
960 ret = [(instance_state(current), current)]
961 else:
962 ret = [(None, None)]
964 if self.key in state.committed_state:
965 original = state.committed_state[self.key]
966 if (
967 original is not None
968 and original is not PASSIVE_NO_RESULT
969 and original is not NEVER_SET
970 and original is not current
971 ):
973 ret.append((instance_state(original), original))
974 return ret
976 def set(
977 self,
978 state,
979 dict_,
980 value,
981 initiator,
982 passive=PASSIVE_OFF,
983 check_old=None,
984 pop=False,
985 ):
986 """Set a value on the given InstanceState.
988 """
989 if self.dispatch._active_history:
990 old = self.get(
991 state,
992 dict_,
993 passive=PASSIVE_ONLY_PERSISTENT
994 | NO_AUTOFLUSH
995 | LOAD_AGAINST_COMMITTED,
996 )
997 else:
998 old = self.get(
999 state,
1000 dict_,
1001 passive=PASSIVE_NO_FETCH ^ INIT_OK
1002 | LOAD_AGAINST_COMMITTED
1003 | NO_RAISE,
1004 )
1006 if (
1007 check_old is not None
1008 and old is not PASSIVE_NO_RESULT
1009 and check_old is not old
1010 ):
1011 if pop:
1012 return
1013 else:
1014 raise ValueError(
1015 "Object %s not associated with %s on attribute '%s'"
1016 % (instance_str(check_old), state_str(state), self.key)
1017 )
1019 value = self.fire_replace_event(state, dict_, value, old, initiator)
1020 dict_[self.key] = value
1022 def fire_remove_event(self, state, dict_, value, initiator):
1023 if self.trackparent and value is not None:
1024 self.sethasparent(instance_state(value), state, False)
1026 for fn in self.dispatch.remove:
1027 fn(state, value, initiator or self._remove_token)
1029 state._modified_event(dict_, self, value)
1031 def fire_replace_event(self, state, dict_, value, previous, initiator):
1032 if self.trackparent:
1033 if previous is not value and previous not in (
1034 None,
1035 PASSIVE_NO_RESULT,
1036 NEVER_SET,
1037 ):
1038 self.sethasparent(instance_state(previous), state, False)
1040 for fn in self.dispatch.set:
1041 value = fn(
1042 state, value, previous, initiator or self._replace_token
1043 )
1045 state._modified_event(dict_, self, previous)
1047 if self.trackparent:
1048 if value is not None:
1049 self.sethasparent(instance_state(value), state, True)
1051 return value
1054class CollectionAttributeImpl(AttributeImpl):
1055 """A collection-holding attribute that instruments changes in membership.
1057 Only handles collections of instrumented objects.
1059 InstrumentedCollectionAttribute holds an arbitrary, user-specified
1060 container object (defaulting to a list) and brokers access to the
1061 CollectionAdapter, a "view" onto that object that presents consistent bag
1062 semantics to the orm layer independent of the user data implementation.
1064 """
1066 default_accepts_scalar_loader = False
1067 uses_objects = True
1068 supports_population = True
1069 collection = True
1070 dynamic = False
1072 __slots__ = (
1073 "copy",
1074 "collection_factory",
1075 "_append_token",
1076 "_remove_token",
1077 "_bulk_replace_token",
1078 "_duck_typed_as",
1079 )
1081 def __init__(
1082 self,
1083 class_,
1084 key,
1085 callable_,
1086 dispatch,
1087 typecallable=None,
1088 trackparent=False,
1089 extension=None,
1090 copy_function=None,
1091 compare_function=None,
1092 **kwargs
1093 ):
1094 super(CollectionAttributeImpl, self).__init__(
1095 class_,
1096 key,
1097 callable_,
1098 dispatch,
1099 trackparent=trackparent,
1100 extension=extension,
1101 compare_function=compare_function,
1102 **kwargs
1103 )
1105 if copy_function is None:
1106 copy_function = self.__copy
1107 self.copy = copy_function
1108 self.collection_factory = typecallable
1109 self._append_token = Event(self, OP_APPEND)
1110 self._remove_token = Event(self, OP_REMOVE)
1111 self._bulk_replace_token = Event(self, OP_BULK_REPLACE)
1112 self._duck_typed_as = util.duck_type_collection(
1113 self.collection_factory()
1114 )
1116 if getattr(self.collection_factory, "_sa_linker", None):
1118 @event.listens_for(self, "init_collection")
1119 def link(target, collection, collection_adapter):
1120 collection._sa_linker(collection_adapter)
1122 @event.listens_for(self, "dispose_collection")
1123 def unlink(target, collection, collection_adapter):
1124 collection._sa_linker(None)
1126 def __copy(self, item):
1127 return [y for y in collections.collection_adapter(item)]
1129 def get_history(self, state, dict_, passive=PASSIVE_OFF):
1130 current = self.get(state, dict_, passive=passive)
1131 if current is PASSIVE_NO_RESULT:
1132 return HISTORY_BLANK
1133 else:
1134 return History.from_collection(self, state, current)
1136 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
1137 # NOTE: passive is ignored here at the moment
1139 if self.key not in dict_:
1140 return []
1142 current = dict_[self.key]
1143 current = getattr(current, "_sa_adapter")
1145 if self.key in state.committed_state:
1146 original = state.committed_state[self.key]
1147 if original not in (NO_VALUE, NEVER_SET):
1148 current_states = [
1149 ((c is not None) and instance_state(c) or None, c)
1150 for c in current
1151 ]
1152 original_states = [
1153 ((c is not None) and instance_state(c) or None, c)
1154 for c in original
1155 ]
1157 current_set = dict(current_states)
1158 original_set = dict(original_states)
1160 return (
1161 [
1162 (s, o)
1163 for s, o in current_states
1164 if s not in original_set
1165 ]
1166 + [(s, o) for s, o in current_states if s in original_set]
1167 + [
1168 (s, o)
1169 for s, o in original_states
1170 if s not in current_set
1171 ]
1172 )
1174 return [(instance_state(o), o) for o in current]
1176 def fire_append_event(self, state, dict_, value, initiator):
1177 for fn in self.dispatch.append:
1178 value = fn(state, value, initiator or self._append_token)
1180 state._modified_event(dict_, self, NEVER_SET, True)
1182 if self.trackparent and value is not None:
1183 self.sethasparent(instance_state(value), state, True)
1185 return value
1187 def fire_pre_remove_event(self, state, dict_, initiator):
1188 """A special event used for pop() operations.
1190 The "remove" event needs to have the item to be removed passed to
1191 it, which in the case of pop from a set, we don't have a way to access
1192 the item before the operation. the event is used for all pop()
1193 operations (even though set.pop is the one where it is really needed).
1195 """
1196 state._modified_event(dict_, self, NEVER_SET, True)
1198 def fire_remove_event(self, state, dict_, value, initiator):
1199 if self.trackparent and value is not None:
1200 self.sethasparent(instance_state(value), state, False)
1202 for fn in self.dispatch.remove:
1203 fn(state, value, initiator or self._remove_token)
1205 state._modified_event(dict_, self, NEVER_SET, True)
1207 def delete(self, state, dict_):
1208 if self.key not in dict_:
1209 return
1211 state._modified_event(dict_, self, NEVER_SET, True)
1213 collection = self.get_collection(state, state.dict)
1214 collection.clear_with_event()
1216 # key is always present because we checked above. e.g.
1217 # del is a no-op if collection not present.
1218 del dict_[self.key]
1220 def initialize(self, state, dict_):
1221 """Initialize this attribute with an empty collection."""
1223 _, user_data = self._initialize_collection(state)
1224 dict_[self.key] = user_data
1225 return user_data
1227 def _initialize_collection(self, state):
1229 adapter, collection = state.manager.initialize_collection(
1230 self.key, state, self.collection_factory
1231 )
1233 self.dispatch.init_collection(state, collection, adapter)
1235 return adapter, collection
1237 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1238 collection = self.get_collection(state, dict_, passive=passive)
1239 if collection is PASSIVE_NO_RESULT:
1240 value = self.fire_append_event(state, dict_, value, initiator)
1241 assert (
1242 self.key not in dict_
1243 ), "Collection was loaded during event handling."
1244 state._get_pending_mutation(self.key).append(value)
1245 else:
1246 collection.append_with_event(value, initiator)
1248 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1249 collection = self.get_collection(state, state.dict, passive=passive)
1250 if collection is PASSIVE_NO_RESULT:
1251 self.fire_remove_event(state, dict_, value, initiator)
1252 assert (
1253 self.key not in dict_
1254 ), "Collection was loaded during event handling."
1255 state._get_pending_mutation(self.key).remove(value)
1256 else:
1257 collection.remove_with_event(value, initiator)
1259 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1260 try:
1261 # TODO: better solution here would be to add
1262 # a "popper" role to collections.py to complement
1263 # "remover".
1264 self.remove(state, dict_, value, initiator, passive=passive)
1265 except (ValueError, KeyError, IndexError):
1266 pass
1268 def set(
1269 self,
1270 state,
1271 dict_,
1272 value,
1273 initiator=None,
1274 passive=PASSIVE_OFF,
1275 pop=False,
1276 _adapt=True,
1277 ):
1278 iterable = orig_iterable = value
1280 # pulling a new collection first so that an adaptation exception does
1281 # not trigger a lazy load of the old collection.
1282 new_collection, user_data = self._initialize_collection(state)
1283 if _adapt:
1284 if new_collection._converter is not None:
1285 iterable = new_collection._converter(iterable)
1286 else:
1287 setting_type = util.duck_type_collection(iterable)
1288 receiving_type = self._duck_typed_as
1290 if setting_type is not receiving_type:
1291 given = (
1292 iterable is None
1293 and "None"
1294 or iterable.__class__.__name__
1295 )
1296 wanted = self._duck_typed_as.__name__
1297 raise TypeError(
1298 "Incompatible collection type: %s is not %s-like"
1299 % (given, wanted)
1300 )
1302 # If the object is an adapted collection, return the (iterable)
1303 # adapter.
1304 if hasattr(iterable, "_sa_iterator"):
1305 iterable = iterable._sa_iterator()
1306 elif setting_type is dict:
1307 if util.py3k:
1308 iterable = iterable.values()
1309 else:
1310 iterable = getattr(
1311 iterable, "itervalues", iterable.values
1312 )()
1313 else:
1314 iterable = iter(iterable)
1315 new_values = list(iterable)
1317 evt = self._bulk_replace_token
1319 self.dispatch.bulk_replace(state, new_values, evt)
1321 old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
1322 if old is PASSIVE_NO_RESULT:
1323 old = self.initialize(state, dict_)
1324 elif old is orig_iterable:
1325 # ignore re-assignment of the current collection, as happens
1326 # implicitly with in-place operators (foo.collection |= other)
1327 return
1329 # place a copy of "old" in state.committed_state
1330 state._modified_event(dict_, self, old, True)
1332 old_collection = old._sa_adapter
1334 dict_[self.key] = user_data
1336 collections.bulk_replace(
1337 new_values, old_collection, new_collection, initiator=evt
1338 )
1340 del old._sa_adapter
1341 self.dispatch.dispose_collection(state, old, old_collection)
1343 def _invalidate_collection(self, collection):
1344 adapter = getattr(collection, "_sa_adapter")
1345 adapter.invalidated = True
1347 def set_committed_value(self, state, dict_, value):
1348 """Set an attribute value on the given instance and 'commit' it."""
1350 collection, user_data = self._initialize_collection(state)
1352 if value:
1353 collection.append_multiple_without_event(value)
1355 state.dict[self.key] = user_data
1357 state._commit(dict_, [self.key])
1359 if self.key in state._pending_mutations:
1360 # pending items exist. issue a modified event,
1361 # add/remove new items.
1362 state._modified_event(dict_, self, user_data, True)
1364 pending = state._pending_mutations.pop(self.key)
1365 added = pending.added_items
1366 removed = pending.deleted_items
1367 for item in added:
1368 collection.append_without_event(item)
1369 for item in removed:
1370 collection.remove_without_event(item)
1372 return user_data
1374 def get_collection(
1375 self, state, dict_, user_data=None, passive=PASSIVE_OFF
1376 ):
1377 """Retrieve the CollectionAdapter associated with the given state.
1379 Creates a new CollectionAdapter if one does not exist.
1381 """
1382 if user_data is None:
1383 user_data = self.get(state, dict_, passive=passive)
1384 if user_data is PASSIVE_NO_RESULT:
1385 return user_data
1387 return getattr(user_data, "_sa_adapter")
1390def backref_listeners(attribute, key, uselist):
1391 """Apply listeners to synchronize a two-way relationship."""
1393 # use easily recognizable names for stack traces.
1395 # in the sections marked "tokens to test for a recursive loop",
1396 # this is somewhat brittle and very performance-sensitive logic
1397 # that is specific to how we might arrive at each event. a marker
1398 # that can target us directly to arguments being invoked against
1399 # the impl might be simpler, but could interfere with other systems.
1401 parent_token = attribute.impl.parent_token
1402 parent_impl = attribute.impl
1404 def _acceptable_key_err(child_state, initiator, child_impl):
1405 raise ValueError(
1406 "Bidirectional attribute conflict detected: "
1407 'Passing object %s to attribute "%s" '
1408 'triggers a modify event on attribute "%s" '
1409 'via the backref "%s".'
1410 % (
1411 state_str(child_state),
1412 initiator.parent_token,
1413 child_impl.parent_token,
1414 attribute.impl.parent_token,
1415 )
1416 )
1418 def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
1419 if oldchild is child:
1420 return child
1421 if (
1422 oldchild is not None
1423 and oldchild is not PASSIVE_NO_RESULT
1424 and oldchild is not NEVER_SET
1425 ):
1426 # With lazy=None, there's no guarantee that the full collection is
1427 # present when updating via a backref.
1428 old_state, old_dict = (
1429 instance_state(oldchild),
1430 instance_dict(oldchild),
1431 )
1432 impl = old_state.manager[key].impl
1434 # tokens to test for a recursive loop.
1435 if not impl.collection and not impl.dynamic:
1436 check_recursive_token = impl._replace_token
1437 else:
1438 check_recursive_token = impl._remove_token
1440 if initiator is not check_recursive_token:
1441 impl.pop(
1442 old_state,
1443 old_dict,
1444 state.obj(),
1445 parent_impl._append_token,
1446 passive=PASSIVE_NO_FETCH,
1447 )
1449 if child is not None:
1450 child_state, child_dict = (
1451 instance_state(child),
1452 instance_dict(child),
1453 )
1454 child_impl = child_state.manager[key].impl
1456 if (
1457 initiator.parent_token is not parent_token
1458 and initiator.parent_token is not child_impl.parent_token
1459 ):
1460 _acceptable_key_err(state, initiator, child_impl)
1462 # tokens to test for a recursive loop.
1463 check_append_token = child_impl._append_token
1464 check_bulk_replace_token = (
1465 child_impl._bulk_replace_token
1466 if child_impl.collection
1467 else None
1468 )
1470 if (
1471 initiator is not check_append_token
1472 and initiator is not check_bulk_replace_token
1473 ):
1474 child_impl.append(
1475 child_state,
1476 child_dict,
1477 state.obj(),
1478 initiator,
1479 passive=PASSIVE_NO_FETCH,
1480 )
1481 return child
1483 def emit_backref_from_collection_append_event(state, child, initiator):
1484 if child is None:
1485 return
1487 child_state, child_dict = instance_state(child), instance_dict(child)
1488 child_impl = child_state.manager[key].impl
1490 if (
1491 initiator.parent_token is not parent_token
1492 and initiator.parent_token is not child_impl.parent_token
1493 ):
1494 _acceptable_key_err(state, initiator, child_impl)
1496 # tokens to test for a recursive loop.
1497 check_append_token = child_impl._append_token
1498 check_bulk_replace_token = (
1499 child_impl._bulk_replace_token if child_impl.collection else None
1500 )
1502 if (
1503 initiator is not check_append_token
1504 and initiator is not check_bulk_replace_token
1505 ):
1506 child_impl.append(
1507 child_state,
1508 child_dict,
1509 state.obj(),
1510 initiator,
1511 passive=PASSIVE_NO_FETCH,
1512 )
1513 return child
1515 def emit_backref_from_collection_remove_event(state, child, initiator):
1516 if (
1517 child is not None
1518 and child is not PASSIVE_NO_RESULT
1519 and child is not NEVER_SET
1520 ):
1521 child_state, child_dict = (
1522 instance_state(child),
1523 instance_dict(child),
1524 )
1525 child_impl = child_state.manager[key].impl
1527 # tokens to test for a recursive loop.
1528 if not child_impl.collection and not child_impl.dynamic:
1529 check_remove_token = child_impl._remove_token
1530 check_replace_token = child_impl._replace_token
1531 check_for_dupes_on_remove = uselist and not parent_impl.dynamic
1532 else:
1533 check_remove_token = child_impl._remove_token
1534 check_replace_token = (
1535 child_impl._bulk_replace_token
1536 if child_impl.collection
1537 else None
1538 )
1539 check_for_dupes_on_remove = False
1541 if (
1542 initiator is not check_remove_token
1543 and initiator is not check_replace_token
1544 ):
1546 if not check_for_dupes_on_remove or not util.has_dupes(
1547 # when this event is called, the item is usually
1548 # present in the list, except for a pop() operation.
1549 state.dict[parent_impl.key],
1550 child,
1551 ):
1552 child_impl.pop(
1553 child_state,
1554 child_dict,
1555 state.obj(),
1556 initiator,
1557 passive=PASSIVE_NO_FETCH,
1558 )
1560 if uselist:
1561 event.listen(
1562 attribute,
1563 "append",
1564 emit_backref_from_collection_append_event,
1565 retval=True,
1566 raw=True,
1567 )
1568 else:
1569 event.listen(
1570 attribute,
1571 "set",
1572 emit_backref_from_scalar_set_event,
1573 retval=True,
1574 raw=True,
1575 )
1576 # TODO: need coverage in test/orm/ of remove event
1577 event.listen(
1578 attribute,
1579 "remove",
1580 emit_backref_from_collection_remove_event,
1581 retval=True,
1582 raw=True,
1583 )
1586_NO_HISTORY = util.symbol("NO_HISTORY")
1587_NO_STATE_SYMBOLS = frozenset(
1588 [id(PASSIVE_NO_RESULT), id(NO_VALUE), id(NEVER_SET)]
1589)
1591History = util.namedtuple("History", ["added", "unchanged", "deleted"])
1594class History(History):
1595 """A 3-tuple of added, unchanged and deleted values,
1596 representing the changes which have occurred on an instrumented
1597 attribute.
1599 The easiest way to get a :class:`.History` object for a particular
1600 attribute on an object is to use the :func:`_sa.inspect` function::
1602 from sqlalchemy import inspect
1604 hist = inspect(myobject).attrs.myattribute.history
1606 Each tuple member is an iterable sequence:
1608 * ``added`` - the collection of items added to the attribute (the first
1609 tuple element).
1611 * ``unchanged`` - the collection of items that have not changed on the
1612 attribute (the second tuple element).
1614 * ``deleted`` - the collection of items that have been removed from the
1615 attribute (the third tuple element).
1617 """
1619 def __bool__(self):
1620 return self != HISTORY_BLANK
1622 __nonzero__ = __bool__
1624 def empty(self):
1625 """Return True if this :class:`.History` has no changes
1626 and no existing, unchanged state.
1628 """
1630 return not bool((self.added or self.deleted) or self.unchanged)
1632 def sum(self):
1633 """Return a collection of added + unchanged + deleted."""
1635 return (
1636 (self.added or []) + (self.unchanged or []) + (self.deleted or [])
1637 )
1639 def non_deleted(self):
1640 """Return a collection of added + unchanged."""
1642 return (self.added or []) + (self.unchanged or [])
1644 def non_added(self):
1645 """Return a collection of unchanged + deleted."""
1647 return (self.unchanged or []) + (self.deleted or [])
1649 def has_changes(self):
1650 """Return True if this :class:`.History` has changes."""
1652 return bool(self.added or self.deleted)
1654 def as_state(self):
1655 return History(
1656 [
1657 (c is not None) and instance_state(c) or None
1658 for c in self.added
1659 ],
1660 [
1661 (c is not None) and instance_state(c) or None
1662 for c in self.unchanged
1663 ],
1664 [
1665 (c is not None) and instance_state(c) or None
1666 for c in self.deleted
1667 ],
1668 )
1670 @classmethod
1671 def from_scalar_attribute(cls, attribute, state, current):
1672 original = state.committed_state.get(attribute.key, _NO_HISTORY)
1674 if original is _NO_HISTORY:
1675 if current is NEVER_SET:
1676 return cls((), (), ())
1677 else:
1678 return cls((), [current], ())
1679 # don't let ClauseElement expressions here trip things up
1680 elif attribute.is_equal(current, original) is True:
1681 return cls((), [current], ())
1682 else:
1683 # current convention on native scalars is to not
1684 # include information
1685 # about missing previous value in "deleted", but
1686 # we do include None, which helps in some primary
1687 # key situations
1688 if id(original) in _NO_STATE_SYMBOLS:
1689 deleted = ()
1690 # indicate a "del" operation occurred when we don't have
1691 # the previous value as: ([None], (), ())
1692 if id(current) in _NO_STATE_SYMBOLS:
1693 current = None
1694 else:
1695 deleted = [original]
1696 if current is NEVER_SET:
1697 return cls((), (), deleted)
1698 else:
1699 return cls([current], (), deleted)
1701 @classmethod
1702 def from_object_attribute(cls, attribute, state, current):
1703 original = state.committed_state.get(attribute.key, _NO_HISTORY)
1705 if original is _NO_HISTORY:
1706 if current is NO_VALUE or current is NEVER_SET:
1707 return cls((), (), ())
1708 else:
1709 return cls((), [current], ())
1710 elif current is original and current is not NEVER_SET:
1711 return cls((), [current], ())
1712 else:
1713 # current convention on related objects is to not
1714 # include information
1715 # about missing previous value in "deleted", and
1716 # to also not include None - the dependency.py rules
1717 # ignore the None in any case.
1718 if id(original) in _NO_STATE_SYMBOLS or original is None:
1719 deleted = ()
1720 # indicate a "del" operation occurred when we don't have
1721 # the previous value as: ([None], (), ())
1722 if id(current) in _NO_STATE_SYMBOLS:
1723 current = None
1724 else:
1725 deleted = [original]
1726 if current is NO_VALUE or current is NEVER_SET:
1727 return cls((), (), deleted)
1728 else:
1729 return cls([current], (), deleted)
1731 @classmethod
1732 def from_collection(cls, attribute, state, current):
1733 original = state.committed_state.get(attribute.key, _NO_HISTORY)
1735 if current is NO_VALUE or current is NEVER_SET:
1736 return cls((), (), ())
1738 current = getattr(current, "_sa_adapter")
1739 if original in (NO_VALUE, NEVER_SET):
1740 return cls(list(current), (), ())
1741 elif original is _NO_HISTORY:
1742 return cls((), list(current), ())
1743 else:
1745 current_states = [
1746 ((c is not None) and instance_state(c) or None, c)
1747 for c in current
1748 ]
1749 original_states = [
1750 ((c is not None) and instance_state(c) or None, c)
1751 for c in original
1752 ]
1754 current_set = dict(current_states)
1755 original_set = dict(original_states)
1757 return cls(
1758 [o for s, o in current_states if s not in original_set],
1759 [o for s, o in current_states if s in original_set],
1760 [o for s, o in original_states if s not in current_set],
1761 )
1764HISTORY_BLANK = History(None, None, None)
1767def get_history(obj, key, passive=PASSIVE_OFF):
1768 """Return a :class:`.History` record for the given object
1769 and attribute key.
1771 This is the **pre-flush** history for a given attribute, which is
1772 reset each time the :class:`.Session` flushes changes to the
1773 current database transaction.
1775 .. note::
1777 Prefer to use the :attr:`.AttributeState.history` and
1778 :meth:`.AttributeState.load_history` accessors to retrieve the
1779 :class:`.History` for instance attributes.
1782 :param obj: an object whose class is instrumented by the
1783 attributes package.
1785 :param key: string attribute name.
1787 :param passive: indicates loading behavior for the attribute
1788 if the value is not already present. This is a
1789 bitflag attribute, which defaults to the symbol
1790 :attr:`.PASSIVE_OFF` indicating all necessary SQL
1791 should be emitted.
1793 .. seealso::
1795 :attr:`.AttributeState.history`
1797 :meth:`.AttributeState.load_history` - retrieve history
1798 using loader callables if the value is not locally present.
1800 """
1801 if passive is True:
1802 util.warn_deprecated(
1803 "Passing True for 'passive' is deprecated. "
1804 "Use attributes.PASSIVE_NO_INITIALIZE"
1805 )
1806 passive = PASSIVE_NO_INITIALIZE
1807 elif passive is False:
1808 util.warn_deprecated(
1809 "Passing False for 'passive' is "
1810 "deprecated. Use attributes.PASSIVE_OFF"
1811 )
1812 passive = PASSIVE_OFF
1814 return get_state_history(instance_state(obj), key, passive)
1817def get_state_history(state, key, passive=PASSIVE_OFF):
1818 return state.get_history(key, passive)
1821def has_parent(cls, obj, key, optimistic=False):
1822 """TODO"""
1823 manager = manager_of_class(cls)
1824 state = instance_state(obj)
1825 return manager.has_parent(state, key, optimistic)
1828def register_attribute(class_, key, **kw):
1829 comparator = kw.pop("comparator", None)
1830 parententity = kw.pop("parententity", None)
1831 doc = kw.pop("doc", None)
1832 desc = register_descriptor(class_, key, comparator, parententity, doc=doc)
1833 register_attribute_impl(class_, key, **kw)
1834 return desc
1837def register_attribute_impl(
1838 class_,
1839 key,
1840 uselist=False,
1841 callable_=None,
1842 useobject=False,
1843 impl_class=None,
1844 backref=None,
1845 **kw
1846):
1848 manager = manager_of_class(class_)
1849 if uselist:
1850 factory = kw.pop("typecallable", None)
1851 typecallable = manager.instrument_collection_class(
1852 key, factory or list
1853 )
1854 else:
1855 typecallable = kw.pop("typecallable", None)
1857 dispatch = manager[key].dispatch
1859 if impl_class:
1860 impl = impl_class(class_, key, typecallable, dispatch, **kw)
1861 elif uselist:
1862 impl = CollectionAttributeImpl(
1863 class_, key, callable_, dispatch, typecallable=typecallable, **kw
1864 )
1865 elif useobject:
1866 impl = ScalarObjectAttributeImpl(
1867 class_, key, callable_, dispatch, **kw
1868 )
1869 else:
1870 impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
1872 manager[key].impl = impl
1874 if backref:
1875 backref_listeners(manager[key], backref, uselist)
1877 manager.post_configure_attribute(key)
1878 return manager[key]
1881def register_descriptor(
1882 class_, key, comparator=None, parententity=None, doc=None
1883):
1884 manager = manager_of_class(class_)
1886 descriptor = InstrumentedAttribute(
1887 class_, key, comparator=comparator, parententity=parententity
1888 )
1890 descriptor.__doc__ = doc
1892 manager.instrument_attribute(key, descriptor)
1893 return descriptor
1896def unregister_attribute(class_, key):
1897 manager_of_class(class_).uninstrument_attribute(key)
1900def init_collection(obj, key):
1901 """Initialize a collection attribute and return the collection adapter.
1903 This function is used to provide direct access to collection internals
1904 for a previously unloaded attribute. e.g.::
1906 collection_adapter = init_collection(someobject, 'elements')
1907 for elem in values:
1908 collection_adapter.append_without_event(elem)
1910 For an easier way to do the above, see
1911 :func:`~sqlalchemy.orm.attributes.set_committed_value`.
1913 :param obj: a mapped object
1915 :param key: string attribute name where the collection is located.
1917 """
1918 state = instance_state(obj)
1919 dict_ = state.dict
1920 return init_state_collection(state, dict_, key)
1923def init_state_collection(state, dict_, key):
1924 """Initialize a collection attribute and return the collection adapter."""
1926 attr = state.manager[key].impl
1927 user_data = attr.initialize(state, dict_)
1928 return attr.get_collection(state, dict_, user_data)
1931def set_committed_value(instance, key, value):
1932 """Set the value of an attribute with no history events.
1934 Cancels any previous history present. The value should be
1935 a scalar value for scalar-holding attributes, or
1936 an iterable for any collection-holding attribute.
1938 This is the same underlying method used when a lazy loader
1939 fires off and loads additional data from the database.
1940 In particular, this method can be used by application code
1941 which has loaded additional attributes or collections through
1942 separate queries, which can then be attached to an instance
1943 as though it were part of its original loaded state.
1945 """
1946 state, dict_ = instance_state(instance), instance_dict(instance)
1947 state.manager[key].impl.set_committed_value(state, dict_, value)
1950def set_attribute(instance, key, value, initiator=None):
1951 """Set the value of an attribute, firing history events.
1953 This function may be used regardless of instrumentation
1954 applied directly to the class, i.e. no descriptors are required.
1955 Custom attribute management schemes will need to make usage
1956 of this method to establish attribute state as understood
1957 by SQLAlchemy.
1959 :param instance: the object that will be modified
1961 :param key: string name of the attribute
1963 :param value: value to assign
1965 :param initiator: an instance of :class:`.Event` that would have
1966 been propagated from a previous event listener. This argument
1967 is used when the :func:`.set_attribute` function is being used within
1968 an existing event listening function where an :class:`.Event` object
1969 is being supplied; the object may be used to track the origin of the
1970 chain of events.
1972 .. versionadded:: 1.2.3
1974 """
1975 state, dict_ = instance_state(instance), instance_dict(instance)
1976 state.manager[key].impl.set(state, dict_, value, initiator)
1979def get_attribute(instance, key):
1980 """Get the value of an attribute, firing any callables required.
1982 This function may be used regardless of instrumentation
1983 applied directly to the class, i.e. no descriptors are required.
1984 Custom attribute management schemes will need to make usage
1985 of this method to make usage of attribute state as understood
1986 by SQLAlchemy.
1988 """
1989 state, dict_ = instance_state(instance), instance_dict(instance)
1990 return state.manager[key].impl.get(state, dict_)
1993def del_attribute(instance, key):
1994 """Delete the value of an attribute, firing history events.
1996 This function may be used regardless of instrumentation
1997 applied directly to the class, i.e. no descriptors are required.
1998 Custom attribute management schemes will need to make usage
1999 of this method to establish attribute state as understood
2000 by SQLAlchemy.
2002 """
2003 state, dict_ = instance_state(instance), instance_dict(instance)
2004 state.manager[key].impl.delete(state, dict_)
2007def flag_modified(instance, key):
2008 """Mark an attribute on an instance as 'modified'.
2010 This sets the 'modified' flag on the instance and
2011 establishes an unconditional change event for the given attribute.
2012 The attribute must have a value present, else an
2013 :class:`.InvalidRequestError` is raised.
2015 To mark an object "dirty" without referring to any specific attribute
2016 so that it is considered within a flush, use the
2017 :func:`.attributes.flag_dirty` call.
2019 .. seealso::
2021 :func:`.attributes.flag_dirty`
2023 """
2024 state, dict_ = instance_state(instance), instance_dict(instance)
2025 impl = state.manager[key].impl
2026 impl.dispatch.modified(state, impl._modified_token)
2027 state._modified_event(dict_, impl, NO_VALUE, is_userland=True)
2030def flag_dirty(instance):
2031 """Mark an instance as 'dirty' without any specific attribute mentioned.
2033 This is a special operation that will allow the object to travel through
2034 the flush process for interception by events such as
2035 :meth:`.SessionEvents.before_flush`. Note that no SQL will be emitted in
2036 the flush process for an object that has no changes, even if marked dirty
2037 via this method. However, a :meth:`.SessionEvents.before_flush` handler
2038 will be able to see the object in the :attr:`.Session.dirty` collection and
2039 may establish changes on it, which will then be included in the SQL
2040 emitted.
2042 .. versionadded:: 1.2
2044 .. seealso::
2046 :func:`.attributes.flag_modified`
2048 """
2050 state, dict_ = instance_state(instance), instance_dict(instance)
2051 state._modified_event(dict_, None, NO_VALUE, is_userland=True)