12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330 |
- # orm/attributes.py
- # Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # This module is part of SQLAlchemy and is released under
- # the MIT License: https://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 collections
- from . import exc as orm_exc
- from . import interfaces
- from .base import ATTR_EMPTY
- from .base import ATTR_WAS_SET
- from .base import CALLABLES_OK
- from .base import DEFERRED_HISTORY_LOAD
- from .base import INIT_OK
- from .base import instance_dict
- from .base import instance_state
- from .base import instance_str
- from .base import LOAD_AGAINST_COMMITTED
- from .base import manager_of_class
- from .base import NEVER_SET # noqa
- from .base import NO_AUTOFLUSH
- from .base import NO_CHANGE # noqa
- from .base import NO_RAISE
- from .base import NO_VALUE
- from .base import NON_PERSISTENT_OK # noqa
- from .base import PASSIVE_CLASS_MISMATCH # noqa
- from .base import PASSIVE_NO_FETCH
- from .base import PASSIVE_NO_FETCH_RELATED # noqa
- from .base import PASSIVE_NO_INITIALIZE
- from .base import PASSIVE_NO_RESULT
- from .base import PASSIVE_OFF
- from .base import PASSIVE_ONLY_PERSISTENT
- from .base import PASSIVE_RETURN_NO_VALUE
- from .base import RELATED_OBJECT_OK # noqa
- from .base import SQL_OK # noqa
- from .base import state_str
- from .. import event
- from .. import exc
- from .. import inspection
- from .. import util
- from ..sql import base as sql_base
- from ..sql import roles
- from ..sql import traversals
- from ..sql import visitors
- class NoKey(str):
- pass
- NO_KEY = NoKey("no name")
- @inspection._self_inspects
- class QueryableAttribute(
- interfaces._MappedAttribute,
- interfaces.InspectionAttr,
- interfaces.PropComparator,
- traversals.HasCopyInternals,
- roles.JoinTargetRole,
- roles.OnClauseRole,
- sql_base.Immutable,
- sql_base.MemoizedHasCacheKey,
- ):
- """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:`_orm.Mapper.all_orm_descriptors`
- :attr:`_orm.Mapper.attrs`
- """
- is_attribute = True
- # PropComparator has a __visit_name__ to participate within
- # traversals. Disambiguate the attribute vs. a comparator.
- __visit_name__ = "orm_instrumented_attribute"
- def __init__(
- self,
- class_,
- key,
- parententity,
- impl=None,
- comparator=None,
- of_type=None,
- extra_criteria=(),
- ):
- self.class_ = class_
- self.key = key
- self._parententity = parententity
- self.impl = impl
- self.comparator = comparator
- self._of_type = of_type
- self._extra_criteria = extra_criteria
- 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)
- if base[key].dispatch._active_history:
- self.dispatch._active_history = True
- _cache_key_traversal = [
- ("key", visitors.ExtendedInternalTraversal.dp_string),
- ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
- ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
- ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
- ]
- def __reduce__(self):
- # this method is only used in terms of the
- # sqlalchemy.ext.serializer extension
- return (
- _queryable_attribute_unreduce,
- (
- self.key,
- self._parententity.mapper.class_,
- self._parententity,
- self._parententity.entity,
- ),
- )
- @util.memoized_property
- def _supports_population(self):
- return self.impl.supports_population
- @property
- def _impl_uses_objects(self):
- return self.impl.uses_objects
- def get_history(self, instance, passive=PASSIVE_OFF):
- return self.impl.get_history(
- instance_state(instance), instance_dict(instance), passive
- )
- @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:`_schema.Column` object, this attribute
- will return the :attr:`.SchemaItem.info` dictionary associated
- with the core-level :class:`_schema.Column` object.
- * If the attribute is a :class:`.ColumnProperty` but is mapped to
- any other kind of SQL expression other than a
- :class:`_schema.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``.
- .. 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:`_orm.Mapper`
- or :class:`.AliasedInsp`, depending upon the nature
- of the parent entity which this attribute is associated
- with.
- """
- return inspection.inspect(self._parententity)
- @util.memoized_property
- def expression(self):
- """The SQL expression object represented by this
- :class:`.QueryableAttribute`.
- This will typically be an instance of a :class:`_sql.ColumnElement`
- subclass representing a column expression.
- """
- if self.key is NO_KEY:
- annotations = {"entity_namespace": self._entity_namespace}
- else:
- annotations = {
- "proxy_key": self.key,
- "proxy_owner": self._parententity,
- "entity_namespace": self._entity_namespace,
- }
- ce = self.comparator.__clause_element__()
- try:
- anno = ce._annotate
- except AttributeError as ae:
- util.raise_(
- exc.InvalidRequestError(
- 'When interpreting attribute "%s" as a SQL expression, '
- "expected __clause_element__() to return "
- "a ClauseElement object, got: %r" % (self, ce)
- ),
- from_=ae,
- )
- else:
- return anno(annotations)
- @property
- def _entity_namespace(self):
- return self._parententity
- @property
- def _annotations(self):
- return self.__clause_element__()._annotations
- def __clause_element__(self):
- return self.expression
- @property
- def _from_objects(self):
- return self.expression._from_objects
- def _bulk_update_tuples(self, value):
- """Return setter tuples for a bulk UPDATE."""
- return self.comparator._bulk_update_tuples(value)
- 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, entity):
- return QueryableAttribute(
- self.class_,
- self.key,
- self._parententity,
- impl=self.impl,
- comparator=self.comparator.of_type(entity),
- of_type=inspection.inspect(entity),
- extra_criteria=self._extra_criteria,
- )
- def and_(self, *other):
- return QueryableAttribute(
- self.class_,
- self.key,
- self._parententity,
- impl=self.impl,
- comparator=self.comparator.and_(*other),
- of_type=self._of_type,
- extra_criteria=self._extra_criteria + other,
- )
- def _clone(self, **kw):
- return QueryableAttribute(
- self.class_,
- self.key,
- self._parententity,
- impl=self.impl,
- comparator=self.comparator,
- of_type=self._of_type,
- extra_criteria=self._extra_criteria,
- )
- def label(self, name):
- return self.__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 as err:
- util.raise_(
- AttributeError(
- "Neither %r object nor %r object associated with %s "
- "has an attribute %r"
- % (
- type(self).__name__,
- type(self.comparator).__name__,
- self,
- key,
- )
- ),
- replace_context=err,
- )
- 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
- def _queryable_attribute_unreduce(key, mapped_class, parententity, entity):
- # this method is only used in terms of the
- # sqlalchemy.ext.serializer extension
- if parententity.is_aliased_class:
- return entity._get_from_serialized(key, mapped_class, parententity)
- else:
- return getattr(entity, key)
- if util.py3k:
- from typing import TypeVar, Generic
- _T = TypeVar("_T")
- _Generic_T = Generic[_T]
- else:
- _Generic_T = type("_Generic_T", (), {})
- class Mapped(QueryableAttribute, _Generic_T):
- """Represent an ORM mapped :term:`descriptor` attribute for typing purposes.
- This class represents the complete descriptor interface for any class
- attribute that will have been :term:`instrumented` by the ORM
- :class:`_orm.Mapper` class. When used with typing stubs, it is the final
- type that would be used by a type checker such as mypy to provide the full
- behavioral contract for the attribute.
- .. tip::
- The :class:`_orm.Mapped` class represents attributes that are handled
- directly by the :class:`_orm.Mapper` class. It does not include other
- Python descriptor classes that are provided as extensions, including
- :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`.
- While these systems still make use of ORM-specific superclasses
- and structures, they are not :term:`instrumented` by the
- :class:`_orm.Mapper` and instead provide their own functionality
- when they are accessed on a class.
- When using the :ref:`SQLAlchemy Mypy plugin <mypy_toplevel>`, the
- :class:`_orm.Mapped` construct is used in typing annotations to indicate to
- the plugin those attributes that are expected to be mapped; the plugin also
- applies :class:`_orm.Mapped` as an annotation automatically when it scans
- through declarative mappings in :ref:`orm_declarative_table` style. For
- more indirect mapping styles such as
- :ref:`imperative table <orm_imperative_table_configuration>` it is
- typically applied explicitly to class level attributes that expect
- to be mapped based on a given :class:`_schema.Table` configuration.
- :class:`_orm.Mapped` is defined in the
- `sqlalchemy2-stubs <https://pypi.org/project/sqlalchemy2-stubs>`_ project
- as a :pep:`484` generic class which may subscribe to any arbitrary Python
- type, which represents the Python type handled by the attribute::
- class MyMappedClass(Base):
- __table_ = Table(
- "some_table", Base.metadata,
- Column("id", Integer, primary_key=True),
- Column("data", String(50)),
- Column("created_at", DateTime)
- )
- id : Mapped[int]
- data: Mapped[str]
- created_at: Mapped[datetime]
- For complete background on how to use :class:`_orm.Mapped` with
- pep-484 tools like Mypy, see the link below for background on SQLAlchemy's
- Mypy plugin.
- .. versionadded:: 1.4
- .. seealso::
- :ref:`mypy_toplevel` - complete background on Mypy integration
- """
- def __get__(self, instance, owner):
- raise NotImplementedError()
- def __set__(self, instance, value):
- raise NotImplementedError()
- def __delete__(self, instance):
- raise NotImplementedError()
- class InstrumentedAttribute(Mapped):
- """Class bound instrumented attribute which adds basic
- :term:`descriptor` methods.
- See :class:`.QueryableAttribute` for a description of most features.
- """
- inherit_cache = True
- 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:
- try:
- state = instance_state(instance)
- except AttributeError as err:
- util.raise_(
- orm_exc.UnmappedInstanceError(instance),
- replace_context=err,
- )
- return self.impl.get(state, dict_)
- HasEntityNamespace = util.namedtuple(
- "HasEntityNamespace", ["entity_namespace"]
- )
- HasEntityNamespace.is_mapper = HasEntityNamespace.is_aliased_class = False
- 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.
- """
- _extra_criteria = ()
- 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
- _is_internal_proxy = True
- _cache_key_traversal = [
- ("key", visitors.ExtendedInternalTraversal.dp_string),
- ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
- ]
- @property
- def _impl_uses_objects(self):
- return (
- self.original_property is not None
- and getattr(self.class_, self.key).impl.uses_objects
- )
- @property
- def _parententity(self):
- return inspection.inspect(self.class_, raiseerr=False)
- @property
- def _entity_namespace(self):
- if hasattr(self._comparator, "_parententity"):
- return self._comparator._parententity
- else:
- # used by hybrid attributes which try to remain
- # agnostic of any ORM concepts like mappers
- return HasEntityNamespace(self.class_)
- @property
- def property(self):
- return self.comparator.property
- @util.memoized_property
- def comparator(self):
- if 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 _clone(self, **kw):
- return self.__class__(
- self.class_,
- self.key,
- self.descriptor,
- self._comparator,
- adapt_to_entity=self._adapt_to_entity,
- original_property=self.original_property,
- )
- def __get__(self, instance, owner):
- retval = self.descriptor.__get__(instance, owner)
- # detect if this is a plain Python @property, which just returns
- # itself for class level access. If so, then return us.
- # Otherwise, return the object returned by the descriptor.
- if retval is self.descriptor and instance is None:
- return self
- else:
- return retval
- 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 as err:
- if attribute == "comparator":
- util.raise_(
- AttributeError("comparator"), replace_context=err
- )
- try:
- # comparator itself might be unreachable
- comparator = self.comparator
- except AttributeError as err2:
- util.raise_(
- AttributeError(
- "Neither %r object nor unconfigured comparator "
- "object associated with %s has an attribute %r"
- % (type(descriptor).__name__, self, attribute)
- ),
- replace_context=err2,
- )
- else:
- try:
- return getattr(comparator, attribute)
- except AttributeError as err3:
- util.raise_(
- AttributeError(
- "Neither %r object nor %r object "
- "associated with %s has an attribute %r"
- % (
- type(descriptor).__name__,
- type(comparator).__name__,
- self,
- attribute,
- )
- ),
- replace_context=err3,
- )
- 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")
- OP_BULK_REPLACE = util.symbol("BULK_REPLACE")
- OP_MODIFIED = util.symbol("MODIFIED")
- class AttributeEvent(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 events such as :meth:`.AttributeEvents.append`,
- :meth:`.AttributeEvents.set`,
- and :meth:`.AttributeEvents.remove`.
- 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
- :attribute impl: The :class:`.AttributeImpl` which is the current event
- initiator.
- :attribute op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`,
- :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_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, AttributeEvent)
- 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)
- Event = AttributeEvent
- class AttributeImpl(object):
- """internal implementation for instrumented attributes."""
- def __init__(
- self,
- class_,
- key,
- callable_,
- dispatch,
- trackparent=False,
- compare_function=None,
- active_history=False,
- parent_token=None,
- load_on_unexpire=True,
- send_modified_events=True,
- accepts_scalar_loader=None,
- **kwargs
- ):
- r"""Construct an AttributeImpl.
- :param \class_: associated class
- :param key: string name of the attribute
- :param \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.
- :param trackparent:
- if True, attempt to track if an instance has a parent attached
- to it via this attribute.
- :param compare_function:
- a function that compares two values which are normally
- assignable to this attribute.
- :param active_history:
- indicates that get_history() should always return the "old" value,
- even if it means executing a lazy callable upon attribute change.
- :param 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.
- :param load_on_unexpire:
- if False, don't include this attribute in a load-on-expired
- operation, i.e. the "expired_attribute_loader" process.
- The attribute can still be in the "expired" list and be
- considered to be "expired". Previously, this flag was called
- "expire_missing" and is only used by a deferred column
- attribute.
- :param 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
- if accepts_scalar_loader is not None:
- self.accepts_scalar_loader = accepts_scalar_loader
- else:
- self.accepts_scalar_loader = self.default_accepts_scalar_loader
- _deferred_history = kwargs.pop("_deferred_history", False)
- self._deferred_history = _deferred_history
- if active_history:
- self.dispatch._active_history = True
- self.load_on_unexpire = load_on_unexpire
- self._modified_token = Event(self, OP_MODIFIED)
- __slots__ = (
- "class_",
- "key",
- "callable_",
- "dispatch",
- "trackparent",
- "parent_token",
- "send_modified_events",
- "is_equal",
- "load_on_unexpire",
- "_modified_token",
- "accepts_scalar_loader",
- "_deferred_history",
- )
- 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 _default_value(self, state, dict_):
- """Produce an empty value for an uninitialized scalar attribute."""
- assert self.key not in dict_, (
- "_default_value should only be invoked for an "
- "uninitialized or expired attribute"
- )
- 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 NO_VALUE
- ):
- if not passive & CALLABLES_OK:
- return PASSIVE_NO_RESULT
- value = self._fire_loader_callables(state, key, passive)
- if value is PASSIVE_NO_RESULT or value is NO_VALUE:
- return value
- elif value is ATTR_WAS_SET:
- try:
- return dict_[key]
- except KeyError as err:
- # TODO: no test coverage here.
- util.raise_(
- KeyError(
- "Deferred loader for attribute "
- "%r failed to populate "
- "correctly" % key
- ),
- replace_context=err,
- )
- elif value is not ATTR_EMPTY:
- return self.set_committed_value(state, dict_, value)
- if not passive & INIT_OK:
- return NO_VALUE
- else:
- return self._default_value(state, dict_)
- def _fire_loader_callables(self, state, key, passive):
- if (
- self.accepts_scalar_loader
- and self.load_on_unexpire
- and key in state.expired_attributes
- ):
- return state._load_expired(state, passive)
- elif key in state.callables:
- callable_ = state.callables[key]
- return callable_(state, passive)
- elif self.callable_:
- return self.callable_(state, passive)
- else:
- return ATTR_EMPTY
- 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 is NO_VALUE:
- 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."""
- default_accepts_scalar_loader = True
- uses_objects = False
- supports_population = True
- collection = False
- dynamic = 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 = Event(self, OP_REPLACE)
- self._remove_token = Event(self, OP_REMOVE)
- def delete(self, state, dict_):
- if self.dispatch._active_history:
- old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
- 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)
- existing = dict_.pop(self.key, NO_VALUE)
- if (
- existing is NO_VALUE
- and old is NO_VALUE
- and not state.expired
- and self.key not in state.expired_attributes
- ):
- raise AttributeError("%s object does not have a value" % self)
- def get_history(self, state, dict_, passive=PASSIVE_OFF):
- if self.key in dict_:
- return History.from_scalar_attribute(self, state, dict_[self.key])
- elif self.key in state.committed_state:
- return History.from_scalar_attribute(self, state, NO_VALUE)
- 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_NO_VALUE)
- 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
- )
- 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)
- @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.
- """
- default_accepts_scalar_loader = False
- uses_objects = True
- supports_population = True
- collection = False
- __slots__ = ()
- def delete(self, state, dict_):
- 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
- | NO_RAISE,
- )
- self.fire_remove_event(state, dict_, old, self._remove_token)
- existing = dict_.pop(self.key, NO_VALUE)
- # if the attribute is expired, we currently have no way to tell
- # that an object-attribute was expired vs. not loaded. So
- # for this test, we look to see if the object has a DB identity.
- if (
- existing is NO_VALUE
- and old is not PASSIVE_NO_RESULT
- and state.key is None
- ):
- raise AttributeError("%s object does not have a value" % self)
- def get_history(self, state, dict_, passive=PASSIVE_OFF):
- if self.key in dict_:
- current = 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
- if not self._deferred_history:
- return History.from_object_attribute(self, state, current)
- else:
- original = state.committed_state.get(self.key, _NO_HISTORY)
- if original is PASSIVE_NO_RESULT:
- loader_passive = passive | (
- PASSIVE_ONLY_PERSISTENT
- | NO_AUTOFLUSH
- | LOAD_AGAINST_COMMITTED
- | NO_RAISE
- | DEFERRED_HISTORY_LOAD
- )
- original = self._fire_loader_callables(
- state, self.key, loader_passive
- )
- return History.from_object_attribute(
- self, state, current, original=original
- )
- 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 NO_VALUE
- ):
- 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 NO_VALUE
- 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
- | NO_RAISE,
- )
- 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 not in (
- None,
- PASSIVE_NO_RESULT,
- NO_VALUE,
- ):
- self.sethasparent(instance_state(value), state, False)
- for fn in self.dispatch.remove:
- fn(state, value, initiator or self._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,
- NO_VALUE,
- ):
- self.sethasparent(instance_state(previous), state, False)
- for fn in self.dispatch.set:
- value = fn(
- state, value, previous, initiator or self._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.
- """
- default_accepts_scalar_loader = False
- uses_objects = True
- supports_population = True
- collection = True
- dynamic = False
- __slots__ = (
- "copy",
- "collection_factory",
- "_append_token",
- "_remove_token",
- "_bulk_replace_token",
- "_duck_typed_as",
- )
- def __init__(
- self,
- class_,
- key,
- callable_,
- dispatch,
- typecallable=None,
- trackparent=False,
- copy_function=None,
- compare_function=None,
- **kwargs
- ):
- super(CollectionAttributeImpl, self).__init__(
- class_,
- key,
- callable_,
- dispatch,
- trackparent=trackparent,
- 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 = Event(self, OP_APPEND)
- self._remove_token = Event(self, OP_REMOVE)
- self._bulk_replace_token = Event(self, OP_BULK_REPLACE)
- 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 __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 is not NO_VALUE:
- 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)
- state._modified_event(dict_, self, NO_VALUE, True)
- if self.trackparent and value is not None:
- self.sethasparent(instance_state(value), state, True)
- return value
- def fire_append_wo_mutation_event(self, state, dict_, value, initiator):
- for fn in self.dispatch.append_wo_mutation:
- value = fn(state, value, initiator or self._append_token)
- return value
- def fire_pre_remove_event(self, state, dict_, initiator):
- """A special event used for pop() operations.
- The "remove" event needs to have the item to be removed passed to
- it, which in the case of pop from a set, we don't have a way to access
- the item before the operation. the event is used for all pop()
- operations (even though set.pop is the one where it is really needed).
- """
- state._modified_event(dict_, self, NO_VALUE, 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)
- state._modified_event(dict_, self, NO_VALUE, True)
- def delete(self, state, dict_):
- if self.key not in dict_:
- return
- state._modified_event(dict_, self, NO_VALUE, True)
- collection = self.get_collection(state, state.dict)
- collection.clear_with_event()
- # key is always present because we checked above. e.g.
- # del is a no-op if collection not present.
- del dict_[self.key]
- def _default_value(self, state, dict_):
- """Produce an empty collection for an un-initialized attribute"""
- assert self.key not in dict_, (
- "_default_value should only be invoked for an "
- "uninitialized or expired attribute"
- )
- if self.key in state._empty_collections:
- return state._empty_collections[self.key]
- adapter, user_data = self._initialize_collection(state)
- adapter._set_empty(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,
- check_old=None,
- 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)
- evt = self._bulk_replace_token
- self.dispatch.bulk_replace(state, new_values, evt)
- old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
- if old is PASSIVE_NO_RESULT:
- old = self._default_value(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, initiator=evt
- )
- self._dispose_previous_collection(state, old, old_collection, True)
- def _dispose_previous_collection(
- self, state, collection, adapter, fire_event
- ):
- del collection._sa_adapter
- # discarding old collection make sure it is not referenced in empty
- # collections.
- state._empty_collections.pop(self.key, None)
- if fire_event:
- self.dispatch.dispose_collection(state, collection, adapter)
- 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.
- if user_data is None, retrieves it from the state using normal
- "get()" rules, which will fire lazy callables or return the "empty"
- collection value.
- """
- if user_data is None:
- user_data = self.get(state, dict_, passive=passive)
- if user_data is PASSIVE_NO_RESULT:
- return user_data
- return 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.
- # in the sections marked "tokens to test for a recursive loop",
- # this is somewhat brittle and very performance-sensitive logic
- # that is specific to how we might arrive at each event. a marker
- # that can target us directly to arguments being invoked against
- # the impl might be simpler, but could interfere with other systems.
- 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 NO_VALUE
- ):
- # 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
- # tokens to test for a recursive loop.
- if not impl.collection and not impl.dynamic:
- check_recursive_token = impl._replace_token
- else:
- check_recursive_token = impl._remove_token
- if initiator is not check_recursive_token:
- impl.pop(
- old_state,
- old_dict,
- state.obj(),
- parent_impl._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)
- # tokens to test for a recursive loop.
- check_append_token = child_impl._append_token
- check_bulk_replace_token = (
- child_impl._bulk_replace_token
- if child_impl.collection
- else None
- )
- if (
- initiator is not check_append_token
- and initiator is not check_bulk_replace_token
- ):
- 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)
- # tokens to test for a recursive loop.
- check_append_token = child_impl._append_token
- check_bulk_replace_token = (
- child_impl._bulk_replace_token if child_impl.collection else None
- )
- if (
- initiator is not check_append_token
- and initiator is not check_bulk_replace_token
- ):
- 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
- and child is not PASSIVE_NO_RESULT
- and child is not NO_VALUE
- ):
- child_state, child_dict = (
- instance_state(child),
- instance_dict(child),
- )
- child_impl = child_state.manager[key].impl
- # tokens to test for a recursive loop.
- if not child_impl.collection and not child_impl.dynamic:
- check_remove_token = child_impl._remove_token
- check_replace_token = child_impl._replace_token
- check_for_dupes_on_remove = uselist and not parent_impl.dynamic
- else:
- check_remove_token = child_impl._remove_token
- check_replace_token = (
- child_impl._bulk_replace_token
- if child_impl.collection
- else None
- )
- check_for_dupes_on_remove = False
- if (
- initiator is not check_remove_token
- and initiator is not check_replace_token
- ):
- if not check_for_dupes_on_remove or not util.has_dupes(
- # when this event is called, the item is usually
- # present in the list, except for a pop() operation.
- state.dict[parent_impl.key],
- child,
- ):
- 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)])
- class History(util.namedtuple("History", ["added", "unchanged", "deleted"])):
- """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:`_sa.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 NO_VALUE:
- return cls((), (), ())
- else:
- return cls((), [current], ())
- # don't let ClauseElement expressions here trip things up
- elif (
- current is not NO_VALUE
- and 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 = ()
- # indicate a "del" operation occurred when we don't have
- # the previous value as: ([None], (), ())
- if id(current) in _NO_STATE_SYMBOLS:
- current = None
- else:
- deleted = [original]
- if current is NO_VALUE:
- return cls((), (), deleted)
- else:
- return cls([current], (), deleted)
- @classmethod
- def from_object_attribute(
- cls, attribute, state, current, original=_NO_HISTORY
- ):
- if original is _NO_HISTORY:
- original = state.committed_state.get(attribute.key, _NO_HISTORY)
- if original is _NO_HISTORY:
- if current is NO_VALUE:
- return cls((), (), ())
- else:
- return cls((), [current], ())
- elif current is original and current is not NO_VALUE:
- 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 = ()
- # indicate a "del" operation occurred when we don't have
- # the previous value as: ([None], (), ())
- if id(current) in _NO_STATE_SYMBOLS:
- current = None
- else:
- deleted = [original]
- if current is NO_VALUE:
- 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:
- return cls((), (), ())
- current = getattr(current, "_sa_adapter")
- if original is NO_VALUE:
- 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.
- This is the **pre-flush** history for a given attribute, which is
- reset each time the :class:`.Session` flushes changes to the
- current database transaction.
- .. note::
- Prefer to use the :attr:`.AttributeState.history` and
- :meth:`.AttributeState.load_history` accessors to retrieve the
- :class:`.History` for instance attributes.
- :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.
- .. seealso::
- :attr:`.AttributeState.history`
- :meth:`.AttributeState.load_history` - retrieve history
- using loader callables if the value is not locally present.
- """
- 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`.
- :param obj: a mapped object
- :param key: string attribute name where the collection is located.
- """
- 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.
- Discards any existing collection which may be there.
- """
- attr = state.manager[key].impl
- old = dict_.pop(key, None) # discard old collection
- if old is not None:
- old_collection = old._sa_adapter
- attr._dispose_previous_collection(state, old, old_collection, False)
- user_data = attr._default_value(state, dict_)
- adapter = attr.get_collection(state, dict_, user_data)
- adapter._reset_empty()
- return adapter
- 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, initiator=None):
- """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.
- :param instance: the object that will be modified
- :param key: string name of the attribute
- :param value: value to assign
- :param initiator: an instance of :class:`.Event` that would have
- been propagated from a previous event listener. This argument
- is used when the :func:`.set_attribute` function is being used within
- an existing event listening function where an :class:`.Event` object
- is being supplied; the object may be used to track the origin of the
- chain of events.
- .. versionadded:: 1.2.3
- """
- state, dict_ = instance_state(instance), instance_dict(instance)
- state.manager[key].impl.set(state, dict_, value, initiator)
- 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.
- The attribute must have a value present, else an
- :class:`.InvalidRequestError` is raised.
- To mark an object "dirty" without referring to any specific attribute
- so that it is considered within a flush, use the
- :func:`.attributes.flag_dirty` call.
- .. seealso::
- :func:`.attributes.flag_dirty`
- """
- state, dict_ = instance_state(instance), instance_dict(instance)
- impl = state.manager[key].impl
- impl.dispatch.modified(state, impl._modified_token)
- state._modified_event(dict_, impl, NO_VALUE, is_userland=True)
- def flag_dirty(instance):
- """Mark an instance as 'dirty' without any specific attribute mentioned.
- This is a special operation that will allow the object to travel through
- the flush process for interception by events such as
- :meth:`.SessionEvents.before_flush`. Note that no SQL will be emitted in
- the flush process for an object that has no changes, even if marked dirty
- via this method. However, a :meth:`.SessionEvents.before_flush` handler
- will be able to see the object in the :attr:`.Session.dirty` collection and
- may establish changes on it, which will then be included in the SQL
- emitted.
- .. versionadded:: 1.2
- .. seealso::
- :func:`.attributes.flag_modified`
- """
- state, dict_ = instance_state(instance), instance_dict(instance)
- state._modified_event(dict_, None, NO_VALUE, is_userland=True)
|