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

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

456 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 _cleanup(self, ref: weakref.ref[_O]) -> None: 

515 """Weakref callback cleanup. 

516 

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

518 collected. 

519 

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

521 Will not work otherwise! 

522 

523 """ 

524 

525 # Python builtins become undefined during interpreter shutdown. 

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

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

528 if dict is None: 

529 return 

530 

531 instance_dict = self._instance_dict() 

532 if instance_dict is not None: 

533 instance_dict._fast_discard(self) 

534 del self._instance_dict 

535 

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

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

538 # is strong referencing! 

539 # assert self not in instance_dict._modified 

540 

541 self.session_id = self._strong_obj = None 

542 

543 @property 

544 def dict(self) -> _InstanceDict: 

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

546 

547 Under normal circumstances, this is always synonymous 

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

549 unless an alternative instrumentation system has been 

550 configured. 

551 

552 In the case that the actual object has been garbage 

553 collected, this accessor returns a blank dictionary. 

554 

555 """ 

556 o = self.obj() 

557 if o is not None: 

558 return base.instance_dict(o) 

559 else: 

560 return {} 

561 

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

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

564 manager = self.manager 

565 

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

567 

568 try: 

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

570 except: 

571 with util.safe_reraise(): 

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

573 

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

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

576 

577 def get_impl(self, key: str) -> AttributeImpl: 

578 return self.manager[key].impl 

579 

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

581 if key not in self._pending_mutations: 

582 self._pending_mutations[key] = PendingCollection() 

583 return self._pending_mutations[key] 

584 

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

586 state_dict: Dict[str, Any] = { 

587 "instance": self.obj(), 

588 "class_": self.class_, 

589 "committed_state": self.committed_state, 

590 "expired_attributes": self.expired_attributes, 

591 } 

592 state_dict.update( 

593 (k, self.__dict__[k]) 

594 for k in ( 

595 "_pending_mutations", 

596 "modified", 

597 "expired", 

598 "callables", 

599 "key", 

600 "parents", 

601 "load_options", 

602 "class_", 

603 "expired_attributes", 

604 "info", 

605 ) 

606 if k in self.__dict__ 

607 ) 

608 if self.load_path: 

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

610 

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

612 

613 return state_dict 

614 

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

616 inst = state_dict["instance"] 

617 if inst is not None: 

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

619 self.class_ = inst.__class__ 

620 else: 

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

622 self.class_ = state_dict["class_"] 

623 

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

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

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

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

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

629 if "info" in state_dict: 

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

631 if "callables" in state_dict: 

632 self.callables = state_dict["callables"] 

633 

634 self.expired_attributes = state_dict["expired_attributes"] 

635 else: 

636 if "expired_attributes" in state_dict: 

637 self.expired_attributes = state_dict["expired_attributes"] 

638 else: 

639 self.expired_attributes = set() 

640 

641 self.__dict__.update( 

642 [ 

643 (k, state_dict[k]) 

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

645 if k in state_dict 

646 ] 

647 ) 

648 if self.key: 

649 self.identity_token = self.key[2] 

650 

651 if "load_path" in state_dict: 

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

653 

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

655 

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

657 """Remove the given attribute and any 

658 callables associated with it.""" 

659 

660 old = dict_.pop(key, None) 

661 manager_impl = self.manager[key].impl 

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

663 manager_impl._invalidate_collection(old) 

664 self.expired_attributes.discard(key) 

665 if self.callables: 

666 self.callables.pop(key, None) 

667 

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

669 if "callables" in from_.__dict__: 

670 self.callables = dict(from_.callables) 

671 

672 @classmethod 

673 def _instance_level_callable_processor( 

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

675 ) -> _InstallLoaderCallableProto[_O]: 

676 impl = manager[key].impl 

677 if is_collection_impl(impl): 

678 fixed_impl = impl 

679 

680 def _set_callable( 

681 state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any] 

682 ) -> None: 

683 if "callables" not in state.__dict__: 

684 state.callables = {} 

685 old = dict_.pop(key, None) 

686 if old is not None: 

687 fixed_impl._invalidate_collection(old) 

688 state.callables[key] = fn 

689 

690 else: 

691 

692 def _set_callable( 

693 state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any] 

694 ) -> None: 

695 if "callables" not in state.__dict__: 

696 state.callables = {} 

697 state.callables[key] = fn 

698 

699 return _set_callable 

700 

701 def _expire( 

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

703 ) -> None: 

704 self.expired = True 

705 if self.modified: 

706 modified_set.discard(self) 

707 self.committed_state.clear() 

708 self.modified = False 

709 

710 self._strong_obj = None 

711 

712 if "_pending_mutations" in self.__dict__: 

713 del self.__dict__["_pending_mutations"] 

714 

715 if "parents" in self.__dict__: 

716 del self.__dict__["parents"] 

717 

718 self.expired_attributes.update( 

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

720 ) 

721 

722 if self.callables: 

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

724 # LoadDeferredColumns, which undefers a column at the instance 

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

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

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

728 # Before 1.4, only column-based 

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

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

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

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

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

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

735 del self.callables[k] 

736 

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

738 collection = dict_.pop(k) 

739 collection._sa_adapter.invalidated = True 

740 

741 if self._last_known_values: 

742 self._last_known_values.update( 

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

744 ) 

745 

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

747 del dict_[key] 

748 

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

750 

751 def _expire_attributes( 

752 self, 

753 dict_: _InstanceDict, 

754 attribute_names: Iterable[str], 

755 no_loader: bool = False, 

756 ) -> None: 

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

758 

759 callables = self.callables 

760 

761 for key in attribute_names: 

762 impl = self.manager[key].impl 

763 if impl.accepts_scalar_loader: 

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

765 continue 

766 

767 self.expired_attributes.add(key) 

768 if callables and key in callables: 

769 del callables[key] 

770 old = dict_.pop(key, NO_VALUE) 

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

772 impl._invalidate_collection(old) 

773 

774 lkv = self._last_known_values 

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

776 lkv[key] = old 

777 

778 self.committed_state.pop(key, None) 

779 if pending: 

780 pending.pop(key, None) 

781 

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

783 

784 def _load_expired( 

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

786 ) -> LoaderCallableStatus: 

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

788 callable for loading expired attributes, which is also 

789 serializable (picklable). 

790 

791 """ 

792 

793 if not passive & SQL_OK: 

794 return PASSIVE_NO_RESULT 

795 

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

797 toload = toload.difference( 

798 attr 

799 for attr in toload 

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

801 ) 

802 

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

804 

805 # if the loader failed, or this 

806 # instance state didn't have an identity, 

807 # the attributes still might be in the callables 

808 # dict. ensure they are removed. 

809 self.expired_attributes.clear() 

810 

811 return ATTR_WAS_SET 

812 

813 @property 

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

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

816 

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

818 

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

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

821 

822 return ( 

823 set(keys) 

824 .intersection(self.manager) 

825 .difference(self.committed_state) 

826 ) 

827 

828 @property 

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

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

831 

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

833 populated or modified. 

834 

835 """ 

836 return ( 

837 set(self.manager) 

838 .difference(self.committed_state) 

839 .difference(self.dict) 

840 ) 

841 

842 @property 

843 @util.deprecated( 

844 "2.0", 

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

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

847 ) 

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

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

850 

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

852 point and should be considered to be private. 

853 

854 """ 

855 return self.unloaded 

856 

857 @property 

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

859 return self.unloaded.intersection( 

860 attr 

861 for attr in self.manager 

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

863 ) 

864 

865 def _modified_event( 

866 self, 

867 dict_: _InstanceDict, 

868 attr: Optional[AttributeImpl], 

869 previous: Any, 

870 collection: bool = False, 

871 is_userland: bool = False, 

872 ) -> None: 

873 if attr: 

874 if not attr.send_modified_events: 

875 return 

876 if is_userland and attr.key not in dict_: 

877 raise sa_exc.InvalidRequestError( 

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

879 "the object state" % attr.key 

880 ) 

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

882 if collection: 

883 if TYPE_CHECKING: 

884 assert is_collection_impl(attr) 

885 if previous is NEVER_SET: 

886 if attr.key in dict_: 

887 previous = dict_[attr.key] 

888 

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

890 previous = attr.copy(previous) 

891 self.committed_state[attr.key] = previous 

892 

893 lkv = self._last_known_values 

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

895 lkv[attr.key] = NO_VALUE 

896 

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

898 

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

900 self.modified = True 

901 instance_dict = self._instance_dict() 

902 if instance_dict: 

903 has_modified = bool(instance_dict._modified) 

904 instance_dict._modified.add(self) 

905 else: 

906 has_modified = False 

907 

908 # only create _strong_obj link if attached 

909 # to a session 

910 

911 inst = self.obj() 

912 if self.session_id: 

913 self._strong_obj = inst 

914 

915 # if identity map already had modified objects, 

916 # assume autobegin already occurred, else check 

917 # for autobegin 

918 if not has_modified: 

919 # inline of autobegin, to ensure session transaction 

920 # snapshot is established 

921 try: 

922 session = _sessions[self.session_id] 

923 except KeyError: 

924 pass 

925 else: 

926 if session._transaction is None: 

927 session._autobegin_t() 

928 

929 if inst is None and attr: 

930 raise orm_exc.ObjectDereferencedError( 

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

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

933 "collected." 

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

935 ) 

936 

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

938 """Commit attributes. 

939 

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

941 those attributes which were refreshed from the database. 

942 

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

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

945 

946 """ 

947 for key in keys: 

948 self.committed_state.pop(key, None) 

949 

950 self.expired = False 

951 

952 self.expired_attributes.difference_update( 

953 set(keys).intersection(dict_) 

954 ) 

955 

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

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

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

959 # ensure this is what it does. 

960 if self.callables: 

961 for key in ( 

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

963 ): 

964 del self.callables[key] 

965 

966 def _commit_all( 

967 self, dict_: _InstanceDict, instance_dict: Optional[IdentityMap] = None 

968 ) -> None: 

969 """commit all attributes unconditionally. 

970 

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

972 to remove all pending state from the instance. 

973 

974 - all attributes are marked as "committed" 

975 - the "strong dirty reference" is removed 

976 - the "modified" flag is set to False 

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

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

979 

980 Attributes marked as "expired" can potentially remain 

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

982 

983 """ 

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

985 

986 @classmethod 

987 def _commit_all_states( 

988 self, 

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

990 instance_dict: Optional[IdentityMap] = None, 

991 ) -> None: 

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

993 

994 for state, dict_ in iter_: 

995 state_dict = state.__dict__ 

996 

997 state.committed_state.clear() 

998 

999 if "_pending_mutations" in state_dict: 

1000 del state_dict["_pending_mutations"] 

1001 

1002 state.expired_attributes.difference_update(dict_) 

1003 

1004 if instance_dict and state.modified: 

1005 instance_dict._modified.discard(state) 

1006 

1007 state.modified = state.expired = False 

1008 state._strong_obj = None 

1009 

1010 

1011class AttributeState: 

1012 """Provide an inspection interface corresponding 

1013 to a particular attribute on a particular mapped object. 

1014 

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

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

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

1018 

1019 from sqlalchemy import inspect 

1020 

1021 insp = inspect(some_mapped_object) 

1022 attr_state = insp.attrs.some_attribute 

1023 

1024 """ 

1025 

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

1027 

1028 state: InstanceState[Any] 

1029 key: str 

1030 

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

1032 self.state = state 

1033 self.key = key 

1034 

1035 @property 

1036 def loaded_value(self) -> Any: 

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

1038 

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

1040 in the object's dictionary, returns NO_VALUE. 

1041 

1042 """ 

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

1044 

1045 @property 

1046 def value(self) -> Any: 

1047 """Return the value of this attribute. 

1048 

1049 This operation is equivalent to accessing the object's 

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

1051 off any pending loader callables if needed. 

1052 

1053 """ 

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

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

1056 ) 

1057 

1058 @property 

1059 def history(self) -> History: 

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

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

1062 

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

1064 attribute is unloaded. 

1065 

1066 .. note:: 

1067 

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

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

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

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

1072 For 

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

1074 

1075 

1076 .. seealso:: 

1077 

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

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

1080 

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

1082 

1083 """ 

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

1085 

1086 def load_history(self) -> History: 

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

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

1089 

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

1091 attribute is unloaded. 

1092 

1093 .. note:: 

1094 

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

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

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

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

1099 For 

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

1101 

1102 .. seealso:: 

1103 

1104 :attr:`.AttributeState.history` 

1105 

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

1107 

1108 """ 

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

1110 

1111 

1112class PendingCollection: 

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

1114 

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

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

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

1118 

1119 """ 

1120 

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

1122 

1123 deleted_items: util.IdentitySet 

1124 added_items: util.OrderedIdentitySet 

1125 

1126 def __init__(self) -> None: 

1127 self.deleted_items = util.IdentitySet() 

1128 self.added_items = util.OrderedIdentitySet() 

1129 

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

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

1132 

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

1134 if value in self.deleted_items: 

1135 self.deleted_items.remove(value) 

1136 else: 

1137 self.added_items.add(value) 

1138 

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

1140 if value in self.added_items: 

1141 self.added_items.remove(value) 

1142 else: 

1143 self.deleted_items.add(value)