1# orm/attributes.py 2# Copyright (C) 2005-2018 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 for class attributes and their interaction 9with instances. 10 11This module is usually not directly visible to user applications, but 12defines a large part of the ORM's interactivity. 13 14 15""" 16 17import operator 18from .. import util, event, inspection 19from . import interfaces, collections, exc as orm_exc 20 21from .base import instance_state, instance_dict, manager_of_class 22 23from .base import PASSIVE_NO_RESULT, ATTR_WAS_SET, ATTR_EMPTY, NO_VALUE,\ 24 NEVER_SET, NO_CHANGE, CALLABLES_OK, SQL_OK, RELATED_OBJECT_OK,\ 25 INIT_OK, NON_PERSISTENT_OK, LOAD_AGAINST_COMMITTED, PASSIVE_OFF,\ 26 PASSIVE_RETURN_NEVER_SET, PASSIVE_NO_INITIALIZE, PASSIVE_NO_FETCH,\ 27 PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT, NO_AUTOFLUSH 28from .base import state_str, instance_str 29 30 31@inspection._self_inspects 32class QueryableAttribute(interfaces._MappedAttribute, 33 interfaces.InspectionAttr, 34 interfaces.PropComparator): 35 """Base class for :term:`descriptor` objects that intercept 36 attribute events on behalf of a :class:`.MapperProperty` 37 object. The actual :class:`.MapperProperty` is accessible 38 via the :attr:`.QueryableAttribute.property` 39 attribute. 40 41 42 .. seealso:: 43 44 :class:`.InstrumentedAttribute` 45 46 :class:`.MapperProperty` 47 48 :attr:`.Mapper.all_orm_descriptors` 49 50 :attr:`.Mapper.attrs` 51 """ 52 53 is_attribute = True 54 55 def __init__(self, class_, key, impl=None, 56 comparator=None, parententity=None, 57 of_type=None): 58 self.class_ = class_ 59 self.key = key 60 self.impl = impl 61 self.comparator = comparator 62 self._parententity = parententity 63 self._of_type = of_type 64 65 manager = manager_of_class(class_) 66 # manager is None in the case of AliasedClass 67 if manager: 68 # propagate existing event listeners from 69 # immediate superclass 70 for base in manager._bases: 71 if key in base: 72 self.dispatch._update(base[key].dispatch) 73 74 @util.memoized_property 75 def _supports_population(self): 76 return self.impl.supports_population 77 78 def get_history(self, instance, passive=PASSIVE_OFF): 79 return self.impl.get_history(instance_state(instance), 80 instance_dict(instance), passive) 81 82 def __selectable__(self): 83 # TODO: conditionally attach this method based on clause_element ? 84 return self 85 86 @util.memoized_property 87 def info(self): 88 """Return the 'info' dictionary for the underlying SQL element. 89 90 The behavior here is as follows: 91 92 * If the attribute is a column-mapped property, i.e. 93 :class:`.ColumnProperty`, which is mapped directly 94 to a schema-level :class:`.Column` object, this attribute 95 will return the :attr:`.SchemaItem.info` dictionary associated 96 with the core-level :class:`.Column` object. 97 98 * If the attribute is a :class:`.ColumnProperty` but is mapped to 99 any other kind of SQL expression other than a :class:`.Column`, 100 the attribute will refer to the :attr:`.MapperProperty.info` 101 dictionary associated directly with the :class:`.ColumnProperty`, 102 assuming the SQL expression itself does not have its own ``.info`` 103 attribute (which should be the case, unless a user-defined SQL 104 construct has defined one). 105 106 * If the attribute refers to any other kind of 107 :class:`.MapperProperty`, including :class:`.RelationshipProperty`, 108 the attribute will refer to the :attr:`.MapperProperty.info` 109 dictionary associated with that :class:`.MapperProperty`. 110 111 * To access the :attr:`.MapperProperty.info` dictionary of the 112 :class:`.MapperProperty` unconditionally, including for a 113 :class:`.ColumnProperty` that's associated directly with a 114 :class:`.schema.Column`, the attribute can be referred to using 115 :attr:`.QueryableAttribute.property` attribute, as 116 ``MyClass.someattribute.property.info``. 117 118 .. versionadded:: 0.8.0 119 120 .. seealso:: 121 122 :attr:`.SchemaItem.info` 123 124 :attr:`.MapperProperty.info` 125 126 """ 127 return self.comparator.info 128 129 @util.memoized_property 130 def parent(self): 131 """Return an inspection instance representing the parent. 132 133 This will be either an instance of :class:`.Mapper` 134 or :class:`.AliasedInsp`, depending upon the nature 135 of the parent entity which this attribute is associated 136 with. 137 138 """ 139 return inspection.inspect(self._parententity) 140 141 @property 142 def expression(self): 143 return self.comparator.__clause_element__() 144 145 def __clause_element__(self): 146 return self.comparator.__clause_element__() 147 148 def _query_clause_element(self): 149 """like __clause_element__(), but called specifically 150 by :class:`.Query` to allow special behavior.""" 151 152 return self.comparator._query_clause_element() 153 154 def adapt_to_entity(self, adapt_to_entity): 155 assert not self._of_type 156 return self.__class__(adapt_to_entity.entity, 157 self.key, impl=self.impl, 158 comparator=self.comparator.adapt_to_entity( 159 adapt_to_entity), 160 parententity=adapt_to_entity) 161 162 def of_type(self, cls): 163 return QueryableAttribute( 164 self.class_, 165 self.key, 166 self.impl, 167 self.comparator.of_type(cls), 168 self._parententity, 169 of_type=cls) 170 171 def label(self, name): 172 return self._query_clause_element().label(name) 173 174 def operate(self, op, *other, **kwargs): 175 return op(self.comparator, *other, **kwargs) 176 177 def reverse_operate(self, op, other, **kwargs): 178 return op(other, self.comparator, **kwargs) 179 180 def hasparent(self, state, optimistic=False): 181 return self.impl.hasparent(state, optimistic=optimistic) is not False 182 183 def __getattr__(self, key): 184 try: 185 return getattr(self.comparator, key) 186 except AttributeError: 187 raise AttributeError( 188 'Neither %r object nor %r object associated with %s ' 189 'has an attribute %r' % ( 190 type(self).__name__, 191 type(self.comparator).__name__, 192 self, 193 key) 194 ) 195 196 def __str__(self): 197 return "%s.%s" % (self.class_.__name__, self.key) 198 199 @util.memoized_property 200 def property(self): 201 """Return the :class:`.MapperProperty` associated with this 202 :class:`.QueryableAttribute`. 203 204 205 Return values here will commonly be instances of 206 :class:`.ColumnProperty` or :class:`.RelationshipProperty`. 207 208 209 """ 210 return self.comparator.property 211 212 213class InstrumentedAttribute(QueryableAttribute): 214 """Class bound instrumented attribute which adds basic 215 :term:`descriptor` methods. 216 217 See :class:`.QueryableAttribute` for a description of most features. 218 219 220 """ 221 222 def __set__(self, instance, value): 223 self.impl.set(instance_state(instance), 224 instance_dict(instance), value, None) 225 226 def __delete__(self, instance): 227 self.impl.delete(instance_state(instance), instance_dict(instance)) 228 229 def __get__(self, instance, owner): 230 if instance is None: 231 return self 232 233 dict_ = instance_dict(instance) 234 if self._supports_population and self.key in dict_: 235 return dict_[self.key] 236 else: 237 return self.impl.get(instance_state(instance), dict_) 238 239 240def create_proxied_attribute(descriptor): 241 """Create an QueryableAttribute / user descriptor hybrid. 242 243 Returns a new QueryableAttribute type that delegates descriptor 244 behavior and getattr() to the given descriptor. 245 """ 246 247 # TODO: can move this to descriptor_props if the need for this 248 # function is removed from ext/hybrid.py 249 250 class Proxy(QueryableAttribute): 251 """Presents the :class:`.QueryableAttribute` interface as a 252 proxy on top of a Python descriptor / :class:`.PropComparator` 253 combination. 254 255 """ 256 257 def __init__(self, class_, key, descriptor, 258 comparator, 259 adapt_to_entity=None, doc=None, 260 original_property=None): 261 self.class_ = class_ 262 self.key = key 263 self.descriptor = descriptor 264 self.original_property = original_property 265 self._comparator = comparator 266 self._adapt_to_entity = adapt_to_entity 267 self.__doc__ = doc 268 269 @property 270 def property(self): 271 return self.comparator.property 272 273 @util.memoized_property 274 def comparator(self): 275 if util.callable(self._comparator): 276 self._comparator = self._comparator() 277 if self._adapt_to_entity: 278 self._comparator = self._comparator.adapt_to_entity( 279 self._adapt_to_entity) 280 return self._comparator 281 282 def adapt_to_entity(self, adapt_to_entity): 283 return self.__class__(adapt_to_entity.entity, 284 self.key, 285 self.descriptor, 286 self._comparator, 287 adapt_to_entity) 288 289 def __get__(self, instance, owner): 290 if instance is None: 291 return self 292 else: 293 return self.descriptor.__get__(instance, owner) 294 295 def __str__(self): 296 return "%s.%s" % (self.class_.__name__, self.key) 297 298 def __getattr__(self, attribute): 299 """Delegate __getattr__ to the original descriptor and/or 300 comparator.""" 301 302 try: 303 return getattr(descriptor, attribute) 304 except AttributeError: 305 try: 306 return getattr(self.comparator, attribute) 307 except AttributeError: 308 raise AttributeError( 309 'Neither %r object nor %r object associated with %s ' 310 'has an attribute %r' % ( 311 type(descriptor).__name__, 312 type(self.comparator).__name__, 313 self, 314 attribute) 315 ) 316 317 Proxy.__name__ = type(descriptor).__name__ + 'Proxy' 318 319 util.monkeypatch_proxied_specials(Proxy, type(descriptor), 320 name='descriptor', 321 from_instance=descriptor) 322 return Proxy 323 324OP_REMOVE = util.symbol("REMOVE") 325OP_APPEND = util.symbol("APPEND") 326OP_REPLACE = util.symbol("REPLACE") 327 328 329class Event(object): 330 """A token propagated throughout the course of a chain of attribute 331 events. 332 333 Serves as an indicator of the source of the event and also provides 334 a means of controlling propagation across a chain of attribute 335 operations. 336 337 The :class:`.Event` object is sent as the ``initiator`` argument 338 when dealing with the :meth:`.AttributeEvents.append`, 339 :meth:`.AttributeEvents.set`, 340 and :meth:`.AttributeEvents.remove` events. 341 342 The :class:`.Event` object is currently interpreted by the backref 343 event handlers, and is used to control the propagation of operations 344 across two mutually-dependent attributes. 345 346 .. versionadded:: 0.9.0 347 348 :var impl: The :class:`.AttributeImpl` which is the current event 349 initiator. 350 351 :var op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE` or 352 :attr:`.OP_REPLACE`, indicating the source operation. 353 354 """ 355 356 __slots__ = 'impl', 'op', 'parent_token' 357 358 def __init__(self, attribute_impl, op): 359 self.impl = attribute_impl 360 self.op = op 361 self.parent_token = self.impl.parent_token 362 363 def __eq__(self, other): 364 return isinstance(other, Event) and \ 365 other.impl is self.impl and \ 366 other.op == self.op 367 368 @property 369 def key(self): 370 return self.impl.key 371 372 def hasparent(self, state): 373 return self.impl.hasparent(state) 374 375 376class AttributeImpl(object): 377 """internal implementation for instrumented attributes.""" 378 379 def __init__(self, class_, key, 380 callable_, dispatch, trackparent=False, extension=None, 381 compare_function=None, active_history=False, 382 parent_token=None, expire_missing=True, 383 send_modified_events=True, 384 **kwargs): 385 r"""Construct an AttributeImpl. 386 387 \class_ 388 associated class 389 390 key 391 string name of the attribute 392 393 \callable_ 394 optional function which generates a callable based on a parent 395 instance, which produces the "default" values for a scalar or 396 collection attribute when it's first accessed, if not present 397 already. 398 399 trackparent 400 if True, attempt to track if an instance has a parent attached 401 to it via this attribute. 402 403 extension 404 a single or list of AttributeExtension object(s) which will 405 receive set/delete/append/remove/etc. events. Deprecated. 406 The event package is now used. 407 408 compare_function 409 a function that compares two values which are normally 410 assignable to this attribute. 411 412 active_history 413 indicates that get_history() should always return the "old" value, 414 even if it means executing a lazy callable upon attribute change. 415 416 parent_token 417 Usually references the MapperProperty, used as a key for 418 the hasparent() function to identify an "owning" attribute. 419 Allows multiple AttributeImpls to all match a single 420 owner attribute. 421 422 expire_missing 423 if False, don't add an "expiry" callable to this attribute 424 during state.expire_attributes(None), if no value is present 425 for this key. 426 427 send_modified_events 428 if False, the InstanceState._modified_event method will have no 429 effect; this means the attribute will never show up as changed in a 430 history entry. 431 """ 432 self.class_ = class_ 433 self.key = key 434 self.callable_ = callable_ 435 self.dispatch = dispatch 436 self.trackparent = trackparent 437 self.parent_token = parent_token or self 438 self.send_modified_events = send_modified_events 439 if compare_function is None: 440 self.is_equal = operator.eq 441 else: 442 self.is_equal = compare_function 443 444 # TODO: pass in the manager here 445 # instead of doing a lookup 446 attr = manager_of_class(class_)[key] 447 448 for ext in util.to_list(extension or []): 449 ext._adapt_listener(attr, ext) 450 451 if active_history: 452 self.dispatch._active_history = True 453 454 self.expire_missing = expire_missing 455 456 __slots__ = ( 457 'class_', 'key', 'callable_', 'dispatch', 'trackparent', 458 'parent_token', 'send_modified_events', 'is_equal', 'expire_missing' 459 ) 460 461 def __str__(self): 462 return "%s.%s" % (self.class_.__name__, self.key) 463 464 def _get_active_history(self): 465 """Backwards compat for impl.active_history""" 466 467 return self.dispatch._active_history 468 469 def _set_active_history(self, value): 470 self.dispatch._active_history = value 471 472 active_history = property(_get_active_history, _set_active_history) 473 474 def hasparent(self, state, optimistic=False): 475 """Return the boolean value of a `hasparent` flag attached to 476 the given state. 477 478 The `optimistic` flag determines what the default return value 479 should be if no `hasparent` flag can be located. 480 481 As this function is used to determine if an instance is an 482 *orphan*, instances that were loaded from storage should be 483 assumed to not be orphans, until a True/False value for this 484 flag is set. 485 486 An instance attribute that is loaded by a callable function 487 will also not have a `hasparent` flag. 488 489 """ 490 msg = "This AttributeImpl is not configured to track parents." 491 assert self.trackparent, msg 492 493 return state.parents.get(id(self.parent_token), optimistic) \ 494 is not False 495 496 def sethasparent(self, state, parent_state, value): 497 """Set a boolean flag on the given item corresponding to 498 whether or not it is attached to a parent object via the 499 attribute represented by this ``InstrumentedAttribute``. 500 501 """ 502 msg = "This AttributeImpl is not configured to track parents." 503 assert self.trackparent, msg 504 505 id_ = id(self.parent_token) 506 if value: 507 state.parents[id_] = parent_state 508 else: 509 if id_ in state.parents: 510 last_parent = state.parents[id_] 511 512 if last_parent is not False and \ 513 last_parent.key != parent_state.key: 514 515 if last_parent.obj() is None: 516 raise orm_exc.StaleDataError( 517 "Removing state %s from parent " 518 "state %s along attribute '%s', " 519 "but the parent record " 520 "has gone stale, can't be sure this " 521 "is the most recent parent." % 522 (state_str(state), 523 state_str(parent_state), 524 self.key)) 525 526 return 527 528 state.parents[id_] = False 529 530 def get_history(self, state, dict_, passive=PASSIVE_OFF): 531 raise NotImplementedError() 532 533 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE): 534 """Return a list of tuples of (state, obj) 535 for all objects in this attribute's current state 536 + history. 537 538 Only applies to object-based attributes. 539 540 This is an inlining of existing functionality 541 which roughly corresponds to: 542 543 get_state_history( 544 state, 545 key, 546 passive=PASSIVE_NO_INITIALIZE).sum() 547 548 """ 549 raise NotImplementedError() 550 551 def initialize(self, state, dict_): 552 """Initialize the given state's attribute with an empty value.""" 553 554 value = None 555 for fn in self.dispatch.init_scalar: 556 ret = fn(state, value, dict_) 557 if ret is not ATTR_EMPTY: 558 value = ret 559 560 return value 561 562 def get(self, state, dict_, passive=PASSIVE_OFF): 563 """Retrieve a value from the given object. 564 If a callable is assembled on this object's attribute, and 565 passive is False, the callable will be executed and the 566 resulting value will be set as the new value for this attribute. 567 """ 568 if self.key in dict_: 569 return dict_[self.key] 570 else: 571 # if history present, don't load 572 key = self.key 573 if key not in state.committed_state or \ 574 state.committed_state[key] is NEVER_SET: 575 if not passive & CALLABLES_OK: 576 return PASSIVE_NO_RESULT 577 578 if key in state.expired_attributes: 579 value = state._load_expired(state, passive) 580 elif key in state.callables: 581 callable_ = state.callables[key] 582 value = callable_(state, passive) 583 elif self.callable_: 584 value = self.callable_(state, passive) 585 else: 586 value = ATTR_EMPTY 587 588 if value is PASSIVE_NO_RESULT or value is NEVER_SET: 589 return value 590 elif value is ATTR_WAS_SET: 591 try: 592 return dict_[key] 593 except KeyError: 594 # TODO: no test coverage here. 595 raise KeyError( 596 "Deferred loader for attribute " 597 "%r failed to populate " 598 "correctly" % key) 599 elif value is not ATTR_EMPTY: 600 return self.set_committed_value(state, dict_, value) 601 602 if not passive & INIT_OK: 603 return NEVER_SET 604 else: 605 # Return a new, empty value 606 return self.initialize(state, dict_) 607 608 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 609 self.set(state, dict_, value, initiator, passive=passive) 610 611 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 612 self.set(state, dict_, None, initiator, 613 passive=passive, check_old=value) 614 615 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 616 self.set(state, dict_, None, initiator, 617 passive=passive, check_old=value, pop=True) 618 619 def set(self, state, dict_, value, initiator, 620 passive=PASSIVE_OFF, check_old=None, pop=False): 621 raise NotImplementedError() 622 623 def get_committed_value(self, state, dict_, passive=PASSIVE_OFF): 624 """return the unchanged value of this attribute""" 625 626 if self.key in state.committed_state: 627 value = state.committed_state[self.key] 628 if value in (NO_VALUE, NEVER_SET): 629 return None 630 else: 631 return value 632 else: 633 return self.get(state, dict_, passive=passive) 634 635 def set_committed_value(self, state, dict_, value): 636 """set an attribute value on the given instance and 'commit' it.""" 637 638 dict_[self.key] = value 639 state._commit(dict_, [self.key]) 640 return value 641 642 643class ScalarAttributeImpl(AttributeImpl): 644 """represents a scalar value-holding InstrumentedAttribute.""" 645 646 accepts_scalar_loader = True 647 uses_objects = False 648 supports_population = True 649 collection = False 650 651 __slots__ = '_replace_token', '_append_token', '_remove_token' 652 653 def __init__(self, *arg, **kw): 654 super(ScalarAttributeImpl, self).__init__(*arg, **kw) 655 self._replace_token = self._append_token = None 656 self._remove_token = None 657 658 def _init_append_token(self): 659 self._replace_token = self._append_token = Event(self, OP_REPLACE) 660 return self._replace_token 661 662 _init_append_or_replace_token = _init_append_token 663 664 def _init_remove_token(self): 665 self._remove_token = Event(self, OP_REMOVE) 666 return self._remove_token 667 668 def delete(self, state, dict_): 669 670 # TODO: catch key errors, convert to attributeerror? 671 if self.dispatch._active_history: 672 old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) 673 else: 674 old = dict_.get(self.key, NO_VALUE) 675 676 if self.dispatch.remove: 677 self.fire_remove_event(state, dict_, old, self._remove_token) 678 state._modified_event(dict_, self, old) 679 del dict_[self.key] 680 681 def get_history(self, state, dict_, passive=PASSIVE_OFF): 682 if self.key in dict_: 683 return History.from_scalar_attribute(self, state, dict_[self.key]) 684 else: 685 if passive & INIT_OK: 686 passive ^= INIT_OK 687 current = self.get(state, dict_, passive=passive) 688 if current is PASSIVE_NO_RESULT: 689 return HISTORY_BLANK 690 else: 691 return History.from_scalar_attribute(self, state, current) 692 693 def set(self, state, dict_, value, initiator, 694 passive=PASSIVE_OFF, check_old=None, pop=False): 695 if self.dispatch._active_history: 696 old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) 697 else: 698 old = dict_.get(self.key, NO_VALUE) 699 700 if self.dispatch.set: 701 value = self.fire_replace_event(state, dict_, 702 value, old, initiator) 703 state._modified_event(dict_, self, old) 704 dict_[self.key] = value 705 706 def fire_replace_event(self, state, dict_, value, previous, initiator): 707 for fn in self.dispatch.set: 708 value = fn( 709 state, value, previous, 710 initiator or self._replace_token or 711 self._init_append_or_replace_token()) 712 return value 713 714 def fire_remove_event(self, state, dict_, value, initiator): 715 for fn in self.dispatch.remove: 716 fn(state, value, 717 initiator or self._remove_token or self._init_remove_token()) 718 719 @property 720 def type(self): 721 self.property.columns[0].type 722 723 724class ScalarObjectAttributeImpl(ScalarAttributeImpl): 725 """represents a scalar-holding InstrumentedAttribute, 726 where the target object is also instrumented. 727 728 Adds events to delete/set operations. 729 730 """ 731 732 accepts_scalar_loader = False 733 uses_objects = True 734 supports_population = True 735 collection = False 736 737 __slots__ = () 738 739 def delete(self, state, dict_): 740 old = self.get(state, dict_) 741 self.fire_remove_event( 742 state, dict_, old, 743 self._remove_token or self._init_remove_token()) 744 del dict_[self.key] 745 746 def get_history(self, state, dict_, passive=PASSIVE_OFF): 747 if self.key in dict_: 748 return History.from_object_attribute(self, state, dict_[self.key]) 749 else: 750 if passive & INIT_OK: 751 passive ^= INIT_OK 752 current = self.get(state, dict_, passive=passive) 753 if current is PASSIVE_NO_RESULT: 754 return HISTORY_BLANK 755 else: 756 return History.from_object_attribute(self, state, current) 757 758 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE): 759 if self.key in dict_: 760 current = dict_[self.key] 761 elif passive & CALLABLES_OK: 762 current = self.get(state, dict_, passive=passive) 763 else: 764 return [] 765 766 # can't use __hash__(), can't use __eq__() here 767 if current is not None and \ 768 current is not PASSIVE_NO_RESULT and \ 769 current is not NEVER_SET: 770 ret = [(instance_state(current), current)] 771 else: 772 ret = [(None, None)] 773 774 if self.key in state.committed_state: 775 original = state.committed_state[self.key] 776 if original is not None and \ 777 original is not PASSIVE_NO_RESULT and \ 778 original is not NEVER_SET and \ 779 original is not current: 780 781 ret.append((instance_state(original), original)) 782 return ret 783 784 def set(self, state, dict_, value, initiator, 785 passive=PASSIVE_OFF, check_old=None, pop=False): 786 """Set a value on the given InstanceState. 787 788 """ 789 if self.dispatch._active_history: 790 old = self.get( 791 state, dict_, 792 passive=PASSIVE_ONLY_PERSISTENT | 793 NO_AUTOFLUSH | LOAD_AGAINST_COMMITTED) 794 else: 795 old = self.get( 796 state, dict_, passive=PASSIVE_NO_FETCH ^ INIT_OK | 797 LOAD_AGAINST_COMMITTED) 798 799 if check_old is not None and \ 800 old is not PASSIVE_NO_RESULT and \ 801 check_old is not old: 802 if pop: 803 return 804 else: 805 raise ValueError( 806 "Object %s not associated with %s on attribute '%s'" % ( 807 instance_str(check_old), 808 state_str(state), 809 self.key 810 )) 811 812 value = self.fire_replace_event(state, dict_, value, old, initiator) 813 dict_[self.key] = value 814 815 def fire_remove_event(self, state, dict_, value, initiator): 816 if self.trackparent and value is not None: 817 self.sethasparent(instance_state(value), state, False) 818 819 for fn in self.dispatch.remove: 820 fn(state, value, initiator or 821 self._remove_token or self._init_remove_token()) 822 823 state._modified_event(dict_, self, value) 824 825 def fire_replace_event(self, state, dict_, value, previous, initiator): 826 if self.trackparent: 827 if (previous is not value and 828 previous not in (None, PASSIVE_NO_RESULT, NEVER_SET)): 829 self.sethasparent(instance_state(previous), state, False) 830 831 for fn in self.dispatch.set: 832 value = fn( 833 state, value, previous, initiator or 834 self._replace_token or self._init_append_or_replace_token()) 835 836 state._modified_event(dict_, self, previous) 837 838 if self.trackparent: 839 if value is not None: 840 self.sethasparent(instance_state(value), state, True) 841 842 return value 843 844 845class CollectionAttributeImpl(AttributeImpl): 846 """A collection-holding attribute that instruments changes in membership. 847 848 Only handles collections of instrumented objects. 849 850 InstrumentedCollectionAttribute holds an arbitrary, user-specified 851 container object (defaulting to a list) and brokers access to the 852 CollectionAdapter, a "view" onto that object that presents consistent bag 853 semantics to the orm layer independent of the user data implementation. 854 855 """ 856 accepts_scalar_loader = False 857 uses_objects = True 858 supports_population = True 859 collection = True 860 861 __slots__ = ( 862 'copy', 'collection_factory', '_append_token', '_remove_token', 863 '_duck_typed_as' 864 ) 865 866 def __init__(self, class_, key, callable_, dispatch, 867 typecallable=None, trackparent=False, extension=None, 868 copy_function=None, compare_function=None, **kwargs): 869 super(CollectionAttributeImpl, self).__init__( 870 class_, 871 key, 872 callable_, dispatch, 873 trackparent=trackparent, 874 extension=extension, 875 compare_function=compare_function, 876 **kwargs) 877 878 if copy_function is None: 879 copy_function = self.__copy 880 self.copy = copy_function 881 self.collection_factory = typecallable 882 self._append_token = None 883 self._remove_token = None 884 self._duck_typed_as = util.duck_type_collection( 885 self.collection_factory()) 886 887 if getattr(self.collection_factory, "_sa_linker", None): 888 889 @event.listens_for(self, "init_collection") 890 def link(target, collection, collection_adapter): 891 collection._sa_linker(collection_adapter) 892 893 @event.listens_for(self, "dispose_collection") 894 def unlink(target, collection, collection_adapter): 895 collection._sa_linker(None) 896 897 def _init_append_token(self): 898 self._append_token = Event(self, OP_APPEND) 899 return self._append_token 900 901 def _init_remove_token(self): 902 self._remove_token = Event(self, OP_REMOVE) 903 return self._remove_token 904 905 def __copy(self, item): 906 return [y for y in collections.collection_adapter(item)] 907 908 def get_history(self, state, dict_, passive=PASSIVE_OFF): 909 current = self.get(state, dict_, passive=passive) 910 if current is PASSIVE_NO_RESULT: 911 return HISTORY_BLANK 912 else: 913 return History.from_collection(self, state, current) 914 915 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE): 916 # NOTE: passive is ignored here at the moment 917 918 if self.key not in dict_: 919 return [] 920 921 current = dict_[self.key] 922 current = getattr(current, '_sa_adapter') 923 924 if self.key in state.committed_state: 925 original = state.committed_state[self.key] 926 if original not in (NO_VALUE, NEVER_SET): 927 current_states = [((c is not None) and 928 instance_state(c) or None, c) 929 for c in current] 930 original_states = [((c is not None) and 931 instance_state(c) or None, c) 932 for c in original] 933 934 current_set = dict(current_states) 935 original_set = dict(original_states) 936 937 return \ 938 [(s, o) for s, o in current_states 939 if s not in original_set] + \ 940 [(s, o) for s, o in current_states 941 if s in original_set] + \ 942 [(s, o) for s, o in original_states 943 if s not in current_set] 944 945 return [(instance_state(o), o) for o in current] 946 947 def fire_append_event(self, state, dict_, value, initiator): 948 for fn in self.dispatch.append: 949 value = fn( 950 state, value, 951 initiator or self._append_token or self._init_append_token()) 952 953 state._modified_event(dict_, self, NEVER_SET, True) 954 955 if self.trackparent and value is not None: 956 self.sethasparent(instance_state(value), state, True) 957 958 return value 959 960 def fire_pre_remove_event(self, state, dict_, initiator): 961 state._modified_event(dict_, self, NEVER_SET, True) 962 963 def fire_remove_event(self, state, dict_, value, initiator): 964 if self.trackparent and value is not None: 965 self.sethasparent(instance_state(value), state, False) 966 967 for fn in self.dispatch.remove: 968 fn(state, value, 969 initiator or self._remove_token or self._init_remove_token()) 970 971 state._modified_event(dict_, self, NEVER_SET, True) 972 973 def delete(self, state, dict_): 974 if self.key not in dict_: 975 return 976 977 state._modified_event(dict_, self, NEVER_SET, True) 978 979 collection = self.get_collection(state, state.dict) 980 collection.clear_with_event() 981 # TODO: catch key errors, convert to attributeerror? 982 del dict_[self.key] 983 984 def initialize(self, state, dict_): 985 """Initialize this attribute with an empty collection.""" 986 987 _, user_data = self._initialize_collection(state) 988 dict_[self.key] = user_data 989 return user_data 990 991 def _initialize_collection(self, state): 992 993 adapter, collection = state.manager.initialize_collection( 994 self.key, state, self.collection_factory) 995 996 self.dispatch.init_collection(state, collection, adapter) 997 998 return adapter, collection 999 1000 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 1001 collection = self.get_collection(state, dict_, passive=passive) 1002 if collection is PASSIVE_NO_RESULT: 1003 value = self.fire_append_event(state, dict_, value, initiator) 1004 assert self.key not in dict_, \ 1005 "Collection was loaded during event handling." 1006 state._get_pending_mutation(self.key).append(value) 1007 else: 1008 collection.append_with_event(value, initiator) 1009 1010 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 1011 collection = self.get_collection(state, state.dict, passive=passive) 1012 if collection is PASSIVE_NO_RESULT: 1013 self.fire_remove_event(state, dict_, value, initiator) 1014 assert self.key not in dict_, \ 1015 "Collection was loaded during event handling." 1016 state._get_pending_mutation(self.key).remove(value) 1017 else: 1018 collection.remove_with_event(value, initiator) 1019 1020 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 1021 try: 1022 # TODO: better solution here would be to add 1023 # a "popper" role to collections.py to complement 1024 # "remover". 1025 self.remove(state, dict_, value, initiator, passive=passive) 1026 except (ValueError, KeyError, IndexError): 1027 pass 1028 1029 def set(self, state, dict_, value, initiator=None, 1030 passive=PASSIVE_OFF, pop=False, _adapt=True): 1031 iterable = orig_iterable = value 1032 1033 # pulling a new collection first so that an adaptation exception does 1034 # not trigger a lazy load of the old collection. 1035 new_collection, user_data = self._initialize_collection(state) 1036 if _adapt: 1037 if new_collection._converter is not None: 1038 iterable = new_collection._converter(iterable) 1039 else: 1040 setting_type = util.duck_type_collection(iterable) 1041 receiving_type = self._duck_typed_as 1042 1043 if setting_type is not receiving_type: 1044 given = iterable is None and 'None' or \ 1045 iterable.__class__.__name__ 1046 wanted = self._duck_typed_as.__name__ 1047 raise TypeError( 1048 "Incompatible collection type: %s is not %s-like" % ( 1049 given, wanted)) 1050 1051 # If the object is an adapted collection, return the (iterable) 1052 # adapter. 1053 if hasattr(iterable, '_sa_iterator'): 1054 iterable = iterable._sa_iterator() 1055 elif setting_type is dict: 1056 if util.py3k: 1057 iterable = iterable.values() 1058 else: 1059 iterable = getattr( 1060 iterable, 'itervalues', iterable.values)() 1061 else: 1062 iterable = iter(iterable) 1063 new_values = list(iterable) 1064 1065 old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT) 1066 if old is PASSIVE_NO_RESULT: 1067 old = self.initialize(state, dict_) 1068 elif old is orig_iterable: 1069 # ignore re-assignment of the current collection, as happens 1070 # implicitly with in-place operators (foo.collection |= other) 1071 return 1072 1073 # place a copy of "old" in state.committed_state 1074 state._modified_event(dict_, self, old, True) 1075 1076 old_collection = old._sa_adapter 1077 1078 dict_[self.key] = user_data 1079 1080 collections.bulk_replace( 1081 new_values, old_collection, new_collection) 1082 1083 del old._sa_adapter 1084 self.dispatch.dispose_collection(state, old, old_collection) 1085 1086 def _invalidate_collection(self, collection): 1087 adapter = getattr(collection, '_sa_adapter') 1088 adapter.invalidated = True 1089 1090 def set_committed_value(self, state, dict_, value): 1091 """Set an attribute value on the given instance and 'commit' it.""" 1092 1093 collection, user_data = self._initialize_collection(state) 1094 1095 if value: 1096 collection.append_multiple_without_event(value) 1097 1098 state.dict[self.key] = user_data 1099 1100 state._commit(dict_, [self.key]) 1101 1102 if self.key in state._pending_mutations: 1103 # pending items exist. issue a modified event, 1104 # add/remove new items. 1105 state._modified_event(dict_, self, user_data, True) 1106 1107 pending = state._pending_mutations.pop(self.key) 1108 added = pending.added_items 1109 removed = pending.deleted_items 1110 for item in added: 1111 collection.append_without_event(item) 1112 for item in removed: 1113 collection.remove_without_event(item) 1114 1115 return user_data 1116 1117 def get_collection(self, state, dict_, 1118 user_data=None, passive=PASSIVE_OFF): 1119 """Retrieve the CollectionAdapter associated with the given state. 1120 1121 Creates a new CollectionAdapter if one does not exist. 1122 1123 """ 1124 if user_data is None: 1125 user_data = self.get(state, dict_, passive=passive) 1126 if user_data is PASSIVE_NO_RESULT: 1127 return user_data 1128 1129 return getattr(user_data, '_sa_adapter') 1130 1131 1132def backref_listeners(attribute, key, uselist): 1133 """Apply listeners to synchronize a two-way relationship.""" 1134 1135 # use easily recognizable names for stack traces 1136 1137 parent_token = attribute.impl.parent_token 1138 parent_impl = attribute.impl 1139 1140 def _acceptable_key_err(child_state, initiator, child_impl): 1141 raise ValueError( 1142 "Bidirectional attribute conflict detected: " 1143 'Passing object %s to attribute "%s" ' 1144 'triggers a modify event on attribute "%s" ' 1145 'via the backref "%s".' % ( 1146 state_str(child_state), 1147 initiator.parent_token, 1148 child_impl.parent_token, 1149 attribute.impl.parent_token 1150 ) 1151 ) 1152 1153 def emit_backref_from_scalar_set_event(state, child, oldchild, initiator): 1154 if oldchild is child: 1155 return child 1156 if oldchild is not None and \ 1157 oldchild is not PASSIVE_NO_RESULT and \ 1158 oldchild is not NEVER_SET: 1159 # With lazy=None, there's no guarantee that the full collection is 1160 # present when updating via a backref. 1161 old_state, old_dict = instance_state(oldchild),\ 1162 instance_dict(oldchild) 1163 impl = old_state.manager[key].impl 1164 1165 if initiator.impl is not impl or \ 1166 initiator.op not in (OP_REPLACE, OP_REMOVE): 1167 impl.pop(old_state, 1168 old_dict, 1169 state.obj(), 1170 parent_impl._append_token or 1171 parent_impl._init_append_token(), 1172 passive=PASSIVE_NO_FETCH) 1173 1174 if child is not None: 1175 child_state, child_dict = instance_state(child),\ 1176 instance_dict(child) 1177 child_impl = child_state.manager[key].impl 1178 if initiator.parent_token is not parent_token and \ 1179 initiator.parent_token is not child_impl.parent_token: 1180 _acceptable_key_err(state, initiator, child_impl) 1181 elif initiator.impl is not child_impl or \ 1182 initiator.op not in (OP_APPEND, OP_REPLACE): 1183 child_impl.append( 1184 child_state, 1185 child_dict, 1186 state.obj(), 1187 initiator, 1188 passive=PASSIVE_NO_FETCH) 1189 return child 1190 1191 def emit_backref_from_collection_append_event(state, child, initiator): 1192 if child is None: 1193 return 1194 1195 child_state, child_dict = instance_state(child), \ 1196 instance_dict(child) 1197 child_impl = child_state.manager[key].impl 1198 1199 if initiator.parent_token is not parent_token and \ 1200 initiator.parent_token is not child_impl.parent_token: 1201 _acceptable_key_err(state, initiator, child_impl) 1202 elif initiator.impl is not child_impl or \ 1203 initiator.op not in (OP_APPEND, OP_REPLACE): 1204 child_impl.append( 1205 child_state, 1206 child_dict, 1207 state.obj(), 1208 initiator, 1209 passive=PASSIVE_NO_FETCH) 1210 return child 1211 1212 def emit_backref_from_collection_remove_event(state, child, initiator): 1213 if child is not None: 1214 child_state, child_dict = instance_state(child),\ 1215 instance_dict(child) 1216 child_impl = child_state.manager[key].impl 1217 if initiator.impl is not child_impl or \ 1218 initiator.op not in (OP_REMOVE, OP_REPLACE): 1219 child_impl.pop( 1220 child_state, 1221 child_dict, 1222 state.obj(), 1223 initiator, 1224 passive=PASSIVE_NO_FETCH) 1225 1226 if uselist: 1227 event.listen(attribute, "append", 1228 emit_backref_from_collection_append_event, 1229 retval=True, raw=True) 1230 else: 1231 event.listen(attribute, "set", 1232 emit_backref_from_scalar_set_event, 1233 retval=True, raw=True) 1234 # TODO: need coverage in test/orm/ of remove event 1235 event.listen(attribute, "remove", 1236 emit_backref_from_collection_remove_event, 1237 retval=True, raw=True) 1238 1239_NO_HISTORY = util.symbol('NO_HISTORY') 1240_NO_STATE_SYMBOLS = frozenset([ 1241 id(PASSIVE_NO_RESULT), 1242 id(NO_VALUE), 1243 id(NEVER_SET)]) 1244 1245History = util.namedtuple("History", [ 1246 "added", "unchanged", "deleted" 1247]) 1248 1249 1250class History(History): 1251 """A 3-tuple of added, unchanged and deleted values, 1252 representing the changes which have occurred on an instrumented 1253 attribute. 1254 1255 The easiest way to get a :class:`.History` object for a particular 1256 attribute on an object is to use the :func:`.inspect` function:: 1257 1258 from sqlalchemy import inspect 1259 1260 hist = inspect(myobject).attrs.myattribute.history 1261 1262 Each tuple member is an iterable sequence: 1263 1264 * ``added`` - the collection of items added to the attribute (the first 1265 tuple element). 1266 1267 * ``unchanged`` - the collection of items that have not changed on the 1268 attribute (the second tuple element). 1269 1270 * ``deleted`` - the collection of items that have been removed from the 1271 attribute (the third tuple element). 1272 1273 """ 1274 1275 def __bool__(self): 1276 return self != HISTORY_BLANK 1277 __nonzero__ = __bool__ 1278 1279 def empty(self): 1280 """Return True if this :class:`.History` has no changes 1281 and no existing, unchanged state. 1282 1283 """ 1284 1285 return not bool( 1286 (self.added or self.deleted) 1287 or self.unchanged 1288 ) 1289 1290 def sum(self): 1291 """Return a collection of added + unchanged + deleted.""" 1292 1293 return (self.added or []) +\ 1294 (self.unchanged or []) +\ 1295 (self.deleted or []) 1296 1297 def non_deleted(self): 1298 """Return a collection of added + unchanged.""" 1299 1300 return (self.added or []) +\ 1301 (self.unchanged or []) 1302 1303 def non_added(self): 1304 """Return a collection of unchanged + deleted.""" 1305 1306 return (self.unchanged or []) +\ 1307 (self.deleted or []) 1308 1309 def has_changes(self): 1310 """Return True if this :class:`.History` has changes.""" 1311 1312 return bool(self.added or self.deleted) 1313 1314 def as_state(self): 1315 return History( 1316 [(c is not None) 1317 and instance_state(c) or None 1318 for c in self.added], 1319 [(c is not None) 1320 and instance_state(c) or None 1321 for c in self.unchanged], 1322 [(c is not None) 1323 and instance_state(c) or None 1324 for c in self.deleted], 1325 ) 1326 1327 @classmethod 1328 def from_scalar_attribute(cls, attribute, state, current): 1329 original = state.committed_state.get(attribute.key, _NO_HISTORY) 1330 1331 if original is _NO_HISTORY: 1332 if current is NEVER_SET: 1333 return cls((), (), ()) 1334 else: 1335 return cls((), [current], ()) 1336 # don't let ClauseElement expressions here trip things up 1337 elif attribute.is_equal(current, original) is True: 1338 return cls((), [current], ()) 1339 else: 1340 # current convention on native scalars is to not 1341 # include information 1342 # about missing previous value in "deleted", but 1343 # we do include None, which helps in some primary 1344 # key situations 1345 if id(original) in _NO_STATE_SYMBOLS: 1346 deleted = () 1347 else: 1348 deleted = [original] 1349 if current is NEVER_SET: 1350 return cls((), (), deleted) 1351 else: 1352 return cls([current], (), deleted) 1353 1354 @classmethod 1355 def from_object_attribute(cls, attribute, state, current): 1356 original = state.committed_state.get(attribute.key, _NO_HISTORY) 1357 1358 if original is _NO_HISTORY: 1359 if current is NO_VALUE or current is NEVER_SET: 1360 return cls((), (), ()) 1361 else: 1362 return cls((), [current], ()) 1363 elif current is original: 1364 return cls((), [current], ()) 1365 else: 1366 # current convention on related objects is to not 1367 # include information 1368 # about missing previous value in "deleted", and 1369 # to also not include None - the dependency.py rules 1370 # ignore the None in any case. 1371 if id(original) in _NO_STATE_SYMBOLS or original is None: 1372 deleted = () 1373 else: 1374 deleted = [original] 1375 if current is NO_VALUE or current is NEVER_SET: 1376 return cls((), (), deleted) 1377 else: 1378 return cls([current], (), deleted) 1379 1380 @classmethod 1381 def from_collection(cls, attribute, state, current): 1382 original = state.committed_state.get(attribute.key, _NO_HISTORY) 1383 1384 if current is NO_VALUE or current is NEVER_SET: 1385 return cls((), (), ()) 1386 1387 current = getattr(current, '_sa_adapter') 1388 if original in (NO_VALUE, NEVER_SET): 1389 return cls(list(current), (), ()) 1390 elif original is _NO_HISTORY: 1391 return cls((), list(current), ()) 1392 else: 1393 1394 current_states = [((c is not None) and instance_state(c) 1395 or None, c) 1396 for c in current 1397 ] 1398 original_states = [((c is not None) and instance_state(c) 1399 or None, c) 1400 for c in original 1401 ] 1402 1403 current_set = dict(current_states) 1404 original_set = dict(original_states) 1405 1406 return cls( 1407 [o for s, o in current_states if s not in original_set], 1408 [o for s, o in current_states if s in original_set], 1409 [o for s, o in original_states if s not in current_set] 1410 ) 1411 1412HISTORY_BLANK = History(None, None, None) 1413 1414 1415def get_history(obj, key, passive=PASSIVE_OFF): 1416 """Return a :class:`.History` record for the given object 1417 and attribute key. 1418 1419 :param obj: an object whose class is instrumented by the 1420 attributes package. 1421 1422 :param key: string attribute name. 1423 1424 :param passive: indicates loading behavior for the attribute 1425 if the value is not already present. This is a 1426 bitflag attribute, which defaults to the symbol 1427 :attr:`.PASSIVE_OFF` indicating all necessary SQL 1428 should be emitted. 1429 1430 """ 1431 if passive is True: 1432 util.warn_deprecated("Passing True for 'passive' is deprecated. " 1433 "Use attributes.PASSIVE_NO_INITIALIZE") 1434 passive = PASSIVE_NO_INITIALIZE 1435 elif passive is False: 1436 util.warn_deprecated("Passing False for 'passive' is " 1437 "deprecated. Use attributes.PASSIVE_OFF") 1438 passive = PASSIVE_OFF 1439 1440 return get_state_history(instance_state(obj), key, passive) 1441 1442 1443def get_state_history(state, key, passive=PASSIVE_OFF): 1444 return state.get_history(key, passive) 1445 1446 1447def has_parent(cls, obj, key, optimistic=False): 1448 """TODO""" 1449 manager = manager_of_class(cls) 1450 state = instance_state(obj) 1451 return manager.has_parent(state, key, optimistic) 1452 1453 1454def register_attribute(class_, key, **kw): 1455 comparator = kw.pop('comparator', None) 1456 parententity = kw.pop('parententity', None) 1457 doc = kw.pop('doc', None) 1458 desc = register_descriptor(class_, key, 1459 comparator, parententity, doc=doc) 1460 register_attribute_impl(class_, key, **kw) 1461 return desc 1462 1463 1464def register_attribute_impl(class_, key, 1465 uselist=False, callable_=None, 1466 useobject=False, 1467 impl_class=None, backref=None, **kw): 1468 1469 manager = manager_of_class(class_) 1470 if uselist: 1471 factory = kw.pop('typecallable', None) 1472 typecallable = manager.instrument_collection_class( 1473 key, factory or list) 1474 else: 1475 typecallable = kw.pop('typecallable', None) 1476 1477 dispatch = manager[key].dispatch 1478 1479 if impl_class: 1480 impl = impl_class(class_, key, typecallable, dispatch, **kw) 1481 elif uselist: 1482 impl = CollectionAttributeImpl(class_, key, callable_, dispatch, 1483 typecallable=typecallable, **kw) 1484 elif useobject: 1485 impl = ScalarObjectAttributeImpl(class_, key, callable_, 1486 dispatch, **kw) 1487 else: 1488 impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw) 1489 1490 manager[key].impl = impl 1491 1492 if backref: 1493 backref_listeners(manager[key], backref, uselist) 1494 1495 manager.post_configure_attribute(key) 1496 return manager[key] 1497 1498 1499def register_descriptor(class_, key, comparator=None, 1500 parententity=None, doc=None): 1501 manager = manager_of_class(class_) 1502 1503 descriptor = InstrumentedAttribute(class_, key, comparator=comparator, 1504 parententity=parententity) 1505 1506 descriptor.__doc__ = doc 1507 1508 manager.instrument_attribute(key, descriptor) 1509 return descriptor 1510 1511 1512def unregister_attribute(class_, key): 1513 manager_of_class(class_).uninstrument_attribute(key) 1514 1515 1516def init_collection(obj, key): 1517 """Initialize a collection attribute and return the collection adapter. 1518 1519 This function is used to provide direct access to collection internals 1520 for a previously unloaded attribute. e.g.:: 1521 1522 collection_adapter = init_collection(someobject, 'elements') 1523 for elem in values: 1524 collection_adapter.append_without_event(elem) 1525 1526 For an easier way to do the above, see 1527 :func:`~sqlalchemy.orm.attributes.set_committed_value`. 1528 1529 obj is an instrumented object instance. An InstanceState 1530 is accepted directly for backwards compatibility but 1531 this usage is deprecated. 1532 1533 """ 1534 state = instance_state(obj) 1535 dict_ = state.dict 1536 return init_state_collection(state, dict_, key) 1537 1538 1539def init_state_collection(state, dict_, key): 1540 """Initialize a collection attribute and return the collection adapter.""" 1541 1542 attr = state.manager[key].impl 1543 user_data = attr.initialize(state, dict_) 1544 return attr.get_collection(state, dict_, user_data) 1545 1546 1547def set_committed_value(instance, key, value): 1548 """Set the value of an attribute with no history events. 1549 1550 Cancels any previous history present. The value should be 1551 a scalar value for scalar-holding attributes, or 1552 an iterable for any collection-holding attribute. 1553 1554 This is the same underlying method used when a lazy loader 1555 fires off and loads additional data from the database. 1556 In particular, this method can be used by application code 1557 which has loaded additional attributes or collections through 1558 separate queries, which can then be attached to an instance 1559 as though it were part of its original loaded state. 1560 1561 """ 1562 state, dict_ = instance_state(instance), instance_dict(instance) 1563 state.manager[key].impl.set_committed_value(state, dict_, value) 1564 1565 1566def set_attribute(instance, key, value): 1567 """Set the value of an attribute, firing history events. 1568 1569 This function may be used regardless of instrumentation 1570 applied directly to the class, i.e. no descriptors are required. 1571 Custom attribute management schemes will need to make usage 1572 of this method to establish attribute state as understood 1573 by SQLAlchemy. 1574 1575 """ 1576 state, dict_ = instance_state(instance), instance_dict(instance) 1577 state.manager[key].impl.set(state, dict_, value, None) 1578 1579 1580def get_attribute(instance, key): 1581 """Get the value of an attribute, firing any callables required. 1582 1583 This function may be used regardless of instrumentation 1584 applied directly to the class, i.e. no descriptors are required. 1585 Custom attribute management schemes will need to make usage 1586 of this method to make usage of attribute state as understood 1587 by SQLAlchemy. 1588 1589 """ 1590 state, dict_ = instance_state(instance), instance_dict(instance) 1591 return state.manager[key].impl.get(state, dict_) 1592 1593 1594def del_attribute(instance, key): 1595 """Delete the value of an attribute, firing history events. 1596 1597 This function may be used regardless of instrumentation 1598 applied directly to the class, i.e. no descriptors are required. 1599 Custom attribute management schemes will need to make usage 1600 of this method to establish attribute state as understood 1601 by SQLAlchemy. 1602 1603 """ 1604 state, dict_ = instance_state(instance), instance_dict(instance) 1605 state.manager[key].impl.delete(state, dict_) 1606 1607 1608def flag_modified(instance, key): 1609 """Mark an attribute on an instance as 'modified'. 1610 1611 This sets the 'modified' flag on the instance and 1612 establishes an unconditional change event for the given attribute. 1613 1614 """ 1615 state, dict_ = instance_state(instance), instance_dict(instance) 1616 impl = state.manager[key].impl 1617 state._modified_event(dict_, impl, NO_VALUE, force=True) 1618