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

464 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 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)