summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/event/base.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/event/base.py')
-rw-r--r--lib/sqlalchemy/event/base.py171
1 files changed, 171 insertions, 0 deletions
diff --git a/lib/sqlalchemy/event/base.py b/lib/sqlalchemy/event/base.py
new file mode 100644
index 000000000..bb7b3b1b4
--- /dev/null
+++ b/lib/sqlalchemy/event/base.py
@@ -0,0 +1,171 @@
+"""Base implementation classes.
+
+The public-facing ``Events`` serves as the base class for an event interface;
+it's public attributes represent different kinds of events. These attributes
+are mirrored onto a ``_Dispatch`` class, which serves as a container for
+collections of listener functions. These collections are represented both
+at the class level of a particular ``_Dispatch`` class as well as within
+instances of ``_Dispatch``.
+
+"""
+from __future__ import absolute_import
+
+from .. import util
+from .attr import _JoinedDispatchDescriptor, _EmptyListener, _DispatchDescriptor
+
+_registrars = util.defaultdict(list)
+
+
+def _is_event_name(name):
+ return not name.startswith('_') and name != 'dispatch'
+
+
+class _UnpickleDispatch(object):
+ """Serializable callable that re-generates an instance of
+ :class:`_Dispatch` given a particular :class:`.Events` subclass.
+
+ """
+ def __call__(self, _parent_cls):
+ for cls in _parent_cls.__mro__:
+ if 'dispatch' in cls.__dict__:
+ return cls.__dict__['dispatch'].dispatch_cls(_parent_cls)
+ else:
+ raise AttributeError("No class with a 'dispatch' member present.")
+
+
+class _Dispatch(object):
+ """Mirror the event listening definitions of an Events class with
+ listener collections.
+
+ Classes which define a "dispatch" member will return a
+ non-instantiated :class:`._Dispatch` subclass when the member
+ is accessed at the class level. When the "dispatch" member is
+ accessed at the instance level of its owner, an instance
+ of the :class:`._Dispatch` class is returned.
+
+ A :class:`._Dispatch` class is generated for each :class:`.Events`
+ class defined, by the :func:`._create_dispatcher_class` function.
+ The original :class:`.Events` classes remain untouched.
+ This decouples the construction of :class:`.Events` subclasses from
+ the implementation used by the event internals, and allows
+ inspecting tools like Sphinx to work in an unsurprising
+ way against the public API.
+
+ """
+
+ def __init__(self, _parent_cls):
+ self._parent_cls = _parent_cls
+
+ def _join(self, other):
+ """Create a 'join' of this :class:`._Dispatch` and another.
+
+ This new dispatcher will dispatch events to both
+ :class:`._Dispatch` objects.
+
+ """
+ if '_joined_dispatch_cls' not in self.__class__.__dict__:
+ cls = type(
+ "Joined%s" % self.__class__.__name__,
+ (_JoinedDispatcher, self.__class__), {}
+ )
+ for ls in _event_descriptors(self):
+ setattr(cls, ls.name, _JoinedDispatchDescriptor(ls.name))
+
+ self.__class__._joined_dispatch_cls = cls
+ return self._joined_dispatch_cls(self, other)
+
+ def __reduce__(self):
+ return _UnpickleDispatch(), (self._parent_cls, )
+
+ def _update(self, other, only_propagate=True):
+ """Populate from the listeners in another :class:`_Dispatch`
+ object."""
+
+ for ls in _event_descriptors(other):
+ if isinstance(ls, _EmptyListener):
+ continue
+ getattr(self, ls.name).\
+ for_modify(self)._update(ls, only_propagate=only_propagate)
+
+ @util.hybridmethod
+ def _clear(self):
+ for attr in dir(self):
+ if _is_event_name(attr):
+ getattr(self, attr).for_modify(self).clear()
+
+
+def _event_descriptors(target):
+ return [getattr(target, k) for k in dir(target) if _is_event_name(k)]
+
+
+class _EventMeta(type):
+ """Intercept new Event subclasses and create
+ associated _Dispatch classes."""
+
+ def __init__(cls, classname, bases, dict_):
+ _create_dispatcher_class(cls, classname, bases, dict_)
+ return type.__init__(cls, classname, bases, dict_)
+
+
+def _create_dispatcher_class(cls, classname, bases, dict_):
+ """Create a :class:`._Dispatch` class corresponding to an
+ :class:`.Events` class."""
+
+ # there's all kinds of ways to do this,
+ # i.e. make a Dispatch class that shares the '_listen' method
+ # of the Event class, this is the straight monkeypatch.
+ dispatch_base = getattr(cls, 'dispatch', _Dispatch)
+ cls.dispatch = dispatch_cls = type("%sDispatch" % classname,
+ (dispatch_base, ), {})
+ dispatch_cls._listen = cls._listen
+
+ for k in dict_:
+ if _is_event_name(k):
+ setattr(dispatch_cls, k, _DispatchDescriptor(cls, dict_[k]))
+ _registrars[k].append(cls)
+
+
+def _remove_dispatcher(cls):
+ for k in dir(cls):
+ if _is_event_name(k):
+ _registrars[k].remove(cls)
+ if not _registrars[k]:
+ del _registrars[k]
+
+class Events(util.with_metaclass(_EventMeta, object)):
+ """Define event listening functions for a particular target type."""
+
+ @classmethod
+ def _accept_with(cls, target):
+ # Mapper, ClassManager, Session override this to
+ # also accept classes, scoped_sessions, sessionmakers, etc.
+ if hasattr(target, 'dispatch') and (
+ isinstance(target.dispatch, cls.dispatch) or \
+ isinstance(target.dispatch, type) and \
+ issubclass(target.dispatch, cls.dispatch)
+ ):
+ return target
+ else:
+ return None
+
+ @classmethod
+ def _listen(cls, event_key, propagate=False, insert=False, named=False):
+ event_key.base_listen(propagate=propagate, insert=insert, named=named)
+
+ @classmethod
+ def _remove(cls, event_key):
+ event_key.remove()
+
+ @classmethod
+ def _clear(cls):
+ cls.dispatch._clear()
+
+
+class _JoinedDispatcher(object):
+ """Represent a connection between two _Dispatch objects."""
+
+ def __init__(self, local, parent):
+ self.local = local
+ self.parent = parent
+ self._parent_cls = local._parent_cls
+