base.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. # event/base.py
  2. # Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. """Base implementation classes.
  8. The public-facing ``Events`` serves as the base class for an event interface;
  9. its public attributes represent different kinds of events. These attributes
  10. are mirrored onto a ``_Dispatch`` class, which serves as a container for
  11. collections of listener functions. These collections are represented both
  12. at the class level of a particular ``_Dispatch`` class as well as within
  13. instances of ``_Dispatch``.
  14. """
  15. from __future__ import annotations
  16. import typing
  17. from typing import Any
  18. from typing import cast
  19. from typing import Dict
  20. from typing import Generic
  21. from typing import Iterator
  22. from typing import List
  23. from typing import Mapping
  24. from typing import MutableMapping
  25. from typing import Optional
  26. from typing import overload
  27. from typing import Tuple
  28. from typing import Type
  29. from typing import Union
  30. import weakref
  31. from .attr import _ClsLevelDispatch
  32. from .attr import _EmptyListener
  33. from .attr import _InstanceLevelDispatch
  34. from .attr import _JoinedListener
  35. from .registry import _ET
  36. from .registry import _EventKey
  37. from .. import util
  38. from ..util.typing import Literal
  39. _registrars: MutableMapping[str, List[Type[_HasEventsDispatch[Any]]]] = (
  40. util.defaultdict(list)
  41. )
  42. def _is_event_name(name: str) -> bool:
  43. # _sa_event prefix is special to support internal-only event names.
  44. # most event names are just plain method names that aren't
  45. # underscored.
  46. return (
  47. not name.startswith("_") and name != "dispatch"
  48. ) or name.startswith("_sa_event")
  49. class _UnpickleDispatch:
  50. """Serializable callable that re-generates an instance of
  51. :class:`_Dispatch` given a particular :class:`.Events` subclass.
  52. """
  53. def __call__(self, _instance_cls: Type[_ET]) -> _Dispatch[_ET]:
  54. for cls in _instance_cls.__mro__:
  55. if "dispatch" in cls.__dict__:
  56. return cast(
  57. "_Dispatch[_ET]", cls.__dict__["dispatch"].dispatch
  58. )._for_class(_instance_cls)
  59. else:
  60. raise AttributeError("No class with a 'dispatch' member present.")
  61. class _DispatchCommon(Generic[_ET]):
  62. __slots__ = ()
  63. _instance_cls: Optional[Type[_ET]]
  64. def _join(self, other: _DispatchCommon[_ET]) -> _JoinedDispatcher[_ET]:
  65. raise NotImplementedError()
  66. def __getattr__(self, name: str) -> _InstanceLevelDispatch[_ET]:
  67. raise NotImplementedError()
  68. @property
  69. def _events(self) -> Type[_HasEventsDispatch[_ET]]:
  70. raise NotImplementedError()
  71. class _Dispatch(_DispatchCommon[_ET]):
  72. """Mirror the event listening definitions of an Events class with
  73. listener collections.
  74. Classes which define a "dispatch" member will return a
  75. non-instantiated :class:`._Dispatch` subclass when the member
  76. is accessed at the class level. When the "dispatch" member is
  77. accessed at the instance level of its owner, an instance
  78. of the :class:`._Dispatch` class is returned.
  79. A :class:`._Dispatch` class is generated for each :class:`.Events`
  80. class defined, by the :meth:`._HasEventsDispatch._create_dispatcher_class`
  81. method. The original :class:`.Events` classes remain untouched.
  82. This decouples the construction of :class:`.Events` subclasses from
  83. the implementation used by the event internals, and allows
  84. inspecting tools like Sphinx to work in an unsurprising
  85. way against the public API.
  86. """
  87. # "active_history" is an ORM case we add here. ideally a better
  88. # system would be in place for ad-hoc attributes.
  89. __slots__ = "_parent", "_instance_cls", "__dict__", "_empty_listeners"
  90. _active_history: bool
  91. _empty_listener_reg: MutableMapping[
  92. Type[_ET], Dict[str, _EmptyListener[_ET]]
  93. ] = weakref.WeakKeyDictionary()
  94. _empty_listeners: Dict[str, _EmptyListener[_ET]]
  95. _event_names: List[str]
  96. _instance_cls: Optional[Type[_ET]]
  97. _joined_dispatch_cls: Type[_JoinedDispatcher[_ET]]
  98. _events: Type[_HasEventsDispatch[_ET]]
  99. """reference back to the Events class.
  100. Bidirectional against _HasEventsDispatch.dispatch
  101. """
  102. def __init__(
  103. self,
  104. parent: Optional[_Dispatch[_ET]],
  105. instance_cls: Optional[Type[_ET]] = None,
  106. ):
  107. self._parent = parent
  108. self._instance_cls = instance_cls
  109. if instance_cls:
  110. assert parent is not None
  111. try:
  112. self._empty_listeners = self._empty_listener_reg[instance_cls]
  113. except KeyError:
  114. self._empty_listeners = self._empty_listener_reg[
  115. instance_cls
  116. ] = {
  117. ls.name: _EmptyListener(ls, instance_cls)
  118. for ls in parent._event_descriptors
  119. }
  120. else:
  121. self._empty_listeners = {}
  122. def __getattr__(self, name: str) -> _InstanceLevelDispatch[_ET]:
  123. # Assign EmptyListeners as attributes on demand
  124. # to reduce startup time for new dispatch objects.
  125. try:
  126. ls = self._empty_listeners[name]
  127. except KeyError:
  128. raise AttributeError(name)
  129. else:
  130. setattr(self, ls.name, ls)
  131. return ls
  132. @property
  133. def _event_descriptors(self) -> Iterator[_ClsLevelDispatch[_ET]]:
  134. for k in self._event_names:
  135. # Yield _ClsLevelDispatch related
  136. # to relevant event name.
  137. yield getattr(self, k)
  138. def _listen(self, event_key: _EventKey[_ET], **kw: Any) -> None:
  139. return self._events._listen(event_key, **kw)
  140. def _for_class(self, instance_cls: Type[_ET]) -> _Dispatch[_ET]:
  141. return self.__class__(self, instance_cls)
  142. def _for_instance(self, instance: _ET) -> _Dispatch[_ET]:
  143. instance_cls = instance.__class__
  144. return self._for_class(instance_cls)
  145. def _join(self, other: _DispatchCommon[_ET]) -> _JoinedDispatcher[_ET]:
  146. """Create a 'join' of this :class:`._Dispatch` and another.
  147. This new dispatcher will dispatch events to both
  148. :class:`._Dispatch` objects.
  149. """
  150. assert "_joined_dispatch_cls" in self.__class__.__dict__
  151. return self._joined_dispatch_cls(self, other)
  152. def __reduce__(self) -> Union[str, Tuple[Any, ...]]:
  153. return _UnpickleDispatch(), (self._instance_cls,)
  154. def _update(
  155. self, other: _Dispatch[_ET], only_propagate: bool = True
  156. ) -> None:
  157. """Populate from the listeners in another :class:`_Dispatch`
  158. object."""
  159. for ls in other._event_descriptors:
  160. if isinstance(ls, _EmptyListener):
  161. continue
  162. getattr(self, ls.name).for_modify(self)._update(
  163. ls, only_propagate=only_propagate
  164. )
  165. def _clear(self) -> None:
  166. for ls in self._event_descriptors:
  167. ls.for_modify(self).clear()
  168. def _remove_dispatcher(cls: Type[_HasEventsDispatch[_ET]]) -> None:
  169. for k in cls.dispatch._event_names:
  170. _registrars[k].remove(cls)
  171. if not _registrars[k]:
  172. del _registrars[k]
  173. class _HasEventsDispatch(Generic[_ET]):
  174. _dispatch_target: Optional[Type[_ET]]
  175. """class which will receive the .dispatch collection"""
  176. dispatch: _Dispatch[_ET]
  177. """reference back to the _Dispatch class.
  178. Bidirectional against _Dispatch._events
  179. """
  180. if typing.TYPE_CHECKING:
  181. def __getattr__(self, name: str) -> _InstanceLevelDispatch[_ET]: ...
  182. def __init_subclass__(cls) -> None:
  183. """Intercept new Event subclasses and create associated _Dispatch
  184. classes."""
  185. cls._create_dispatcher_class(cls.__name__, cls.__bases__, cls.__dict__)
  186. @classmethod
  187. def _accept_with(
  188. cls, target: Union[_ET, Type[_ET]], identifier: str
  189. ) -> Optional[Union[_ET, Type[_ET]]]:
  190. raise NotImplementedError()
  191. @classmethod
  192. def _listen(
  193. cls,
  194. event_key: _EventKey[_ET],
  195. *,
  196. propagate: bool = False,
  197. insert: bool = False,
  198. named: bool = False,
  199. asyncio: bool = False,
  200. ) -> None:
  201. raise NotImplementedError()
  202. @staticmethod
  203. def _set_dispatch(
  204. klass: Type[_HasEventsDispatch[_ET]],
  205. dispatch_cls: Type[_Dispatch[_ET]],
  206. ) -> _Dispatch[_ET]:
  207. # This allows an Events subclass to define additional utility
  208. # methods made available to the target via
  209. # "self.dispatch._events.<utilitymethod>"
  210. # @staticmethod to allow easy "super" calls while in a metaclass
  211. # constructor.
  212. klass.dispatch = dispatch_cls(None)
  213. dispatch_cls._events = klass
  214. return klass.dispatch
  215. @classmethod
  216. def _create_dispatcher_class(
  217. cls, classname: str, bases: Tuple[type, ...], dict_: Mapping[str, Any]
  218. ) -> None:
  219. """Create a :class:`._Dispatch` class corresponding to an
  220. :class:`.Events` class."""
  221. # there's all kinds of ways to do this,
  222. # i.e. make a Dispatch class that shares the '_listen' method
  223. # of the Event class, this is the straight monkeypatch.
  224. if hasattr(cls, "dispatch"):
  225. dispatch_base = cls.dispatch.__class__
  226. else:
  227. dispatch_base = _Dispatch
  228. event_names = [k for k in dict_ if _is_event_name(k)]
  229. dispatch_cls = cast(
  230. "Type[_Dispatch[_ET]]",
  231. type(
  232. "%sDispatch" % classname,
  233. (dispatch_base,),
  234. {"__slots__": event_names},
  235. ),
  236. )
  237. dispatch_cls._event_names = event_names
  238. dispatch_inst = cls._set_dispatch(cls, dispatch_cls)
  239. for k in dispatch_cls._event_names:
  240. setattr(dispatch_inst, k, _ClsLevelDispatch(cls, dict_[k]))
  241. _registrars[k].append(cls)
  242. for super_ in dispatch_cls.__bases__:
  243. if issubclass(super_, _Dispatch) and super_ is not _Dispatch:
  244. for ls in super_._events.dispatch._event_descriptors:
  245. setattr(dispatch_inst, ls.name, ls)
  246. dispatch_cls._event_names.append(ls.name)
  247. if getattr(cls, "_dispatch_target", None):
  248. dispatch_target_cls = cls._dispatch_target
  249. assert dispatch_target_cls is not None
  250. if (
  251. hasattr(dispatch_target_cls, "__slots__")
  252. and "_slots_dispatch" in dispatch_target_cls.__slots__
  253. ):
  254. dispatch_target_cls.dispatch = slots_dispatcher(cls)
  255. else:
  256. dispatch_target_cls.dispatch = dispatcher(cls)
  257. klass = type(
  258. "Joined%s" % dispatch_cls.__name__,
  259. (_JoinedDispatcher,),
  260. {"__slots__": event_names},
  261. )
  262. dispatch_cls._joined_dispatch_cls = klass
  263. # establish pickle capability by adding it to this module
  264. globals()[klass.__name__] = klass
  265. class _JoinedDispatcher(_DispatchCommon[_ET]):
  266. """Represent a connection between two _Dispatch objects."""
  267. __slots__ = "local", "parent", "_instance_cls"
  268. local: _DispatchCommon[_ET]
  269. parent: _DispatchCommon[_ET]
  270. _instance_cls: Optional[Type[_ET]]
  271. def __init__(
  272. self, local: _DispatchCommon[_ET], parent: _DispatchCommon[_ET]
  273. ):
  274. self.local = local
  275. self.parent = parent
  276. self._instance_cls = self.local._instance_cls
  277. def __reduce__(self) -> Any:
  278. return (self.__class__, (self.local, self.parent))
  279. def __getattr__(self, name: str) -> _JoinedListener[_ET]:
  280. # Assign _JoinedListeners as attributes on demand
  281. # to reduce startup time for new dispatch objects.
  282. ls = getattr(self.local, name)
  283. jl = _JoinedListener(self.parent, ls.name, ls)
  284. setattr(self, ls.name, jl)
  285. return jl
  286. def _listen(self, event_key: _EventKey[_ET], **kw: Any) -> None:
  287. return self.parent._listen(event_key, **kw)
  288. @property
  289. def _events(self) -> Type[_HasEventsDispatch[_ET]]:
  290. return self.parent._events
  291. class Events(_HasEventsDispatch[_ET]):
  292. """Define event listening functions for a particular target type."""
  293. @classmethod
  294. def _accept_with(
  295. cls, target: Union[_ET, Type[_ET]], identifier: str
  296. ) -> Optional[Union[_ET, Type[_ET]]]:
  297. def dispatch_is(*types: Type[Any]) -> bool:
  298. return all(isinstance(target.dispatch, t) for t in types)
  299. def dispatch_parent_is(t: Type[Any]) -> bool:
  300. parent = cast("_JoinedDispatcher[_ET]", target.dispatch).parent
  301. while isinstance(parent, _JoinedDispatcher):
  302. parent = cast("_JoinedDispatcher[_ET]", parent).parent
  303. return isinstance(parent, t)
  304. # Mapper, ClassManager, Session override this to
  305. # also accept classes, scoped_sessions, sessionmakers, etc.
  306. if hasattr(target, "dispatch"):
  307. if (
  308. dispatch_is(cls.dispatch.__class__)
  309. or dispatch_is(type, cls.dispatch.__class__)
  310. or (
  311. dispatch_is(_JoinedDispatcher)
  312. and dispatch_parent_is(cls.dispatch.__class__)
  313. )
  314. ):
  315. return target
  316. return None
  317. @classmethod
  318. def _listen(
  319. cls,
  320. event_key: _EventKey[_ET],
  321. *,
  322. propagate: bool = False,
  323. insert: bool = False,
  324. named: bool = False,
  325. asyncio: bool = False,
  326. ) -> None:
  327. event_key.base_listen(
  328. propagate=propagate, insert=insert, named=named, asyncio=asyncio
  329. )
  330. @classmethod
  331. def _remove(cls, event_key: _EventKey[_ET]) -> None:
  332. event_key.remove()
  333. @classmethod
  334. def _clear(cls) -> None:
  335. cls.dispatch._clear()
  336. class dispatcher(Generic[_ET]):
  337. """Descriptor used by target classes to
  338. deliver the _Dispatch class at the class level
  339. and produce new _Dispatch instances for target
  340. instances.
  341. """
  342. def __init__(self, events: Type[_HasEventsDispatch[_ET]]):
  343. self.dispatch = events.dispatch
  344. self.events = events
  345. @overload
  346. def __get__(
  347. self, obj: Literal[None], cls: Type[Any]
  348. ) -> Type[_Dispatch[_ET]]: ...
  349. @overload
  350. def __get__(self, obj: Any, cls: Type[Any]) -> _DispatchCommon[_ET]: ...
  351. def __get__(self, obj: Any, cls: Type[Any]) -> Any:
  352. if obj is None:
  353. return self.dispatch
  354. disp = self.dispatch._for_instance(obj)
  355. try:
  356. obj.__dict__["dispatch"] = disp
  357. except AttributeError as ae:
  358. raise TypeError(
  359. "target %r doesn't have __dict__, should it be "
  360. "defining _slots_dispatch?" % (obj,)
  361. ) from ae
  362. return disp
  363. class slots_dispatcher(dispatcher[_ET]):
  364. def __get__(self, obj: Any, cls: Type[Any]) -> Any:
  365. if obj is None:
  366. return self.dispatch
  367. if hasattr(obj, "_slots_dispatch"):
  368. return obj._slots_dispatch
  369. disp = self.dispatch._for_instance(obj)
  370. obj._slots_dispatch = disp
  371. return disp