Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
state.py1169 linesDownload Raw Back to orm
1# orm/state.py
2# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""Defines instrumentation of instances.
9
10This module is usually not directly visible to user applications, but
11defines a large part of the ORM's interactivity.
12
13"""
14
15from __future__ import annotations
16
17from typing import Any
18from typing import Callable
19from typing import Dict
20from typing import Generic
21from typing import Iterable
22from typing import Optional
23from typing import Set
24from typing import Tuple
25from typing import TYPE_CHECKING
26from typing import Union
27import weakref
28
29from . import base
30from . import exc as orm_exc
31from . import interfaces
32from ._typing import _O
33from ._typing import is_collection_impl
34from .base import ATTR_WAS_SET
35from .base import INIT_OK
36from .base import LoaderCallableStatus
37from .base import NEVER_SET
38from .base import NO_VALUE
39from .base import PASSIVE_NO_INITIALIZE
40from .base import PASSIVE_NO_RESULT
41from .base import PASSIVE_OFF
42from .base import SQL_OK
43from .path_registry import PathRegistry
44from .. import exc as sa_exc
45from .. import inspection
46from .. import util
47from ..util.typing import Literal
48from ..util.typing import Protocol
49
50if TYPE_CHECKING:
51    from ._typing import _IdentityKeyType
52    from ._typing import _InstanceDict
53    from ._typing import _LoaderCallable
54    from .attributes import AttributeImpl
55    from .attributes import History
56    from .base import PassiveFlag
57    from .collections import _AdaptedCollectionProtocol
58    from .identity import IdentityMap
59    from .instrumentation import ClassManager
60    from .interfaces import ORMOption
61    from .mapper import Mapper
62    from .session import Session
63    from ..engine import Row
64    from ..ext.asyncio.session import async_session as _async_provider
65    from ..ext.asyncio.session import AsyncSession
66
67if TYPE_CHECKING:
68    _sessions: weakref.WeakValueDictionary[int, Session]
69else:
70    # late-populated by session.py
71    _sessions = None
72
73
74if not TYPE_CHECKING:
75    # optionally late-provided by sqlalchemy.ext.asyncio.session
76
77    _async_provider = None  # noqa
78
79
80class _InstanceDictProto(Protocol):
81    def __call__(self) -> Optional[IdentityMap]: ...
82
83
84class _InstallLoaderCallableProto(Protocol[_O]):
85    """used at result loading time to install a _LoaderCallable callable
86    upon a specific InstanceState, which will be used to populate an
87    attribute when that attribute is accessed.
88
89    Concrete examples are per-instance deferred column loaders and
90    relationship lazy loaders.
91
92    """
93
94    def __call__(
95        self, state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
96    ) -> None: ...
97
98
99@inspection._self_inspects
100class InstanceState(interfaces.InspectionAttrInfo, Generic[_O]):
101    """Tracks state information at the instance level.
102
103    The :class:`.InstanceState` is a key object used by the
104    SQLAlchemy ORM in order to track the state of an object;
105    it is created the moment an object is instantiated, typically
106    as a result of :term:`instrumentation` which SQLAlchemy applies
107    to the ``__init__()`` method of the class.
108
109    :class:`.InstanceState` is also a semi-public object,
110    available for runtime inspection as to the state of a
111    mapped instance, including information such as its current
112    status within a particular :class:`.Session` and details
113    about data on individual attributes.  The public API
114    in order to acquire a :class:`.InstanceState` object
115    is to use the :func:`_sa.inspect` system::
116
117        >>> from sqlalchemy import inspect
118        >>> insp = inspect(some_mapped_object)
119        >>> insp.attrs.nickname.history
120        History(added=['new nickname'], unchanged=(), deleted=['nickname'])
121
122    .. seealso::
123
124        :ref:`orm_mapper_inspection_instancestate`
125
126    """
127
128    __slots__ = (
129        "__dict__",
130        "__weakref__",
131        "class_",
132        "manager",
133        "obj",
134        "committed_state",
135        "expired_attributes",
136    )
137
138    manager: ClassManager[_O]
139    session_id: Optional[int] = None
140    key: Optional[_IdentityKeyType[_O]] = None
141    runid: Optional[int] = None
142    load_options: Tuple[ORMOption, ...] = ()
143    load_path: PathRegistry = PathRegistry.root
144    insert_order: Optional[int] = None
145    _strong_obj: Optional[object] = None
146    obj: weakref.ref[_O]
147
148    committed_state: Dict[str, Any]
149
150    modified: bool = False
151    """When ``True`` the object was modified."""
152    expired: bool = False
153    """When ``True`` the object is :term:`expired`.
154
155    .. seealso::
156
157        :ref:`session_expire`
158    """
159    _deleted: bool = False
160    _load_pending: bool = False
161    _orphaned_outside_of_session: bool = False
162    is_instance: bool = True
163    identity_token: object = None
164    _last_known_values: Optional[Dict[str, Any]] = None
165
166    _instance_dict: _InstanceDictProto
167    """A weak reference, or in the default case a plain callable, that
168    returns a reference to the current :class:`.IdentityMap`, if any.
169
170    """
171    if not TYPE_CHECKING:
172
173        def _instance_dict(self):
174            """default 'weak reference' for _instance_dict"""
175            return None
176
177    expired_attributes: Set[str]
178    """The set of keys which are 'expired' to be loaded by
179    the manager's deferred scalar loader, assuming no pending
180    changes.
181
182    See also the ``unmodified`` collection which is intersected
183    against this set when a refresh operation occurs.
184    """
185
186    callables: Dict[str, Callable[[InstanceState[_O], PassiveFlag], Any]]
187    """A namespace where a per-state loader callable can be associated.
188
189    In SQLAlchemy 1.0, this is only used for lazy loaders / deferred
190    loaders that were set up via query option.
191
192    Previously, callables was used also to indicate expired attributes
193    by storing a link to the InstanceState itself in this dictionary.
194    This role is now handled by the expired_attributes set.
195
196    """
197
198    if not TYPE_CHECKING:
199        callables = util.EMPTY_DICT
200
201    def __init__(self, obj: _O, manager: ClassManager[_O]):
202        self.class_ = obj.__class__
203        self.manager = manager
204        self.obj = weakref.ref(obj, self._cleanup)
205        self.committed_state = {}
206        self.expired_attributes = set()
207
208    @util.memoized_property
209    def attrs(self) -> util.ReadOnlyProperties[AttributeState]:
210        """Return a namespace representing each attribute on
211        the mapped object, including its current value
212        and history.
213
214        The returned object is an instance of :class:`.AttributeState`.
215        This object allows inspection of the current data
216        within an attribute as well as attribute history
217        since the last flush.
218
219        """
220        return util.ReadOnlyProperties(
221            {key: AttributeState(self, key) for key in self.manager}
222        )
223
224    @property
225    def transient(self) -> bool:
226        """Return ``True`` if the object is :term:`transient`.
227
228        .. seealso::
229
230            :ref:`session_object_states`
231
232        """
233        return self.key is None and not self._attached
234
235    @property
236    def pending(self) -> bool:
237        """Return ``True`` if the object is :term:`pending`.
238
239        .. seealso::
240
241            :ref:`session_object_states`
242
243        """
244        return self.key is None and self._attached
245
246    @property
247    def deleted(self) -> bool:
248        """Return ``True`` if the object is :term:`deleted`.
249
250        An object that is in the deleted state is guaranteed to
251        not be within the :attr:`.Session.identity_map` of its parent
252        :class:`.Session`; however if the session's transaction is rolled
253        back, the object will be restored to the persistent state and
254        the identity map.
255
256        .. note::
257
258            The :attr:`.InstanceState.deleted` attribute refers to a specific
259            state of the object that occurs between the "persistent" and
260            "detached" states; once the object is :term:`detached`, the
261            :attr:`.InstanceState.deleted` attribute **no longer returns
262            True**; in order to detect that a state was deleted, regardless
263            of whether or not the object is associated with a
264            :class:`.Session`, use the :attr:`.InstanceState.was_deleted`
265            accessor.
266
267        .. versionadded: 1.1
268
269        .. seealso::
270
271            :ref:`session_object_states`
272
273        """
274        return self.key is not None and self._attached and self._deleted
275
276    @property
277    def was_deleted(self) -> bool:
278        """Return True if this object is or was previously in the
279        "deleted" state and has not been reverted to persistent.
280
281        This flag returns True once the object was deleted in flush.
282        When the object is expunged from the session either explicitly
283        or via transaction commit and enters the "detached" state,
284        this flag will continue to report True.
285
286        .. seealso::
287
288            :attr:`.InstanceState.deleted` - refers to the "deleted" state
289
290            :func:`.orm.util.was_deleted` - standalone function
291
292            :ref:`session_object_states`
293
294        """
295        return self._deleted
296
297    @property
298    def persistent(self) -> bool:
299        """Return ``True`` if the object is :term:`persistent`.
300
301        An object that is in the persistent state is guaranteed to
302        be within the :attr:`.Session.identity_map` of its parent
303        :class:`.Session`.
304
305        .. seealso::
306
307            :ref:`session_object_states`
308
309        """
310        return self.key is not None and self._attached and not self._deleted
311
312    @property
313    def detached(self) -> bool:
314        """Return ``True`` if the object is :term:`detached`.
315
316        .. seealso::
317
318            :ref:`session_object_states`
319
320        """
321        return self.key is not None and not self._attached
322
323    @util.non_memoized_property
324    @util.preload_module("sqlalchemy.orm.session")
325    def _attached(self) -> bool:
326        return (
327            self.session_id is not None
328            and self.session_id in util.preloaded.orm_session._sessions
329        )
330
331    def _track_last_known_value(self, key: str) -> None:
332        """Track the last known value of a particular key after expiration
333        operations.
334
335        .. versionadded:: 1.3
336
337        """
338
339        lkv = self._last_known_values
340        if lkv is None:
341            self._last_known_values = lkv = {}
342        if key not in lkv:
343            lkv[key] = NO_VALUE
344
345    @property
346    def session(self) -> Optional[Session]:
347        """Return the owning :class:`.Session` for this instance,
348        or ``None`` if none available.
349
350        Note that the result here can in some cases be *different*
351        from that of ``obj in session``; an object that's been deleted
352        will report as not ``in session``, however if the transaction is
353        still in progress, this attribute will still refer to that session.
354        Only when the transaction is completed does the object become
355        fully detached under normal circumstances.
356
357        .. seealso::
358
359            :attr:`_orm.InstanceState.async_session`
360
361        """
362        if self.session_id:
363            try:
364                return _sessions[self.session_id]
365            except KeyError:
366                pass
367        return None
368
369    @property
370    def async_session(self) -> Optional[AsyncSession]:
371        """Return the owning :class:`_asyncio.AsyncSession` for this instance,
372        or ``None`` if none available.
373
374        This attribute is only non-None when the :mod:`sqlalchemy.ext.asyncio`
375        API is in use for this ORM object. The returned
376        :class:`_asyncio.AsyncSession` object will be a proxy for the
377        :class:`_orm.Session` object that would be returned from the
378        :attr:`_orm.InstanceState.session` attribute for this
379        :class:`_orm.InstanceState`.
380
381        .. versionadded:: 1.4.18
382
383        .. seealso::
384
385            :ref:`asyncio_toplevel`
386
387        """
388        if _async_provider is None:
389            return None
390
391        sess = self.session
392        if sess is not None:
393            return _async_provider(sess)
394        else:
395            return None
396
397    @property
398    def object(self) -> Optional[_O]:
399        """Return the mapped object represented by this
400        :class:`.InstanceState`.
401
402        Returns None if the object has been garbage collected
403
404        """
405        return self.obj()
406
407    @property
408    def identity(self) -> Optional[Tuple[Any, ...]]:
409        """Return the mapped identity of the mapped object.
410        This is the primary key identity as persisted by the ORM
411        which can always be passed directly to
412        :meth:`_query.Query.get`.
413
414        Returns ``None`` if the object has no primary key identity.
415
416        .. note::
417            An object which is :term:`transient` or :term:`pending`
418            does **not** have a mapped identity until it is flushed,
419            even if its attributes include primary key values.
420
421        """
422        if self.key is None:
423            return None
424        else:
425            return self.key[1]
426
427    @property
428    def identity_key(self) -> Optional[_IdentityKeyType[_O]]:
429        """Return the identity key for the mapped object.
430
431        This is the key used to locate the object within
432        the :attr:`.Session.identity_map` mapping.   It contains
433        the identity as returned by :attr:`.identity` within it.
434
435
436        """
437        return self.key
438
439    @util.memoized_property
440    def parents(self) -> Dict[int, Union[Literal[False], InstanceState[Any]]]:
441        return {}
442
443    @util.memoized_property
444    def _pending_mutations(self) -> Dict[str, PendingCollection]:
445        return {}
446
447    @util.memoized_property
448    def _empty_collections(self) -> Dict[str, _AdaptedCollectionProtocol]:
449        return {}
450
451    @util.memoized_property
452    def mapper(self) -> Mapper[_O]:
453        """Return the :class:`_orm.Mapper` used for this mapped object."""
454        return self.manager.mapper
455
456    @property
457    def has_identity(self) -> bool:
458        """Return ``True`` if this object has an identity key.
459
460        This should always have the same value as the
461        expression ``state.persistent`` or ``state.detached``.
462
463        """
464        return bool(self.key)
465
466    @classmethod
467    def _detach_states(
468        self,
469        states: Iterable[InstanceState[_O]],
470        session: Session,
471        to_transient: bool = False,
472    ) -> None:
473        persistent_to_detached = (
474            session.dispatch.persistent_to_detached or None
475        )
476        deleted_to_detached = session.dispatch.deleted_to_detached or None
477        pending_to_transient = session.dispatch.pending_to_transient or None
478        persistent_to_transient = (
479            session.dispatch.persistent_to_transient or None
480        )
481
482        for state in states:
483            deleted = state._deleted
484            pending = state.key is None
485            persistent = not pending and not deleted
486
487            state.session_id = None
488
489            if to_transient and state.key:
490                del state.key
491            if persistent:
492                if to_transient:
493                    if persistent_to_transient is not None:
494                        persistent_to_transient(session, state)
495                elif persistent_to_detached is not None:
496                    persistent_to_detached(session, state)
497            elif deleted and deleted_to_detached is not None:
498                deleted_to_detached(session, state)
499            elif pending and pending_to_transient is not None:
500                pending_to_transient(session, state)
501
502            state._strong_obj = None
503
504    def _detach(self, session: Optional[Session] = None) -> None:
505        if session:
506            InstanceState._detach_states([self], session)
507        else:
508            self.session_id = self._strong_obj = None
509
510    def _dispose(self) -> None:
511        # used by the test suite, apparently
512        self._detach()
513
514    def _force_dereference(self) -> None:
515        """Force this InstanceState to act as though its weakref has
516        been GC'ed.
517
518        this is used for test code that has to test reactions to objects
519        being GC'ed.  We can't reliably force GCs to happen under all
520        CI circumstances.
521
522        """
523
524        # if _strong_obj is set, then our object would not be getting
525        # GC'ed (at least within the scope of what we use this for in tests).
526        # so make sure this is not set
527        assert self._strong_obj is None
528
529        obj = self.obj()
530        if obj is None:
531            # object was GC'ed and we're done!  woop
532            return
533
534        del obj
535
536        self._cleanup(self.obj)
537        self.obj = lambda: None  # type: ignore
538
539    def _cleanup(self, ref: weakref.ref[_O]) -> None:
540        """Weakref callback cleanup.
541
542        This callable cleans out the state when it is being garbage
543        collected.
544
545        this _cleanup **assumes** that there are no strong refs to us!
546        Will not work otherwise!
547
548        """
549
550        # Python builtins become undefined during interpreter shutdown.
551        # Guard against exceptions during this phase, as the method cannot
552        # proceed in any case if builtins have been undefined.
553        if dict is None:
554            return
555
556        instance_dict = self._instance_dict()
557        if instance_dict is not None:
558            instance_dict._fast_discard(self)
559            del self._instance_dict
560
561            # we can't possibly be in instance_dict._modified
562            # b.c. this is weakref cleanup only, that set
563            # is strong referencing!
564            # assert self not in instance_dict._modified
565
566        self.session_id = self._strong_obj = None
567
568    @property
569    def dict(self) -> _InstanceDict:
570        """Return the instance dict used by the object.
571
572        Under normal circumstances, this is always synonymous
573        with the ``__dict__`` attribute of the mapped object,
574        unless an alternative instrumentation system has been
575        configured.
576
577        In the case that the actual object has been garbage
578        collected, this accessor returns a blank dictionary.
579
580        """
581        o = self.obj()
582        if o is not None:
583            return base.instance_dict(o)
584        else:
585            return {}
586
587    def _initialize_instance(*mixed: Any, **kwargs: Any) -> None:
588        self, instance, args = mixed[0], mixed[1], mixed[2:]  # noqa
589        manager = self.manager
590
591        manager.dispatch.init(self, args, kwargs)
592
593        try:
594            manager.original_init(*mixed[1:], **kwargs)
595        except:
596            with util.safe_reraise():
597                manager.dispatch.init_failure(self, args, kwargs)
598
599    def get_history(self, key: str, passive: PassiveFlag) -> History:
600        return self.manager[key].impl.get_history(self, self.dict, passive)
601
602    def get_impl(self, key: str) -> AttributeImpl:
603        return self.manager[key].impl
604
605    def _get_pending_mutation(self, key: str) -> PendingCollection:
606        if key not in self._pending_mutations:
607            self._pending_mutations[key] = PendingCollection()
608        return self._pending_mutations[key]
609
610    def __getstate__(self) -> Dict[str, Any]:
611        state_dict: Dict[str, Any] = {
612            "instance": self.obj(),
613            "class_": self.class_,
614            "committed_state": self.committed_state,
615            "expired_attributes": self.expired_attributes,
616        }
617        state_dict.update(
618            (k, self.__dict__[k])
619            for k in (
620                "_pending_mutations",
621                "modified",
622                "expired",
623                "callables",
624                "key",
625                "parents",
626                "load_options",
627                "class_",
628                "expired_attributes",
629                "info",
630            )
631            if k in self.__dict__
632        )
633        if self.load_path:
634            state_dict["load_path"] = self.load_path.serialize()
635
636        state_dict["manager"] = self.manager._serialize(self, state_dict)
637
638        return state_dict
639
640    def __setstate__(self, state_dict: Dict[str, Any]) -> None:
641        inst = state_dict["instance"]
642        if inst is not None:
643            self.obj = weakref.ref(inst, self._cleanup)
644            self.class_ = inst.__class__
645        else:
646            self.obj = lambda: None  # type: ignore
647            self.class_ = state_dict["class_"]
648
649        self.committed_state = state_dict.get("committed_state", {})
650        self._pending_mutations = state_dict.get("_pending_mutations", {})
651        self.parents = state_dict.get("parents", {})
652        self.modified = state_dict.get("modified", False)
653        self.expired = state_dict.get("expired", False)
654        if "info" in state_dict:
655            self.info.update(state_dict["info"])
656        if "callables" in state_dict:
657            self.callables = state_dict["callables"]
658
659            self.expired_attributes = state_dict["expired_attributes"]
660        else:
661            if "expired_attributes" in state_dict:
662                self.expired_attributes = state_dict["expired_attributes"]
663            else:
664                self.expired_attributes = set()
665
666        self.__dict__.update(
667            [
668                (k, state_dict[k])
669                for k in ("key", "load_options")
670                if k in state_dict
671            ]
672        )
673        if self.key:
674            self.identity_token = self.key[2]
675
676        if "load_path" in state_dict:
677            self.load_path = PathRegistry.deserialize(state_dict["load_path"])
678
679        state_dict["manager"](self, inst, state_dict)
680
681    def _reset(self, dict_: _InstanceDict, key: str) -> None:
682        """Remove the given attribute and any
683        callables associated with it."""
684
685        old = dict_.pop(key, None)
686        manager_impl = self.manager[key].impl
687        if old is not None and is_collection_impl(manager_impl):
688            manager_impl._invalidate_collection(old)
689        self.expired_attributes.discard(key)
690        if self.callables:
691            self.callables.pop(key, None)
692
693    def _copy_callables(self, from_: InstanceState[Any]) -> None:
694        if "callables" in from_.__dict__:
695            self.callables = dict(from_.callables)
696
697    @classmethod
698    def _instance_level_callable_processor(
699        cls, manager: ClassManager[_O], fn: _LoaderCallable, key: Any
700    ) -> _InstallLoaderCallableProto[_O]:
701        impl = manager[key].impl
702        if is_collection_impl(impl):
703            fixed_impl = impl
704
705            def _set_callable(
706                state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
707            ) -> None:
708                if "callables" not in state.__dict__:
709                    state.callables = {}
710                old = dict_.pop(key, None)
711                if old is not None:
712                    fixed_impl._invalidate_collection(old)
713                state.callables[key] = fn
714
715        else:
716
717            def _set_callable(
718                state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
719            ) -> None:
720                if "callables" not in state.__dict__:
721                    state.callables = {}
722                state.callables[key] = fn
723
724        return _set_callable
725
726    def _expire(
727        self, dict_: _InstanceDict, modified_set: Set[InstanceState[Any]]
728    ) -> None:
729        self.expired = True
730        if self.modified:
731            modified_set.discard(self)
732            self.committed_state.clear()
733            self.modified = False
734
735        self._strong_obj = None
736
737        if "_pending_mutations" in self.__dict__:
738            del self.__dict__["_pending_mutations"]
739
740        if "parents" in self.__dict__:
741            del self.__dict__["parents"]
742
743        self.expired_attributes.update(
744            [impl.key for impl in self.manager._loader_impls]
745        )
746
747        if self.callables:
748            # the per state loader callables we can remove here are
749            # LoadDeferredColumns, which undefers a column at the instance
750            # level that is mapped with deferred, and LoadLazyAttribute,
751            # which lazy loads a relationship at the instance level that
752            # is mapped with "noload" or perhaps "immediateload".
753            # Before 1.4, only column-based
754            # attributes could be considered to be "expired", so here they
755            # were the only ones "unexpired", which means to make them deferred
756            # again.   For the moment, as of 1.4 we also apply the same
757            # treatment relationships now, that is, an instance level lazy
758            # loader is reset in the same way as a column loader.
759            for k in self.expired_attributes.intersection(self.callables):
760                del self.callables[k]
761
762        for k in self.manager._collection_impl_keys.intersection(dict_):
763            collection = dict_.pop(k)
764            collection._sa_adapter.invalidated = True
765
766        if self._last_known_values:
767            self._last_known_values.update(
768                {k: dict_[k] for k in self._last_known_values if k in dict_}
769            )
770
771        for key in self.manager._all_key_set.intersection(dict_):
772            del dict_[key]
773
774        self.manager.dispatch.expire(self, None)
775
776    def _expire_attributes(
777        self,
778        dict_: _InstanceDict,
779        attribute_names: Iterable[str],
780        no_loader: bool = False,
781    ) -> None:
782        pending = self.__dict__.get("_pending_mutations", None)
783
784        callables = self.callables
785
786        for key in attribute_names:
787            impl = self.manager[key].impl
788            if impl.accepts_scalar_loader:
789                if no_loader and (impl.callable_ or key in callables):
790                    continue
791
792                self.expired_attributes.add(key)
793                if callables and key in callables:
794                    del callables[key]
795            old = dict_.pop(key, NO_VALUE)
796            if is_collection_impl(impl) and old is not NO_VALUE:
797                impl._invalidate_collection(old)
798
799            lkv = self._last_known_values
800            if lkv is not None and key in lkv and old is not NO_VALUE:
801                lkv[key] = old
802
803            self.committed_state.pop(key, None)
804            if pending:
805                pending.pop(key, None)
806
807        self.manager.dispatch.expire(self, attribute_names)
808
809    def _load_expired(
810        self, state: InstanceState[_O], passive: PassiveFlag
811    ) -> LoaderCallableStatus:
812        """__call__ allows the InstanceState to act as a deferred
813        callable for loading expired attributes, which is also
814        serializable (picklable).
815
816        """
817
818        if not passive & SQL_OK:
819            return PASSIVE_NO_RESULT
820
821        toload = self.expired_attributes.intersection(self.unmodified)
822        toload = toload.difference(
823            attr
824            for attr in toload
825            if not self.manager[attr].impl.load_on_unexpire
826        )
827
828        self.manager.expired_attribute_loader(self, toload, passive)
829
830        # if the loader failed, or this
831        # instance state didn't have an identity,
832        # the attributes still might be in the callables
833        # dict.  ensure they are removed.
834        self.expired_attributes.clear()
835
836        return ATTR_WAS_SET
837
838    @property
839    def unmodified(self) -> Set[str]:
840        """Return the set of keys which have no uncommitted changes"""
841
842        return set(self.manager).difference(self.committed_state)
843
844    def unmodified_intersection(self, keys: Iterable[str]) -> Set[str]:
845        """Return self.unmodified.intersection(keys)."""
846
847        return (
848            set(keys)
849            .intersection(self.manager)
850            .difference(self.committed_state)
851        )
852
853    @property
854    def unloaded(self) -> Set[str]:
855        """Return the set of keys which do not have a loaded value.
856
857        This includes expired attributes and any other attribute that was never
858        populated or modified.
859
860        """
861        return (
862            set(self.manager)
863            .difference(self.committed_state)
864            .difference(self.dict)
865        )
866
867    @property
868    @util.deprecated(
869        "2.0",
870        "The :attr:`.InstanceState.unloaded_expirable` attribute is "
871        "deprecated.  Please use :attr:`.InstanceState.unloaded`.",
872    )
873    def unloaded_expirable(self) -> Set[str]:
874        """Synonymous with :attr:`.InstanceState.unloaded`.
875
876        This attribute was added as an implementation-specific detail at some
877        point and should be considered to be private.
878
879        """
880        return self.unloaded
881
882    @property
883    def _unloaded_non_object(self) -> Set[str]:
884        return self.unloaded.intersection(
885            attr
886            for attr in self.manager
887            if self.manager[attr].impl.accepts_scalar_loader
888        )
889
890    def _modified_event(
891        self,
892        dict_: _InstanceDict,
893        attr: Optional[AttributeImpl],
894        previous: Any,
895        collection: bool = False,
896        is_userland: bool = False,
897    ) -> None:
898        if attr:
899            if not attr.send_modified_events:
900                return
901            if is_userland and attr.key not in dict_:
902                raise sa_exc.InvalidRequestError(
903                    "Can't flag attribute '%s' modified; it's not present in "
904                    "the object state" % attr.key
905                )
906            if attr.key not in self.committed_state or is_userland:
907                if collection:
908                    if TYPE_CHECKING:
909                        assert is_collection_impl(attr)
910                    if previous is NEVER_SET:
911                        if attr.key in dict_:
912                            previous = dict_[attr.key]
913
914                    if previous not in (None, NO_VALUE, NEVER_SET):
915                        previous = attr.copy(previous)
916                self.committed_state[attr.key] = previous
917
918            lkv = self._last_known_values
919            if lkv is not None and attr.key in lkv:
920                lkv[attr.key] = NO_VALUE
921
922        # assert self._strong_obj is None or self.modified
923
924        if (self.session_id and self._strong_obj is None) or not self.modified:
925            self.modified = True
926            instance_dict = self._instance_dict()
927            if instance_dict:
928                has_modified = bool(instance_dict._modified)
929                instance_dict._modified.add(self)
930            else:
931                has_modified = False
932
933            # only create _strong_obj link if attached
934            # to a session
935
936            inst = self.obj()
937            if self.session_id:
938                self._strong_obj = inst
939
940                # if identity map already had modified objects,
941                # assume autobegin already occurred, else check
942                # for autobegin
943                if not has_modified:
944                    # inline of autobegin, to ensure session transaction
945                    # snapshot is established
946                    try:
947                        session = _sessions[self.session_id]
948                    except KeyError:
949                        pass
950                    else:
951                        if session._transaction is None:
952                            session._autobegin_t()
953
954            if inst is None and attr:
955                raise orm_exc.ObjectDereferencedError(
956                    "Can't emit change event for attribute '%s' - "
957                    "parent object of type %s has been garbage "
958                    "collected."
959                    % (self.manager[attr.key], base.state_class_str(self))
960                )
961
962    def _commit(self, dict_: _InstanceDict, keys: Iterable[str]) -> None:
963        """Commit attributes.
964
965        This is used by a partial-attribute load operation to mark committed
966        those attributes which were refreshed from the database.
967
968        Attributes marked as "expired" can potentially remain "expired" after
969        this step if a value was not populated in state.dict.
970
971        """
972        for key in keys:
973            self.committed_state.pop(key, None)
974
975        self.expired = False
976
977        self.expired_attributes.difference_update(
978            set(keys).intersection(dict_)
979        )
980
981        # the per-keys commit removes object-level callables,
982        # while that of commit_all does not.  it's not clear
983        # if this behavior has a clear rationale, however tests do
984        # ensure this is what it does.
985        if self.callables:
986            for key in (
987                set(self.callables).intersection(keys).intersection(dict_)
988            ):
989                del self.callables[key]
990
991    def _commit_all(
992        self, dict_: _InstanceDict, instance_dict: Optional[IdentityMap] = None
993    ) -> None:
994        """commit all attributes unconditionally.
995
996        This is used after a flush() or a full load/refresh
997        to remove all pending state from the instance.
998
999         - all attributes are marked as "committed"
1000         - the "strong dirty reference" is removed
1001         - the "modified" flag is set to False
1002         - any "expired" markers for scalar attributes loaded are removed.
1003         - lazy load callables for objects / collections *stay*
1004
1005        Attributes marked as "expired" can potentially remain
1006        "expired" after this step if a value was not populated in state.dict.
1007
1008        """
1009        self._commit_all_states([(self, dict_)], instance_dict)
1010
1011    @classmethod
1012    def _commit_all_states(
1013        self,
1014        iter_: Iterable[Tuple[InstanceState[Any], _InstanceDict]],
1015        instance_dict: Optional[IdentityMap] = None,
1016    ) -> None:
1017        """Mass / highly inlined version of commit_all()."""
1018
1019        for state, dict_ in iter_:
1020            state_dict = state.__dict__
1021
1022            state.committed_state.clear()
1023
1024            if "_pending_mutations" in state_dict:
1025                del state_dict["_pending_mutations"]
1026
1027            state.expired_attributes.difference_update(dict_)
1028
1029            if instance_dict and state.modified:
1030                instance_dict._modified.discard(state)
1031
1032            state.modified = state.expired = False
1033            state._strong_obj = None
1034
1035
1036class AttributeState:
1037    """Provide an inspection interface corresponding
1038    to a particular attribute on a particular mapped object.
1039
1040    The :class:`.AttributeState` object is accessed
1041    via the :attr:`.InstanceState.attrs` collection
1042    of a particular :class:`.InstanceState`::
1043
1044        from sqlalchemy import inspect
1045
1046        insp = inspect(some_mapped_object)
1047        attr_state = insp.attrs.some_attribute
1048
1049    """
1050
1051    __slots__ = ("state", "key")
1052
1053    state: InstanceState[Any]
1054    key: str
1055
1056    def __init__(self, state: InstanceState[Any], key: str):
1057        self.state = state
1058        self.key = key
1059
1060    @property
1061    def loaded_value(self) -> Any:
1062        """The current value of this attribute as loaded from the database.
1063
1064        If the value has not been loaded, or is otherwise not present
1065        in the object's dictionary, returns NO_VALUE.
1066
1067        """
1068        return self.state.dict.get(self.key, NO_VALUE)
1069
1070    @property
1071    def value(self) -> Any:
1072        """Return the value of this attribute.
1073
1074        This operation is equivalent to accessing the object's
1075        attribute directly or via ``getattr()``, and will fire
1076        off any pending loader callables if needed.
1077
1078        """
1079        return self.state.manager[self.key].__get__(
1080            self.state.obj(), self.state.class_
1081        )
1082
1083    @property
1084    def history(self) -> History:
1085        """Return the current **pre-flush** change history for
1086        this attribute, via the :class:`.History` interface.
1087
1088        This method will **not** emit loader callables if the value of the
1089        attribute is unloaded.
1090
1091        .. note::
1092
1093            The attribute history system tracks changes on a **per flush
1094            basis**. Each time the :class:`.Session` is flushed, the history
1095            of each attribute is reset to empty.   The :class:`.Session` by
1096            default autoflushes each time a :class:`_query.Query` is invoked.
1097            For
1098            options on how to control this, see :ref:`session_flushing`.
1099
1100
1101        .. seealso::
1102
1103            :meth:`.AttributeState.load_history` - retrieve history
1104            using loader callables if the value is not locally present.
1105
1106            :func:`.attributes.get_history` - underlying function
1107
1108        """
1109        return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE)
1110
1111    def load_history(self) -> History:
1112        """Return the current **pre-flush** change history for
1113        this attribute, via the :class:`.History` interface.
1114
1115        This method **will** emit loader callables if the value of the
1116        attribute is unloaded.
1117
1118        .. note::
1119
1120            The attribute history system tracks changes on a **per flush
1121            basis**. Each time the :class:`.Session` is flushed, the history
1122            of each attribute is reset to empty.   The :class:`.Session` by
1123            default autoflushes each time a :class:`_query.Query` is invoked.
1124            For
1125            options on how to control this, see :ref:`session_flushing`.
1126
1127        .. seealso::
1128
1129            :attr:`.AttributeState.history`
1130
1131            :func:`.attributes.get_history` - underlying function
1132
1133        """
1134        return self.state.get_history(self.key, PASSIVE_OFF ^ INIT_OK)
1135
1136
1137class PendingCollection:
1138    """A writable placeholder for an unloaded collection.
1139
1140    Stores items appended to and removed from a collection that has not yet
1141    been loaded. When the collection is loaded, the changes stored in
1142    PendingCollection are applied to it to produce the final result.
1143
1144    """
1145
1146    __slots__ = ("deleted_items", "added_items")
1147
1148    deleted_items: util.IdentitySet
1149    added_items: util.OrderedIdentitySet
1150
1151    def __init__(self) -> None:
1152        self.deleted_items = util.IdentitySet()
1153        self.added_items = util.OrderedIdentitySet()
1154
1155    def merge_with_history(self, history: History) -> History:
1156        return history._merge(self.added_items, self.deleted_items)
1157
1158    def append(self, value: Any) -> None:
1159        if value in self.deleted_items:
1160            self.deleted_items.remove(value)
1161        else:
1162            self.added_items.add(value)
1163
1164    def remove(self, value: Any) -> None:
1165        if value in self.added_items:
1166            self.added_items.remove(value)
1167        else:
1168            self.deleted_items.add(value)
1169 
codekingpro/portable-devtools · Team Ai