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

402 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1# orm/state.py 

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

15import weakref 

16 

17from . import base 

18from . import exc as orm_exc 

19from . import interfaces 

20from .base import ATTR_WAS_SET 

21from .base import INIT_OK 

22from .base import NEVER_SET 

23from .base import NO_VALUE 

24from .base import PASSIVE_NO_INITIALIZE 

25from .base import PASSIVE_NO_RESULT 

26from .base import PASSIVE_OFF 

27from .base import SQL_OK 

28from .path_registry import PathRegistry 

29from .. import exc as sa_exc 

30from .. import inspection 

31from .. import util 

32 

33 

34# late-populated by session.py 

35_sessions = None 

36 

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

38_async_provider = None 

39 

40 

41@inspection._self_inspects 

42class InstanceState(interfaces.InspectionAttrInfo): 

43 """tracks state information at the instance level. 

44 

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

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

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

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

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

50 

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

52 available for runtime inspection as to the state of a 

53 mapped instance, including information such as its current 

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

55 about data on individual attributes. The public API 

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

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

58 

59 >>> from sqlalchemy import inspect 

60 >>> insp = inspect(some_mapped_object) 

61 >>> insp.attrs.nickname.history 

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

63 

64 .. seealso:: 

65 

66 :ref:`orm_mapper_inspection_instancestate` 

67 

68 """ 

69 

70 session_id = None 

71 key = None 

72 runid = None 

73 load_options = () 

74 load_path = PathRegistry.root 

75 insert_order = None 

76 _strong_obj = None 

77 modified = False 

78 expired = False 

79 _deleted = False 

80 _load_pending = False 

81 _orphaned_outside_of_session = False 

82 is_instance = True 

83 identity_token = None 

84 _last_known_values = () 

85 

86 callables = () 

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

88 

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

90 loaders that were set up via query option. 

91 

92 Previously, callables was used also to indicate expired attributes 

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

94 This role is now handled by the expired_attributes set. 

95 

96 """ 

97 

98 def __init__(self, obj, manager): 

99 self.class_ = obj.__class__ 

100 self.manager = manager 

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

102 self.committed_state = {} 

103 self.expired_attributes = set() 

104 

105 expired_attributes = None 

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

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

108 changes. 

109 

110 see also the ``unmodified`` collection which is intersected 

111 against this set when a refresh operation occurs.""" 

112 

113 @util.memoized_property 

114 def attrs(self): 

115 """Return a namespace representing each attribute on 

116 the mapped object, including its current value 

117 and history. 

118 

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

120 This object allows inspection of the current data 

121 within an attribute as well as attribute history 

122 since the last flush. 

123 

124 """ 

125 return util.ImmutableProperties( 

126 dict((key, AttributeState(self, key)) for key in self.manager) 

127 ) 

128 

129 @property 

130 def transient(self): 

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

132 

133 .. seealso:: 

134 

135 :ref:`session_object_states` 

136 

137 """ 

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

139 

140 @property 

141 def pending(self): 

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

143 

144 

145 .. seealso:: 

146 

147 :ref:`session_object_states` 

148 

149 """ 

150 return self.key is None and self._attached 

151 

152 @property 

153 def deleted(self): 

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

155 

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

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

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

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

160 the identity map. 

161 

162 .. note:: 

163 

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

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

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

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

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

169 of whether or not the object is associated with a 

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

171 accessor. 

172 

173 .. versionadded: 1.1 

174 

175 .. seealso:: 

176 

177 :ref:`session_object_states` 

178 

179 """ 

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

181 

182 @property 

183 def was_deleted(self): 

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

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

186 

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

188 When the object is expunged from the session either explicitly 

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

190 this flag will continue to report True. 

191 

192 .. versionadded:: 1.1 - added a local method form of 

193 :func:`.orm.util.was_deleted`. 

194 

195 .. seealso:: 

196 

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

198 

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

200 

201 :ref:`session_object_states` 

202 

203 """ 

204 return self._deleted 

205 

206 @property 

207 def persistent(self): 

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

209 

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

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

212 :class:`.Session`. 

213 

214 .. versionchanged:: 1.1 The :attr:`.InstanceState.persistent` 

215 accessor no longer returns True for an object that was 

216 "deleted" within a flush; use the :attr:`.InstanceState.deleted` 

217 accessor to detect this state. This allows the "persistent" 

218 state to guarantee membership in the identity map. 

219 

220 .. seealso:: 

221 

222 :ref:`session_object_states` 

223 

224 """ 

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

226 

227 @property 

228 def detached(self): 

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

230 

231 .. seealso:: 

232 

233 :ref:`session_object_states` 

234 

235 """ 

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

237 

238 @property 

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

240 def _attached(self): 

241 return ( 

242 self.session_id is not None 

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

244 ) 

245 

246 def _track_last_known_value(self, key): 

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

248 operations. 

249 

250 .. versionadded:: 1.3 

251 

252 """ 

253 

254 if key not in self._last_known_values: 

255 self._last_known_values = dict(self._last_known_values) 

256 self._last_known_values[key] = NO_VALUE 

257 

258 @property 

259 def session(self): 

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

261 or ``None`` if none available. 

262 

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

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

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

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

267 Only when the transaction is completed does the object become 

268 fully detached under normal circumstances. 

269 

270 .. seealso:: 

271 

272 :attr:`_orm.InstanceState.async_session` 

273 

274 """ 

275 if self.session_id: 

276 try: 

277 return _sessions[self.session_id] 

278 except KeyError: 

279 pass 

280 return None 

281 

282 @property 

283 def async_session(self): 

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

285 or ``None`` if none available. 

286 

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

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

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

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

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

292 :class:`_orm.InstanceState`. 

293 

294 .. versionadded:: 1.4.18 

295 

296 .. seealso:: 

297 

298 :ref:`asyncio_toplevel` 

299 

300 """ 

301 if _async_provider is None: 

302 return None 

303 

304 sess = self.session 

305 if sess is not None: 

306 return _async_provider(sess) 

307 else: 

308 return None 

309 

310 @property 

311 def object(self): 

312 """Return the mapped object represented by this 

313 :class:`.InstanceState`.""" 

314 return self.obj() 

315 

316 @property 

317 def identity(self): 

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

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

320 which can always be passed directly to 

321 :meth:`_query.Query.get`. 

322 

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

324 

325 .. note:: 

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

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

328 even if its attributes include primary key values. 

329 

330 """ 

331 if self.key is None: 

332 return None 

333 else: 

334 return self.key[1] 

335 

336 @property 

337 def identity_key(self): 

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

339 

340 This is the key used to locate the object within 

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

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

343 

344 

345 """ 

346 # TODO: just change .key to .identity_key across 

347 # the board ? probably 

348 return self.key 

349 

350 @util.memoized_property 

351 def parents(self): 

352 return {} 

353 

354 @util.memoized_property 

355 def _pending_mutations(self): 

356 return {} 

357 

358 @util.memoized_property 

359 def _empty_collections(self): 

360 return {} 

361 

362 @util.memoized_property 

363 def mapper(self): 

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

365 return self.manager.mapper 

366 

367 @property 

368 def has_identity(self): 

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

370 

371 This should always have the same value as the 

372 expression ``state.persistent`` or ``state.detached``. 

373 

374 """ 

375 return bool(self.key) 

376 

377 @classmethod 

378 def _detach_states(self, states, session, to_transient=False): 

379 persistent_to_detached = ( 

380 session.dispatch.persistent_to_detached or None 

381 ) 

382 deleted_to_detached = session.dispatch.deleted_to_detached or None 

383 pending_to_transient = session.dispatch.pending_to_transient or None 

384 persistent_to_transient = ( 

385 session.dispatch.persistent_to_transient or None 

386 ) 

387 

388 for state in states: 

389 deleted = state._deleted 

390 pending = state.key is None 

391 persistent = not pending and not deleted 

392 

393 state.session_id = None 

394 

395 if to_transient and state.key: 

396 del state.key 

397 if persistent: 

398 if to_transient: 

399 if persistent_to_transient is not None: 

400 persistent_to_transient(session, state) 

401 elif persistent_to_detached is not None: 

402 persistent_to_detached(session, state) 

403 elif deleted and deleted_to_detached is not None: 

404 deleted_to_detached(session, state) 

405 elif pending and pending_to_transient is not None: 

406 pending_to_transient(session, state) 

407 

408 state._strong_obj = None 

409 

410 def _detach(self, session=None): 

411 if session: 

412 InstanceState._detach_states([self], session) 

413 else: 

414 self.session_id = self._strong_obj = None 

415 

416 def _dispose(self): 

417 self._detach() 

418 del self.obj 

419 

420 def _cleanup(self, ref): 

421 """Weakref callback cleanup. 

422 

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

424 collected. 

425 

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

427 Will not work otherwise! 

428 

429 """ 

430 

431 # Python builtins become undefined during interpreter shutdown. 

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

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

434 if dict is None: 

435 return 

436 

437 instance_dict = self._instance_dict() 

438 if instance_dict is not None: 

439 instance_dict._fast_discard(self) 

440 del self._instance_dict 

441 

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

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

444 # is strong referencing! 

445 # assert self not in instance_dict._modified 

446 

447 self.session_id = self._strong_obj = None 

448 del self.obj 

449 

450 def obj(self): 

451 return None 

452 

453 @property 

454 def dict(self): 

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

456 

457 Under normal circumstances, this is always synonymous 

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

459 unless an alternative instrumentation system has been 

460 configured. 

461 

462 In the case that the actual object has been garbage 

463 collected, this accessor returns a blank dictionary. 

464 

465 """ 

466 o = self.obj() 

467 if o is not None: 

468 return base.instance_dict(o) 

469 else: 

470 return {} 

471 

472 def _initialize_instance(*mixed, **kwargs): 

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

474 manager = self.manager 

475 

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

477 

478 try: 

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

480 except: 

481 with util.safe_reraise(): 

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

483 

484 def get_history(self, key, passive): 

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

486 

487 def get_impl(self, key): 

488 return self.manager[key].impl 

489 

490 def _get_pending_mutation(self, key): 

491 if key not in self._pending_mutations: 

492 self._pending_mutations[key] = PendingCollection() 

493 return self._pending_mutations[key] 

494 

495 def __getstate__(self): 

496 state_dict = {"instance": self.obj()} 

497 state_dict.update( 

498 (k, self.__dict__[k]) 

499 for k in ( 

500 "committed_state", 

501 "_pending_mutations", 

502 "modified", 

503 "expired", 

504 "callables", 

505 "key", 

506 "parents", 

507 "load_options", 

508 "class_", 

509 "expired_attributes", 

510 "info", 

511 ) 

512 if k in self.__dict__ 

513 ) 

514 if self.load_path: 

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

516 

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

518 

519 return state_dict 

520 

521 def __setstate__(self, state_dict): 

522 inst = state_dict["instance"] 

523 if inst is not None: 

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

525 self.class_ = inst.__class__ 

526 else: 

527 # None being possible here generally new as of 0.7.4 

528 # due to storage of state in "parents". "class_" 

529 # also new. 

530 self.obj = None 

531 self.class_ = state_dict["class_"] 

532 

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

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

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

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

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

538 if "info" in state_dict: 

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

540 if "callables" in state_dict: 

541 self.callables = state_dict["callables"] 

542 

543 try: 

544 self.expired_attributes = state_dict["expired_attributes"] 

545 except KeyError: 

546 self.expired_attributes = set() 

547 # 0.9 and earlier compat 

548 for k in list(self.callables): 

549 if self.callables[k] is self: 

550 self.expired_attributes.add(k) 

551 del self.callables[k] 

552 else: 

553 if "expired_attributes" in state_dict: 

554 self.expired_attributes = state_dict["expired_attributes"] 

555 else: 

556 self.expired_attributes = set() 

557 

558 self.__dict__.update( 

559 [ 

560 (k, state_dict[k]) 

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

562 if k in state_dict 

563 ] 

564 ) 

565 if self.key: 

566 try: 

567 self.identity_token = self.key[2] 

568 except IndexError: 

569 # 1.1 and earlier compat before identity_token 

570 assert len(self.key) == 2 

571 self.key = self.key + (None,) 

572 self.identity_token = None 

573 

574 if "load_path" in state_dict: 

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

576 

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

578 

579 def _reset(self, dict_, key): 

580 """Remove the given attribute and any 

581 callables associated with it.""" 

582 

583 old = dict_.pop(key, None) 

584 if old is not None and self.manager[key].impl.collection: 

585 self.manager[key].impl._invalidate_collection(old) 

586 self.expired_attributes.discard(key) 

587 if self.callables: 

588 self.callables.pop(key, None) 

589 

590 def _copy_callables(self, from_): 

591 if "callables" in from_.__dict__: 

592 self.callables = dict(from_.callables) 

593 

594 @classmethod 

595 def _instance_level_callable_processor(cls, manager, fn, key): 

596 impl = manager[key].impl 

597 if impl.collection: 

598 

599 def _set_callable(state, dict_, row): 

600 if "callables" not in state.__dict__: 

601 state.callables = {} 

602 old = dict_.pop(key, None) 

603 if old is not None: 

604 impl._invalidate_collection(old) 

605 state.callables[key] = fn 

606 

607 else: 

608 

609 def _set_callable(state, dict_, row): 

610 if "callables" not in state.__dict__: 

611 state.callables = {} 

612 state.callables[key] = fn 

613 

614 return _set_callable 

615 

616 def _expire(self, dict_, modified_set): 

617 self.expired = True 

618 if self.modified: 

619 modified_set.discard(self) 

620 self.committed_state.clear() 

621 self.modified = False 

622 

623 self._strong_obj = None 

624 

625 if "_pending_mutations" in self.__dict__: 

626 del self.__dict__["_pending_mutations"] 

627 

628 if "parents" in self.__dict__: 

629 del self.__dict__["parents"] 

630 

631 self.expired_attributes.update( 

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

633 ) 

634 

635 if self.callables: 

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

637 # LoadDeferredColumns, which undefers a column at the instance 

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

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

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

641 # Before 1.4, only column-based 

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

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

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

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

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

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

648 del self.callables[k] 

649 

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

651 collection = dict_.pop(k) 

652 collection._sa_adapter.invalidated = True 

653 

654 if self._last_known_values: 

655 self._last_known_values.update( 

656 (k, dict_[k]) for k in self._last_known_values if k in dict_ 

657 ) 

658 

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

660 del dict_[key] 

661 

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

663 

664 def _expire_attributes(self, dict_, attribute_names, no_loader=False): 

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

666 

667 callables = self.callables 

668 

669 for key in attribute_names: 

670 impl = self.manager[key].impl 

671 if impl.accepts_scalar_loader: 

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

673 continue 

674 

675 self.expired_attributes.add(key) 

676 if callables and key in callables: 

677 del callables[key] 

678 old = dict_.pop(key, NO_VALUE) 

679 if impl.collection and old is not NO_VALUE: 

680 impl._invalidate_collection(old) 

681 

682 if ( 

683 self._last_known_values 

684 and key in self._last_known_values 

685 and old is not NO_VALUE 

686 ): 

687 self._last_known_values[key] = old 

688 

689 self.committed_state.pop(key, None) 

690 if pending: 

691 pending.pop(key, None) 

692 

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

694 

695 def _load_expired(self, state, passive): 

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

697 callable for loading expired attributes, which is also 

698 serializable (picklable). 

699 

700 """ 

701 

702 if not passive & SQL_OK: 

703 return PASSIVE_NO_RESULT 

704 

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

706 toload = toload.difference( 

707 attr 

708 for attr in toload 

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

710 ) 

711 

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

713 

714 # if the loader failed, or this 

715 # instance state didn't have an identity, 

716 # the attributes still might be in the callables 

717 # dict. ensure they are removed. 

718 self.expired_attributes.clear() 

719 

720 return ATTR_WAS_SET 

721 

722 @property 

723 def unmodified(self): 

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

725 

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

727 

728 def unmodified_intersection(self, keys): 

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

730 

731 return ( 

732 set(keys) 

733 .intersection(self.manager) 

734 .difference(self.committed_state) 

735 ) 

736 

737 @property 

738 def unloaded(self): 

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

740 

741 This includes expired attributes and any other attribute that 

742 was never populated or modified. 

743 

744 """ 

745 return ( 

746 set(self.manager) 

747 .difference(self.committed_state) 

748 .difference(self.dict) 

749 ) 

750 

751 @property 

752 def unloaded_expirable(self): 

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

754 

755 This includes expired attributes and any other attribute that 

756 was never populated or modified. 

757 

758 """ 

759 return self.unloaded 

760 

761 @property 

762 def _unloaded_non_object(self): 

763 return self.unloaded.intersection( 

764 attr 

765 for attr in self.manager 

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

767 ) 

768 

769 def _instance_dict(self): 

770 return None 

771 

772 def _modified_event( 

773 self, dict_, attr, previous, collection=False, is_userland=False 

774 ): 

775 if attr: 

776 if not attr.send_modified_events: 

777 return 

778 if is_userland and attr.key not in dict_: 

779 raise sa_exc.InvalidRequestError( 

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

781 "the object state" % attr.key 

782 ) 

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

784 if collection: 

785 if previous is NEVER_SET: 

786 if attr.key in dict_: 

787 previous = dict_[attr.key] 

788 

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

790 previous = attr.copy(previous) 

791 self.committed_state[attr.key] = previous 

792 

793 if attr.key in self._last_known_values: 

794 self._last_known_values[attr.key] = NO_VALUE 

795 

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

797 

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

799 self.modified = True 

800 instance_dict = self._instance_dict() 

801 if instance_dict: 

802 has_modified = bool(instance_dict._modified) 

803 instance_dict._modified.add(self) 

804 else: 

805 has_modified = False 

806 

807 # only create _strong_obj link if attached 

808 # to a session 

809 

810 inst = self.obj() 

811 if self.session_id: 

812 self._strong_obj = inst 

813 

814 # if identity map already had modified objects, 

815 # assume autobegin already occurred, else check 

816 # for autobegin 

817 if not has_modified: 

818 # inline of autobegin, to ensure session transaction 

819 # snapshot is established 

820 try: 

821 session = _sessions[self.session_id] 

822 except KeyError: 

823 pass 

824 else: 

825 if session._transaction is None: 

826 session._autobegin() 

827 

828 if inst is None and attr: 

829 raise orm_exc.ObjectDereferencedError( 

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

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

832 "collected." 

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

834 ) 

835 

836 def _commit(self, dict_, keys): 

837 """Commit attributes. 

838 

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

840 those attributes which were refreshed from the database. 

841 

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

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

844 

845 """ 

846 for key in keys: 

847 self.committed_state.pop(key, None) 

848 

849 self.expired = False 

850 

851 self.expired_attributes.difference_update( 

852 set(keys).intersection(dict_) 

853 ) 

854 

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

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

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

858 # ensure this is what it does. 

859 if self.callables: 

860 for key in ( 

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

862 ): 

863 del self.callables[key] 

864 

865 def _commit_all(self, dict_, instance_dict=None): 

866 """commit all attributes unconditionally. 

867 

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

869 to remove all pending state from the instance. 

870 

871 - all attributes are marked as "committed" 

872 - the "strong dirty reference" is removed 

873 - the "modified" flag is set to False 

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

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

876 

877 Attributes marked as "expired" can potentially remain 

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

879 

880 """ 

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

882 

883 @classmethod 

884 def _commit_all_states(self, iter_, instance_dict=None): 

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

886 

887 for state, dict_ in iter_: 

888 state_dict = state.__dict__ 

889 

890 state.committed_state.clear() 

891 

892 if "_pending_mutations" in state_dict: 

893 del state_dict["_pending_mutations"] 

894 

895 state.expired_attributes.difference_update(dict_) 

896 

897 if instance_dict and state.modified: 

898 instance_dict._modified.discard(state) 

899 

900 state.modified = state.expired = False 

901 state._strong_obj = None 

902 

903 

904class AttributeState(object): 

905 """Provide an inspection interface corresponding 

906 to a particular attribute on a particular mapped object. 

907 

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

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

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

911 

912 from sqlalchemy import inspect 

913 

914 insp = inspect(some_mapped_object) 

915 attr_state = insp.attrs.some_attribute 

916 

917 """ 

918 

919 def __init__(self, state, key): 

920 self.state = state 

921 self.key = key 

922 

923 @property 

924 def loaded_value(self): 

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

926 

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

928 in the object's dictionary, returns NO_VALUE. 

929 

930 """ 

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

932 

933 @property 

934 def value(self): 

935 """Return the value of this attribute. 

936 

937 This operation is equivalent to accessing the object's 

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

939 off any pending loader callables if needed. 

940 

941 """ 

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

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

944 ) 

945 

946 @property 

947 def history(self): 

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

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

950 

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

952 attribute is unloaded. 

953 

954 .. note:: 

955 

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

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

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

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

960 For 

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

962 

963 

964 .. seealso:: 

965 

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

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

968 

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

970 

971 """ 

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

973 

974 def load_history(self): 

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

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

977 

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

979 attribute is unloaded. 

980 

981 .. note:: 

982 

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

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

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

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

987 For 

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

989 

990 .. seealso:: 

991 

992 :attr:`.AttributeState.history` 

993 

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

995 

996 .. versionadded:: 0.9.0 

997 

998 """ 

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

1000 

1001 

1002class PendingCollection(object): 

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

1004 

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

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

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

1008 

1009 """ 

1010 

1011 def __init__(self): 

1012 self.deleted_items = util.IdentitySet() 

1013 self.added_items = util.OrderedIdentitySet() 

1014 

1015 def append(self, value): 

1016 if value in self.deleted_items: 

1017 self.deleted_items.remove(value) 

1018 else: 

1019 self.added_items.add(value) 

1020 

1021 def remove(self, value): 

1022 if value in self.added_items: 

1023 self.added_items.remove(value) 

1024 else: 

1025 self.deleted_items.add(value)