123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617 |
- # orm/attributes.py
- # Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # This module is part of SQLAlchemy and is released under
- # the MIT License: http://www.opensource.org/licenses/mit-license.php
- """Defines instrumentation for class attributes and their interaction
- with instances.
- This module is usually not directly visible to user applications, but
- defines a large part of the ORM's interactivity.
- """
- import operator
- from .. import util, event, inspection
- from . import interfaces, collections, exc as orm_exc
- from .base import instance_state, instance_dict, manager_of_class
- from .base import PASSIVE_NO_RESULT, ATTR_WAS_SET, ATTR_EMPTY, NO_VALUE,\
- NEVER_SET, NO_CHANGE, CALLABLES_OK, SQL_OK, RELATED_OBJECT_OK,\
- INIT_OK, NON_PERSISTENT_OK, LOAD_AGAINST_COMMITTED, PASSIVE_OFF,\
- PASSIVE_RETURN_NEVER_SET, PASSIVE_NO_INITIALIZE, PASSIVE_NO_FETCH,\
- PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT, NO_AUTOFLUSH
- from .base import state_str, instance_str
- @inspection._self_inspects
- class QueryableAttribute(interfaces._MappedAttribute,
- interfaces.InspectionAttr,
- interfaces.PropComparator):
- """Base class for :term:`descriptor` objects that intercept
- attribute events on behalf of a :class:`.MapperProperty`
- object. The actual :class:`.MapperProperty` is accessible
- via the :attr:`.QueryableAttribute.property`
- attribute.
- .. seealso::
- :class:`.InstrumentedAttribute`
- :class:`.MapperProperty`
- :attr:`.Mapper.all_orm_descriptors`
- :attr:`.Mapper.attrs`
- """
- is_attribute = True
- def __init__(self, class_, key, impl=None,
- comparator=None, parententity=None,
- of_type=None):
- self.class_ = class_
- self.key = key
- self.impl = impl
- self.comparator = comparator
- self._parententity = parententity
- self._of_type = of_type
- manager = manager_of_class(class_)
- # manager is None in the case of AliasedClass
- if manager:
- # propagate existing event listeners from
- # immediate superclass
- for base in manager._bases:
- if key in base:
- self.dispatch._update(base[key].dispatch)
- @util.memoized_property
- def _supports_population(self):
- return self.impl.supports_population
- def get_history(self, instance, passive=PASSIVE_OFF):
- return self.impl.get_history(instance_state(instance),
- instance_dict(instance), passive)
- def __selectable__(self):
- # TODO: conditionally attach this method based on clause_element ?
- return self
- @util.memoized_property
- def info(self):
- """Return the 'info' dictionary for the underlying SQL element.
- The behavior here is as follows:
- * If the attribute is a column-mapped property, i.e.
- :class:`.ColumnProperty`, which is mapped directly
- to a schema-level :class:`.Column` object, this attribute
- will return the :attr:`.SchemaItem.info` dictionary associated
- with the core-level :class:`.Column` object.
- * If the attribute is a :class:`.ColumnProperty` but is mapped to
- any other kind of SQL expression other than a :class:`.Column`,
- the attribute will refer to the :attr:`.MapperProperty.info`
- dictionary associated directly with the :class:`.ColumnProperty`,
- assuming the SQL expression itself does not have its own ``.info``
- attribute (which should be the case, unless a user-defined SQL
- construct has defined one).
- * If the attribute refers to any other kind of
- :class:`.MapperProperty`, including :class:`.RelationshipProperty`,
- the attribute will refer to the :attr:`.MapperProperty.info`
- dictionary associated with that :class:`.MapperProperty`.
- * To access the :attr:`.MapperProperty.info` dictionary of the
- :class:`.MapperProperty` unconditionally, including for a
- :class:`.ColumnProperty` that's associated directly with a
- :class:`.schema.Column`, the attribute can be referred to using
- :attr:`.QueryableAttribute.property` attribute, as
- ``MyClass.someattribute.property.info``.
- .. versionadded:: 0.8.0
- .. seealso::
- :attr:`.SchemaItem.info`
- :attr:`.MapperProperty.info`
- """
- return self.comparator.info
- @util.memoized_property
- def parent(self):
- """Return an inspection instance representing the parent.
- This will be either an instance of :class:`.Mapper`
- or :class:`.AliasedInsp`, depending upon the nature
- of the parent entity which this attribute is associated
- with.
- """
- return inspection.inspect(self._parententity)
- @property
- def expression(self):
- return self.comparator.__clause_element__()
- def __clause_element__(self):
- return self.comparator.__clause_element__()
- def _query_clause_element(self):
- """like __clause_element__(), but called specifically
- by :class:`.Query` to allow special behavior."""
- return self.comparator._query_clause_element()
- def adapt_to_entity(self, adapt_to_entity):
- assert not self._of_type
- return self.__class__(adapt_to_entity.entity,
- self.key, impl=self.impl,
- comparator=self.comparator.adapt_to_entity(
- adapt_to_entity),
- parententity=adapt_to_entity)
- def of_type(self, cls):
- return QueryableAttribute(
- self.class_,
- self.key,
- self.impl,
- self.comparator.of_type(cls),
- self._parententity,
- of_type=cls)
- def label(self, name):
- return self._query_clause_element().label(name)
- def operate(self, op, *other, **kwargs):
- return op(self.comparator, *other, **kwargs)
- def reverse_operate(self, op, other, **kwargs):
- return op(other, self.comparator, **kwargs)
- def hasparent(self, state, optimistic=False):
- return self.impl.hasparent(state, optimistic=optimistic) is not False
- def __getattr__(self, key):
- try:
- return getattr(self.comparator, key)
- except AttributeError:
- raise AttributeError(
- 'Neither %r object nor %r object associated with %s '
- 'has an attribute %r' % (
- type(self).__name__,
- type(self.comparator).__name__,
- self,
- key)
- )
- def __str__(self):
- return "%s.%s" % (self.class_.__name__, self.key)
- @util.memoized_property
- def property(self):
- """Return the :class:`.MapperProperty` associated with this
- :class:`.QueryableAttribute`.
- Return values here will commonly be instances of
- :class:`.ColumnProperty` or :class:`.RelationshipProperty`.
- """
- return self.comparator.property
- class InstrumentedAttribute(QueryableAttribute):
- """Class bound instrumented attribute which adds basic
- :term:`descriptor` methods.
- See :class:`.QueryableAttribute` for a description of most features.
- """
- def __set__(self, instance, value):
- self.impl.set(instance_state(instance),
- instance_dict(instance), value, None)
- def __delete__(self, instance):
- self.impl.delete(instance_state(instance), instance_dict(instance))
- def __get__(self, instance, owner):
- if instance is None:
- return self
- dict_ = instance_dict(instance)
- if self._supports_population and self.key in dict_:
- return dict_[self.key]
- else:
- return self.impl.get(instance_state(instance), dict_)
- def create_proxied_attribute(descriptor):
- """Create an QueryableAttribute / user descriptor hybrid.
- Returns a new QueryableAttribute type that delegates descriptor
- behavior and getattr() to the given descriptor.
- """
- # TODO: can move this to descriptor_props if the need for this
- # function is removed from ext/hybrid.py
- class Proxy(QueryableAttribute):
- """Presents the :class:`.QueryableAttribute` interface as a
- proxy on top of a Python descriptor / :class:`.PropComparator`
- combination.
- """
- def __init__(self, class_, key, descriptor,
- comparator,
- adapt_to_entity=None, doc=None,
- original_property=None):
- self.class_ = class_
- self.key = key
- self.descriptor = descriptor
- self.original_property = original_property
- self._comparator = comparator
- self._adapt_to_entity = adapt_to_entity
- self.__doc__ = doc
- @property
- def property(self):
- return self.comparator.property
- @util.memoized_property
- def comparator(self):
- if util.callable(self._comparator):
- self._comparator = self._comparator()
- if self._adapt_to_entity:
- self._comparator = self._comparator.adapt_to_entity(
- self._adapt_to_entity)
- return self._comparator
- def adapt_to_entity(self, adapt_to_entity):
- return self.__class__(adapt_to_entity.entity,
- self.key,
- self.descriptor,
- self._comparator,
- adapt_to_entity)
- def __get__(self, instance, owner):
- if instance is None:
- return self
- else:
- return self.descriptor.__get__(instance, owner)
- def __str__(self):
- return "%s.%s" % (self.class_.__name__, self.key)
- def __getattr__(self, attribute):
- """Delegate __getattr__ to the original descriptor and/or
- comparator."""
- try:
- return getattr(descriptor, attribute)
- except AttributeError:
- try:
- return getattr(self.comparator, attribute)
- except AttributeError:
- raise AttributeError(
- 'Neither %r object nor %r object associated with %s '
- 'has an attribute %r' % (
- type(descriptor).__name__,
- type(self.comparator).__name__,
- self,
- attribute)
- )
- Proxy.__name__ = type(descriptor).__name__ + 'Proxy'
- util.monkeypatch_proxied_specials(Proxy, type(descriptor),
- name='descriptor',
- from_instance=descriptor)
- return Proxy
- OP_REMOVE = util.symbol("REMOVE")
- OP_APPEND = util.symbol("APPEND")
- OP_REPLACE = util.symbol("REPLACE")
- class Event(object):
- """A token propagated throughout the course of a chain of attribute
- events.
- Serves as an indicator of the source of the event and also provides
- a means of controlling propagation across a chain of attribute
- operations.
- The :class:`.Event` object is sent as the ``initiator`` argument
- when dealing with the :meth:`.AttributeEvents.append`,
- :meth:`.AttributeEvents.set`,
- and :meth:`.AttributeEvents.remove` events.
- The :class:`.Event` object is currently interpreted by the backref
- event handlers, and is used to control the propagation of operations
- across two mutually-dependent attributes.
- .. versionadded:: 0.9.0
- :var impl: The :class:`.AttributeImpl` which is the current event
- initiator.
- :var op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE` or
- :attr:`.OP_REPLACE`, indicating the source operation.
- """
- __slots__ = 'impl', 'op', 'parent_token'
- def __init__(self, attribute_impl, op):
- self.impl = attribute_impl
- self.op = op
- self.parent_token = self.impl.parent_token
- def __eq__(self, other):
- return isinstance(other, Event) and \
- other.impl is self.impl and \
- other.op == self.op
- @property
- def key(self):
- return self.impl.key
- def hasparent(self, state):
- return self.impl.hasparent(state)
- class AttributeImpl(object):
- """internal implementation for instrumented attributes."""
- def __init__(self, class_, key,
- callable_, dispatch, trackparent=False, extension=None,
- compare_function=None, active_history=False,
- parent_token=None, expire_missing=True,
- send_modified_events=True,
- **kwargs):
- r"""Construct an AttributeImpl.
- \class_
- associated class
- key
- string name of the attribute
- \callable_
- optional function which generates a callable based on a parent
- instance, which produces the "default" values for a scalar or
- collection attribute when it's first accessed, if not present
- already.
- trackparent
- if True, attempt to track if an instance has a parent attached
- to it via this attribute.
- extension
- a single or list of AttributeExtension object(s) which will
- receive set/delete/append/remove/etc. events. Deprecated.
- The event package is now used.
- compare_function
- a function that compares two values which are normally
- assignable to this attribute.
- active_history
- indicates that get_history() should always return the "old" value,
- even if it means executing a lazy callable upon attribute change.
- parent_token
- Usually references the MapperProperty, used as a key for
- the hasparent() function to identify an "owning" attribute.
- Allows multiple AttributeImpls to all match a single
- owner attribute.
- expire_missing
- if False, don't add an "expiry" callable to this attribute
- during state.expire_attributes(None), if no value is present
- for this key.
- send_modified_events
- if False, the InstanceState._modified_event method will have no
- effect; this means the attribute will never show up as changed in a
- history entry.
- """
- self.class_ = class_
- self.key = key
- self.callable_ = callable_
- self.dispatch = dispatch
- self.trackparent = trackparent
- self.parent_token = parent_token or self
- self.send_modified_events = send_modified_events
- if compare_function is None:
- self.is_equal = operator.eq
- else:
- self.is_equal = compare_function
- # TODO: pass in the manager here
- # instead of doing a lookup
- attr = manager_of_class(class_)[key]
- for ext in util.to_list(extension or []):
- ext._adapt_listener(attr, ext)
- if active_history:
- self.dispatch._active_history = True
- self.expire_missing = expire_missing
- __slots__ = (
- 'class_', 'key', 'callable_', 'dispatch', 'trackparent',
- 'parent_token', 'send_modified_events', 'is_equal', 'expire_missing'
- )
- def __str__(self):
- return "%s.%s" % (self.class_.__name__, self.key)
- def _get_active_history(self):
- """Backwards compat for impl.active_history"""
- return self.dispatch._active_history
- def _set_active_history(self, value):
- self.dispatch._active_history = value
- active_history = property(_get_active_history, _set_active_history)
- def hasparent(self, state, optimistic=False):
- """Return the boolean value of a `hasparent` flag attached to
- the given state.
- The `optimistic` flag determines what the default return value
- should be if no `hasparent` flag can be located.
- As this function is used to determine if an instance is an
- *orphan*, instances that were loaded from storage should be
- assumed to not be orphans, until a True/False value for this
- flag is set.
- An instance attribute that is loaded by a callable function
- will also not have a `hasparent` flag.
- """
- msg = "This AttributeImpl is not configured to track parents."
- assert self.trackparent, msg
- return state.parents.get(id(self.parent_token), optimistic) \
- is not False
- def sethasparent(self, state, parent_state, value):
- """Set a boolean flag on the given item corresponding to
- whether or not it is attached to a parent object via the
- attribute represented by this ``InstrumentedAttribute``.
- """
- msg = "This AttributeImpl is not configured to track parents."
- assert self.trackparent, msg
- id_ = id(self.parent_token)
- if value:
- state.parents[id_] = parent_state
- else:
- if id_ in state.parents:
- last_parent = state.parents[id_]
- if last_parent is not False and \
- last_parent.key != parent_state.key:
- if last_parent.obj() is None:
- raise orm_exc.StaleDataError(
- "Removing state %s from parent "
- "state %s along attribute '%s', "
- "but the parent record "
- "has gone stale, can't be sure this "
- "is the most recent parent." %
- (state_str(state),
- state_str(parent_state),
- self.key))
- return
- state.parents[id_] = False
- def get_history(self, state, dict_, passive=PASSIVE_OFF):
- raise NotImplementedError()
- def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
- """Return a list of tuples of (state, obj)
- for all objects in this attribute's current state
- + history.
- Only applies to object-based attributes.
- This is an inlining of existing functionality
- which roughly corresponds to:
- get_state_history(
- state,
- key,
- passive=PASSIVE_NO_INITIALIZE).sum()
- """
- raise NotImplementedError()
- def initialize(self, state, dict_):
- """Initialize the given state's attribute with an empty value."""
- value = None
- for fn in self.dispatch.init_scalar:
- ret = fn(state, value, dict_)
- if ret is not ATTR_EMPTY:
- value = ret
- return value
- def get(self, state, dict_, passive=PASSIVE_OFF):
- """Retrieve a value from the given object.
- If a callable is assembled on this object's attribute, and
- passive is False, the callable will be executed and the
- resulting value will be set as the new value for this attribute.
- """
- if self.key in dict_:
- return dict_[self.key]
- else:
- # if history present, don't load
- key = self.key
- if key not in state.committed_state or \
- state.committed_state[key] is NEVER_SET:
- if not passive & CALLABLES_OK:
- return PASSIVE_NO_RESULT
- if key in state.expired_attributes:
- value = state._load_expired(state, passive)
- elif key in state.callables:
- callable_ = state.callables[key]
- value = callable_(state, passive)
- elif self.callable_:
- value = self.callable_(state, passive)
- else:
- value = ATTR_EMPTY
- if value is PASSIVE_NO_RESULT or value is NEVER_SET:
- return value
- elif value is ATTR_WAS_SET:
- try:
- return dict_[key]
- except KeyError:
- # TODO: no test coverage here.
- raise KeyError(
- "Deferred loader for attribute "
- "%r failed to populate "
- "correctly" % key)
- elif value is not ATTR_EMPTY:
- return self.set_committed_value(state, dict_, value)
- if not passive & INIT_OK:
- return NEVER_SET
- else:
- # Return a new, empty value
- return self.initialize(state, dict_)
- def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
- self.set(state, dict_, value, initiator, passive=passive)
- def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
- self.set(state, dict_, None, initiator,
- passive=passive, check_old=value)
- def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
- self.set(state, dict_, None, initiator,
- passive=passive, check_old=value, pop=True)
- def set(self, state, dict_, value, initiator,
- passive=PASSIVE_OFF, check_old=None, pop=False):
- raise NotImplementedError()
- def get_committed_value(self, state, dict_, passive=PASSIVE_OFF):
- """return the unchanged value of this attribute"""
- if self.key in state.committed_state:
- value = state.committed_state[self.key]
- if value in (NO_VALUE, NEVER_SET):
- return None
- else:
- return value
- else:
- return self.get(state, dict_, passive=passive)
- def set_committed_value(self, state, dict_, value):
- """set an attribute value on the given instance and 'commit' it."""
- dict_[self.key] = value
- state._commit(dict_, [self.key])
- return value
- class ScalarAttributeImpl(AttributeImpl):
- """represents a scalar value-holding InstrumentedAttribute."""
- accepts_scalar_loader = True
- uses_objects = False
- supports_population = True
- collection = False
- __slots__ = '_replace_token', '_append_token', '_remove_token'
- def __init__(self, *arg, **kw):
- super(ScalarAttributeImpl, self).__init__(*arg, **kw)
- self._replace_token = self._append_token = None
- self._remove_token = None
- def _init_append_token(self):
- self._replace_token = self._append_token = Event(self, OP_REPLACE)
- return self._replace_token
- _init_append_or_replace_token = _init_append_token
- def _init_remove_token(self):
- self._remove_token = Event(self, OP_REMOVE)
- return self._remove_token
- def delete(self, state, dict_):
- # TODO: catch key errors, convert to attributeerror?
- if self.dispatch._active_history:
- old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
- else:
- old = dict_.get(self.key, NO_VALUE)
- if self.dispatch.remove:
- self.fire_remove_event(state, dict_, old, self._remove_token)
- state._modified_event(dict_, self, old)
- del dict_[self.key]
- def get_history(self, state, dict_, passive=PASSIVE_OFF):
- if self.key in dict_:
- return History.from_scalar_attribute(self, state, dict_[self.key])
- else:
- if passive & INIT_OK:
- passive ^= INIT_OK
- current = self.get(state, dict_, passive=passive)
- if current is PASSIVE_NO_RESULT:
- return HISTORY_BLANK
- else:
- return History.from_scalar_attribute(self, state, current)
- def set(self, state, dict_, value, initiator,
- passive=PASSIVE_OFF, check_old=None, pop=False):
- if self.dispatch._active_history:
- old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
- else:
- old = dict_.get(self.key, NO_VALUE)
- if self.dispatch.set:
- value = self.fire_replace_event(state, dict_,
- value, old, initiator)
- state._modified_event(dict_, self, old)
- dict_[self.key] = value
- def fire_replace_event(self, state, dict_, value, previous, initiator):
- for fn in self.dispatch.set:
- value = fn(
- state, value, previous,
- initiator or self._replace_token or
- self._init_append_or_replace_token())
- return value
- def fire_remove_event(self, state, dict_, value, initiator):
- for fn in self.dispatch.remove:
- fn(state, value,
- initiator or self._remove_token or self._init_remove_token())
- @property
- def type(self):
- self.property.columns[0].type
- class ScalarObjectAttributeImpl(ScalarAttributeImpl):
- """represents a scalar-holding InstrumentedAttribute,
- where the target object is also instrumented.
- Adds events to delete/set operations.
- """
- accepts_scalar_loader = False
- uses_objects = True
- supports_population = True
- collection = False
- __slots__ = ()
- def delete(self, state, dict_):
- old = self.get(state, dict_)
- self.fire_remove_event(
- state, dict_, old,
- self._remove_token or self._init_remove_token())
- del dict_[self.key]
- def get_history(self, state, dict_, passive=PASSIVE_OFF):
- if self.key in dict_:
- return History.from_object_attribute(self, state, dict_[self.key])
- else:
- if passive & INIT_OK:
- passive ^= INIT_OK
- current = self.get(state, dict_, passive=passive)
- if current is PASSIVE_NO_RESULT:
- return HISTORY_BLANK
- else:
- return History.from_object_attribute(self, state, current)
- def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
- if self.key in dict_:
- current = dict_[self.key]
- elif passive & CALLABLES_OK:
- current = self.get(state, dict_, passive=passive)
- else:
- return []
- # can't use __hash__(), can't use __eq__() here
- if current is not None and \
- current is not PASSIVE_NO_RESULT and \
- current is not NEVER_SET:
- ret = [(instance_state(current), current)]
- else:
- ret = [(None, None)]
- if self.key in state.committed_state:
- original = state.committed_state[self.key]
- if original is not None and \
- original is not PASSIVE_NO_RESULT and \
- original is not NEVER_SET and \
- original is not current:
- ret.append((instance_state(original), original))
- return ret
- def set(self, state, dict_, value, initiator,
- passive=PASSIVE_OFF, check_old=None, pop=False):
- """Set a value on the given InstanceState.
- """
- if self.dispatch._active_history:
- old = self.get(
- state, dict_,
- passive=PASSIVE_ONLY_PERSISTENT |
- NO_AUTOFLUSH | LOAD_AGAINST_COMMITTED)
- else:
- old = self.get(
- state, dict_, passive=PASSIVE_NO_FETCH ^ INIT_OK |
- LOAD_AGAINST_COMMITTED)
- if check_old is not None and \
- old is not PASSIVE_NO_RESULT and \
- check_old is not old:
- if pop:
- return
- else:
- raise ValueError(
- "Object %s not associated with %s on attribute '%s'" % (
- instance_str(check_old),
- state_str(state),
- self.key
- ))
- value = self.fire_replace_event(state, dict_, value, old, initiator)
- dict_[self.key] = value
- def fire_remove_event(self, state, dict_, value, initiator):
- if self.trackparent and value is not None:
- self.sethasparent(instance_state(value), state, False)
- for fn in self.dispatch.remove:
- fn(state, value, initiator or
- self._remove_token or self._init_remove_token())
- state._modified_event(dict_, self, value)
- def fire_replace_event(self, state, dict_, value, previous, initiator):
- if self.trackparent:
- if (previous is not value and
- previous not in (None, PASSIVE_NO_RESULT, NEVER_SET)):
- self.sethasparent(instance_state(previous), state, False)
- for fn in self.dispatch.set:
- value = fn(
- state, value, previous, initiator or
- self._replace_token or self._init_append_or_replace_token())
- state._modified_event(dict_, self, previous)
- if self.trackparent:
- if value is not None:
- self.sethasparent(instance_state(value), state, True)
- return value
- class CollectionAttributeImpl(AttributeImpl):
- """A collection-holding attribute that instruments changes in membership.
- Only handles collections of instrumented objects.
- InstrumentedCollectionAttribute holds an arbitrary, user-specified
- container object (defaulting to a list) and brokers access to the
- CollectionAdapter, a "view" onto that object that presents consistent bag
- semantics to the orm layer independent of the user data implementation.
- """
- accepts_scalar_loader = False
- uses_objects = True
- supports_population = True
- collection = True
- __slots__ = (
- 'copy', 'collection_factory', '_append_token', '_remove_token',
- '_duck_typed_as'
- )
- def __init__(self, class_, key, callable_, dispatch,
- typecallable=None, trackparent=False, extension=None,
- copy_function=None, compare_function=None, **kwargs):
- super(CollectionAttributeImpl, self).__init__(
- class_,
- key,
- callable_, dispatch,
- trackparent=trackparent,
- extension=extension,
- compare_function=compare_function,
- **kwargs)
- if copy_function is None:
- copy_function = self.__copy
- self.copy = copy_function
- self.collection_factory = typecallable
- self._append_token = None
- self._remove_token = None
- self._duck_typed_as = util.duck_type_collection(
- self.collection_factory())
- if getattr(self.collection_factory, "_sa_linker", None):
- @event.listens_for(self, "init_collection")
- def link(target, collection, collection_adapter):
- collection._sa_linker(collection_adapter)
- @event.listens_for(self, "dispose_collection")
- def unlink(target, collection, collection_adapter):
- collection._sa_linker(None)
- def _init_append_token(self):
- self._append_token = Event(self, OP_APPEND)
- return self._append_token
- def _init_remove_token(self):
- self._remove_token = Event(self, OP_REMOVE)
- return self._remove_token
- def __copy(self, item):
- return [y for y in collections.collection_adapter(item)]
- def get_history(self, state, dict_, passive=PASSIVE_OFF):
- current = self.get(state, dict_, passive=passive)
- if current is PASSIVE_NO_RESULT:
- return HISTORY_BLANK
- else:
- return History.from_collection(self, state, current)
- def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
- # NOTE: passive is ignored here at the moment
- if self.key not in dict_:
- return []
- current = dict_[self.key]
- current = getattr(current, '_sa_adapter')
- if self.key in state.committed_state:
- original = state.committed_state[self.key]
- if original not in (NO_VALUE, NEVER_SET):
- current_states = [((c is not None) and
- instance_state(c) or None, c)
- for c in current]
- original_states = [((c is not None) and
- instance_state(c) or None, c)
- for c in original]
- current_set = dict(current_states)
- original_set = dict(original_states)
- return \
- [(s, o) for s, o in current_states
- if s not in original_set] + \
- [(s, o) for s, o in current_states
- if s in original_set] + \
- [(s, o) for s, o in original_states
- if s not in current_set]
- return [(instance_state(o), o) for o in current]
- def fire_append_event(self, state, dict_, value, initiator):
- for fn in self.dispatch.append:
- value = fn(
- state, value,
- initiator or self._append_token or self._init_append_token())
- state._modified_event(dict_, self, NEVER_SET, True)
- if self.trackparent and value is not None:
- self.sethasparent(instance_state(value), state, True)
- return value
- def fire_pre_remove_event(self, state, dict_, initiator):
- state._modified_event(dict_, self, NEVER_SET, True)
- def fire_remove_event(self, state, dict_, value, initiator):
- if self.trackparent and value is not None:
- self.sethasparent(instance_state(value), state, False)
- for fn in self.dispatch.remove:
- fn(state, value,
- initiator or self._remove_token or self._init_remove_token())
- state._modified_event(dict_, self, NEVER_SET, True)
- def delete(self, state, dict_):
- if self.key not in dict_:
- return
- state._modified_event(dict_, self, NEVER_SET, True)
- collection = self.get_collection(state, state.dict)
- collection.clear_with_event()
- # TODO: catch key errors, convert to attributeerror?
- del dict_[self.key]
- def initialize(self, state, dict_):
- """Initialize this attribute with an empty collection."""
- _, user_data = self._initialize_collection(state)
- dict_[self.key] = user_data
- return user_data
- def _initialize_collection(self, state):
- adapter, collection = state.manager.initialize_collection(
- self.key, state, self.collection_factory)
- self.dispatch.init_collection(state, collection, adapter)
- return adapter, collection
- def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
- collection = self.get_collection(state, dict_, passive=passive)
- if collection is PASSIVE_NO_RESULT:
- value = self.fire_append_event(state, dict_, value, initiator)
- assert self.key not in dict_, \
- "Collection was loaded during event handling."
- state._get_pending_mutation(self.key).append(value)
- else:
- collection.append_with_event(value, initiator)
- def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
- collection = self.get_collection(state, state.dict, passive=passive)
- if collection is PASSIVE_NO_RESULT:
- self.fire_remove_event(state, dict_, value, initiator)
- assert self.key not in dict_, \
- "Collection was loaded during event handling."
- state._get_pending_mutation(self.key).remove(value)
- else:
- collection.remove_with_event(value, initiator)
- def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
- try:
- # TODO: better solution here would be to add
- # a "popper" role to collections.py to complement
- # "remover".
- self.remove(state, dict_, value, initiator, passive=passive)
- except (ValueError, KeyError, IndexError):
- pass
- def set(self, state, dict_, value, initiator=None,
- passive=PASSIVE_OFF, pop=False, _adapt=True):
- iterable = orig_iterable = value
- # pulling a new collection first so that an adaptation exception does
- # not trigger a lazy load of the old collection.
- new_collection, user_data = self._initialize_collection(state)
- if _adapt:
- if new_collection._converter is not None:
- iterable = new_collection._converter(iterable)
- else:
- setting_type = util.duck_type_collection(iterable)
- receiving_type = self._duck_typed_as
- if setting_type is not receiving_type:
- given = iterable is None and 'None' or \
- iterable.__class__.__name__
- wanted = self._duck_typed_as.__name__
- raise TypeError(
- "Incompatible collection type: %s is not %s-like" % (
- given, wanted))
- # If the object is an adapted collection, return the (iterable)
- # adapter.
- if hasattr(iterable, '_sa_iterator'):
- iterable = iterable._sa_iterator()
- elif setting_type is dict:
- if util.py3k:
- iterable = iterable.values()
- else:
- iterable = getattr(
- iterable, 'itervalues', iterable.values)()
- else:
- iterable = iter(iterable)
- new_values = list(iterable)
- old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
- if old is PASSIVE_NO_RESULT:
- old = self.initialize(state, dict_)
- elif old is orig_iterable:
- # ignore re-assignment of the current collection, as happens
- # implicitly with in-place operators (foo.collection |= other)
- return
- # place a copy of "old" in state.committed_state
- state._modified_event(dict_, self, old, True)
- old_collection = old._sa_adapter
- dict_[self.key] = user_data
- collections.bulk_replace(
- new_values, old_collection, new_collection)
- del old._sa_adapter
- self.dispatch.dispose_collection(state, old, old_collection)
- def _invalidate_collection(self, collection):
- adapter = getattr(collection, '_sa_adapter')
- adapter.invalidated = True
- def set_committed_value(self, state, dict_, value):
- """Set an attribute value on the given instance and 'commit' it."""
- collection, user_data = self._initialize_collection(state)
- if value:
- collection.append_multiple_without_event(value)
- state.dict[self.key] = user_data
- state._commit(dict_, [self.key])
- if self.key in state._pending_mutations:
- # pending items exist. issue a modified event,
- # add/remove new items.
- state._modified_event(dict_, self, user_data, True)
- pending = state._pending_mutations.pop(self.key)
- added = pending.added_items
- removed = pending.deleted_items
- for item in added:
- collection.append_without_event(item)
- for item in removed:
- collection.remove_without_event(item)
- return user_data
- def get_collection(self, state, dict_,
- user_data=None, passive=PASSIVE_OFF):
- """Retrieve the CollectionAdapter associated with the given state.
- Creates a new CollectionAdapter if one does not exist.
- """
- if user_data is None:
- user_data = self.get(state, dict_, passive=passive)
- if user_data is PASSIVE_NO_RESULT:
- return user_data
- return getattr(user_data, '_sa_adapter')
- def backref_listeners(attribute, key, uselist):
- """Apply listeners to synchronize a two-way relationship."""
- # use easily recognizable names for stack traces
- parent_token = attribute.impl.parent_token
- parent_impl = attribute.impl
- def _acceptable_key_err(child_state, initiator, child_impl):
- raise ValueError(
- "Bidirectional attribute conflict detected: "
- 'Passing object %s to attribute "%s" '
- 'triggers a modify event on attribute "%s" '
- 'via the backref "%s".' % (
- state_str(child_state),
- initiator.parent_token,
- child_impl.parent_token,
- attribute.impl.parent_token
- )
- )
- def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
- if oldchild is child:
- return child
- if oldchild is not None and \
- oldchild is not PASSIVE_NO_RESULT and \
- oldchild is not NEVER_SET:
- # With lazy=None, there's no guarantee that the full collection is
- # present when updating via a backref.
- old_state, old_dict = instance_state(oldchild),\
- instance_dict(oldchild)
- impl = old_state.manager[key].impl
- if initiator.impl is not impl or \
- initiator.op not in (OP_REPLACE, OP_REMOVE):
- impl.pop(old_state,
- old_dict,
- state.obj(),
- parent_impl._append_token or
- parent_impl._init_append_token(),
- passive=PASSIVE_NO_FETCH)
- if child is not None:
- child_state, child_dict = instance_state(child),\
- instance_dict(child)
- child_impl = child_state.manager[key].impl
- if initiator.parent_token is not parent_token and \
- initiator.parent_token is not child_impl.parent_token:
- _acceptable_key_err(state, initiator, child_impl)
- elif initiator.impl is not child_impl or \
- initiator.op not in (OP_APPEND, OP_REPLACE):
- child_impl.append(
- child_state,
- child_dict,
- state.obj(),
- initiator,
- passive=PASSIVE_NO_FETCH)
- return child
- def emit_backref_from_collection_append_event(state, child, initiator):
- if child is None:
- return
- child_state, child_dict = instance_state(child), \
- instance_dict(child)
- child_impl = child_state.manager[key].impl
- if initiator.parent_token is not parent_token and \
- initiator.parent_token is not child_impl.parent_token:
- _acceptable_key_err(state, initiator, child_impl)
- elif initiator.impl is not child_impl or \
- initiator.op not in (OP_APPEND, OP_REPLACE):
- child_impl.append(
- child_state,
- child_dict,
- state.obj(),
- initiator,
- passive=PASSIVE_NO_FETCH)
- return child
- def emit_backref_from_collection_remove_event(state, child, initiator):
- if child is not None:
- child_state, child_dict = instance_state(child),\
- instance_dict(child)
- child_impl = child_state.manager[key].impl
- if initiator.impl is not child_impl or \
- initiator.op not in (OP_REMOVE, OP_REPLACE):
- child_impl.pop(
- child_state,
- child_dict,
- state.obj(),
- initiator,
- passive=PASSIVE_NO_FETCH)
- if uselist:
- event.listen(attribute, "append",
- emit_backref_from_collection_append_event,
- retval=True, raw=True)
- else:
- event.listen(attribute, "set",
- emit_backref_from_scalar_set_event,
- retval=True, raw=True)
- # TODO: need coverage in test/orm/ of remove event
- event.listen(attribute, "remove",
- emit_backref_from_collection_remove_event,
- retval=True, raw=True)
- _NO_HISTORY = util.symbol('NO_HISTORY')
- _NO_STATE_SYMBOLS = frozenset([
- id(PASSIVE_NO_RESULT),
- id(NO_VALUE),
- id(NEVER_SET)])
- History = util.namedtuple("History", [
- "added", "unchanged", "deleted"
- ])
- class History(History):
- """A 3-tuple of added, unchanged and deleted values,
- representing the changes which have occurred on an instrumented
- attribute.
- The easiest way to get a :class:`.History` object for a particular
- attribute on an object is to use the :func:`.inspect` function::
- from sqlalchemy import inspect
- hist = inspect(myobject).attrs.myattribute.history
- Each tuple member is an iterable sequence:
- * ``added`` - the collection of items added to the attribute (the first
- tuple element).
- * ``unchanged`` - the collection of items that have not changed on the
- attribute (the second tuple element).
- * ``deleted`` - the collection of items that have been removed from the
- attribute (the third tuple element).
- """
- def __bool__(self):
- return self != HISTORY_BLANK
- __nonzero__ = __bool__
- def empty(self):
- """Return True if this :class:`.History` has no changes
- and no existing, unchanged state.
- """
- return not bool(
- (self.added or self.deleted)
- or self.unchanged
- )
- def sum(self):
- """Return a collection of added + unchanged + deleted."""
- return (self.added or []) +\
- (self.unchanged or []) +\
- (self.deleted or [])
- def non_deleted(self):
- """Return a collection of added + unchanged."""
- return (self.added or []) +\
- (self.unchanged or [])
- def non_added(self):
- """Return a collection of unchanged + deleted."""
- return (self.unchanged or []) +\
- (self.deleted or [])
- def has_changes(self):
- """Return True if this :class:`.History` has changes."""
- return bool(self.added or self.deleted)
- def as_state(self):
- return History(
- [(c is not None)
- and instance_state(c) or None
- for c in self.added],
- [(c is not None)
- and instance_state(c) or None
- for c in self.unchanged],
- [(c is not None)
- and instance_state(c) or None
- for c in self.deleted],
- )
- @classmethod
- def from_scalar_attribute(cls, attribute, state, current):
- original = state.committed_state.get(attribute.key, _NO_HISTORY)
- if original is _NO_HISTORY:
- if current is NEVER_SET:
- return cls((), (), ())
- else:
- return cls((), [current], ())
- # don't let ClauseElement expressions here trip things up
- elif attribute.is_equal(current, original) is True:
- return cls((), [current], ())
- else:
- # current convention on native scalars is to not
- # include information
- # about missing previous value in "deleted", but
- # we do include None, which helps in some primary
- # key situations
- if id(original) in _NO_STATE_SYMBOLS:
- deleted = ()
- else:
- deleted = [original]
- if current is NEVER_SET:
- return cls((), (), deleted)
- else:
- return cls([current], (), deleted)
- @classmethod
- def from_object_attribute(cls, attribute, state, current):
- original = state.committed_state.get(attribute.key, _NO_HISTORY)
- if original is _NO_HISTORY:
- if current is NO_VALUE or current is NEVER_SET:
- return cls((), (), ())
- else:
- return cls((), [current], ())
- elif current is original:
- return cls((), [current], ())
- else:
- # current convention on related objects is to not
- # include information
- # about missing previous value in "deleted", and
- # to also not include None - the dependency.py rules
- # ignore the None in any case.
- if id(original) in _NO_STATE_SYMBOLS or original is None:
- deleted = ()
- else:
- deleted = [original]
- if current is NO_VALUE or current is NEVER_SET:
- return cls((), (), deleted)
- else:
- return cls([current], (), deleted)
- @classmethod
- def from_collection(cls, attribute, state, current):
- original = state.committed_state.get(attribute.key, _NO_HISTORY)
- if current is NO_VALUE or current is NEVER_SET:
- return cls((), (), ())
- current = getattr(current, '_sa_adapter')
- if original in (NO_VALUE, NEVER_SET):
- return cls(list(current), (), ())
- elif original is _NO_HISTORY:
- return cls((), list(current), ())
- else:
- current_states = [((c is not None) and instance_state(c)
- or None, c)
- for c in current
- ]
- original_states = [((c is not None) and instance_state(c)
- or None, c)
- for c in original
- ]
- current_set = dict(current_states)
- original_set = dict(original_states)
- return cls(
- [o for s, o in current_states if s not in original_set],
- [o for s, o in current_states if s in original_set],
- [o for s, o in original_states if s not in current_set]
- )
- HISTORY_BLANK = History(None, None, None)
- def get_history(obj, key, passive=PASSIVE_OFF):
- """Return a :class:`.History` record for the given object
- and attribute key.
- :param obj: an object whose class is instrumented by the
- attributes package.
- :param key: string attribute name.
- :param passive: indicates loading behavior for the attribute
- if the value is not already present. This is a
- bitflag attribute, which defaults to the symbol
- :attr:`.PASSIVE_OFF` indicating all necessary SQL
- should be emitted.
- """
- if passive is True:
- util.warn_deprecated("Passing True for 'passive' is deprecated. "
- "Use attributes.PASSIVE_NO_INITIALIZE")
- passive = PASSIVE_NO_INITIALIZE
- elif passive is False:
- util.warn_deprecated("Passing False for 'passive' is "
- "deprecated. Use attributes.PASSIVE_OFF")
- passive = PASSIVE_OFF
- return get_state_history(instance_state(obj), key, passive)
- def get_state_history(state, key, passive=PASSIVE_OFF):
- return state.get_history(key, passive)
- def has_parent(cls, obj, key, optimistic=False):
- """TODO"""
- manager = manager_of_class(cls)
- state = instance_state(obj)
- return manager.has_parent(state, key, optimistic)
- def register_attribute(class_, key, **kw):
- comparator = kw.pop('comparator', None)
- parententity = kw.pop('parententity', None)
- doc = kw.pop('doc', None)
- desc = register_descriptor(class_, key,
- comparator, parententity, doc=doc)
- register_attribute_impl(class_, key, **kw)
- return desc
- def register_attribute_impl(class_, key,
- uselist=False, callable_=None,
- useobject=False,
- impl_class=None, backref=None, **kw):
- manager = manager_of_class(class_)
- if uselist:
- factory = kw.pop('typecallable', None)
- typecallable = manager.instrument_collection_class(
- key, factory or list)
- else:
- typecallable = kw.pop('typecallable', None)
- dispatch = manager[key].dispatch
- if impl_class:
- impl = impl_class(class_, key, typecallable, dispatch, **kw)
- elif uselist:
- impl = CollectionAttributeImpl(class_, key, callable_, dispatch,
- typecallable=typecallable, **kw)
- elif useobject:
- impl = ScalarObjectAttributeImpl(class_, key, callable_,
- dispatch, **kw)
- else:
- impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
- manager[key].impl = impl
- if backref:
- backref_listeners(manager[key], backref, uselist)
- manager.post_configure_attribute(key)
- return manager[key]
- def register_descriptor(class_, key, comparator=None,
- parententity=None, doc=None):
- manager = manager_of_class(class_)
- descriptor = InstrumentedAttribute(class_, key, comparator=comparator,
- parententity=parententity)
- descriptor.__doc__ = doc
- manager.instrument_attribute(key, descriptor)
- return descriptor
- def unregister_attribute(class_, key):
- manager_of_class(class_).uninstrument_attribute(key)
- def init_collection(obj, key):
- """Initialize a collection attribute and return the collection adapter.
- This function is used to provide direct access to collection internals
- for a previously unloaded attribute. e.g.::
- collection_adapter = init_collection(someobject, 'elements')
- for elem in values:
- collection_adapter.append_without_event(elem)
- For an easier way to do the above, see
- :func:`~sqlalchemy.orm.attributes.set_committed_value`.
- obj is an instrumented object instance. An InstanceState
- is accepted directly for backwards compatibility but
- this usage is deprecated.
- """
- state = instance_state(obj)
- dict_ = state.dict
- return init_state_collection(state, dict_, key)
- def init_state_collection(state, dict_, key):
- """Initialize a collection attribute and return the collection adapter."""
- attr = state.manager[key].impl
- user_data = attr.initialize(state, dict_)
- return attr.get_collection(state, dict_, user_data)
- def set_committed_value(instance, key, value):
- """Set the value of an attribute with no history events.
- Cancels any previous history present. The value should be
- a scalar value for scalar-holding attributes, or
- an iterable for any collection-holding attribute.
- This is the same underlying method used when a lazy loader
- fires off and loads additional data from the database.
- In particular, this method can be used by application code
- which has loaded additional attributes or collections through
- separate queries, which can then be attached to an instance
- as though it were part of its original loaded state.
- """
- state, dict_ = instance_state(instance), instance_dict(instance)
- state.manager[key].impl.set_committed_value(state, dict_, value)
- def set_attribute(instance, key, value):
- """Set the value of an attribute, firing history events.
- This function may be used regardless of instrumentation
- applied directly to the class, i.e. no descriptors are required.
- Custom attribute management schemes will need to make usage
- of this method to establish attribute state as understood
- by SQLAlchemy.
- """
- state, dict_ = instance_state(instance), instance_dict(instance)
- state.manager[key].impl.set(state, dict_, value, None)
- def get_attribute(instance, key):
- """Get the value of an attribute, firing any callables required.
- This function may be used regardless of instrumentation
- applied directly to the class, i.e. no descriptors are required.
- Custom attribute management schemes will need to make usage
- of this method to make usage of attribute state as understood
- by SQLAlchemy.
- """
- state, dict_ = instance_state(instance), instance_dict(instance)
- return state.manager[key].impl.get(state, dict_)
- def del_attribute(instance, key):
- """Delete the value of an attribute, firing history events.
- This function may be used regardless of instrumentation
- applied directly to the class, i.e. no descriptors are required.
- Custom attribute management schemes will need to make usage
- of this method to establish attribute state as understood
- by SQLAlchemy.
- """
- state, dict_ = instance_state(instance), instance_dict(instance)
- state.manager[key].impl.delete(state, dict_)
- def flag_modified(instance, key):
- """Mark an attribute on an instance as 'modified'.
- This sets the 'modified' flag on the instance and
- establishes an unconditional change event for the given attribute.
- """
- state, dict_ = instance_state(instance), instance_dict(instance)
- impl = state.manager[key].impl
- state._modified_event(dict_, impl, NO_VALUE, force=True)
|