1# orm/attributes.py
2# Copyright (C) 2005-2018 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
7
8"""Defines instrumentation for class attributes and their interaction
9with instances.
10
11This module is usually not directly visible to user applications, but
12defines a large part of the ORM's interactivity.
13
14
15"""
16
17import operator
18from .. import util, event, inspection
19from . import interfaces, collections, exc as orm_exc
20
21from .base import instance_state, instance_dict, manager_of_class
22
23from .base import PASSIVE_NO_RESULT, ATTR_WAS_SET, ATTR_EMPTY, NO_VALUE,\
24    NEVER_SET, NO_CHANGE, CALLABLES_OK, SQL_OK, RELATED_OBJECT_OK,\
25    INIT_OK, NON_PERSISTENT_OK, LOAD_AGAINST_COMMITTED, PASSIVE_OFF,\
26    PASSIVE_RETURN_NEVER_SET, PASSIVE_NO_INITIALIZE, PASSIVE_NO_FETCH,\
27    PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT, NO_AUTOFLUSH
28from .base import state_str, instance_str
29
30
31@inspection._self_inspects
32class QueryableAttribute(interfaces._MappedAttribute,
33                         interfaces.InspectionAttr,
34                         interfaces.PropComparator):
35    """Base class for :term:`descriptor` objects that intercept
36    attribute events on behalf of a :class:`.MapperProperty`
37    object.  The actual :class:`.MapperProperty` is accessible
38    via the :attr:`.QueryableAttribute.property`
39    attribute.
40
41
42    .. seealso::
43
44        :class:`.InstrumentedAttribute`
45
46        :class:`.MapperProperty`
47
48        :attr:`.Mapper.all_orm_descriptors`
49
50        :attr:`.Mapper.attrs`
51    """
52
53    is_attribute = True
54
55    def __init__(self, class_, key, impl=None,
56                 comparator=None, parententity=None,
57                 of_type=None):
58        self.class_ = class_
59        self.key = key
60        self.impl = impl
61        self.comparator = comparator
62        self._parententity = parententity
63        self._of_type = of_type
64
65        manager = manager_of_class(class_)
66        # manager is None in the case of AliasedClass
67        if manager:
68            # propagate existing event listeners from
69            # immediate superclass
70            for base in manager._bases:
71                if key in base:
72                    self.dispatch._update(base[key].dispatch)
73
74    @util.memoized_property
75    def _supports_population(self):
76        return self.impl.supports_population
77
78    def get_history(self, instance, passive=PASSIVE_OFF):
79        return self.impl.get_history(instance_state(instance),
80                                     instance_dict(instance), passive)
81
82    def __selectable__(self):
83        # TODO: conditionally attach this method based on clause_element ?
84        return self
85
86    @util.memoized_property
87    def info(self):
88        """Return the 'info' dictionary for the underlying SQL element.
89
90        The behavior here is as follows:
91
92        * If the attribute is a column-mapped property, i.e.
93          :class:`.ColumnProperty`, which is mapped directly
94          to a schema-level :class:`.Column` object, this attribute
95          will return the :attr:`.SchemaItem.info` dictionary associated
96          with the core-level :class:`.Column` object.
97
98        * If the attribute is a :class:`.ColumnProperty` but is mapped to
99          any other kind of SQL expression other than a :class:`.Column`,
100          the attribute will refer to the :attr:`.MapperProperty.info`
101          dictionary associated directly with the :class:`.ColumnProperty`,
102          assuming the SQL expression itself does not have its own ``.info``
103          attribute (which should be the case, unless a user-defined SQL
104          construct has defined one).
105
106        * If the attribute refers to any other kind of
107          :class:`.MapperProperty`, including :class:`.RelationshipProperty`,
108          the attribute will refer to the :attr:`.MapperProperty.info`
109          dictionary associated with that :class:`.MapperProperty`.
110
111        * To access the :attr:`.MapperProperty.info` dictionary of the
112          :class:`.MapperProperty` unconditionally, including for a
113          :class:`.ColumnProperty` that's associated directly with a
114          :class:`.schema.Column`, the attribute can be referred to using
115          :attr:`.QueryableAttribute.property` attribute, as
116          ``MyClass.someattribute.property.info``.
117
118        .. versionadded:: 0.8.0
119
120        .. seealso::
121
122            :attr:`.SchemaItem.info`
123
124            :attr:`.MapperProperty.info`
125
126        """
127        return self.comparator.info
128
129    @util.memoized_property
130    def parent(self):
131        """Return an inspection instance representing the parent.
132
133        This will be either an instance of :class:`.Mapper`
134        or :class:`.AliasedInsp`, depending upon the nature
135        of the parent entity which this attribute is associated
136        with.
137
138        """
139        return inspection.inspect(self._parententity)
140
141    @property
142    def expression(self):
143        return self.comparator.__clause_element__()
144
145    def __clause_element__(self):
146        return self.comparator.__clause_element__()
147
148    def _query_clause_element(self):
149        """like __clause_element__(), but called specifically
150        by :class:`.Query` to allow special behavior."""
151
152        return self.comparator._query_clause_element()
153
154    def adapt_to_entity(self, adapt_to_entity):
155        assert not self._of_type
156        return self.__class__(adapt_to_entity.entity,
157                              self.key, impl=self.impl,
158                              comparator=self.comparator.adapt_to_entity(
159                                  adapt_to_entity),
160                              parententity=adapt_to_entity)
161
162    def of_type(self, cls):
163        return QueryableAttribute(
164            self.class_,
165            self.key,
166            self.impl,
167            self.comparator.of_type(cls),
168            self._parententity,
169            of_type=cls)
170
171    def label(self, name):
172        return self._query_clause_element().label(name)
173
174    def operate(self, op, *other, **kwargs):
175        return op(self.comparator, *other, **kwargs)
176
177    def reverse_operate(self, op, other, **kwargs):
178        return op(other, self.comparator, **kwargs)
179
180    def hasparent(self, state, optimistic=False):
181        return self.impl.hasparent(state, optimistic=optimistic) is not False
182
183    def __getattr__(self, key):
184        try:
185            return getattr(self.comparator, key)
186        except AttributeError:
187            raise AttributeError(
188                'Neither %r object nor %r object associated with %s '
189                'has an attribute %r' % (
190                    type(self).__name__,
191                    type(self.comparator).__name__,
192                    self,
193                    key)
194            )
195
196    def __str__(self):
197        return "%s.%s" % (self.class_.__name__, self.key)
198
199    @util.memoized_property
200    def property(self):
201        """Return the :class:`.MapperProperty` associated with this
202        :class:`.QueryableAttribute`.
203
204
205        Return values here will commonly be instances of
206        :class:`.ColumnProperty` or :class:`.RelationshipProperty`.
207
208
209        """
210        return self.comparator.property
211
212
213class InstrumentedAttribute(QueryableAttribute):
214    """Class bound instrumented attribute which adds basic
215    :term:`descriptor` methods.
216
217    See :class:`.QueryableAttribute` for a description of most features.
218
219
220    """
221
222    def __set__(self, instance, value):
223        self.impl.set(instance_state(instance),
224                      instance_dict(instance), value, None)
225
226    def __delete__(self, instance):
227        self.impl.delete(instance_state(instance), instance_dict(instance))
228
229    def __get__(self, instance, owner):
230        if instance is None:
231            return self
232
233        dict_ = instance_dict(instance)
234        if self._supports_population and self.key in dict_:
235            return dict_[self.key]
236        else:
237            return self.impl.get(instance_state(instance), dict_)
238
239
240def create_proxied_attribute(descriptor):
241    """Create an QueryableAttribute / user descriptor hybrid.
242
243    Returns a new QueryableAttribute type that delegates descriptor
244    behavior and getattr() to the given descriptor.
245    """
246
247    # TODO: can move this to descriptor_props if the need for this
248    # function is removed from ext/hybrid.py
249
250    class Proxy(QueryableAttribute):
251        """Presents the :class:`.QueryableAttribute` interface as a
252        proxy on top of a Python descriptor / :class:`.PropComparator`
253        combination.
254
255        """
256
257        def __init__(self, class_, key, descriptor,
258                     comparator,
259                     adapt_to_entity=None, doc=None,
260                     original_property=None):
261            self.class_ = class_
262            self.key = key
263            self.descriptor = descriptor
264            self.original_property = original_property
265            self._comparator = comparator
266            self._adapt_to_entity = adapt_to_entity
267            self.__doc__ = doc
268
269        @property
270        def property(self):
271            return self.comparator.property
272
273        @util.memoized_property
274        def comparator(self):
275            if util.callable(self._comparator):
276                self._comparator = self._comparator()
277            if self._adapt_to_entity:
278                self._comparator = self._comparator.adapt_to_entity(
279                    self._adapt_to_entity)
280            return self._comparator
281
282        def adapt_to_entity(self, adapt_to_entity):
283            return self.__class__(adapt_to_entity.entity,
284                                  self.key,
285                                  self.descriptor,
286                                  self._comparator,
287                                  adapt_to_entity)
288
289        def __get__(self, instance, owner):
290            if instance is None:
291                return self
292            else:
293                return self.descriptor.__get__(instance, owner)
294
295        def __str__(self):
296            return "%s.%s" % (self.class_.__name__, self.key)
297
298        def __getattr__(self, attribute):
299            """Delegate __getattr__ to the original descriptor and/or
300            comparator."""
301
302            try:
303                return getattr(descriptor, attribute)
304            except AttributeError:
305                try:
306                    return getattr(self.comparator, attribute)
307                except AttributeError:
308                    raise AttributeError(
309                        'Neither %r object nor %r object associated with %s '
310                        'has an attribute %r' % (
311                            type(descriptor).__name__,
312                            type(self.comparator).__name__,
313                            self,
314                            attribute)
315                    )
316
317    Proxy.__name__ = type(descriptor).__name__ + 'Proxy'
318
319    util.monkeypatch_proxied_specials(Proxy, type(descriptor),
320                                      name='descriptor',
321                                      from_instance=descriptor)
322    return Proxy
323
324OP_REMOVE = util.symbol("REMOVE")
325OP_APPEND = util.symbol("APPEND")
326OP_REPLACE = util.symbol("REPLACE")
327
328
329class Event(object):
330    """A token propagated throughout the course of a chain of attribute
331    events.
332
333    Serves as an indicator of the source of the event and also provides
334    a means of controlling propagation across a chain of attribute
335    operations.
336
337    The :class:`.Event` object is sent as the ``initiator`` argument
338    when dealing with the :meth:`.AttributeEvents.append`,
339    :meth:`.AttributeEvents.set`,
340    and :meth:`.AttributeEvents.remove` events.
341
342    The :class:`.Event` object is currently interpreted by the backref
343    event handlers, and is used to control the propagation of operations
344    across two mutually-dependent attributes.
345
346    .. versionadded:: 0.9.0
347
348    :var impl: The :class:`.AttributeImpl` which is the current event
349     initiator.
350
351    :var op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE` or
352     :attr:`.OP_REPLACE`, indicating the source operation.
353
354    """
355
356    __slots__ = 'impl', 'op', 'parent_token'
357
358    def __init__(self, attribute_impl, op):
359        self.impl = attribute_impl
360        self.op = op
361        self.parent_token = self.impl.parent_token
362
363    def __eq__(self, other):
364        return isinstance(other, Event) and \
365            other.impl is self.impl and \
366            other.op == self.op
367
368    @property
369    def key(self):
370        return self.impl.key
371
372    def hasparent(self, state):
373        return self.impl.hasparent(state)
374
375
376class AttributeImpl(object):
377    """internal implementation for instrumented attributes."""
378
379    def __init__(self, class_, key,
380                 callable_, dispatch, trackparent=False, extension=None,
381                 compare_function=None, active_history=False,
382                 parent_token=None, expire_missing=True,
383                 send_modified_events=True,
384                 **kwargs):
385        r"""Construct an AttributeImpl.
386
387        \class_
388          associated class
389
390        key
391          string name of the attribute
392
393        \callable_
394          optional function which generates a callable based on a parent
395          instance, which produces the "default" values for a scalar or
396          collection attribute when it's first accessed, if not present
397          already.
398
399        trackparent
400          if True, attempt to track if an instance has a parent attached
401          to it via this attribute.
402
403        extension
404          a single or list of AttributeExtension object(s) which will
405          receive set/delete/append/remove/etc. events.  Deprecated.
406          The event package is now used.
407
408        compare_function
409          a function that compares two values which are normally
410          assignable to this attribute.
411
412        active_history
413          indicates that get_history() should always return the "old" value,
414          even if it means executing a lazy callable upon attribute change.
415
416        parent_token
417          Usually references the MapperProperty, used as a key for
418          the hasparent() function to identify an "owning" attribute.
419          Allows multiple AttributeImpls to all match a single
420          owner attribute.
421
422        expire_missing
423          if False, don't add an "expiry" callable to this attribute
424          during state.expire_attributes(None), if no value is present
425          for this key.
426
427        send_modified_events
428          if False, the InstanceState._modified_event method will have no
429          effect; this means the attribute will never show up as changed in a
430          history entry.
431        """
432        self.class_ = class_
433        self.key = key
434        self.callable_ = callable_
435        self.dispatch = dispatch
436        self.trackparent = trackparent
437        self.parent_token = parent_token or self
438        self.send_modified_events = send_modified_events
439        if compare_function is None:
440            self.is_equal = operator.eq
441        else:
442            self.is_equal = compare_function
443
444        # TODO: pass in the manager here
445        # instead of doing a lookup
446        attr = manager_of_class(class_)[key]
447
448        for ext in util.to_list(extension or []):
449            ext._adapt_listener(attr, ext)
450
451        if active_history:
452            self.dispatch._active_history = True
453
454        self.expire_missing = expire_missing
455
456    __slots__ = (
457        'class_', 'key', 'callable_', 'dispatch', 'trackparent',
458        'parent_token', 'send_modified_events', 'is_equal', 'expire_missing'
459    )
460
461    def __str__(self):
462        return "%s.%s" % (self.class_.__name__, self.key)
463
464    def _get_active_history(self):
465        """Backwards compat for impl.active_history"""
466
467        return self.dispatch._active_history
468
469    def _set_active_history(self, value):
470        self.dispatch._active_history = value
471
472    active_history = property(_get_active_history, _set_active_history)
473
474    def hasparent(self, state, optimistic=False):
475        """Return the boolean value of a `hasparent` flag attached to
476        the given state.
477
478        The `optimistic` flag determines what the default return value
479        should be if no `hasparent` flag can be located.
480
481        As this function is used to determine if an instance is an
482        *orphan*, instances that were loaded from storage should be
483        assumed to not be orphans, until a True/False value for this
484        flag is set.
485
486        An instance attribute that is loaded by a callable function
487        will also not have a `hasparent` flag.
488
489        """
490        msg = "This AttributeImpl is not configured to track parents."
491        assert self.trackparent, msg
492
493        return state.parents.get(id(self.parent_token), optimistic) \
494            is not False
495
496    def sethasparent(self, state, parent_state, value):
497        """Set a boolean flag on the given item corresponding to
498        whether or not it is attached to a parent object via the
499        attribute represented by this ``InstrumentedAttribute``.
500
501        """
502        msg = "This AttributeImpl is not configured to track parents."
503        assert self.trackparent, msg
504
505        id_ = id(self.parent_token)
506        if value:
507            state.parents[id_] = parent_state
508        else:
509            if id_ in state.parents:
510                last_parent = state.parents[id_]
511
512                if last_parent is not False and \
513                        last_parent.key != parent_state.key:
514
515                    if last_parent.obj() is None:
516                        raise orm_exc.StaleDataError(
517                            "Removing state %s from parent "
518                            "state %s along attribute '%s', "
519                            "but the parent record "
520                            "has gone stale, can't be sure this "
521                            "is the most recent parent." %
522                            (state_str(state),
523                             state_str(parent_state),
524                             self.key))
525
526                    return
527
528            state.parents[id_] = False
529
530    def get_history(self, state, dict_, passive=PASSIVE_OFF):
531        raise NotImplementedError()
532
533    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
534        """Return a list of tuples of (state, obj)
535        for all objects in this attribute's current state
536        + history.
537
538        Only applies to object-based attributes.
539
540        This is an inlining of existing functionality
541        which roughly corresponds to:
542
543            get_state_history(
544                        state,
545                        key,
546                        passive=PASSIVE_NO_INITIALIZE).sum()
547
548        """
549        raise NotImplementedError()
550
551    def initialize(self, state, dict_):
552        """Initialize the given state's attribute with an empty value."""
553
554        value = None
555        for fn in self.dispatch.init_scalar:
556            ret = fn(state, value, dict_)
557            if ret is not ATTR_EMPTY:
558                value = ret
559
560        return value
561
562    def get(self, state, dict_, passive=PASSIVE_OFF):
563        """Retrieve a value from the given object.
564        If a callable is assembled on this object's attribute, and
565        passive is False, the callable will be executed and the
566        resulting value will be set as the new value for this attribute.
567        """
568        if self.key in dict_:
569            return dict_[self.key]
570        else:
571            # if history present, don't load
572            key = self.key
573            if key not in state.committed_state or \
574                    state.committed_state[key] is NEVER_SET:
575                if not passive & CALLABLES_OK:
576                    return PASSIVE_NO_RESULT
577
578                if key in state.expired_attributes:
579                    value = state._load_expired(state, passive)
580                elif key in state.callables:
581                    callable_ = state.callables[key]
582                    value = callable_(state, passive)
583                elif self.callable_:
584                    value = self.callable_(state, passive)
585                else:
586                    value = ATTR_EMPTY
587
588                if value is PASSIVE_NO_RESULT or value is NEVER_SET:
589                    return value
590                elif value is ATTR_WAS_SET:
591                    try:
592                        return dict_[key]
593                    except KeyError:
594                        # TODO: no test coverage here.
595                        raise KeyError(
596                            "Deferred loader for attribute "
597                            "%r failed to populate "
598                            "correctly" % key)
599                elif value is not ATTR_EMPTY:
600                    return self.set_committed_value(state, dict_, value)
601
602            if not passive & INIT_OK:
603                return NEVER_SET
604            else:
605                # Return a new, empty value
606                return self.initialize(state, dict_)
607
608    def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
609        self.set(state, dict_, value, initiator, passive=passive)
610
611    def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
612        self.set(state, dict_, None, initiator,
613                 passive=passive, check_old=value)
614
615    def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
616        self.set(state, dict_, None, initiator,
617                 passive=passive, check_old=value, pop=True)
618
619    def set(self, state, dict_, value, initiator,
620            passive=PASSIVE_OFF, check_old=None, pop=False):
621        raise NotImplementedError()
622
623    def get_committed_value(self, state, dict_, passive=PASSIVE_OFF):
624        """return the unchanged value of this attribute"""
625
626        if self.key in state.committed_state:
627            value = state.committed_state[self.key]
628            if value in (NO_VALUE, NEVER_SET):
629                return None
630            else:
631                return value
632        else:
633            return self.get(state, dict_, passive=passive)
634
635    def set_committed_value(self, state, dict_, value):
636        """set an attribute value on the given instance and 'commit' it."""
637
638        dict_[self.key] = value
639        state._commit(dict_, [self.key])
640        return value
641
642
643class ScalarAttributeImpl(AttributeImpl):
644    """represents a scalar value-holding InstrumentedAttribute."""
645
646    accepts_scalar_loader = True
647    uses_objects = False
648    supports_population = True
649    collection = False
650
651    __slots__ = '_replace_token', '_append_token', '_remove_token'
652
653    def __init__(self, *arg, **kw):
654        super(ScalarAttributeImpl, self).__init__(*arg, **kw)
655        self._replace_token = self._append_token = None
656        self._remove_token = None
657
658    def _init_append_token(self):
659        self._replace_token = self._append_token = Event(self, OP_REPLACE)
660        return self._replace_token
661
662    _init_append_or_replace_token = _init_append_token
663
664    def _init_remove_token(self):
665        self._remove_token = Event(self, OP_REMOVE)
666        return self._remove_token
667
668    def delete(self, state, dict_):
669
670        # TODO: catch key errors, convert to attributeerror?
671        if self.dispatch._active_history:
672            old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
673        else:
674            old = dict_.get(self.key, NO_VALUE)
675
676        if self.dispatch.remove:
677            self.fire_remove_event(state, dict_, old, self._remove_token)
678        state._modified_event(dict_, self, old)
679        del dict_[self.key]
680
681    def get_history(self, state, dict_, passive=PASSIVE_OFF):
682        if self.key in dict_:
683            return History.from_scalar_attribute(self, state, dict_[self.key])
684        else:
685            if passive & INIT_OK:
686                passive ^= INIT_OK
687            current = self.get(state, dict_, passive=passive)
688            if current is PASSIVE_NO_RESULT:
689                return HISTORY_BLANK
690            else:
691                return History.from_scalar_attribute(self, state, current)
692
693    def set(self, state, dict_, value, initiator,
694            passive=PASSIVE_OFF, check_old=None, pop=False):
695        if self.dispatch._active_history:
696            old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
697        else:
698            old = dict_.get(self.key, NO_VALUE)
699
700        if self.dispatch.set:
701            value = self.fire_replace_event(state, dict_,
702                                            value, old, initiator)
703        state._modified_event(dict_, self, old)
704        dict_[self.key] = value
705
706    def fire_replace_event(self, state, dict_, value, previous, initiator):
707        for fn in self.dispatch.set:
708            value = fn(
709                state, value, previous,
710                initiator or self._replace_token or
711                self._init_append_or_replace_token())
712        return value
713
714    def fire_remove_event(self, state, dict_, value, initiator):
715        for fn in self.dispatch.remove:
716            fn(state, value,
717               initiator or self._remove_token or self._init_remove_token())
718
719    @property
720    def type(self):
721        self.property.columns[0].type
722
723
724class ScalarObjectAttributeImpl(ScalarAttributeImpl):
725    """represents a scalar-holding InstrumentedAttribute,
726       where the target object is also instrumented.
727
728       Adds events to delete/set operations.
729
730    """
731
732    accepts_scalar_loader = False
733    uses_objects = True
734    supports_population = True
735    collection = False
736
737    __slots__ = ()
738
739    def delete(self, state, dict_):
740        old = self.get(state, dict_)
741        self.fire_remove_event(
742            state, dict_, old,
743            self._remove_token or self._init_remove_token())
744        del dict_[self.key]
745
746    def get_history(self, state, dict_, passive=PASSIVE_OFF):
747        if self.key in dict_:
748            return History.from_object_attribute(self, state, dict_[self.key])
749        else:
750            if passive & INIT_OK:
751                passive ^= INIT_OK
752            current = self.get(state, dict_, passive=passive)
753            if current is PASSIVE_NO_RESULT:
754                return HISTORY_BLANK
755            else:
756                return History.from_object_attribute(self, state, current)
757
758    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
759        if self.key in dict_:
760            current = dict_[self.key]
761        elif passive & CALLABLES_OK:
762            current = self.get(state, dict_, passive=passive)
763        else:
764            return []
765
766        # can't use __hash__(), can't use __eq__() here
767        if current is not None and \
768                current is not PASSIVE_NO_RESULT and \
769                current is not NEVER_SET:
770            ret = [(instance_state(current), current)]
771        else:
772            ret = [(None, None)]
773
774        if self.key in state.committed_state:
775            original = state.committed_state[self.key]
776            if original is not None and \
777                    original is not PASSIVE_NO_RESULT and \
778                    original is not NEVER_SET and \
779                    original is not current:
780
781                ret.append((instance_state(original), original))
782        return ret
783
784    def set(self, state, dict_, value, initiator,
785            passive=PASSIVE_OFF, check_old=None, pop=False):
786        """Set a value on the given InstanceState.
787
788        """
789        if self.dispatch._active_history:
790            old = self.get(
791                state, dict_,
792                passive=PASSIVE_ONLY_PERSISTENT |
793                NO_AUTOFLUSH | LOAD_AGAINST_COMMITTED)
794        else:
795            old = self.get(
796                state, dict_, passive=PASSIVE_NO_FETCH ^ INIT_OK |
797                LOAD_AGAINST_COMMITTED)
798
799        if check_old is not None and \
800                old is not PASSIVE_NO_RESULT and \
801                check_old is not old:
802            if pop:
803                return
804            else:
805                raise ValueError(
806                    "Object %s not associated with %s on attribute '%s'" % (
807                        instance_str(check_old),
808                        state_str(state),
809                        self.key
810                    ))
811
812        value = self.fire_replace_event(state, dict_, value, old, initiator)
813        dict_[self.key] = value
814
815    def fire_remove_event(self, state, dict_, value, initiator):
816        if self.trackparent and value is not None:
817            self.sethasparent(instance_state(value), state, False)
818
819        for fn in self.dispatch.remove:
820            fn(state, value, initiator or
821               self._remove_token or self._init_remove_token())
822
823        state._modified_event(dict_, self, value)
824
825    def fire_replace_event(self, state, dict_, value, previous, initiator):
826        if self.trackparent:
827            if (previous is not value and
828                    previous not in (None, PASSIVE_NO_RESULT, NEVER_SET)):
829                self.sethasparent(instance_state(previous), state, False)
830
831        for fn in self.dispatch.set:
832            value = fn(
833                state, value, previous, initiator or
834                self._replace_token or self._init_append_or_replace_token())
835
836        state._modified_event(dict_, self, previous)
837
838        if self.trackparent:
839            if value is not None:
840                self.sethasparent(instance_state(value), state, True)
841
842        return value
843
844
845class CollectionAttributeImpl(AttributeImpl):
846    """A collection-holding attribute that instruments changes in membership.
847
848    Only handles collections of instrumented objects.
849
850    InstrumentedCollectionAttribute holds an arbitrary, user-specified
851    container object (defaulting to a list) and brokers access to the
852    CollectionAdapter, a "view" onto that object that presents consistent bag
853    semantics to the orm layer independent of the user data implementation.
854
855    """
856    accepts_scalar_loader = False
857    uses_objects = True
858    supports_population = True
859    collection = True
860
861    __slots__ = (
862        'copy', 'collection_factory', '_append_token', '_remove_token',
863        '_duck_typed_as'
864    )
865
866    def __init__(self, class_, key, callable_, dispatch,
867                 typecallable=None, trackparent=False, extension=None,
868                 copy_function=None, compare_function=None, **kwargs):
869        super(CollectionAttributeImpl, self).__init__(
870            class_,
871            key,
872            callable_, dispatch,
873            trackparent=trackparent,
874            extension=extension,
875            compare_function=compare_function,
876            **kwargs)
877
878        if copy_function is None:
879            copy_function = self.__copy
880        self.copy = copy_function
881        self.collection_factory = typecallable
882        self._append_token = None
883        self._remove_token = None
884        self._duck_typed_as = util.duck_type_collection(
885            self.collection_factory())
886
887        if getattr(self.collection_factory, "_sa_linker", None):
888
889            @event.listens_for(self, "init_collection")
890            def link(target, collection, collection_adapter):
891                collection._sa_linker(collection_adapter)
892
893            @event.listens_for(self, "dispose_collection")
894            def unlink(target, collection, collection_adapter):
895                collection._sa_linker(None)
896
897    def _init_append_token(self):
898        self._append_token = Event(self, OP_APPEND)
899        return self._append_token
900
901    def _init_remove_token(self):
902        self._remove_token = Event(self, OP_REMOVE)
903        return self._remove_token
904
905    def __copy(self, item):
906        return [y for y in collections.collection_adapter(item)]
907
908    def get_history(self, state, dict_, passive=PASSIVE_OFF):
909        current = self.get(state, dict_, passive=passive)
910        if current is PASSIVE_NO_RESULT:
911            return HISTORY_BLANK
912        else:
913            return History.from_collection(self, state, current)
914
915    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
916        # NOTE: passive is ignored here at the moment
917
918        if self.key not in dict_:
919            return []
920
921        current = dict_[self.key]
922        current = getattr(current, '_sa_adapter')
923
924        if self.key in state.committed_state:
925            original = state.committed_state[self.key]
926            if original not in (NO_VALUE, NEVER_SET):
927                current_states = [((c is not None) and
928                                   instance_state(c) or None, c)
929                                  for c in current]
930                original_states = [((c is not None) and
931                                    instance_state(c) or None, c)
932                                   for c in original]
933
934                current_set = dict(current_states)
935                original_set = dict(original_states)
936
937                return \
938                    [(s, o) for s, o in current_states
939                        if s not in original_set] + \
940                    [(s, o) for s, o in current_states
941                        if s in original_set] + \
942                    [(s, o) for s, o in original_states
943                        if s not in current_set]
944
945        return [(instance_state(o), o) for o in current]
946
947    def fire_append_event(self, state, dict_, value, initiator):
948        for fn in self.dispatch.append:
949            value = fn(
950                state, value,
951                initiator or self._append_token or self._init_append_token())
952
953        state._modified_event(dict_, self, NEVER_SET, True)
954
955        if self.trackparent and value is not None:
956            self.sethasparent(instance_state(value), state, True)
957
958        return value
959
960    def fire_pre_remove_event(self, state, dict_, initiator):
961        state._modified_event(dict_, self, NEVER_SET, True)
962
963    def fire_remove_event(self, state, dict_, value, initiator):
964        if self.trackparent and value is not None:
965            self.sethasparent(instance_state(value), state, False)
966
967        for fn in self.dispatch.remove:
968            fn(state, value,
969               initiator or self._remove_token or self._init_remove_token())
970
971        state._modified_event(dict_, self, NEVER_SET, True)
972
973    def delete(self, state, dict_):
974        if self.key not in dict_:
975            return
976
977        state._modified_event(dict_, self, NEVER_SET, True)
978
979        collection = self.get_collection(state, state.dict)
980        collection.clear_with_event()
981        # TODO: catch key errors, convert to attributeerror?
982        del dict_[self.key]
983
984    def initialize(self, state, dict_):
985        """Initialize this attribute with an empty collection."""
986
987        _, user_data = self._initialize_collection(state)
988        dict_[self.key] = user_data
989        return user_data
990
991    def _initialize_collection(self, state):
992
993        adapter, collection = state.manager.initialize_collection(
994            self.key, state, self.collection_factory)
995
996        self.dispatch.init_collection(state, collection, adapter)
997
998        return adapter, collection
999
1000    def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1001        collection = self.get_collection(state, dict_, passive=passive)
1002        if collection is PASSIVE_NO_RESULT:
1003            value = self.fire_append_event(state, dict_, value, initiator)
1004            assert self.key not in dict_, \
1005                "Collection was loaded during event handling."
1006            state._get_pending_mutation(self.key).append(value)
1007        else:
1008            collection.append_with_event(value, initiator)
1009
1010    def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1011        collection = self.get_collection(state, state.dict, passive=passive)
1012        if collection is PASSIVE_NO_RESULT:
1013            self.fire_remove_event(state, dict_, value, initiator)
1014            assert self.key not in dict_, \
1015                "Collection was loaded during event handling."
1016            state._get_pending_mutation(self.key).remove(value)
1017        else:
1018            collection.remove_with_event(value, initiator)
1019
1020    def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
1021        try:
1022            # TODO: better solution here would be to add
1023            # a "popper" role to collections.py to complement
1024            # "remover".
1025            self.remove(state, dict_, value, initiator, passive=passive)
1026        except (ValueError, KeyError, IndexError):
1027            pass
1028
1029    def set(self, state, dict_, value, initiator=None,
1030            passive=PASSIVE_OFF, pop=False, _adapt=True):
1031        iterable = orig_iterable = value
1032
1033        # pulling a new collection first so that an adaptation exception does
1034        # not trigger a lazy load of the old collection.
1035        new_collection, user_data = self._initialize_collection(state)
1036        if _adapt:
1037            if new_collection._converter is not None:
1038                iterable = new_collection._converter(iterable)
1039            else:
1040                setting_type = util.duck_type_collection(iterable)
1041                receiving_type = self._duck_typed_as
1042
1043                if setting_type is not receiving_type:
1044                    given = iterable is None and 'None' or \
1045                        iterable.__class__.__name__
1046                    wanted = self._duck_typed_as.__name__
1047                    raise TypeError(
1048                        "Incompatible collection type: %s is not %s-like" % (
1049                            given, wanted))
1050
1051                # If the object is an adapted collection, return the (iterable)
1052                # adapter.
1053                if hasattr(iterable, '_sa_iterator'):
1054                    iterable = iterable._sa_iterator()
1055                elif setting_type is dict:
1056                    if util.py3k:
1057                        iterable = iterable.values()
1058                    else:
1059                        iterable = getattr(
1060                            iterable, 'itervalues', iterable.values)()
1061                else:
1062                    iterable = iter(iterable)
1063        new_values = list(iterable)
1064
1065        old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
1066        if old is PASSIVE_NO_RESULT:
1067            old = self.initialize(state, dict_)
1068        elif old is orig_iterable:
1069            # ignore re-assignment of the current collection, as happens
1070            # implicitly with in-place operators (foo.collection |= other)
1071            return
1072
1073        # place a copy of "old" in state.committed_state
1074        state._modified_event(dict_, self, old, True)
1075
1076        old_collection = old._sa_adapter
1077
1078        dict_[self.key] = user_data
1079
1080        collections.bulk_replace(
1081            new_values, old_collection, new_collection)
1082
1083        del old._sa_adapter
1084        self.dispatch.dispose_collection(state, old, old_collection)
1085
1086    def _invalidate_collection(self, collection):
1087        adapter = getattr(collection, '_sa_adapter')
1088        adapter.invalidated = True
1089
1090    def set_committed_value(self, state, dict_, value):
1091        """Set an attribute value on the given instance and 'commit' it."""
1092
1093        collection, user_data = self._initialize_collection(state)
1094
1095        if value:
1096            collection.append_multiple_without_event(value)
1097
1098        state.dict[self.key] = user_data
1099
1100        state._commit(dict_, [self.key])
1101
1102        if self.key in state._pending_mutations:
1103            # pending items exist.  issue a modified event,
1104            # add/remove new items.
1105            state._modified_event(dict_, self, user_data, True)
1106
1107            pending = state._pending_mutations.pop(self.key)
1108            added = pending.added_items
1109            removed = pending.deleted_items
1110            for item in added:
1111                collection.append_without_event(item)
1112            for item in removed:
1113                collection.remove_without_event(item)
1114
1115        return user_data
1116
1117    def get_collection(self, state, dict_,
1118                       user_data=None, passive=PASSIVE_OFF):
1119        """Retrieve the CollectionAdapter associated with the given state.
1120
1121        Creates a new CollectionAdapter if one does not exist.
1122
1123        """
1124        if user_data is None:
1125            user_data = self.get(state, dict_, passive=passive)
1126            if user_data is PASSIVE_NO_RESULT:
1127                return user_data
1128
1129        return getattr(user_data, '_sa_adapter')
1130
1131
1132def backref_listeners(attribute, key, uselist):
1133    """Apply listeners to synchronize a two-way relationship."""
1134
1135    # use easily recognizable names for stack traces
1136
1137    parent_token = attribute.impl.parent_token
1138    parent_impl = attribute.impl
1139
1140    def _acceptable_key_err(child_state, initiator, child_impl):
1141        raise ValueError(
1142            "Bidirectional attribute conflict detected: "
1143            'Passing object %s to attribute "%s" '
1144            'triggers a modify event on attribute "%s" '
1145            'via the backref "%s".' % (
1146                state_str(child_state),
1147                initiator.parent_token,
1148                child_impl.parent_token,
1149                attribute.impl.parent_token
1150            )
1151        )
1152
1153    def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
1154        if oldchild is child:
1155            return child
1156        if oldchild is not None and \
1157                oldchild is not PASSIVE_NO_RESULT and \
1158                oldchild is not NEVER_SET:
1159            # With lazy=None, there's no guarantee that the full collection is
1160            # present when updating via a backref.
1161            old_state, old_dict = instance_state(oldchild),\
1162                instance_dict(oldchild)
1163            impl = old_state.manager[key].impl
1164
1165            if initiator.impl is not impl or \
1166                    initiator.op not in (OP_REPLACE, OP_REMOVE):
1167                impl.pop(old_state,
1168                         old_dict,
1169                         state.obj(),
1170                         parent_impl._append_token or
1171                            parent_impl._init_append_token(),
1172                         passive=PASSIVE_NO_FETCH)
1173
1174        if child is not None:
1175            child_state, child_dict = instance_state(child),\
1176                instance_dict(child)
1177            child_impl = child_state.manager[key].impl
1178            if initiator.parent_token is not parent_token and \
1179                    initiator.parent_token is not child_impl.parent_token:
1180                _acceptable_key_err(state, initiator, child_impl)
1181            elif initiator.impl is not child_impl or \
1182                    initiator.op not in (OP_APPEND, OP_REPLACE):
1183                child_impl.append(
1184                    child_state,
1185                    child_dict,
1186                    state.obj(),
1187                    initiator,
1188                    passive=PASSIVE_NO_FETCH)
1189        return child
1190
1191    def emit_backref_from_collection_append_event(state, child, initiator):
1192        if child is None:
1193            return
1194
1195        child_state, child_dict = instance_state(child), \
1196            instance_dict(child)
1197        child_impl = child_state.manager[key].impl
1198
1199        if initiator.parent_token is not parent_token and \
1200                initiator.parent_token is not child_impl.parent_token:
1201            _acceptable_key_err(state, initiator, child_impl)
1202        elif initiator.impl is not child_impl or \
1203                initiator.op not in (OP_APPEND, OP_REPLACE):
1204            child_impl.append(
1205                child_state,
1206                child_dict,
1207                state.obj(),
1208                initiator,
1209                passive=PASSIVE_NO_FETCH)
1210        return child
1211
1212    def emit_backref_from_collection_remove_event(state, child, initiator):
1213        if child is not None:
1214            child_state, child_dict = instance_state(child),\
1215                instance_dict(child)
1216            child_impl = child_state.manager[key].impl
1217            if initiator.impl is not child_impl or \
1218                    initiator.op not in (OP_REMOVE, OP_REPLACE):
1219                child_impl.pop(
1220                    child_state,
1221                    child_dict,
1222                    state.obj(),
1223                    initiator,
1224                    passive=PASSIVE_NO_FETCH)
1225
1226    if uselist:
1227        event.listen(attribute, "append",
1228                     emit_backref_from_collection_append_event,
1229                     retval=True, raw=True)
1230    else:
1231        event.listen(attribute, "set",
1232                     emit_backref_from_scalar_set_event,
1233                     retval=True, raw=True)
1234    # TODO: need coverage in test/orm/ of remove event
1235    event.listen(attribute, "remove",
1236                 emit_backref_from_collection_remove_event,
1237                 retval=True, raw=True)
1238
1239_NO_HISTORY = util.symbol('NO_HISTORY')
1240_NO_STATE_SYMBOLS = frozenset([
1241    id(PASSIVE_NO_RESULT),
1242    id(NO_VALUE),
1243    id(NEVER_SET)])
1244
1245History = util.namedtuple("History", [
1246    "added", "unchanged", "deleted"
1247])
1248
1249
1250class History(History):
1251    """A 3-tuple of added, unchanged and deleted values,
1252    representing the changes which have occurred on an instrumented
1253    attribute.
1254
1255    The easiest way to get a :class:`.History` object for a particular
1256    attribute on an object is to use the :func:`.inspect` function::
1257
1258        from sqlalchemy import inspect
1259
1260        hist = inspect(myobject).attrs.myattribute.history
1261
1262    Each tuple member is an iterable sequence:
1263
1264    * ``added`` - the collection of items added to the attribute (the first
1265      tuple element).
1266
1267    * ``unchanged`` - the collection of items that have not changed on the
1268      attribute (the second tuple element).
1269
1270    * ``deleted`` - the collection of items that have been removed from the
1271      attribute (the third tuple element).
1272
1273    """
1274
1275    def __bool__(self):
1276        return self != HISTORY_BLANK
1277    __nonzero__ = __bool__
1278
1279    def empty(self):
1280        """Return True if this :class:`.History` has no changes
1281        and no existing, unchanged state.
1282
1283        """
1284
1285        return not bool(
1286            (self.added or self.deleted)
1287            or self.unchanged
1288        )
1289
1290    def sum(self):
1291        """Return a collection of added + unchanged + deleted."""
1292
1293        return (self.added or []) +\
1294            (self.unchanged or []) +\
1295            (self.deleted or [])
1296
1297    def non_deleted(self):
1298        """Return a collection of added + unchanged."""
1299
1300        return (self.added or []) +\
1301            (self.unchanged or [])
1302
1303    def non_added(self):
1304        """Return a collection of unchanged + deleted."""
1305
1306        return (self.unchanged or []) +\
1307            (self.deleted or [])
1308
1309    def has_changes(self):
1310        """Return True if this :class:`.History` has changes."""
1311
1312        return bool(self.added or self.deleted)
1313
1314    def as_state(self):
1315        return History(
1316            [(c is not None)
1317             and instance_state(c) or None
1318             for c in self.added],
1319            [(c is not None)
1320             and instance_state(c) or None
1321             for c in self.unchanged],
1322            [(c is not None)
1323             and instance_state(c) or None
1324             for c in self.deleted],
1325        )
1326
1327    @classmethod
1328    def from_scalar_attribute(cls, attribute, state, current):
1329        original = state.committed_state.get(attribute.key, _NO_HISTORY)
1330
1331        if original is _NO_HISTORY:
1332            if current is NEVER_SET:
1333                return cls((), (), ())
1334            else:
1335                return cls((), [current], ())
1336        # don't let ClauseElement expressions here trip things up
1337        elif attribute.is_equal(current, original) is True:
1338            return cls((), [current], ())
1339        else:
1340            # current convention on native scalars is to not
1341            # include information
1342            # about missing previous value in "deleted", but
1343            # we do include None, which helps in some primary
1344            # key situations
1345            if id(original) in _NO_STATE_SYMBOLS:
1346                deleted = ()
1347            else:
1348                deleted = [original]
1349            if current is NEVER_SET:
1350                return cls((), (), deleted)
1351            else:
1352                return cls([current], (), deleted)
1353
1354    @classmethod
1355    def from_object_attribute(cls, attribute, state, current):
1356        original = state.committed_state.get(attribute.key, _NO_HISTORY)
1357
1358        if original is _NO_HISTORY:
1359            if current is NO_VALUE or current is NEVER_SET:
1360                return cls((), (), ())
1361            else:
1362                return cls((), [current], ())
1363        elif current is original:
1364            return cls((), [current], ())
1365        else:
1366            # current convention on related objects is to not
1367            # include information
1368            # about missing previous value in "deleted", and
1369            # to also not include None - the dependency.py rules
1370            # ignore the None in any case.
1371            if id(original) in _NO_STATE_SYMBOLS or original is None:
1372                deleted = ()
1373            else:
1374                deleted = [original]
1375            if current is NO_VALUE or current is NEVER_SET:
1376                return cls((), (), deleted)
1377            else:
1378                return cls([current], (), deleted)
1379
1380    @classmethod
1381    def from_collection(cls, attribute, state, current):
1382        original = state.committed_state.get(attribute.key, _NO_HISTORY)
1383
1384        if current is NO_VALUE or current is NEVER_SET:
1385            return cls((), (), ())
1386
1387        current = getattr(current, '_sa_adapter')
1388        if original in (NO_VALUE, NEVER_SET):
1389            return cls(list(current), (), ())
1390        elif original is _NO_HISTORY:
1391            return cls((), list(current), ())
1392        else:
1393
1394            current_states = [((c is not None) and instance_state(c)
1395                               or None, c)
1396                              for c in current
1397                              ]
1398            original_states = [((c is not None) and instance_state(c)
1399                                or None, c)
1400                               for c in original
1401                               ]
1402
1403            current_set = dict(current_states)
1404            original_set = dict(original_states)
1405
1406            return cls(
1407                [o for s, o in current_states if s not in original_set],
1408                [o for s, o in current_states if s in original_set],
1409                [o for s, o in original_states if s not in current_set]
1410            )
1411
1412HISTORY_BLANK = History(None, None, None)
1413
1414
1415def get_history(obj, key, passive=PASSIVE_OFF):
1416    """Return a :class:`.History` record for the given object
1417    and attribute key.
1418
1419    :param obj: an object whose class is instrumented by the
1420      attributes package.
1421
1422    :param key: string attribute name.
1423
1424    :param passive: indicates loading behavior for the attribute
1425       if the value is not already present.   This is a
1426       bitflag attribute, which defaults to the symbol
1427       :attr:`.PASSIVE_OFF` indicating all necessary SQL
1428       should be emitted.
1429
1430    """
1431    if passive is True:
1432        util.warn_deprecated("Passing True for 'passive' is deprecated. "
1433                             "Use attributes.PASSIVE_NO_INITIALIZE")
1434        passive = PASSIVE_NO_INITIALIZE
1435    elif passive is False:
1436        util.warn_deprecated("Passing False for 'passive' is "
1437                             "deprecated.  Use attributes.PASSIVE_OFF")
1438        passive = PASSIVE_OFF
1439
1440    return get_state_history(instance_state(obj), key, passive)
1441
1442
1443def get_state_history(state, key, passive=PASSIVE_OFF):
1444    return state.get_history(key, passive)
1445
1446
1447def has_parent(cls, obj, key, optimistic=False):
1448    """TODO"""
1449    manager = manager_of_class(cls)
1450    state = instance_state(obj)
1451    return manager.has_parent(state, key, optimistic)
1452
1453
1454def register_attribute(class_, key, **kw):
1455    comparator = kw.pop('comparator', None)
1456    parententity = kw.pop('parententity', None)
1457    doc = kw.pop('doc', None)
1458    desc = register_descriptor(class_, key,
1459                               comparator, parententity, doc=doc)
1460    register_attribute_impl(class_, key, **kw)
1461    return desc
1462
1463
1464def register_attribute_impl(class_, key,
1465                            uselist=False, callable_=None,
1466                            useobject=False,
1467                            impl_class=None, backref=None, **kw):
1468
1469    manager = manager_of_class(class_)
1470    if uselist:
1471        factory = kw.pop('typecallable', None)
1472        typecallable = manager.instrument_collection_class(
1473            key, factory or list)
1474    else:
1475        typecallable = kw.pop('typecallable', None)
1476
1477    dispatch = manager[key].dispatch
1478
1479    if impl_class:
1480        impl = impl_class(class_, key, typecallable, dispatch, **kw)
1481    elif uselist:
1482        impl = CollectionAttributeImpl(class_, key, callable_, dispatch,
1483                                       typecallable=typecallable, **kw)
1484    elif useobject:
1485        impl = ScalarObjectAttributeImpl(class_, key, callable_,
1486                                         dispatch, **kw)
1487    else:
1488        impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
1489
1490    manager[key].impl = impl
1491
1492    if backref:
1493        backref_listeners(manager[key], backref, uselist)
1494
1495    manager.post_configure_attribute(key)
1496    return manager[key]
1497
1498
1499def register_descriptor(class_, key, comparator=None,
1500                        parententity=None, doc=None):
1501    manager = manager_of_class(class_)
1502
1503    descriptor = InstrumentedAttribute(class_, key, comparator=comparator,
1504                                       parententity=parententity)
1505
1506    descriptor.__doc__ = doc
1507
1508    manager.instrument_attribute(key, descriptor)
1509    return descriptor
1510
1511
1512def unregister_attribute(class_, key):
1513    manager_of_class(class_).uninstrument_attribute(key)
1514
1515
1516def init_collection(obj, key):
1517    """Initialize a collection attribute and return the collection adapter.
1518
1519    This function is used to provide direct access to collection internals
1520    for a previously unloaded attribute.  e.g.::
1521
1522        collection_adapter = init_collection(someobject, 'elements')
1523        for elem in values:
1524            collection_adapter.append_without_event(elem)
1525
1526    For an easier way to do the above, see
1527    :func:`~sqlalchemy.orm.attributes.set_committed_value`.
1528
1529    obj is an instrumented object instance.  An InstanceState
1530    is accepted directly for backwards compatibility but
1531    this usage is deprecated.
1532
1533    """
1534    state = instance_state(obj)
1535    dict_ = state.dict
1536    return init_state_collection(state, dict_, key)
1537
1538
1539def init_state_collection(state, dict_, key):
1540    """Initialize a collection attribute and return the collection adapter."""
1541
1542    attr = state.manager[key].impl
1543    user_data = attr.initialize(state, dict_)
1544    return attr.get_collection(state, dict_, user_data)
1545
1546
1547def set_committed_value(instance, key, value):
1548    """Set the value of an attribute with no history events.
1549
1550    Cancels any previous history present.  The value should be
1551    a scalar value for scalar-holding attributes, or
1552    an iterable for any collection-holding attribute.
1553
1554    This is the same underlying method used when a lazy loader
1555    fires off and loads additional data from the database.
1556    In particular, this method can be used by application code
1557    which has loaded additional attributes or collections through
1558    separate queries, which can then be attached to an instance
1559    as though it were part of its original loaded state.
1560
1561    """
1562    state, dict_ = instance_state(instance), instance_dict(instance)
1563    state.manager[key].impl.set_committed_value(state, dict_, value)
1564
1565
1566def set_attribute(instance, key, value):
1567    """Set the value of an attribute, firing history events.
1568
1569    This function may be used regardless of instrumentation
1570    applied directly to the class, i.e. no descriptors are required.
1571    Custom attribute management schemes will need to make usage
1572    of this method to establish attribute state as understood
1573    by SQLAlchemy.
1574
1575    """
1576    state, dict_ = instance_state(instance), instance_dict(instance)
1577    state.manager[key].impl.set(state, dict_, value, None)
1578
1579
1580def get_attribute(instance, key):
1581    """Get the value of an attribute, firing any callables required.
1582
1583    This function may be used regardless of instrumentation
1584    applied directly to the class, i.e. no descriptors are required.
1585    Custom attribute management schemes will need to make usage
1586    of this method to make usage of attribute state as understood
1587    by SQLAlchemy.
1588
1589    """
1590    state, dict_ = instance_state(instance), instance_dict(instance)
1591    return state.manager[key].impl.get(state, dict_)
1592
1593
1594def del_attribute(instance, key):
1595    """Delete the value of an attribute, firing history events.
1596
1597    This function may be used regardless of instrumentation
1598    applied directly to the class, i.e. no descriptors are required.
1599    Custom attribute management schemes will need to make usage
1600    of this method to establish attribute state as understood
1601    by SQLAlchemy.
1602
1603    """
1604    state, dict_ = instance_state(instance), instance_dict(instance)
1605    state.manager[key].impl.delete(state, dict_)
1606
1607
1608def flag_modified(instance, key):
1609    """Mark an attribute on an instance as 'modified'.
1610
1611    This sets the 'modified' flag on the instance and
1612    establishes an unconditional change event for the given attribute.
1613
1614    """
1615    state, dict_ = instance_state(instance), instance_dict(instance)
1616    impl = state.manager[key].impl
1617    state._modified_event(dict_, impl, NO_VALUE, force=True)
1618