Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/state.py: 37%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

466 statements  

1# orm/state.py 

2# Copyright (C) 2005-2025 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 Literal 

23from typing import Optional 

24from typing import Protocol 

25from typing import Set 

26from typing import Tuple 

27from typing import TYPE_CHECKING 

28from typing import Union 

29import weakref 

30 

31from . import base 

32from . import exc as orm_exc 

33from . import interfaces 

34from ._typing import _O 

35from ._typing import is_collection_impl 

36from .base import ATTR_WAS_SET 

37from .base import INIT_OK 

38from .base import LoaderCallableStatus 

39from .base import NEVER_SET 

40from .base import NO_VALUE 

41from .base import PASSIVE_NO_INITIALIZE 

42from .base import PASSIVE_NO_RESULT 

43from .base import PASSIVE_OFF 

44from .base import SQL_OK 

45from .path_registry import PathRegistry 

46from .. import exc as sa_exc 

47from .. import inspection 

48from .. import util 

49from ..util.typing import TupleAny 

50from ..util.typing import Unpack 

51 

52if TYPE_CHECKING: 

53 from ._typing import _IdentityKeyType 

54 from ._typing import _InstanceDict 

55 from ._typing import _LoaderCallable 

56 from .attributes import _AttributeImpl 

57 from .attributes import History 

58 from .base import PassiveFlag 

59 from .collections import _AdaptedCollectionProtocol 

60 from .identity import IdentityMap 

61 from .instrumentation import ClassManager 

62 from .interfaces import ORMOption 

63 from .mapper import Mapper 

64 from .session import Session 

65 from ..engine import Row 

66 from ..ext.asyncio.session import async_session as _async_provider 

67 from ..ext.asyncio.session import AsyncSession 

68 

69if TYPE_CHECKING: 

70 _sessions: weakref.WeakValueDictionary[int, Session] 

71else: 

72 # late-populated by session.py 

73 _sessions = None 

74 

75 

76if not TYPE_CHECKING: 

77 # optionally late-provided by sqlalchemy.ext.asyncio.session 

78 

79 _async_provider = None # noqa 

80 

81 

82class _InstanceDictProto(Protocol): 

83 def __call__(self) -> Optional[IdentityMap]: ... 

84 

85 

86class _InstallLoaderCallableProto(Protocol[_O]): 

87 """used at result loading time to install a _LoaderCallable callable 

88 upon a specific InstanceState, which will be used to populate an 

89 attribute when that attribute is accessed. 

90 

91 Concrete examples are per-instance deferred column loaders and 

92 relationship lazy loaders. 

93 

94 """ 

95 

96 def __call__( 

97 self, 

98 state: InstanceState[_O], 

99 dict_: _InstanceDict, 

100 row: Row[Unpack[TupleAny]], 

101 ) -> None: ... 

102 

103 

104@inspection._self_inspects 

105class InstanceState(interfaces.InspectionAttrInfo, Generic[_O]): 

106 """Tracks state information at the instance level. 

107 

108 The :class:`.InstanceState` is a key object used by the 

109 SQLAlchemy ORM in order to track the state of an object; 

110 it is created the moment an object is instantiated, typically 

111 as a result of :term:`instrumentation` which SQLAlchemy applies 

112 to the ``__init__()`` method of the class. 

113 

114 :class:`.InstanceState` is also a semi-public object, 

115 available for runtime inspection as to the state of a 

116 mapped instance, including information such as its current 

117 status within a particular :class:`.Session` and details 

118 about data on individual attributes. The public API 

119 in order to acquire a :class:`.InstanceState` object 

120 is to use the :func:`_sa.inspect` system:: 

121 

122 >>> from sqlalchemy import inspect 

123 >>> insp = inspect(some_mapped_object) 

124 >>> insp.attrs.nickname.history 

125 History(added=['new nickname'], unchanged=(), deleted=['nickname']) 

126 

127 .. seealso:: 

128 

129 :ref:`orm_mapper_inspection_instancestate` 

130 

131 """ 

132 

133 __slots__ = ( 

134 "__dict__", 

135 "__weakref__", 

136 "class_", 

137 "manager", 

138 "obj", 

139 "committed_state", 

140 "expired_attributes", 

141 ) 

142 

143 manager: ClassManager[_O] 

144 session_id: Optional[int] = None 

145 key: Optional[_IdentityKeyType[_O]] = None 

146 runid: Optional[int] = None 

147 load_options: Tuple[ORMOption, ...] = () 

148 load_path: PathRegistry = PathRegistry.root 

149 insert_order: Optional[int] = None 

150 _strong_obj: Optional[object] = None 

151 obj: weakref.ref[_O] 

152 

153 committed_state: Dict[str, Any] 

154 

155 modified: bool = False 

156 """When ``True`` the object was modified.""" 

157 expired: bool = False 

158 """When ``True`` the object is :term:`expired`. 

159 

160 .. seealso:: 

161 

162 :ref:`session_expire` 

163 """ 

164 _deleted: bool = False 

165 _load_pending: bool = False 

166 _orphaned_outside_of_session: bool = False 

167 is_instance: bool = True 

168 identity_token: object = None 

169 _last_known_values: Optional[Dict[str, Any]] = None 

170 

171 _instance_dict: _InstanceDictProto 

172 """A weak reference, or in the default case a plain callable, that 

173 returns a reference to the current :class:`.IdentityMap`, if any. 

174 

175 """ 

176 if not TYPE_CHECKING: 

177 

178 def _instance_dict(self): 

179 """default 'weak reference' for _instance_dict""" 

180 return None 

181 

182 expired_attributes: Set[str] 

183 """The set of keys which are 'expired' to be loaded by 

184 the manager's deferred scalar loader, assuming no pending 

185 changes. 

186 

187 See also the ``unmodified`` collection which is intersected 

188 against this set when a refresh operation occurs. 

189 """ 

190 

191 callables: Dict[str, Callable[[InstanceState[_O], PassiveFlag], Any]] 

192 """A namespace where a per-state loader callable can be associated. 

193 

194 In SQLAlchemy 1.0, this is only used for lazy loaders / deferred 

195 loaders that were set up via query option. 

196 

197 Previously, callables was used also to indicate expired attributes 

198 by storing a link to the InstanceState itself in this dictionary. 

199 This role is now handled by the expired_attributes set. 

200 

201 """ 

202 

203 if not TYPE_CHECKING: 

204 callables = util.EMPTY_DICT 

205 

206 def __init__(self, obj: _O, manager: ClassManager[_O]): 

207 self.class_ = obj.__class__ 

208 self.manager = manager 

209 self.obj = weakref.ref(obj, self._cleanup) 

210 self.committed_state = {} 

211 self.expired_attributes = set() 

212 

213 @util.memoized_property 

214 def attrs(self) -> util.ReadOnlyProperties[AttributeState]: 

215 """Return a namespace representing each attribute on 

216 the mapped object, including its current value 

217 and history. 

218 

219 The returned object is an instance of :class:`.AttributeState`. 

220 This object allows inspection of the current data 

221 within an attribute as well as attribute history 

222 since the last flush. 

223 

224 """ 

225 return util.ReadOnlyProperties( 

226 {key: AttributeState(self, key) for key in self.manager} 

227 ) 

228 

229 @property 

230 def transient(self) -> bool: 

231 """Return ``True`` if the object is :term:`transient`. 

232 

233 .. seealso:: 

234 

235 :ref:`session_object_states` 

236 

237 """ 

238 return self.key is None and not self._attached 

239 

240 @property 

241 def pending(self) -> bool: 

242 """Return ``True`` if the object is :term:`pending`. 

243 

244 .. seealso:: 

245 

246 :ref:`session_object_states` 

247 

248 """ 

249 return self.key is None and self._attached 

250 

251 @property 

252 def deleted(self) -> bool: 

253 """Return ``True`` if the object is :term:`deleted`. 

254 

255 An object that is in the deleted state is guaranteed to 

256 not be within the :attr:`.Session.identity_map` of its parent 

257 :class:`.Session`; however if the session's transaction is rolled 

258 back, the object will be restored to the persistent state and 

259 the identity map. 

260 

261 .. note:: 

262 

263 The :attr:`.InstanceState.deleted` attribute refers to a specific 

264 state of the object that occurs between the "persistent" and 

265 "detached" states; once the object is :term:`detached`, the 

266 :attr:`.InstanceState.deleted` attribute **no longer returns 

267 True**; in order to detect that a state was deleted, regardless 

268 of whether or not the object is associated with a 

269 :class:`.Session`, use the :attr:`.InstanceState.was_deleted` 

270 accessor. 

271 

272 .. seealso:: 

273 

274 :ref:`session_object_states` 

275 

276 """ 

277 return self.key is not None and self._attached and self._deleted 

278 

279 @property 

280 def was_deleted(self) -> bool: 

281 """Return True if this object is or was previously in the 

282 "deleted" state and has not been reverted to persistent. 

283 

284 This flag returns True once the object was deleted in flush. 

285 When the object is expunged from the session either explicitly 

286 or via transaction commit and enters the "detached" state, 

287 this flag will continue to report True. 

288 

289 .. seealso:: 

290 

291 :attr:`.InstanceState.deleted` - refers to the "deleted" state 

292 

293 :func:`.orm.util.was_deleted` - standalone function 

294 

295 :ref:`session_object_states` 

296 

297 """ 

298 return self._deleted 

299 

300 @property 

301 def persistent(self) -> bool: 

302 """Return ``True`` if the object is :term:`persistent`. 

303 

304 An object that is in the persistent state is guaranteed to 

305 be within the :attr:`.Session.identity_map` of its parent 

306 :class:`.Session`. 

307 

308 .. seealso:: 

309 

310 :ref:`session_object_states` 

311 

312 """ 

313 return self.key is not None and self._attached and not self._deleted 

314 

315 @property 

316 def detached(self) -> bool: 

317 """Return ``True`` if the object is :term:`detached`. 

318 

319 .. seealso:: 

320 

321 :ref:`session_object_states` 

322 

323 """ 

324 return self.key is not None and not self._attached 

325 

326 @util.non_memoized_property 

327 @util.preload_module("sqlalchemy.orm.session") 

328 def _attached(self) -> bool: 

329 return ( 

330 self.session_id is not None 

331 and self.session_id in util.preloaded.orm_session._sessions 

332 ) 

333 

334 def _track_last_known_value(self, key: str) -> None: 

335 """Track the last known value of a particular key after expiration 

336 operations. 

337 

338 """ 

339 

340 lkv = self._last_known_values 

341 if lkv is None: 

342 self._last_known_values = lkv = {} 

343 if key not in lkv: 

344 lkv[key] = NO_VALUE 

345 

346 @property 

347 def session(self) -> Optional[Session]: 

348 """Return the owning :class:`.Session` for this instance, 

349 or ``None`` if none available. 

350 

351 Note that the result here can in some cases be *different* 

352 from that of ``obj in session``; an object that's been deleted 

353 will report as not ``in session``, however if the transaction is 

354 still in progress, this attribute will still refer to that session. 

355 Only when the transaction is completed does the object become 

356 fully detached under normal circumstances. 

357 

358 .. seealso:: 

359 

360 :attr:`_orm.InstanceState.async_session` 

361 

362 """ 

363 if self.session_id: 

364 try: 

365 return _sessions[self.session_id] 

366 except KeyError: 

367 pass 

368 return None 

369 

370 @property 

371 def async_session(self) -> Optional[AsyncSession]: 

372 """Return the owning :class:`_asyncio.AsyncSession` for this instance, 

373 or ``None`` if none available. 

374 

375 This attribute is only non-None when the :mod:`sqlalchemy.ext.asyncio` 

376 API is in use for this ORM object. The returned 

377 :class:`_asyncio.AsyncSession` object will be a proxy for the 

378 :class:`_orm.Session` object that would be returned from the 

379 :attr:`_orm.InstanceState.session` attribute for this 

380 :class:`_orm.InstanceState`. 

381 

382 .. versionadded:: 1.4.18 

383 

384 .. seealso:: 

385 

386 :ref:`asyncio_toplevel` 

387 

388 """ 

389 if _async_provider is None: 

390 return None 

391 

392 sess = self.session 

393 if sess is not None: 

394 return _async_provider(sess) 

395 else: 

396 return None 

397 

398 @property 

399 def object(self) -> Optional[_O]: 

400 """Return the mapped object represented by this 

401 :class:`.InstanceState`. 

402 

403 Returns None if the object has been garbage collected 

404 

405 """ 

406 return self.obj() 

407 

408 @property 

409 def identity(self) -> Optional[Tuple[Any, ...]]: 

410 """Return the mapped identity of the mapped object. 

411 This is the primary key identity as persisted by the ORM 

412 which can always be passed directly to 

413 :meth:`_query.Query.get`. 

414 

415 Returns ``None`` if the object has no primary key identity. 

416 

417 .. note:: 

418 An object which is :term:`transient` or :term:`pending` 

419 does **not** have a mapped identity until it is flushed, 

420 even if its attributes include primary key values. 

421 

422 """ 

423 if self.key is None: 

424 return None 

425 else: 

426 return self.key[1] 

427 

428 @property 

429 def identity_key(self) -> Optional[_IdentityKeyType[_O]]: 

430 """Return the identity key for the mapped object. 

431 

432 This is the key used to locate the object within 

433 the :attr:`.Session.identity_map` mapping. It contains 

434 the identity as returned by :attr:`.identity` within it. 

435 

436 

437 """ 

438 return self.key 

439 

440 @util.memoized_property 

441 def parents(self) -> Dict[int, Union[Literal[False], InstanceState[Any]]]: 

442 return {} 

443 

444 @util.memoized_property 

445 def _pending_mutations(self) -> Dict[str, PendingCollection]: 

446 return {} 

447 

448 @util.memoized_property 

449 def _empty_collections(self) -> Dict[str, _AdaptedCollectionProtocol]: 

450 return {} 

451 

452 @util.memoized_property 

453 def mapper(self) -> Mapper[_O]: 

454 """Return the :class:`_orm.Mapper` used for this mapped object.""" 

455 return self.manager.mapper 

456 

457 @property 

458 def has_identity(self) -> bool: 

459 """Return ``True`` if this object has an identity key. 

460 

461 This should always have the same value as the 

462 expression ``state.persistent`` or ``state.detached``. 

463 

464 """ 

465 return bool(self.key) 

466 

467 @classmethod 

468 def _detach_states( 

469 self, 

470 states: Iterable[InstanceState[_O]], 

471 session: Session, 

472 to_transient: bool = False, 

473 ) -> None: 

474 persistent_to_detached = ( 

475 session.dispatch.persistent_to_detached or None 

476 ) 

477 deleted_to_detached = session.dispatch.deleted_to_detached or None 

478 pending_to_transient = session.dispatch.pending_to_transient or None 

479 persistent_to_transient = ( 

480 session.dispatch.persistent_to_transient or None 

481 ) 

482 

483 for state in states: 

484 deleted = state._deleted 

485 pending = state.key is None 

486 persistent = not pending and not deleted 

487 

488 state.session_id = None 

489 

490 if to_transient and state.key: 

491 del state.key 

492 if persistent: 

493 if to_transient: 

494 if persistent_to_transient is not None: 

495 persistent_to_transient(session, state) 

496 elif persistent_to_detached is not None: 

497 persistent_to_detached(session, state) 

498 elif deleted and deleted_to_detached is not None: 

499 deleted_to_detached(session, state) 

500 elif pending and pending_to_transient is not None: 

501 pending_to_transient(session, state) 

502 

503 state._strong_obj = None 

504 

505 def _detach(self, session: Optional[Session] = None) -> None: 

506 if session: 

507 InstanceState._detach_states([self], session) 

508 else: 

509 self.session_id = self._strong_obj = None 

510 

511 def _dispose(self) -> None: 

512 # used by the test suite, apparently 

513 self._detach() 

514 

515 def _force_dereference(self) -> None: 

516 """Force this InstanceState to act as though its weakref has 

517 been GC'ed. 

518 

519 this is used for test code that has to test reactions to objects 

520 being GC'ed. We can't reliably force GCs to happen under all 

521 CI circumstances. 

522 

523 """ 

524 

525 # if _strong_obj is set, then our object would not be getting 

526 # GC'ed (at least within the scope of what we use this for in tests). 

527 # so make sure this is not set 

528 assert self._strong_obj is None 

529 

530 obj = self.obj() 

531 if obj is None: 

532 # object was GC'ed and we're done! woop 

533 return 

534 

535 del obj 

536 

537 self._cleanup(self.obj) 

538 self.obj = lambda: None # type: ignore 

539 

540 def _cleanup(self, ref: weakref.ref[_O]) -> None: 

541 """Weakref callback cleanup. 

542 

543 This callable cleans out the state when it is being garbage 

544 collected. 

545 

546 this _cleanup **assumes** that there are no strong refs to us! 

547 Will not work otherwise! 

548 

549 """ 

550 

551 # Python builtins become undefined during interpreter shutdown. 

552 # Guard against exceptions during this phase, as the method cannot 

553 # proceed in any case if builtins have been undefined. 

554 if dict is None: 

555 return 

556 

557 instance_dict = self._instance_dict() 

558 if instance_dict is not None: 

559 instance_dict._fast_discard(self) 

560 del self._instance_dict 

561 

562 # we can't possibly be in instance_dict._modified 

563 # b.c. this is weakref cleanup only, that set 

564 # is strong referencing! 

565 # assert self not in instance_dict._modified 

566 

567 self.session_id = self._strong_obj = None 

568 

569 @property 

570 def dict(self) -> _InstanceDict: 

571 """Return the instance dict used by the object. 

572 

573 Under normal circumstances, this is always synonymous 

574 with the ``__dict__`` attribute of the mapped object, 

575 unless an alternative instrumentation system has been 

576 configured. 

577 

578 In the case that the actual object has been garbage 

579 collected, this accessor returns a blank dictionary. 

580 

581 """ 

582 o = self.obj() 

583 if o is not None: 

584 return base.instance_dict(o) 

585 else: 

586 return {} 

587 

588 def _initialize_instance(*mixed: Any, **kwargs: Any) -> None: 

589 self, instance, args = mixed[0], mixed[1], mixed[2:] # noqa 

590 manager = self.manager 

591 

592 manager.dispatch.init(self, args, kwargs) 

593 

594 try: 

595 manager.original_init(*mixed[1:], **kwargs) 

596 except: 

597 with util.safe_reraise(): 

598 manager.dispatch.init_failure(self, args, kwargs) 

599 

600 def get_history(self, key: str, passive: PassiveFlag) -> History: 

601 return self.manager[key].impl.get_history(self, self.dict, passive) 

602 

603 def get_impl(self, key: str) -> _AttributeImpl: 

604 return self.manager[key].impl 

605 

606 def _get_pending_mutation(self, key: str) -> PendingCollection: 

607 if key not in self._pending_mutations: 

608 self._pending_mutations[key] = PendingCollection() 

609 return self._pending_mutations[key] 

610 

611 def __getstate__(self) -> Dict[str, Any]: 

612 state_dict: Dict[str, Any] = { 

613 "instance": self.obj(), 

614 "class_": self.class_, 

615 "committed_state": self.committed_state, 

616 "expired_attributes": self.expired_attributes, 

617 } 

618 state_dict.update( 

619 (k, self.__dict__[k]) 

620 for k in ( 

621 "_pending_mutations", 

622 "modified", 

623 "expired", 

624 "callables", 

625 "key", 

626 "parents", 

627 "load_options", 

628 "class_", 

629 "expired_attributes", 

630 "info", 

631 ) 

632 if k in self.__dict__ 

633 ) 

634 if self.load_path: 

635 state_dict["load_path"] = self.load_path.serialize() 

636 

637 state_dict["manager"] = self.manager._serialize(self, state_dict) 

638 

639 return state_dict 

640 

641 def __setstate__(self, state_dict: Dict[str, Any]) -> None: 

642 inst = state_dict["instance"] 

643 if inst is not None: 

644 self.obj = weakref.ref(inst, self._cleanup) 

645 self.class_ = inst.__class__ 

646 else: 

647 self.obj = lambda: None # type: ignore 

648 self.class_ = state_dict["class_"] 

649 

650 self.committed_state = state_dict.get("committed_state", {}) 

651 self._pending_mutations = state_dict.get("_pending_mutations", {}) 

652 self.parents = state_dict.get("parents", {}) 

653 self.modified = state_dict.get("modified", False) 

654 self.expired = state_dict.get("expired", False) 

655 if "info" in state_dict: 

656 self.info.update(state_dict["info"]) 

657 if "callables" in state_dict: 

658 self.callables = state_dict["callables"] 

659 

660 self.expired_attributes = state_dict["expired_attributes"] 

661 else: 

662 if "expired_attributes" in state_dict: 

663 self.expired_attributes = state_dict["expired_attributes"] 

664 else: 

665 self.expired_attributes = set() 

666 

667 self.__dict__.update( 

668 [ 

669 (k, state_dict[k]) 

670 for k in ("key", "load_options") 

671 if k in state_dict 

672 ] 

673 ) 

674 if self.key: 

675 self.identity_token = self.key[2] 

676 

677 if "load_path" in state_dict: 

678 self.load_path = PathRegistry.deserialize(state_dict["load_path"]) 

679 

680 state_dict["manager"](self, inst, state_dict) 

681 

682 def _reset(self, dict_: _InstanceDict, key: str) -> None: 

683 """Remove the given attribute and any 

684 callables associated with it.""" 

685 

686 old = dict_.pop(key, None) 

687 manager_impl = self.manager[key].impl 

688 if old is not None and is_collection_impl(manager_impl): 

689 manager_impl._invalidate_collection(old) 

690 self.expired_attributes.discard(key) 

691 if self.callables: 

692 self.callables.pop(key, None) 

693 

694 def _copy_callables(self, from_: InstanceState[Any]) -> None: 

695 if "callables" in from_.__dict__: 

696 self.callables = dict(from_.callables) 

697 

698 @classmethod 

699 def _instance_level_callable_processor( 

700 cls, manager: ClassManager[_O], fn: _LoaderCallable, key: Any 

701 ) -> _InstallLoaderCallableProto[_O]: 

702 impl = manager[key].impl 

703 if is_collection_impl(impl): 

704 fixed_impl = impl 

705 

706 def _set_callable( 

707 state: InstanceState[_O], 

708 dict_: _InstanceDict, 

709 row: Row[Unpack[TupleAny]], 

710 ) -> None: 

711 if "callables" not in state.__dict__: 

712 state.callables = {} 

713 old = dict_.pop(key, None) 

714 if old is not None: 

715 fixed_impl._invalidate_collection(old) 

716 state.callables[key] = fn 

717 

718 else: 

719 

720 def _set_callable( 

721 state: InstanceState[_O], 

722 dict_: _InstanceDict, 

723 row: Row[Unpack[TupleAny]], 

724 ) -> None: 

725 if "callables" not in state.__dict__: 

726 state.callables = {} 

727 state.callables[key] = fn 

728 

729 return _set_callable 

730 

731 def _expire( 

732 self, dict_: _InstanceDict, modified_set: Set[InstanceState[Any]] 

733 ) -> None: 

734 self.expired = True 

735 if self.modified: 

736 modified_set.discard(self) 

737 self.committed_state.clear() 

738 self.modified = False 

739 

740 self._strong_obj = None 

741 

742 if "_pending_mutations" in self.__dict__: 

743 del self.__dict__["_pending_mutations"] 

744 

745 if "parents" in self.__dict__: 

746 del self.__dict__["parents"] 

747 

748 self.expired_attributes.update( 

749 [impl.key for impl in self.manager._loader_impls] 

750 ) 

751 

752 if self.callables: 

753 # the per state loader callables we can remove here are 

754 # LoadDeferredColumns, which undefers a column at the instance 

755 # level that is mapped with deferred, and LoadLazyAttribute, 

756 # which lazy loads a relationship at the instance level that 

757 # is mapped with "noload" or perhaps "immediateload". 

758 # Before 1.4, only column-based 

759 # attributes could be considered to be "expired", so here they 

760 # were the only ones "unexpired", which means to make them deferred 

761 # again. For the moment, as of 1.4 we also apply the same 

762 # treatment relationships now, that is, an instance level lazy 

763 # loader is reset in the same way as a column loader. 

764 for k in self.expired_attributes.intersection(self.callables): 

765 del self.callables[k] 

766 

767 for k in self.manager._collection_impl_keys.intersection(dict_): 

768 collection = dict_.pop(k) 

769 collection._sa_adapter.invalidated = True 

770 

771 if self._last_known_values: 

772 self._last_known_values.update( 

773 {k: dict_[k] for k in self._last_known_values if k in dict_} 

774 ) 

775 

776 for key in self.manager._all_key_set.intersection(dict_): 

777 del dict_[key] 

778 

779 self.manager.dispatch.expire(self, None) 

780 

781 def _expire_attributes( 

782 self, 

783 dict_: _InstanceDict, 

784 attribute_names: Iterable[str], 

785 no_loader: bool = False, 

786 ) -> None: 

787 pending = self.__dict__.get("_pending_mutations", None) 

788 

789 callables = self.callables 

790 

791 for key in attribute_names: 

792 impl = self.manager[key].impl 

793 if impl.accepts_scalar_loader: 

794 if no_loader and (impl.callable_ or key in callables): 

795 continue 

796 

797 self.expired_attributes.add(key) 

798 if callables and key in callables: 

799 del callables[key] 

800 old = dict_.pop(key, NO_VALUE) 

801 if is_collection_impl(impl) and old is not NO_VALUE: 

802 impl._invalidate_collection(old) 

803 

804 lkv = self._last_known_values 

805 if lkv is not None and key in lkv and old is not NO_VALUE: 

806 lkv[key] = old 

807 

808 self.committed_state.pop(key, None) 

809 if pending: 

810 pending.pop(key, None) 

811 

812 self.manager.dispatch.expire(self, attribute_names) 

813 

814 def _load_expired( 

815 self, state: InstanceState[_O], passive: PassiveFlag 

816 ) -> LoaderCallableStatus: 

817 """__call__ allows the InstanceState to act as a deferred 

818 callable for loading expired attributes, which is also 

819 serializable (picklable). 

820 

821 """ 

822 

823 if not passive & SQL_OK: 

824 return PASSIVE_NO_RESULT 

825 

826 toload = self.expired_attributes.intersection(self.unmodified) 

827 toload = toload.difference( 

828 attr 

829 for attr in toload 

830 if not self.manager[attr].impl.load_on_unexpire 

831 ) 

832 

833 self.manager.expired_attribute_loader(self, toload, passive) 

834 

835 # if the loader failed, or this 

836 # instance state didn't have an identity, 

837 # the attributes still might be in the callables 

838 # dict. ensure they are removed. 

839 self.expired_attributes.clear() 

840 

841 return ATTR_WAS_SET 

842 

843 @property 

844 def unmodified(self) -> Set[str]: 

845 """Return the set of keys which have no uncommitted changes""" 

846 

847 return set(self.manager).difference(self.committed_state) 

848 

849 def unmodified_intersection(self, keys: Iterable[str]) -> Set[str]: 

850 """Return self.unmodified.intersection(keys).""" 

851 

852 return ( 

853 set(keys) 

854 .intersection(self.manager) 

855 .difference(self.committed_state) 

856 ) 

857 

858 @property 

859 def unloaded(self) -> Set[str]: 

860 """Return the set of keys which do not have a loaded value. 

861 

862 This includes expired attributes and any other attribute that was never 

863 populated or modified. 

864 

865 """ 

866 return ( 

867 set(self.manager) 

868 .difference(self.committed_state) 

869 .difference(self.dict) 

870 ) 

871 

872 @property 

873 @util.deprecated( 

874 "2.0", 

875 "The :attr:`.InstanceState.unloaded_expirable` attribute is " 

876 "deprecated. Please use :attr:`.InstanceState.unloaded`.", 

877 ) 

878 def unloaded_expirable(self) -> Set[str]: 

879 """Synonymous with :attr:`.InstanceState.unloaded`. 

880 

881 This attribute was added as an implementation-specific detail at some 

882 point and should be considered to be private. 

883 

884 """ 

885 return self.unloaded 

886 

887 @property 

888 def _unloaded_non_object(self) -> Set[str]: 

889 return self.unloaded.intersection( 

890 attr 

891 for attr in self.manager 

892 if self.manager[attr].impl.accepts_scalar_loader 

893 ) 

894 

895 def _modified_event( 

896 self, 

897 dict_: _InstanceDict, 

898 attr: Optional[_AttributeImpl], 

899 previous: Any, 

900 collection: bool = False, 

901 is_userland: bool = False, 

902 ) -> None: 

903 if attr: 

904 if not attr.send_modified_events: 

905 return 

906 if is_userland and attr.key not in dict_: 

907 raise sa_exc.InvalidRequestError( 

908 "Can't flag attribute '%s' modified; it's not present in " 

909 "the object state" % attr.key 

910 ) 

911 if attr.key not in self.committed_state or is_userland: 

912 if collection: 

913 if TYPE_CHECKING: 

914 assert is_collection_impl(attr) 

915 if previous is NEVER_SET: 

916 if attr.key in dict_: 

917 previous = dict_[attr.key] 

918 

919 if previous not in (None, NO_VALUE, NEVER_SET): 

920 previous = attr.copy(previous) 

921 self.committed_state[attr.key] = previous 

922 

923 lkv = self._last_known_values 

924 if lkv is not None and attr.key in lkv: 

925 lkv[attr.key] = NO_VALUE 

926 

927 # assert self._strong_obj is None or self.modified 

928 

929 if (self.session_id and self._strong_obj is None) or not self.modified: 

930 self.modified = True 

931 instance_dict = self._instance_dict() 

932 if instance_dict: 

933 has_modified = bool(instance_dict._modified) 

934 instance_dict._modified.add(self) 

935 else: 

936 has_modified = False 

937 

938 # only create _strong_obj link if attached 

939 # to a session 

940 

941 inst = self.obj() 

942 if self.session_id: 

943 self._strong_obj = inst 

944 

945 # if identity map already had modified objects, 

946 # assume autobegin already occurred, else check 

947 # for autobegin 

948 if not has_modified: 

949 # inline of autobegin, to ensure session transaction 

950 # snapshot is established 

951 try: 

952 session = _sessions[self.session_id] 

953 except KeyError: 

954 pass 

955 else: 

956 if session._transaction is None: 

957 session._autobegin_t() 

958 

959 if inst is None and attr: 

960 raise orm_exc.ObjectDereferencedError( 

961 "Can't emit change event for attribute '%s' - " 

962 "parent object of type %s has been garbage " 

963 "collected." 

964 % (self.manager[attr.key], base.state_class_str(self)) 

965 ) 

966 

967 def _commit(self, dict_: _InstanceDict, keys: Iterable[str]) -> None: 

968 """Commit attributes. 

969 

970 This is used by a partial-attribute load operation to mark committed 

971 those attributes which were refreshed from the database. 

972 

973 Attributes marked as "expired" can potentially remain "expired" after 

974 this step if a value was not populated in state.dict. 

975 

976 """ 

977 for key in keys: 

978 self.committed_state.pop(key, None) 

979 

980 self.expired = False 

981 

982 self.expired_attributes.difference_update( 

983 set(keys).intersection(dict_) 

984 ) 

985 

986 # the per-keys commit removes object-level callables, 

987 # while that of commit_all does not. it's not clear 

988 # if this behavior has a clear rationale, however tests do 

989 # ensure this is what it does. 

990 if self.callables: 

991 for key in ( 

992 set(self.callables).intersection(keys).intersection(dict_) 

993 ): 

994 del self.callables[key] 

995 

996 def _commit_all( 

997 self, 

998 dict_: _InstanceDict, 

999 instance_dict: Optional[IdentityMap] = None, 

1000 ) -> None: 

1001 """commit all attributes unconditionally. 

1002 

1003 This is used after a flush() or a full load/refresh 

1004 to remove all pending state from the instance. 

1005 

1006 - all attributes are marked as "committed" 

1007 - the "strong dirty reference" is removed 

1008 - the "modified" flag is set to False 

1009 - any "expired" markers for scalar attributes loaded are removed. 

1010 - lazy load callables for objects / collections *stay* 

1011 

1012 Attributes marked as "expired" can potentially remain 

1013 "expired" after this step if a value was not populated in state.dict. 

1014 

1015 """ 

1016 self._commit_all_states([(self, dict_)], instance_dict) 

1017 

1018 @classmethod 

1019 def _commit_all_states( 

1020 self, 

1021 iter_: Iterable[Tuple[InstanceState[Any], _InstanceDict]], 

1022 instance_dict: Optional[IdentityMap] = None, 

1023 ) -> None: 

1024 """Mass / highly inlined version of commit_all().""" 

1025 

1026 for state, dict_ in iter_: 

1027 state_dict = state.__dict__ 

1028 

1029 state.committed_state.clear() 

1030 

1031 if "_pending_mutations" in state_dict: 

1032 del state_dict["_pending_mutations"] 

1033 

1034 state.expired_attributes.difference_update(dict_) 

1035 

1036 if instance_dict and state.modified: 

1037 instance_dict._modified.discard(state) 

1038 

1039 state.modified = state.expired = False 

1040 state._strong_obj = None 

1041 

1042 

1043class AttributeState: 

1044 """Provide an inspection interface corresponding 

1045 to a particular attribute on a particular mapped object. 

1046 

1047 The :class:`.AttributeState` object is accessed 

1048 via the :attr:`.InstanceState.attrs` collection 

1049 of a particular :class:`.InstanceState`:: 

1050 

1051 from sqlalchemy import inspect 

1052 

1053 insp = inspect(some_mapped_object) 

1054 attr_state = insp.attrs.some_attribute 

1055 

1056 """ 

1057 

1058 __slots__ = ("state", "key") 

1059 

1060 state: InstanceState[Any] 

1061 key: str 

1062 

1063 def __init__(self, state: InstanceState[Any], key: str): 

1064 self.state = state 

1065 self.key = key 

1066 

1067 @property 

1068 def loaded_value(self) -> Any: 

1069 """The current value of this attribute as loaded from the database. 

1070 

1071 If the value has not been loaded, or is otherwise not present 

1072 in the object's dictionary, returns NO_VALUE. 

1073 

1074 """ 

1075 return self.state.dict.get(self.key, NO_VALUE) 

1076 

1077 @property 

1078 def value(self) -> Any: 

1079 """Return the value of this attribute. 

1080 

1081 This operation is equivalent to accessing the object's 

1082 attribute directly or via ``getattr()``, and will fire 

1083 off any pending loader callables if needed. 

1084 

1085 """ 

1086 return self.state.manager[self.key].__get__( 

1087 self.state.obj(), self.state.class_ 

1088 ) 

1089 

1090 @property 

1091 def history(self) -> History: 

1092 """Return the current **pre-flush** change history for 

1093 this attribute, via the :class:`.History` interface. 

1094 

1095 This method will **not** emit loader callables if the value of the 

1096 attribute is unloaded. 

1097 

1098 .. note:: 

1099 

1100 The attribute history system tracks changes on a **per flush 

1101 basis**. Each time the :class:`.Session` is flushed, the history 

1102 of each attribute is reset to empty. The :class:`.Session` by 

1103 default autoflushes each time a :class:`_query.Query` is invoked. 

1104 For 

1105 options on how to control this, see :ref:`session_flushing`. 

1106 

1107 

1108 .. seealso:: 

1109 

1110 :meth:`.AttributeState.load_history` - retrieve history 

1111 using loader callables if the value is not locally present. 

1112 

1113 :func:`.attributes.get_history` - underlying function 

1114 

1115 """ 

1116 return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE) 

1117 

1118 def load_history(self) -> History: 

1119 """Return the current **pre-flush** change history for 

1120 this attribute, via the :class:`.History` interface. 

1121 

1122 This method **will** emit loader callables if the value of the 

1123 attribute is unloaded. 

1124 

1125 .. note:: 

1126 

1127 The attribute history system tracks changes on a **per flush 

1128 basis**. Each time the :class:`.Session` is flushed, the history 

1129 of each attribute is reset to empty. The :class:`.Session` by 

1130 default autoflushes each time a :class:`_query.Query` is invoked. 

1131 For 

1132 options on how to control this, see :ref:`session_flushing`. 

1133 

1134 .. seealso:: 

1135 

1136 :attr:`.AttributeState.history` 

1137 

1138 :func:`.attributes.get_history` - underlying function 

1139 

1140 """ 

1141 return self.state.get_history(self.key, PASSIVE_OFF ^ INIT_OK) 

1142 

1143 

1144class PendingCollection: 

1145 """A writable placeholder for an unloaded collection. 

1146 

1147 Stores items appended to and removed from a collection that has not yet 

1148 been loaded. When the collection is loaded, the changes stored in 

1149 PendingCollection are applied to it to produce the final result. 

1150 

1151 """ 

1152 

1153 __slots__ = ("deleted_items", "added_items") 

1154 

1155 deleted_items: util.IdentitySet 

1156 added_items: util.OrderedIdentitySet 

1157 

1158 def __init__(self) -> None: 

1159 self.deleted_items = util.IdentitySet() 

1160 self.added_items = util.OrderedIdentitySet() 

1161 

1162 def merge_with_history(self, history: History) -> History: 

1163 return history._merge(self.added_items, self.deleted_items) 

1164 

1165 def append(self, value: Any) -> None: 

1166 if value in self.deleted_items: 

1167 self.deleted_items.remove(value) 

1168 else: 

1169 self.added_items.add(value) 

1170 

1171 def remove(self, value: Any) -> None: 

1172 if value in self.added_items: 

1173 self.added_items.remove(value) 

1174 else: 

1175 self.deleted_items.add(value)