Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

452 statements  

1# orm/state.py 

2# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors 

3# <see AUTHORS file> 

4# 

5# This module is part of SQLAlchemy and is released under 

6# the MIT License: https://www.opensource.org/licenses/mit-license.php 

7 

8"""Defines instrumentation of instances. 

9 

10This module is usually not directly visible to user applications, but 

11defines a large part of the ORM's interactivity. 

12 

13""" 

14 

15from __future__ import annotations 

16 

17from typing import Any 

18from typing import Callable 

19from typing import Dict 

20from typing import Generic 

21from typing import Iterable 

22from typing import Optional 

23from typing import Protocol 

24from typing import Set 

25from typing import Tuple 

26from typing import TYPE_CHECKING 

27from typing import Union 

28import weakref 

29 

30from . import base 

31from . import exc as orm_exc 

32from . import interfaces 

33from ._typing import _O 

34from ._typing import is_collection_impl 

35from .base import ATTR_WAS_SET 

36from .base import INIT_OK 

37from .base import LoaderCallableStatus 

38from .base import NEVER_SET 

39from .base import NO_VALUE 

40from .base import PASSIVE_NO_INITIALIZE 

41from .base import PASSIVE_NO_RESULT 

42from .base import PASSIVE_OFF 

43from .base import SQL_OK 

44from .path_registry import PathRegistry 

45from .. import exc as sa_exc 

46from .. import inspection 

47from .. import util 

48from ..util.typing import Literal 

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 .. versionadded: 1.1 

273 

274 .. seealso:: 

275 

276 :ref:`session_object_states` 

277 

278 """ 

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

280 

281 @property 

282 def was_deleted(self) -> bool: 

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

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

285 

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

287 When the object is expunged from the session either explicitly 

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

289 this flag will continue to report True. 

290 

291 .. seealso:: 

292 

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

294 

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

296 

297 :ref:`session_object_states` 

298 

299 """ 

300 return self._deleted 

301 

302 @property 

303 def persistent(self) -> bool: 

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

305 

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

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

308 :class:`.Session`. 

309 

310 .. seealso:: 

311 

312 :ref:`session_object_states` 

313 

314 """ 

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

316 

317 @property 

318 def detached(self) -> bool: 

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

320 

321 .. seealso:: 

322 

323 :ref:`session_object_states` 

324 

325 """ 

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

327 

328 @util.non_memoized_property 

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

330 def _attached(self) -> bool: 

331 return ( 

332 self.session_id is not None 

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

334 ) 

335 

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

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

338 operations. 

339 

340 .. versionadded:: 1.3 

341 

342 """ 

343 

344 lkv = self._last_known_values 

345 if lkv is None: 

346 self._last_known_values = lkv = {} 

347 if key not in lkv: 

348 lkv[key] = NO_VALUE 

349 

350 @property 

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

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

353 or ``None`` if none available. 

354 

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

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

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

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

359 Only when the transaction is completed does the object become 

360 fully detached under normal circumstances. 

361 

362 .. seealso:: 

363 

364 :attr:`_orm.InstanceState.async_session` 

365 

366 """ 

367 if self.session_id: 

368 try: 

369 return _sessions[self.session_id] 

370 except KeyError: 

371 pass 

372 return None 

373 

374 @property 

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

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

377 or ``None`` if none available. 

378 

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

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

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

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

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

384 :class:`_orm.InstanceState`. 

385 

386 .. versionadded:: 1.4.18 

387 

388 .. seealso:: 

389 

390 :ref:`asyncio_toplevel` 

391 

392 """ 

393 if _async_provider is None: 

394 return None 

395 

396 sess = self.session 

397 if sess is not None: 

398 return _async_provider(sess) 

399 else: 

400 return None 

401 

402 @property 

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

404 """Return the mapped object represented by this 

405 :class:`.InstanceState`. 

406 

407 Returns None if the object has been garbage collected 

408 

409 """ 

410 return self.obj() 

411 

412 @property 

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

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

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

416 which can always be passed directly to 

417 :meth:`_query.Query.get`. 

418 

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

420 

421 .. note:: 

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

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

424 even if its attributes include primary key values. 

425 

426 """ 

427 if self.key is None: 

428 return None 

429 else: 

430 return self.key[1] 

431 

432 @property 

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

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

435 

436 This is the key used to locate the object within 

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

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

439 

440 

441 """ 

442 return self.key 

443 

444 @util.memoized_property 

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

446 return {} 

447 

448 @util.memoized_property 

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

450 return {} 

451 

452 @util.memoized_property 

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

454 return {} 

455 

456 @util.memoized_property 

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

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

459 return self.manager.mapper 

460 

461 @property 

462 def has_identity(self) -> bool: 

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

464 

465 This should always have the same value as the 

466 expression ``state.persistent`` or ``state.detached``. 

467 

468 """ 

469 return bool(self.key) 

470 

471 @classmethod 

472 def _detach_states( 

473 self, 

474 states: Iterable[InstanceState[_O]], 

475 session: Session, 

476 to_transient: bool = False, 

477 ) -> None: 

478 persistent_to_detached = ( 

479 session.dispatch.persistent_to_detached or None 

480 ) 

481 deleted_to_detached = session.dispatch.deleted_to_detached or None 

482 pending_to_transient = session.dispatch.pending_to_transient or None 

483 persistent_to_transient = ( 

484 session.dispatch.persistent_to_transient or None 

485 ) 

486 

487 for state in states: 

488 deleted = state._deleted 

489 pending = state.key is None 

490 persistent = not pending and not deleted 

491 

492 state.session_id = None 

493 

494 if to_transient and state.key: 

495 del state.key 

496 if persistent: 

497 if to_transient: 

498 if persistent_to_transient is not None: 

499 persistent_to_transient(session, state) 

500 elif persistent_to_detached is not None: 

501 persistent_to_detached(session, state) 

502 elif deleted and deleted_to_detached is not None: 

503 deleted_to_detached(session, state) 

504 elif pending and pending_to_transient is not None: 

505 pending_to_transient(session, state) 

506 

507 state._strong_obj = None 

508 

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

510 if session: 

511 InstanceState._detach_states([self], session) 

512 else: 

513 self.session_id = self._strong_obj = None 

514 

515 def _dispose(self) -> None: 

516 # used by the test suite, apparently 

517 self._detach() 

518 

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

520 """Weakref callback cleanup. 

521 

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

523 collected. 

524 

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

526 Will not work otherwise! 

527 

528 """ 

529 

530 # Python builtins become undefined during interpreter shutdown. 

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

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

533 if dict is None: 

534 return 

535 

536 instance_dict = self._instance_dict() 

537 if instance_dict is not None: 

538 instance_dict._fast_discard(self) 

539 del self._instance_dict 

540 

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

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

543 # is strong referencing! 

544 # assert self not in instance_dict._modified 

545 

546 self.session_id = self._strong_obj = None 

547 

548 @property 

549 def dict(self) -> _InstanceDict: 

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

551 

552 Under normal circumstances, this is always synonymous 

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

554 unless an alternative instrumentation system has been 

555 configured. 

556 

557 In the case that the actual object has been garbage 

558 collected, this accessor returns a blank dictionary. 

559 

560 """ 

561 o = self.obj() 

562 if o is not None: 

563 return base.instance_dict(o) 

564 else: 

565 return {} 

566 

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

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

569 manager = self.manager 

570 

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

572 

573 try: 

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

575 except: 

576 with util.safe_reraise(): 

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

578 

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

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

581 

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

583 return self.manager[key].impl 

584 

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

586 if key not in self._pending_mutations: 

587 self._pending_mutations[key] = PendingCollection() 

588 return self._pending_mutations[key] 

589 

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

591 state_dict: Dict[str, Any] = { 

592 "instance": self.obj(), 

593 "class_": self.class_, 

594 "committed_state": self.committed_state, 

595 "expired_attributes": self.expired_attributes, 

596 } 

597 state_dict.update( 

598 (k, self.__dict__[k]) 

599 for k in ( 

600 "_pending_mutations", 

601 "modified", 

602 "expired", 

603 "callables", 

604 "key", 

605 "parents", 

606 "load_options", 

607 "class_", 

608 "expired_attributes", 

609 "info", 

610 ) 

611 if k in self.__dict__ 

612 ) 

613 if self.load_path: 

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

615 

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

617 

618 return state_dict 

619 

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

621 inst = state_dict["instance"] 

622 if inst is not None: 

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

624 self.class_ = inst.__class__ 

625 else: 

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

627 self.class_ = state_dict["class_"] 

628 

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

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

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

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

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

634 if "info" in state_dict: 

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

636 if "callables" in state_dict: 

637 self.callables = state_dict["callables"] 

638 

639 self.expired_attributes = state_dict["expired_attributes"] 

640 else: 

641 if "expired_attributes" in state_dict: 

642 self.expired_attributes = state_dict["expired_attributes"] 

643 else: 

644 self.expired_attributes = set() 

645 

646 self.__dict__.update( 

647 [ 

648 (k, state_dict[k]) 

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

650 if k in state_dict 

651 ] 

652 ) 

653 if self.key: 

654 self.identity_token = self.key[2] 

655 

656 if "load_path" in state_dict: 

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

658 

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

660 

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

662 """Remove the given attribute and any 

663 callables associated with it.""" 

664 

665 old = dict_.pop(key, None) 

666 manager_impl = self.manager[key].impl 

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

668 manager_impl._invalidate_collection(old) 

669 self.expired_attributes.discard(key) 

670 if self.callables: 

671 self.callables.pop(key, None) 

672 

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

674 if "callables" in from_.__dict__: 

675 self.callables = dict(from_.callables) 

676 

677 @classmethod 

678 def _instance_level_callable_processor( 

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

680 ) -> _InstallLoaderCallableProto[_O]: 

681 impl = manager[key].impl 

682 if is_collection_impl(impl): 

683 fixed_impl = impl 

684 

685 def _set_callable( 

686 state: InstanceState[_O], 

687 dict_: _InstanceDict, 

688 row: Row[Unpack[TupleAny]], 

689 ) -> None: 

690 if "callables" not in state.__dict__: 

691 state.callables = {} 

692 old = dict_.pop(key, None) 

693 if old is not None: 

694 fixed_impl._invalidate_collection(old) 

695 state.callables[key] = fn 

696 

697 else: 

698 

699 def _set_callable( 

700 state: InstanceState[_O], 

701 dict_: _InstanceDict, 

702 row: Row[Unpack[TupleAny]], 

703 ) -> None: 

704 if "callables" not in state.__dict__: 

705 state.callables = {} 

706 state.callables[key] = fn 

707 

708 return _set_callable 

709 

710 def _expire( 

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

712 ) -> None: 

713 self.expired = True 

714 if self.modified: 

715 modified_set.discard(self) 

716 self.committed_state.clear() 

717 self.modified = False 

718 

719 self._strong_obj = None 

720 

721 if "_pending_mutations" in self.__dict__: 

722 del self.__dict__["_pending_mutations"] 

723 

724 if "parents" in self.__dict__: 

725 del self.__dict__["parents"] 

726 

727 self.expired_attributes.update( 

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

729 ) 

730 

731 if self.callables: 

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

733 # LoadDeferredColumns, which undefers a column at the instance 

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

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

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

737 # Before 1.4, only column-based 

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

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

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

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

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

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

744 del self.callables[k] 

745 

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

747 collection = dict_.pop(k) 

748 collection._sa_adapter.invalidated = True 

749 

750 if self._last_known_values: 

751 self._last_known_values.update( 

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

753 ) 

754 

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

756 del dict_[key] 

757 

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

759 

760 def _expire_attributes( 

761 self, 

762 dict_: _InstanceDict, 

763 attribute_names: Iterable[str], 

764 no_loader: bool = False, 

765 ) -> None: 

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

767 

768 callables = self.callables 

769 

770 for key in attribute_names: 

771 impl = self.manager[key].impl 

772 if impl.accepts_scalar_loader: 

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

774 continue 

775 

776 self.expired_attributes.add(key) 

777 if callables and key in callables: 

778 del callables[key] 

779 old = dict_.pop(key, NO_VALUE) 

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

781 impl._invalidate_collection(old) 

782 

783 lkv = self._last_known_values 

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

785 lkv[key] = old 

786 

787 self.committed_state.pop(key, None) 

788 if pending: 

789 pending.pop(key, None) 

790 

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

792 

793 def _load_expired( 

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

795 ) -> LoaderCallableStatus: 

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

797 callable for loading expired attributes, which is also 

798 serializable (picklable). 

799 

800 """ 

801 

802 if not passive & SQL_OK: 

803 return PASSIVE_NO_RESULT 

804 

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

806 toload = toload.difference( 

807 attr 

808 for attr in toload 

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

810 ) 

811 

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

813 

814 # if the loader failed, or this 

815 # instance state didn't have an identity, 

816 # the attributes still might be in the callables 

817 # dict. ensure they are removed. 

818 self.expired_attributes.clear() 

819 

820 return ATTR_WAS_SET 

821 

822 @property 

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

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

825 

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

827 

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

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

830 

831 return ( 

832 set(keys) 

833 .intersection(self.manager) 

834 .difference(self.committed_state) 

835 ) 

836 

837 @property 

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

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

840 

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

842 populated or modified. 

843 

844 """ 

845 return ( 

846 set(self.manager) 

847 .difference(self.committed_state) 

848 .difference(self.dict) 

849 ) 

850 

851 @property 

852 @util.deprecated( 

853 "2.0", 

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

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

856 ) 

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

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

859 

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

861 point and should be considered to be private. 

862 

863 """ 

864 return self.unloaded 

865 

866 @property 

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

868 return self.unloaded.intersection( 

869 attr 

870 for attr in self.manager 

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

872 ) 

873 

874 def _modified_event( 

875 self, 

876 dict_: _InstanceDict, 

877 attr: Optional[AttributeImpl], 

878 previous: Any, 

879 collection: bool = False, 

880 is_userland: bool = False, 

881 ) -> None: 

882 if attr: 

883 if not attr.send_modified_events: 

884 return 

885 if is_userland and attr.key not in dict_: 

886 raise sa_exc.InvalidRequestError( 

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

888 "the object state" % attr.key 

889 ) 

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

891 if collection: 

892 if TYPE_CHECKING: 

893 assert is_collection_impl(attr) 

894 if previous is NEVER_SET: 

895 if attr.key in dict_: 

896 previous = dict_[attr.key] 

897 

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

899 previous = attr.copy(previous) 

900 self.committed_state[attr.key] = previous 

901 

902 lkv = self._last_known_values 

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

904 lkv[attr.key] = NO_VALUE 

905 

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

907 

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

909 self.modified = True 

910 instance_dict = self._instance_dict() 

911 if instance_dict: 

912 has_modified = bool(instance_dict._modified) 

913 instance_dict._modified.add(self) 

914 else: 

915 has_modified = False 

916 

917 # only create _strong_obj link if attached 

918 # to a session 

919 

920 inst = self.obj() 

921 if self.session_id: 

922 self._strong_obj = inst 

923 

924 # if identity map already had modified objects, 

925 # assume autobegin already occurred, else check 

926 # for autobegin 

927 if not has_modified: 

928 # inline of autobegin, to ensure session transaction 

929 # snapshot is established 

930 try: 

931 session = _sessions[self.session_id] 

932 except KeyError: 

933 pass 

934 else: 

935 if session._transaction is None: 

936 session._autobegin_t() 

937 

938 if inst is None and attr: 

939 raise orm_exc.ObjectDereferencedError( 

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

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

942 "collected." 

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

944 ) 

945 

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

947 """Commit attributes. 

948 

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

950 those attributes which were refreshed from the database. 

951 

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

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

954 

955 """ 

956 for key in keys: 

957 self.committed_state.pop(key, None) 

958 

959 self.expired = False 

960 

961 self.expired_attributes.difference_update( 

962 set(keys).intersection(dict_) 

963 ) 

964 

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

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

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

968 # ensure this is what it does. 

969 if self.callables: 

970 for key in ( 

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

972 ): 

973 del self.callables[key] 

974 

975 def _commit_all( 

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

977 ) -> None: 

978 """commit all attributes unconditionally. 

979 

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

981 to remove all pending state from the instance. 

982 

983 - all attributes are marked as "committed" 

984 - the "strong dirty reference" is removed 

985 - the "modified" flag is set to False 

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

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

988 

989 Attributes marked as "expired" can potentially remain 

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

991 

992 """ 

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

994 

995 @classmethod 

996 def _commit_all_states( 

997 self, 

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

999 instance_dict: Optional[IdentityMap] = None, 

1000 ) -> None: 

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

1002 

1003 for state, dict_ in iter_: 

1004 state_dict = state.__dict__ 

1005 

1006 state.committed_state.clear() 

1007 

1008 if "_pending_mutations" in state_dict: 

1009 del state_dict["_pending_mutations"] 

1010 

1011 state.expired_attributes.difference_update(dict_) 

1012 

1013 if instance_dict and state.modified: 

1014 instance_dict._modified.discard(state) 

1015 

1016 state.modified = state.expired = False 

1017 state._strong_obj = None 

1018 

1019 

1020class AttributeState: 

1021 """Provide an inspection interface corresponding 

1022 to a particular attribute on a particular mapped object. 

1023 

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

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

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

1027 

1028 from sqlalchemy import inspect 

1029 

1030 insp = inspect(some_mapped_object) 

1031 attr_state = insp.attrs.some_attribute 

1032 

1033 """ 

1034 

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

1036 

1037 state: InstanceState[Any] 

1038 key: str 

1039 

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

1041 self.state = state 

1042 self.key = key 

1043 

1044 @property 

1045 def loaded_value(self) -> Any: 

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

1047 

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

1049 in the object's dictionary, returns NO_VALUE. 

1050 

1051 """ 

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

1053 

1054 @property 

1055 def value(self) -> Any: 

1056 """Return the value of this attribute. 

1057 

1058 This operation is equivalent to accessing the object's 

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

1060 off any pending loader callables if needed. 

1061 

1062 """ 

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

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

1065 ) 

1066 

1067 @property 

1068 def history(self) -> History: 

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

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

1071 

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

1073 attribute is unloaded. 

1074 

1075 .. note:: 

1076 

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

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

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

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

1081 For 

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

1083 

1084 

1085 .. seealso:: 

1086 

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

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

1089 

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

1091 

1092 """ 

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

1094 

1095 def load_history(self) -> History: 

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

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

1098 

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

1100 attribute is unloaded. 

1101 

1102 .. note:: 

1103 

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

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

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

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

1108 For 

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

1110 

1111 .. seealso:: 

1112 

1113 :attr:`.AttributeState.history` 

1114 

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

1116 

1117 """ 

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

1119 

1120 

1121class PendingCollection: 

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

1123 

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

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

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

1127 

1128 """ 

1129 

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

1131 

1132 deleted_items: util.IdentitySet 

1133 added_items: util.OrderedIdentitySet 

1134 

1135 def __init__(self) -> None: 

1136 self.deleted_items = util.IdentitySet() 

1137 self.added_items = util.OrderedIdentitySet() 

1138 

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

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

1141 

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

1143 if value in self.deleted_items: 

1144 self.deleted_items.remove(value) 

1145 else: 

1146 self.added_items.add(value) 

1147 

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

1149 if value in self.added_items: 

1150 self.added_items.remove(value) 

1151 else: 

1152 self.deleted_items.add(value)