Team Ai
Datasetpublic

codekingpro/portable-devtools

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