Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# orm/state.py 

2# Copyright (C) 2005-2020 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: http://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@inspection._self_inspects 

35class InstanceState(interfaces.InspectionAttrInfo): 

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

37 

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

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

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

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

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

43 

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

45 available for runtime inspection as to the state of a 

46 mapped instance, including information such as its current 

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

48 about data on individual attributes. The public API 

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

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

51 

52 >>> from sqlalchemy import inspect 

53 >>> insp = inspect(some_mapped_object) 

54 

55 .. seealso:: 

56 

57 :ref:`core_inspection_toplevel` 

58 

59 """ 

60 

61 session_id = None 

62 key = None 

63 runid = None 

64 load_options = util.EMPTY_SET 

65 load_path = PathRegistry.root 

66 insert_order = None 

67 _strong_obj = None 

68 modified = False 

69 expired = False 

70 _deleted = False 

71 _load_pending = False 

72 _orphaned_outside_of_session = False 

73 is_instance = True 

74 identity_token = None 

75 _last_known_values = () 

76 

77 callables = () 

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

79 

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

81 loaders that were set up via query option. 

82 

83 Previously, callables was used also to indicate expired attributes 

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

85 This role is now handled by the expired_attributes set. 

86 

87 """ 

88 

89 def __init__(self, obj, manager): 

90 self.class_ = obj.__class__ 

91 self.manager = manager 

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

93 self.committed_state = {} 

94 self.expired_attributes = set() 

95 

96 expired_attributes = None 

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

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

99 changes. 

100 

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

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

103 

104 @util.memoized_property 

105 def attrs(self): 

106 """Return a namespace representing each attribute on 

107 the mapped object, including its current value 

108 and history. 

109 

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

111 This object allows inspection of the current data 

112 within an attribute as well as attribute history 

113 since the last flush. 

114 

115 """ 

116 return util.ImmutableProperties( 

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

118 ) 

119 

120 @property 

121 def transient(self): 

122 """Return true if the object is :term:`transient`. 

123 

124 .. seealso:: 

125 

126 :ref:`session_object_states` 

127 

128 """ 

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

130 

131 @property 

132 def pending(self): 

133 """Return true if the object is :term:`pending`. 

134 

135 

136 .. seealso:: 

137 

138 :ref:`session_object_states` 

139 

140 """ 

141 return self.key is None and self._attached 

142 

143 @property 

144 def deleted(self): 

145 """Return true if the object is :term:`deleted`. 

146 

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

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

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

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

151 the identity map. 

152 

153 .. note:: 

154 

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

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

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

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

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

160 of whether or not the object is associated with a 

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

162 accessor. 

163 

164 .. versionadded: 1.1 

165 

166 .. seealso:: 

167 

168 :ref:`session_object_states` 

169 

170 """ 

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

172 

173 @property 

174 def was_deleted(self): 

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

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

177 

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

179 When the object is expunged from the session either explicitly 

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

181 this flag will continue to report True. 

182 

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

184 :func:`.orm.util.was_deleted`. 

185 

186 .. seealso:: 

187 

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

189 

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

191 

192 :ref:`session_object_states` 

193 

194 """ 

195 return self._deleted 

196 

197 @property 

198 def persistent(self): 

199 """Return true if the object is :term:`persistent`. 

200 

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

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

203 :class:`.Session`. 

204 

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

206 accessor no longer returns True for an object that was 

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

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

209 state to guarantee membership in the identity map. 

210 

211 .. seealso:: 

212 

213 :ref:`session_object_states` 

214 

215 """ 

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

217 

218 @property 

219 def detached(self): 

220 """Return true if the object is :term:`detached`. 

221 

222 .. seealso:: 

223 

224 :ref:`session_object_states` 

225 

226 """ 

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

228 

229 @property 

230 @util.dependencies("sqlalchemy.orm.session") 

231 def _attached(self, sessionlib): 

232 return ( 

233 self.session_id is not None 

234 and self.session_id in sessionlib._sessions 

235 ) 

236 

237 def _track_last_known_value(self, key): 

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

239 operations. 

240 

241 .. versionadded:: 1.3 

242 

243 """ 

244 

245 if key not in self._last_known_values: 

246 self._last_known_values = dict(self._last_known_values) 

247 self._last_known_values[key] = NO_VALUE 

248 

249 @property 

250 @util.dependencies("sqlalchemy.orm.session") 

251 def session(self, sessionlib): 

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

253 or ``None`` if none available. 

254 

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

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

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

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

259 Only when the transaction is completed does the object become 

260 fully detached under normal circumstances. 

261 

262 """ 

263 return sessionlib._state_session(self) 

264 

265 @property 

266 def object(self): 

267 """Return the mapped object represented by this 

268 :class:`.InstanceState`.""" 

269 return self.obj() 

270 

271 @property 

272 def identity(self): 

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

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

275 which can always be passed directly to 

276 :meth:`_query.Query.get`. 

277 

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

279 

280 .. note:: 

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

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

283 even if its attributes include primary key values. 

284 

285 """ 

286 if self.key is None: 

287 return None 

288 else: 

289 return self.key[1] 

290 

291 @property 

292 def identity_key(self): 

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

294 

295 This is the key used to locate the object within 

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

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

298 

299 

300 """ 

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

302 # the board ? probably 

303 return self.key 

304 

305 @util.memoized_property 

306 def parents(self): 

307 return {} 

308 

309 @util.memoized_property 

310 def _pending_mutations(self): 

311 return {} 

312 

313 @util.memoized_property 

314 def mapper(self): 

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

316 return self.manager.mapper 

317 

318 @property 

319 def has_identity(self): 

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

321 

322 This should always have the same value as the 

323 expression ``state.persistent or state.detached``. 

324 

325 """ 

326 return bool(self.key) 

327 

328 @classmethod 

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

330 persistent_to_detached = ( 

331 session.dispatch.persistent_to_detached or None 

332 ) 

333 deleted_to_detached = session.dispatch.deleted_to_detached or None 

334 pending_to_transient = session.dispatch.pending_to_transient or None 

335 persistent_to_transient = ( 

336 session.dispatch.persistent_to_transient or None 

337 ) 

338 

339 for state in states: 

340 deleted = state._deleted 

341 pending = state.key is None 

342 persistent = not pending and not deleted 

343 

344 state.session_id = None 

345 

346 if to_transient and state.key: 

347 del state.key 

348 if persistent: 

349 if to_transient: 

350 if persistent_to_transient is not None: 

351 persistent_to_transient(session, state) 

352 elif persistent_to_detached is not None: 

353 persistent_to_detached(session, state) 

354 elif deleted and deleted_to_detached is not None: 

355 deleted_to_detached(session, state) 

356 elif pending and pending_to_transient is not None: 

357 pending_to_transient(session, state) 

358 

359 state._strong_obj = None 

360 

361 def _detach(self, session=None): 

362 if session: 

363 InstanceState._detach_states([self], session) 

364 else: 

365 self.session_id = self._strong_obj = None 

366 

367 def _dispose(self): 

368 self._detach() 

369 del self.obj 

370 

371 def _cleanup(self, ref): 

372 """Weakref callback cleanup. 

373 

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

375 collected. 

376 

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

378 Will not work otherwise! 

379 

380 """ 

381 

382 # Python builtins become undefined during interpreter shutdown. 

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

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

385 if dict is None: 

386 return 

387 

388 instance_dict = self._instance_dict() 

389 if instance_dict is not None: 

390 instance_dict._fast_discard(self) 

391 del self._instance_dict 

392 

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

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

395 # is strong referencing! 

396 # assert self not in instance_dict._modified 

397 

398 self.session_id = self._strong_obj = None 

399 del self.obj 

400 

401 def obj(self): 

402 return None 

403 

404 @property 

405 def dict(self): 

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

407 

408 Under normal circumstances, this is always synonymous 

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

410 unless an alternative instrumentation system has been 

411 configured. 

412 

413 In the case that the actual object has been garbage 

414 collected, this accessor returns a blank dictionary. 

415 

416 """ 

417 o = self.obj() 

418 if o is not None: 

419 return base.instance_dict(o) 

420 else: 

421 return {} 

422 

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

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

425 manager = self.manager 

426 

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

428 

429 try: 

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

431 except: 

432 with util.safe_reraise(): 

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

434 

435 def get_history(self, key, passive): 

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

437 

438 def get_impl(self, key): 

439 return self.manager[key].impl 

440 

441 def _get_pending_mutation(self, key): 

442 if key not in self._pending_mutations: 

443 self._pending_mutations[key] = PendingCollection() 

444 return self._pending_mutations[key] 

445 

446 def __getstate__(self): 

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

448 state_dict.update( 

449 (k, self.__dict__[k]) 

450 for k in ( 

451 "committed_state", 

452 "_pending_mutations", 

453 "modified", 

454 "expired", 

455 "callables", 

456 "key", 

457 "parents", 

458 "load_options", 

459 "class_", 

460 "expired_attributes", 

461 "info", 

462 ) 

463 if k in self.__dict__ 

464 ) 

465 if self.load_path: 

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

467 

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

469 

470 return state_dict 

471 

472 def __setstate__(self, state_dict): 

473 inst = state_dict["instance"] 

474 if inst is not None: 

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

476 self.class_ = inst.__class__ 

477 else: 

478 # None being possible here generally new as of 0.7.4 

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

480 # also new. 

481 self.obj = None 

482 self.class_ = state_dict["class_"] 

483 

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

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

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

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

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

489 if "info" in state_dict: 

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

491 if "callables" in state_dict: 

492 self.callables = state_dict["callables"] 

493 

494 try: 

495 self.expired_attributes = state_dict["expired_attributes"] 

496 except KeyError: 

497 self.expired_attributes = set() 

498 # 0.9 and earlier compat 

499 for k in list(self.callables): 

500 if self.callables[k] is self: 

501 self.expired_attributes.add(k) 

502 del self.callables[k] 

503 else: 

504 if "expired_attributes" in state_dict: 

505 self.expired_attributes = state_dict["expired_attributes"] 

506 else: 

507 self.expired_attributes = set() 

508 

509 self.__dict__.update( 

510 [ 

511 (k, state_dict[k]) 

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

513 if k in state_dict 

514 ] 

515 ) 

516 if self.key: 

517 try: 

518 self.identity_token = self.key[2] 

519 except IndexError: 

520 # 1.1 and earlier compat before identity_token 

521 assert len(self.key) == 2 

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

523 self.identity_token = None 

524 

525 if "load_path" in state_dict: 

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

527 

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

529 

530 def _reset(self, dict_, key): 

531 """Remove the given attribute and any 

532 callables associated with it.""" 

533 

534 old = dict_.pop(key, None) 

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

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

537 self.expired_attributes.discard(key) 

538 if self.callables: 

539 self.callables.pop(key, None) 

540 

541 def _copy_callables(self, from_): 

542 if "callables" in from_.__dict__: 

543 self.callables = dict(from_.callables) 

544 

545 @classmethod 

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

547 impl = manager[key].impl 

548 if impl.collection: 

549 

550 def _set_callable(state, dict_, row): 

551 if "callables" not in state.__dict__: 

552 state.callables = {} 

553 old = dict_.pop(key, None) 

554 if old is not None: 

555 impl._invalidate_collection(old) 

556 state.callables[key] = fn 

557 

558 else: 

559 

560 def _set_callable(state, dict_, row): 

561 if "callables" not in state.__dict__: 

562 state.callables = {} 

563 state.callables[key] = fn 

564 

565 return _set_callable 

566 

567 def _expire(self, dict_, modified_set): 

568 self.expired = True 

569 

570 if self.modified: 

571 modified_set.discard(self) 

572 self.committed_state.clear() 

573 self.modified = False 

574 

575 self._strong_obj = None 

576 

577 if "_pending_mutations" in self.__dict__: 

578 del self.__dict__["_pending_mutations"] 

579 

580 if "parents" in self.__dict__: 

581 del self.__dict__["parents"] 

582 

583 self.expired_attributes.update( 

584 [ 

585 impl.key 

586 for impl in self.manager._scalar_loader_impls 

587 if impl.expire_missing or impl.key in dict_ 

588 ] 

589 ) 

590 

591 if self.callables: 

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

593 del self.callables[k] 

594 

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

596 collection = dict_.pop(k) 

597 collection._sa_adapter.invalidated = True 

598 

599 if self._last_known_values: 

600 self._last_known_values.update( 

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

602 ) 

603 

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

605 del dict_[key] 

606 

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

608 

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

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

611 

612 callables = self.callables 

613 

614 for key in attribute_names: 

615 impl = self.manager[key].impl 

616 if impl.accepts_scalar_loader: 

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

618 continue 

619 

620 self.expired_attributes.add(key) 

621 if callables and key in callables: 

622 del callables[key] 

623 old = dict_.pop(key, NO_VALUE) 

624 if impl.collection and old is not NO_VALUE: 

625 impl._invalidate_collection(old) 

626 

627 if ( 

628 self._last_known_values 

629 and key in self._last_known_values 

630 and old is not NO_VALUE 

631 ): 

632 self._last_known_values[key] = old 

633 

634 self.committed_state.pop(key, None) 

635 if pending: 

636 pending.pop(key, None) 

637 

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

639 

640 def _load_expired(self, state, passive): 

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

642 callable for loading expired attributes, which is also 

643 serializable (picklable). 

644 

645 """ 

646 

647 if not passive & SQL_OK: 

648 return PASSIVE_NO_RESULT 

649 

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

651 

652 self.manager.deferred_scalar_loader(self, toload) 

653 

654 # if the loader failed, or this 

655 # instance state didn't have an identity, 

656 # the attributes still might be in the callables 

657 # dict. ensure they are removed. 

658 self.expired_attributes.clear() 

659 

660 return ATTR_WAS_SET 

661 

662 @property 

663 def unmodified(self): 

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

665 

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

667 

668 def unmodified_intersection(self, keys): 

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

670 

671 return ( 

672 set(keys) 

673 .intersection(self.manager) 

674 .difference(self.committed_state) 

675 ) 

676 

677 @property 

678 def unloaded(self): 

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

680 

681 This includes expired attributes and any other attribute that 

682 was never populated or modified. 

683 

684 """ 

685 return ( 

686 set(self.manager) 

687 .difference(self.committed_state) 

688 .difference(self.dict) 

689 ) 

690 

691 @property 

692 def unloaded_expirable(self): 

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

694 

695 This includes expired attributes and any other attribute that 

696 was never populated or modified. 

697 

698 """ 

699 return self.unloaded.intersection( 

700 attr 

701 for attr in self.manager 

702 if self.manager[attr].impl.expire_missing 

703 ) 

704 

705 @property 

706 def _unloaded_non_object(self): 

707 return self.unloaded.intersection( 

708 attr 

709 for attr in self.manager 

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

711 ) 

712 

713 def _instance_dict(self): 

714 return None 

715 

716 def _modified_event( 

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

718 ): 

719 if attr: 

720 if not attr.send_modified_events: 

721 return 

722 if is_userland and attr.key not in dict_: 

723 raise sa_exc.InvalidRequestError( 

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

725 "the object state" % attr.key 

726 ) 

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

728 if collection: 

729 if previous is NEVER_SET: 

730 if attr.key in dict_: 

731 previous = dict_[attr.key] 

732 

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

734 previous = attr.copy(previous) 

735 self.committed_state[attr.key] = previous 

736 

737 if attr.key in self._last_known_values: 

738 self._last_known_values[attr.key] = NO_VALUE 

739 

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

741 

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

743 self.modified = True 

744 instance_dict = self._instance_dict() 

745 if instance_dict: 

746 instance_dict._modified.add(self) 

747 

748 # only create _strong_obj link if attached 

749 # to a session 

750 

751 inst = self.obj() 

752 if self.session_id: 

753 self._strong_obj = inst 

754 

755 if inst is None and attr: 

756 raise orm_exc.ObjectDereferencedError( 

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

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

759 "collected." 

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

761 ) 

762 

763 def _commit(self, dict_, keys): 

764 """Commit attributes. 

765 

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

767 those attributes which were refreshed from the database. 

768 

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

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

771 

772 """ 

773 for key in keys: 

774 self.committed_state.pop(key, None) 

775 

776 self.expired = False 

777 

778 self.expired_attributes.difference_update( 

779 set(keys).intersection(dict_) 

780 ) 

781 

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

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

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

785 # ensure this is what it does. 

786 if self.callables: 

787 for key in ( 

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

789 ): 

790 del self.callables[key] 

791 

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

793 """commit all attributes unconditionally. 

794 

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

796 to remove all pending state from the instance. 

797 

798 - all attributes are marked as "committed" 

799 - the "strong dirty reference" is removed 

800 - the "modified" flag is set to False 

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

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

803 

804 Attributes marked as "expired" can potentially remain 

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

806 

807 """ 

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

809 

810 @classmethod 

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

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

813 

814 for state, dict_ in iter_: 

815 state_dict = state.__dict__ 

816 

817 state.committed_state.clear() 

818 

819 if "_pending_mutations" in state_dict: 

820 del state_dict["_pending_mutations"] 

821 

822 state.expired_attributes.difference_update(dict_) 

823 

824 if instance_dict and state.modified: 

825 instance_dict._modified.discard(state) 

826 

827 state.modified = state.expired = False 

828 state._strong_obj = None 

829 

830 

831class AttributeState(object): 

832 """Provide an inspection interface corresponding 

833 to a particular attribute on a particular mapped object. 

834 

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

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

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

838 

839 from sqlalchemy import inspect 

840 

841 insp = inspect(some_mapped_object) 

842 attr_state = insp.attrs.some_attribute 

843 

844 """ 

845 

846 def __init__(self, state, key): 

847 self.state = state 

848 self.key = key 

849 

850 @property 

851 def loaded_value(self): 

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

853 

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

855 in the object's dictionary, returns NO_VALUE. 

856 

857 """ 

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

859 

860 @property 

861 def value(self): 

862 """Return the value of this attribute. 

863 

864 This operation is equivalent to accessing the object's 

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

866 off any pending loader callables if needed. 

867 

868 """ 

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

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

871 ) 

872 

873 @property 

874 def history(self): 

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

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

877 

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

879 attribute is unloaded. 

880 

881 .. note:: 

882 

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

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

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

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

887 For 

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

889 

890 

891 .. seealso:: 

892 

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

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

895 

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

897 

898 """ 

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

900 

901 def load_history(self): 

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

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

904 

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

906 attribute is unloaded. 

907 

908 .. note:: 

909 

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

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

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

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

914 For 

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

916 

917 .. seealso:: 

918 

919 :attr:`.AttributeState.history` 

920 

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

922 

923 .. versionadded:: 0.9.0 

924 

925 """ 

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

927 

928 

929class PendingCollection(object): 

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

931 

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

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

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

935 

936 """ 

937 

938 def __init__(self): 

939 self.deleted_items = util.IdentitySet() 

940 self.added_items = util.OrderedIdentitySet() 

941 

942 def append(self, value): 

943 if value in self.deleted_items: 

944 self.deleted_items.remove(value) 

945 else: 

946 self.added_items.add(value) 

947 

948 def remove(self, value): 

949 if value in self.added_items: 

950 self.added_items.remove(value) 

951 else: 

952 self.deleted_items.add(value)