# orm/instrumentation.py# Copyright (C) 2005-2025 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# mypy: allow-untyped-defs, allow-untyped-calls"""Defines SQLAlchemy's system of class instrumentation.This module is usually not directly visible to user applications, butdefines a large part of the ORM's interactivity.instrumentation.py deals with registration of end-user classesfor state tracking. It interacts closely with state.pyand attributes.py which establish per-instance and per-class-attributeinstrumentation, respectively.The class instrumentation system can be customized on a per-classor global basis using the :mod:`sqlalchemy.ext.instrumentation`module, which provides the means to build and specifyalternate instrumentation forms... versionchanged: 0.8 The instrumentation extension system was moved out of the ORM and into the external :mod:`sqlalchemy.ext.instrumentation` package. When that package is imported, it installs itself within sqlalchemy.orm so that its more comprehensive resolution mechanics take effect."""from__future__importannotationsfromtypingimportAnyfromtypingimportCallablefromtypingimportcastfromtypingimportCollectionfromtypingimportDictfromtypingimportGenericfromtypingimportIterablefromtypingimportListfromtypingimportOptionalfromtypingimportSetfromtypingimportTuplefromtypingimportTypefromtypingimportTYPE_CHECKINGfromtypingimportTypeVarfromtypingimportUnionimportweakreffrom.importbasefrom.importcollectionsfrom.importexcfrom.importinterfacesfrom.importstatefrom._typingimport_Ofrom.attributesimport_is_collection_attribute_implfrom..importutilfrom..eventimportEventTargetfrom..utilimportHasMemoizedfrom..util.typingimportLiteralfrom..util.typingimportProtocolifTYPE_CHECKING:from._typingimport_RegistryTypefrom.attributesimportAttributeImplfrom.attributesimportQueryableAttributefrom.collectionsimport_AdaptedCollectionProtocolfrom.collectionsimport_CollectionFactoryTypefrom.decl_baseimport_MapperConfigfrom.eventsimportInstanceEventsfrom.mapperimportMapperfrom.stateimportInstanceStatefrom..eventimportdispatcher_T=TypeVar("_T",bound=Any)DEL_ATTR=util.symbol("DEL_ATTR")class_ExpiredAttributeLoaderProto(Protocol):def__call__(self,state:state.InstanceState[Any],toload:Set[str],passive:base.PassiveFlag,)->None:...class_ManagerFactory(Protocol):def__call__(self,class_:Type[_O])->ClassManager[_O]:...classClassManager(HasMemoized,Dict[str,"QueryableAttribute[Any]"],Generic[_O],EventTarget,):"""Tracks state information at the class level."""dispatch:dispatcher[ClassManager[_O]]MANAGER_ATTR=base.DEFAULT_MANAGER_ATTRSTATE_ATTR=base.DEFAULT_STATE_ATTR_state_setter=staticmethod(util.attrsetter(STATE_ATTR))expired_attribute_loader:_ExpiredAttributeLoaderProto"previously known as deferred_scalar_loader"init_method:Optional[Callable[...,None]]original_init:Optional[Callable[...,None]]=Nonefactory:Optional[_ManagerFactory]declarative_scan:Optional[weakref.ref[_MapperConfig]]=Noneregistry:_RegistryTypeifnotTYPE_CHECKING:# starts as None during setupregistry=Noneclass_:Type[_O]_bases:List[ClassManager[Any]]@property@util.deprecated("1.4",message="The ClassManager.deferred_scalar_loader attribute is now ""named expired_attribute_loader",)defdeferred_scalar_loader(self):returnself.expired_attribute_loader@deferred_scalar_loader.setter@util.deprecated("1.4",message="The ClassManager.deferred_scalar_loader attribute is now ""named expired_attribute_loader",)defdeferred_scalar_loader(self,obj):self.expired_attribute_loader=objdef__init__(self,class_):self.class_=class_self.info={}self.new_init=Noneself.local_attrs={}self.originals={}self._finalized=Falseself.factory=Noneself.init_method=Noneself._bases=[mgrformgrincast("List[Optional[ClassManager[Any]]]",[opt_manager_of_class(base)forbaseinself.class_.__bases__ifisinstance(base,type)],)ifmgrisnotNone]forbase_inself._bases:self.update(base_)cast("InstanceEvents",self.dispatch._events)._new_classmanager_instance(class_,self)forbaseclsinclass_.__mro__:mgr=opt_manager_of_class(basecls)ifmgrisnotNone:self.dispatch._update(mgr.dispatch)self.manage()if"__del__"inclass_.__dict__:util.warn("__del__() method on class %s will ""cause unreachable cycles and memory leaks, ""as SQLAlchemy instrumentation often creates ""reference cycles. Please remove this method."%class_)def_update_state(self,finalize:bool=False,mapper:Optional[Mapper[_O]]=None,registry:Optional[_RegistryType]=None,declarative_scan:Optional[_MapperConfig]=None,expired_attribute_loader:Optional[_ExpiredAttributeLoaderProto]=None,init_method:Optional[Callable[...,None]]=None,)->None:ifmapper:self.mapper=mapper#ifregistry:registry._add_manager(self)ifdeclarative_scan:self.declarative_scan=weakref.ref(declarative_scan)ifexpired_attribute_loader:self.expired_attribute_loader=expired_attribute_loaderifinit_method:assertnotself._finalized,("class is already instrumented, ""init_method %s can't be applied"%init_method)self.init_method=init_methodifnotself._finalized:self.original_init=(self.init_methodifself.init_methodisnotNoneandself.class_.__init__isobject.__init__elseself.class_.__init__)iffinalizeandnotself._finalized:self._finalize()def_finalize(self)->None:ifself._finalized:returnself._finalized=Trueself._instrument_init()_instrumentation_factory.dispatch.class_instrument(self.class_)def__hash__(self)->int:# type: ignore[override]returnid(self)def__eq__(self,other:Any)->bool:returnotherisself@propertydefis_mapped(self)->bool:return"mapper"inself.__dict__@HasMemoized.memoized_attributedef_all_key_set(self):returnfrozenset(self)@HasMemoized.memoized_attributedef_collection_impl_keys(self):returnfrozenset([attr.keyforattrinself.values()ifattr.impl.collection])@HasMemoized.memoized_attributedef_scalar_loader_impls(self):returnfrozenset([attr.implforattrinself.values()ifattr.impl.accepts_scalar_loader])@HasMemoized.memoized_attributedef_loader_impls(self):returnfrozenset([attr.implforattrinself.values()])@util.memoized_propertydefmapper(self)->Mapper[_O]:# raises unless self.mapper has been assignedraiseexc.UnmappedClassError(self.class_)def_all_sqla_attributes(self,exclude=None):"""return an iterator of all classbound attributes that are implement :class:`.InspectionAttr`. This includes :class:`.QueryableAttribute` as well as extension types such as :class:`.hybrid_property` and :class:`.AssociationProxy`. """found:Dict[str,Any]={}# constraints:# 1. yield keys in cls.__dict__ order# 2. if a subclass has the same key as a superclass, include that# key as part of the ordering of the superclass, because an# overridden key is usually installed by the mapper which is going# on a different ordering# 3. don't use getattr() as this fires off descriptorsforsuperclsinself.class_.__mro__[0:-1]:inherits=supercls.__mro__[1]forkeyinsupercls.__dict__:found.setdefault(key,supercls)ifkeyininherits.__dict__:continueval=found[key].__dict__[key]if(isinstance(val,interfaces.InspectionAttr)andval.is_attribute):yieldkey,valdef_get_class_attr_mro(self,key,default=None):"""return an attribute on the class without tripping it."""forsuperclsinself.class_.__mro__:ifkeyinsupercls.__dict__:returnsupercls.__dict__[key]else:returndefaultdef_attr_has_impl(self,key:str)->bool:"""Return True if the given attribute is fully initialized. i.e. has an impl. """returnkeyinselfandself[key].implisnotNonedef_subclass_manager(self,cls:Type[_T])->ClassManager[_T]:"""Create a new ClassManager for a subclass of this ClassManager's class. This is called automatically when attributes are instrumented so that the attributes can be propagated to subclasses against their own class-local manager, without the need for mappers etc. to have already pre-configured managers for the full class hierarchy. Mappers can post-configure the auto-generated ClassManager when needed. """returnregister_class(cls,finalize=False)def_instrument_init(self):self.new_init=_generate_init(self.class_,self,self.original_init)self.install_member("__init__",self.new_init)@util.memoized_propertydef_state_constructor(self)->Type[state.InstanceState[_O]]:self.dispatch.first_init(self,self.class_)returnstate.InstanceStatedefmanage(self):"""Mark this instance as the manager for its class."""setattr(self.class_,self.MANAGER_ATTR,self)@util.hybridmethoddefmanager_getter(self):return_default_manager_getter@util.hybridmethoddefstate_getter(self):"""Return a (instance) -> InstanceState callable. "state getter" callables should raise either KeyError or AttributeError if no InstanceState could be found for the instance. """return_default_state_getter@util.hybridmethoddefdict_getter(self):return_default_dict_getterdefinstrument_attribute(self,key:str,inst:QueryableAttribute[Any],propagated:bool=False,)->None:ifpropagated:ifkeyinself.local_attrs:return# don't override local attr with inherited attrelse:self.local_attrs[key]=instself.install_descriptor(key,inst)self._reset_memoizations()self[key]=instforclsinself.class_.__subclasses__():manager=self._subclass_manager(cls)manager.instrument_attribute(key,inst,True)defsubclass_managers(self,recursive):forclsinself.class_.__subclasses__():mgr=opt_manager_of_class(cls)ifmgrisnotNoneandmgrisnotself:yieldmgrifrecursive:yield frommgr.subclass_managers(True)defpost_configure_attribute(self,key):_instrumentation_factory.dispatch.attribute_instrument(self.class_,key,self[key])defuninstrument_attribute(self,key,propagated=False):ifkeynotinself:returnifpropagated:ifkeyinself.local_attrs:return# don't get rid of local attrelse:delself.local_attrs[key]self.uninstall_descriptor(key)self._reset_memoizations()delself[key]forclsinself.class_.__subclasses__():manager=opt_manager_of_class(cls)ifmanager:manager.uninstrument_attribute(key,True)defunregister(self)->None:"""remove all instrumentation established by this ClassManager."""forkeyinlist(self.originals):self.uninstall_member(key)self.mapper=Noneself.dispatch=None# type: ignoreself.new_init=Noneself.info.clear()forkeyinlist(self):ifkeyinself.local_attrs:self.uninstrument_attribute(key)ifself.MANAGER_ATTRinself.class_.__dict__:delattr(self.class_,self.MANAGER_ATTR)definstall_descriptor(self,key:str,inst:QueryableAttribute[Any])->None:ifkeyin(self.STATE_ATTR,self.MANAGER_ATTR):raiseKeyError("%r: requested attribute name conflicts with ""instrumentation attribute of the same name."%key)setattr(self.class_,key,inst)defuninstall_descriptor(self,key:str)->None:delattr(self.class_,key)definstall_member(self,key:str,implementation:Any)->None:ifkeyin(self.STATE_ATTR,self.MANAGER_ATTR):raiseKeyError("%r: requested attribute name conflicts with ""instrumentation attribute of the same name."%key)self.originals.setdefault(key,self.class_.__dict__.get(key,DEL_ATTR))setattr(self.class_,key,implementation)defuninstall_member(self,key:str)->None:original=self.originals.pop(key,None)iforiginalisnotDEL_ATTR:setattr(self.class_,key,original)else:delattr(self.class_,key)definstrument_collection_class(self,key:str,collection_class:Type[Collection[Any]])->_CollectionFactoryType:returncollections.prepare_instrumentation(collection_class)definitialize_collection(self,key:str,state:InstanceState[_O],factory:_CollectionFactoryType,)->Tuple[collections.CollectionAdapter,_AdaptedCollectionProtocol]:user_data=factory()impl=self.get_impl(key)assert_is_collection_attribute_impl(impl)adapter=collections.CollectionAdapter(impl,state,user_data)returnadapter,user_datadefis_instrumented(self,key:str,search:bool=False)->bool:ifsearch:returnkeyinselfelse:returnkeyinself.local_attrsdefget_impl(self,key:str)->AttributeImpl:returnself[key].impl@propertydefattributes(self)->Iterable[Any]:returniter(self.values())# InstanceState managementdefnew_instance(self,state:Optional[InstanceState[_O]]=None)->_O:# here, we would prefer _O to be bound to "object"# so that mypy sees that __new__ is present. currently# it's bound to Any as there were other problems not having# it that way but these can be revisitedinstance=self.class_.__new__(self.class_)ifstateisNone:state=self._state_constructor(instance,self)self._state_setter(instance,state)returninstancedefsetup_instance(self,instance:_O,state:Optional[InstanceState[_O]]=None)->None:ifstateisNone:state=self._state_constructor(instance,self)self._state_setter(instance,state)defteardown_instance(self,instance:_O)->None:delattr(instance,self.STATE_ATTR)def_serialize(self,state:InstanceState[_O],state_dict:Dict[str,Any])->_SerializeManager:return_SerializeManager(state,state_dict)def_new_state_if_none(self,instance:_O)->Union[Literal[False],InstanceState[_O]]:"""Install a default InstanceState if none is present. A private convenience method used by the __init__ decorator. """ifhasattr(instance,self.STATE_ATTR):returnFalseelifself.class_isnotinstance.__class__andself.is_mapped:# this will create a new ClassManager for the# subclass, without a mapper. This is likely a# user error situation but allow the object# to be constructed, so that it is usable# in a non-ORM context at least.returnself._subclass_manager(instance.__class__)._new_state_if_none(instance)else:state=self._state_constructor(instance,self)self._state_setter(instance,state)returnstatedefhas_state(self,instance:_O)->bool:returnhasattr(instance,self.STATE_ATTR)defhas_parent(self,state:InstanceState[_O],key:str,optimistic:bool=False)->bool:"""TODO"""returnself.get_impl(key).hasparent(state,optimistic=optimistic)def__bool__(self)->bool:"""All ClassManagers are non-zero regardless of attribute state."""returnTruedef__repr__(self)->str:return"<%s of %r at %x>"%(self.__class__.__name__,self.class_,id(self),)class_SerializeManager:"""Provide serialization of a :class:`.ClassManager`. The :class:`.InstanceState` uses ``__init__()`` on serialize and ``__call__()`` on deserialize. """def__init__(self,state:state.InstanceState[Any],d:Dict[str,Any]):self.class_=state.class_manager=state.managermanager.dispatch.pickle(state,d)def__call__(self,state,inst,state_dict):state.manager=manager=opt_manager_of_class(self.class_)ifmanagerisNone:raiseexc.UnmappedInstanceError(inst,"Cannot deserialize object of type %r - ""no mapper() has ""been configured for this class within the current ""Python process!"%self.class_,)elifmanager.is_mappedandnotmanager.mapper.configured:manager.mapper._check_configure()# setup _sa_instance_state ahead of time so that# unpickle events can access the object normally.# see [ticket:2362]ifinstisnotNone:manager.setup_instance(inst,state)manager.dispatch.unpickle(state,state_dict)classInstrumentationFactory(EventTarget):"""Factory for new ClassManager instances."""dispatch:dispatcher[InstrumentationFactory]defcreate_manager_for_cls(self,class_:Type[_O])->ClassManager[_O]:assertclass_isnotNoneassertopt_manager_of_class(class_)isNone# give a more complicated subclass# a chance to do what it wants heremanager,factory=self._locate_extended_factory(class_)iffactoryisNone:factory=ClassManagermanager=ClassManager(class_)else:assertmanagerisnotNoneself._check_conflicts(class_,factory)manager.factory=factoryreturnmanagerdef_locate_extended_factory(self,class_:Type[_O])->Tuple[Optional[ClassManager[_O]],Optional[_ManagerFactory]]:"""Overridden by a subclass to do an extended lookup."""returnNone,Nonedef_check_conflicts(self,class_:Type[_O],factory:Callable[[Type[_O]],ClassManager[_O]])->None:"""Overridden by a subclass to test for conflicting factories."""defunregister(self,class_:Type[_O])->None:manager=manager_of_class(class_)manager.unregister()self.dispatch.class_uninstrument(class_)# this attribute is replaced by sqlalchemy.ext.instrumentation# when imported._instrumentation_factory=InstrumentationFactory()# these attributes are replaced by sqlalchemy.ext.instrumentation# when a non-standard InstrumentationManager class is first# used to instrument a class.instance_state=_default_state_getter=base.instance_stateinstance_dict=_default_dict_getter=base.instance_dictmanager_of_class=_default_manager_getter=base.manager_of_classopt_manager_of_class=_default_opt_manager_getter=base.opt_manager_of_classdefregister_class(class_:Type[_O],finalize:bool=True,mapper:Optional[Mapper[_O]]=None,registry:Optional[_RegistryType]=None,declarative_scan:Optional[_MapperConfig]=None,expired_attribute_loader:Optional[_ExpiredAttributeLoaderProto]=None,init_method:Optional[Callable[...,None]]=None,)->ClassManager[_O]:"""Register class instrumentation. Returns the existing or newly created class manager. """manager=opt_manager_of_class(class_)ifmanagerisNone:manager=_instrumentation_factory.create_manager_for_cls(class_)manager._update_state(mapper=mapper,registry=registry,declarative_scan=declarative_scan,expired_attribute_loader=expired_attribute_loader,init_method=init_method,finalize=finalize,)returnmanagerdefunregister_class(class_):"""Unregister class instrumentation."""_instrumentation_factory.unregister(class_)defis_instrumented(instance,key):"""Return True if the given attribute on the given instance is instrumented by the attributes package. This function may be used regardless of instrumentation applied directly to the class, i.e. no descriptors are required. """returnmanager_of_class(instance.__class__).is_instrumented(key,search=True)def_generate_init(class_,class_manager,original_init):"""Build an __init__ decorator that triggers ClassManager events."""# TODO: we should use the ClassManager's notion of the# original '__init__' method, once ClassManager is fixed# to always reference that.iforiginal_initisNone:original_init=class_.__init__# Go through some effort here and don't change the user's __init__# calling signature, including the unlikely case that it has# a return value.# FIXME: need to juggle local names to avoid constructor argument# clashes.func_body="""\def __init__(%(apply_pos)s): new_state = class_manager._new_state_if_none(%(self_arg)s) if new_state: return new_state._initialize_instance(%(apply_kw)s) else: return original_init(%(apply_kw)s)"""func_vars=util.format_argspec_init(original_init,grouped=False)func_text=func_body%func_varsfunc_defaults=getattr(original_init,"__defaults__",None)func_kw_defaults=getattr(original_init,"__kwdefaults__",None)env=locals().copy()env["__name__"]=__name__exec(func_text,env)__init__=env["__init__"]__init__.__doc__=original_init.__doc____init__._sa_original_init=original_initiffunc_defaults:__init__.__defaults__=func_defaultsiffunc_kw_defaults:__init__.__kwdefaults__=func_kw_defaultsreturn__init__