You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

346 lines
11KB

  1. # event/base.py
  2. # Copyright (C) 2005-2021 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  7. """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 absolute_import
  16. import weakref
  17. from .attr import _ClsLevelDispatch
  18. from .attr import _EmptyListener
  19. from .attr import _JoinedListener
  20. from .. import util
  21. _registrars = util.defaultdict(list)
  22. def _is_event_name(name):
  23. # _sa_event prefix is special to support internal-only event names.
  24. # most event names are just plain method names that aren't
  25. # underscored.
  26. return (
  27. not name.startswith("_") and name != "dispatch"
  28. ) or name.startswith("_sa_event")
  29. class _UnpickleDispatch(object):
  30. """Serializable callable that re-generates an instance of
  31. :class:`_Dispatch` given a particular :class:`.Events` subclass.
  32. """
  33. def __call__(self, _instance_cls):
  34. for cls in _instance_cls.__mro__:
  35. if "dispatch" in cls.__dict__:
  36. return cls.__dict__["dispatch"].dispatch._for_class(
  37. _instance_cls
  38. )
  39. else:
  40. raise AttributeError("No class with a 'dispatch' member present.")
  41. class _Dispatch(object):
  42. """Mirror the event listening definitions of an Events class with
  43. listener collections.
  44. Classes which define a "dispatch" member will return a
  45. non-instantiated :class:`._Dispatch` subclass when the member
  46. is accessed at the class level. When the "dispatch" member is
  47. accessed at the instance level of its owner, an instance
  48. of the :class:`._Dispatch` class is returned.
  49. A :class:`._Dispatch` class is generated for each :class:`.Events`
  50. class defined, by the :func:`._create_dispatcher_class` function.
  51. The original :class:`.Events` classes remain untouched.
  52. This decouples the construction of :class:`.Events` subclasses from
  53. the implementation used by the event internals, and allows
  54. inspecting tools like Sphinx to work in an unsurprising
  55. way against the public API.
  56. """
  57. # In one ORM edge case, an attribute is added to _Dispatch,
  58. # so __dict__ is used in just that case and potentially others.
  59. __slots__ = "_parent", "_instance_cls", "__dict__", "_empty_listeners"
  60. _empty_listener_reg = weakref.WeakKeyDictionary()
  61. def __init__(self, parent, instance_cls=None):
  62. self._parent = parent
  63. self._instance_cls = instance_cls
  64. if instance_cls:
  65. try:
  66. self._empty_listeners = self._empty_listener_reg[instance_cls]
  67. except KeyError:
  68. self._empty_listeners = self._empty_listener_reg[
  69. instance_cls
  70. ] = {
  71. ls.name: _EmptyListener(ls, instance_cls)
  72. for ls in parent._event_descriptors
  73. }
  74. else:
  75. self._empty_listeners = {}
  76. def __getattr__(self, name):
  77. # Assign EmptyListeners as attributes on demand
  78. # to reduce startup time for new dispatch objects.
  79. try:
  80. ls = self._empty_listeners[name]
  81. except KeyError:
  82. raise AttributeError(name)
  83. else:
  84. setattr(self, ls.name, ls)
  85. return ls
  86. @property
  87. def _event_descriptors(self):
  88. for k in self._event_names:
  89. # Yield _ClsLevelDispatch related
  90. # to relevant event name.
  91. yield getattr(self, k)
  92. @property
  93. def _listen(self):
  94. return self._events._listen
  95. def _for_class(self, instance_cls):
  96. return self.__class__(self, instance_cls)
  97. def _for_instance(self, instance):
  98. instance_cls = instance.__class__
  99. return self._for_class(instance_cls)
  100. def _join(self, other):
  101. """Create a 'join' of this :class:`._Dispatch` and another.
  102. This new dispatcher will dispatch events to both
  103. :class:`._Dispatch` objects.
  104. """
  105. if "_joined_dispatch_cls" not in self.__class__.__dict__:
  106. cls = type(
  107. "Joined%s" % self.__class__.__name__,
  108. (_JoinedDispatcher,),
  109. {"__slots__": self._event_names},
  110. )
  111. self.__class__._joined_dispatch_cls = cls
  112. return self._joined_dispatch_cls(self, other)
  113. def __reduce__(self):
  114. return _UnpickleDispatch(), (self._instance_cls,)
  115. def _update(self, other, only_propagate=True):
  116. """Populate from the listeners in another :class:`_Dispatch`
  117. object."""
  118. for ls in other._event_descriptors:
  119. if isinstance(ls, _EmptyListener):
  120. continue
  121. getattr(self, ls.name).for_modify(self)._update(
  122. ls, only_propagate=only_propagate
  123. )
  124. def _clear(self):
  125. for ls in self._event_descriptors:
  126. ls.for_modify(self).clear()
  127. class _EventMeta(type):
  128. """Intercept new Event subclasses and create
  129. associated _Dispatch classes."""
  130. def __init__(cls, classname, bases, dict_):
  131. _create_dispatcher_class(cls, classname, bases, dict_)
  132. type.__init__(cls, classname, bases, dict_)
  133. def _create_dispatcher_class(cls, classname, bases, dict_):
  134. """Create a :class:`._Dispatch` class corresponding to an
  135. :class:`.Events` class."""
  136. # there's all kinds of ways to do this,
  137. # i.e. make a Dispatch class that shares the '_listen' method
  138. # of the Event class, this is the straight monkeypatch.
  139. if hasattr(cls, "dispatch"):
  140. dispatch_base = cls.dispatch.__class__
  141. else:
  142. dispatch_base = _Dispatch
  143. event_names = [k for k in dict_ if _is_event_name(k)]
  144. dispatch_cls = type(
  145. "%sDispatch" % classname, (dispatch_base,), {"__slots__": event_names}
  146. )
  147. dispatch_cls._event_names = event_names
  148. dispatch_inst = cls._set_dispatch(cls, dispatch_cls)
  149. for k in dispatch_cls._event_names:
  150. setattr(dispatch_inst, k, _ClsLevelDispatch(cls, dict_[k]))
  151. _registrars[k].append(cls)
  152. for super_ in dispatch_cls.__bases__:
  153. if issubclass(super_, _Dispatch) and super_ is not _Dispatch:
  154. for ls in super_._events.dispatch._event_descriptors:
  155. setattr(dispatch_inst, ls.name, ls)
  156. dispatch_cls._event_names.append(ls.name)
  157. if getattr(cls, "_dispatch_target", None):
  158. the_cls = cls._dispatch_target
  159. if (
  160. hasattr(the_cls, "__slots__")
  161. and "_slots_dispatch" in the_cls.__slots__
  162. ):
  163. cls._dispatch_target.dispatch = slots_dispatcher(cls)
  164. else:
  165. cls._dispatch_target.dispatch = dispatcher(cls)
  166. def _remove_dispatcher(cls):
  167. for k in cls.dispatch._event_names:
  168. _registrars[k].remove(cls)
  169. if not _registrars[k]:
  170. del _registrars[k]
  171. class Events(util.with_metaclass(_EventMeta, object)):
  172. """Define event listening functions for a particular target type."""
  173. @staticmethod
  174. def _set_dispatch(cls, dispatch_cls):
  175. # This allows an Events subclass to define additional utility
  176. # methods made available to the target via
  177. # "self.dispatch._events.<utilitymethod>"
  178. # @staticmethod to allow easy "super" calls while in a metaclass
  179. # constructor.
  180. cls.dispatch = dispatch_cls(None)
  181. dispatch_cls._events = cls
  182. return cls.dispatch
  183. @classmethod
  184. def _accept_with(cls, target):
  185. def dispatch_is(*types):
  186. return all(isinstance(target.dispatch, t) for t in types)
  187. def dispatch_parent_is(t):
  188. return isinstance(target.dispatch.parent, t)
  189. # Mapper, ClassManager, Session override this to
  190. # also accept classes, scoped_sessions, sessionmakers, etc.
  191. if hasattr(target, "dispatch"):
  192. if (
  193. dispatch_is(cls.dispatch.__class__)
  194. or dispatch_is(type, cls.dispatch.__class__)
  195. or (
  196. dispatch_is(_JoinedDispatcher)
  197. and dispatch_parent_is(cls.dispatch.__class__)
  198. )
  199. ):
  200. return target
  201. @classmethod
  202. def _listen(
  203. cls,
  204. event_key,
  205. propagate=False,
  206. insert=False,
  207. named=False,
  208. asyncio=False,
  209. ):
  210. event_key.base_listen(
  211. propagate=propagate, insert=insert, named=named, asyncio=asyncio
  212. )
  213. @classmethod
  214. def _remove(cls, event_key):
  215. event_key.remove()
  216. @classmethod
  217. def _clear(cls):
  218. cls.dispatch._clear()
  219. class _JoinedDispatcher(object):
  220. """Represent a connection between two _Dispatch objects."""
  221. __slots__ = "local", "parent", "_instance_cls"
  222. def __init__(self, local, parent):
  223. self.local = local
  224. self.parent = parent
  225. self._instance_cls = self.local._instance_cls
  226. def __getattr__(self, name):
  227. # Assign _JoinedListeners as attributes on demand
  228. # to reduce startup time for new dispatch objects.
  229. ls = getattr(self.local, name)
  230. jl = _JoinedListener(self.parent, ls.name, ls)
  231. setattr(self, ls.name, jl)
  232. return jl
  233. @property
  234. def _listen(self):
  235. return self.parent._listen
  236. @property
  237. def _events(self):
  238. return self.parent._events
  239. class dispatcher(object):
  240. """Descriptor used by target classes to
  241. deliver the _Dispatch class at the class level
  242. and produce new _Dispatch instances for target
  243. instances.
  244. """
  245. def __init__(self, events):
  246. self.dispatch = events.dispatch
  247. self.events = events
  248. def __get__(self, obj, cls):
  249. if obj is None:
  250. return self.dispatch
  251. disp = self.dispatch._for_instance(obj)
  252. try:
  253. obj.__dict__["dispatch"] = disp
  254. except AttributeError as ae:
  255. util.raise_(
  256. TypeError(
  257. "target %r doesn't have __dict__, should it be "
  258. "defining _slots_dispatch?" % (obj,)
  259. ),
  260. replace_context=ae,
  261. )
  262. return disp
  263. class slots_dispatcher(dispatcher):
  264. def __get__(self, obj, cls):
  265. if obj is None:
  266. return self.dispatch
  267. if hasattr(obj, "_slots_dispatch"):
  268. return obj._slots_dispatch
  269. disp = self.dispatch._for_instance(obj)
  270. obj._slots_dispatch = disp
  271. return disp