1# orm/state.py 2# Copyright (C) 2005-2021 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) 953