summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/event.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/event.py')
-rw-r--r--lib/sqlalchemy/event.py198
1 files changed, 189 insertions, 9 deletions
diff --git a/lib/sqlalchemy/event.py b/lib/sqlalchemy/event.py
index bfd027ead..64ae49976 100644
--- a/lib/sqlalchemy/event.py
+++ b/lib/sqlalchemy/event.py
@@ -6,6 +6,8 @@
"""Base event API."""
+from __future__ import absolute_import
+
from . import util, exc
from itertools import chain
import weakref
@@ -77,6 +79,15 @@ def remove(target, identifier, fn):
tgt.dispatch._remove(identifier, tgt, fn)
return
+def _legacy_signature(since, argnames, converter=None):
+ def leg(fn):
+ if not hasattr(fn, '_legacy_signatures'):
+ fn._legacy_signatures = []
+ fn._legacy_signatures.append((since, argnames, converter))
+ return fn
+ return leg
+
+
_registrars = util.defaultdict(list)
@@ -189,7 +200,7 @@ def _create_dispatcher_class(cls, classname, bases, dict_):
for k in dict_:
if _is_event_name(k):
- setattr(dispatch_cls, k, _DispatchDescriptor(dict_[k]))
+ setattr(dispatch_cls, k, _DispatchDescriptor(cls, dict_[k]))
_registrars[k].append(cls)
@@ -217,12 +228,16 @@ class Events(util.with_metaclass(_EventMeta, object)):
return None
@classmethod
- def _listen(cls, target, identifier, fn, propagate=False, insert=False):
+ def _listen(cls, target, identifier, fn, propagate=False, insert=False,
+ named=False):
+ dispatch_descriptor = getattr(target.dispatch, identifier)
+ fn = dispatch_descriptor._adjust_fn_spec(fn, named)
+
if insert:
- getattr(target.dispatch, identifier).\
+ dispatch_descriptor.\
for_modify(target.dispatch).insert(fn, target, propagate)
else:
- getattr(target.dispatch, identifier).\
+ dispatch_descriptor.\
for_modify(target.dispatch).append(fn, target, propagate)
@classmethod
@@ -237,12 +252,169 @@ class Events(util.with_metaclass(_EventMeta, object)):
class _DispatchDescriptor(object):
"""Class-level attributes on :class:`._Dispatch` classes."""
- def __init__(self, fn):
+ def __init__(self, parent_dispatch_cls, fn):
self.__name__ = fn.__name__
- self.__doc__ = fn.__doc__
+ argspec = util.inspect_getargspec(fn)
+ self.arg_names = argspec.args[1:]
+ self.has_kw = bool(argspec.keywords)
+ self.legacy_signatures = list(reversed(
+ sorted(
+ getattr(fn, '_legacy_signatures', []),
+ key=lambda s: s[0]
+ )
+ ))
+ self.__doc__ = fn.__doc__ = self._augment_fn_docs(parent_dispatch_cls, fn)
+
self._clslevel = weakref.WeakKeyDictionary()
self._empty_listeners = weakref.WeakKeyDictionary()
+ def _adjust_fn_spec(self, fn, named):
+ argspec = util.get_callable_argspec(fn, no_self=True)
+ if named:
+ fn = self._wrap_fn_for_kw(fn)
+ fn = self._wrap_fn_for_legacy(fn, argspec)
+ return fn
+
+ def _wrap_fn_for_kw(self, fn):
+ def wrap_kw(*args, **kw):
+ argdict = dict(zip(self.arg_names, args))
+ argdict.update(kw)
+ return fn(**argdict)
+ return wrap_kw
+
+ def _wrap_fn_for_legacy(self, fn, argspec):
+ for since, argnames, conv in self.legacy_signatures:
+ if argnames[-1] == "**kw":
+ has_kw = True
+ argnames = argnames[0:-1]
+ else:
+ has_kw = False
+
+ if len(argnames) == len(argspec.args) \
+ and has_kw is bool(argspec.keywords):
+
+ if conv:
+ assert not has_kw
+ def wrap_leg(*args):
+ return fn(*conv(*args))
+ else:
+ def wrap_leg(*args, **kw):
+ argdict = dict(zip(self.arg_names, args))
+ args = [argdict[name] for name in argnames]
+ if has_kw:
+ return fn(*args, **kw)
+ else:
+ return fn(*args)
+ return wrap_leg
+ else:
+ return fn
+
+ def _indent(self, text, indent):
+ return "\n".join(
+ indent + line
+ for line in text.split("\n")
+ )
+
+ def _standard_listen_example(self, sample_target, fn):
+ example_kw_arg = self._indent(
+ "\n".join(
+ "%(arg)s = kw['%(arg)s']" % {"arg": arg}
+ for arg in self.arg_names[0:2]
+ ),
+ " ")
+ if self.legacy_signatures:
+ current_since = max(since for since, args, conv in self.legacy_signatures)
+ else:
+ current_since = None
+ text = (
+ "from sqlalchemy import event\n\n"
+ "# standard decorator style%(current_since)s\n"
+ "@event.listens_for(%(sample_target)s, '%(event_name)s')\n"
+ "def receive_%(event_name)s(%(named_event_arguments)s%(has_kw_arguments)s):\n"
+ " \"listen for the '%(event_name)s' event\"\n"
+ "\n # ... (event handling logic) ...\n"
+ )
+
+ if len(self.arg_names) > 2:
+ text += (
+
+ "\n# named argument style (new in 0.9)\n"
+ "@event.listens_for(%(sample_target)s, '%(event_name)s', named=True)\n"
+ "def receive_%(event_name)s(**kw):\n"
+ " \"listen for the '%(event_name)s' event\"\n"
+ "%(example_kw_arg)s\n"
+ "\n # ... (event handling logic) ...\n"
+ )
+
+ text %= {
+ "current_since": " (arguments as of %s)" %
+ current_since if current_since else "",
+ "event_name": fn.__name__,
+ "has_kw_arguments": " **kw" if self.has_kw else "",
+ "named_event_arguments": ", ".join(self.arg_names),
+ "example_kw_arg": example_kw_arg,
+ "sample_target": sample_target
+ }
+ return text
+
+ def _legacy_listen_examples(self, sample_target, fn):
+ text = ""
+ for since, args, conv in self.legacy_signatures:
+ text += (
+ "\n# legacy calling style (pre-%(since)s)\n"
+ "@event.listens_for(%(sample_target)s, '%(event_name)s')\n"
+ "def receive_%(event_name)s(%(named_event_arguments)s%(has_kw_arguments)s):\n"
+ " \"listen for the '%(event_name)s' event\"\n"
+ "\n # ... (event handling logic) ...\n" % {
+ "since": since,
+ "event_name": fn.__name__,
+ "has_kw_arguments": " **kw" if self.has_kw else "",
+ "named_event_arguments": ", ".join(args),
+ "sample_target": sample_target
+ }
+ )
+ return text
+
+ def _version_signature_changes(self):
+ since, args, conv = self.legacy_signatures[0]
+ return (
+ "\n.. versionchanged:: %(since)s\n"
+ " The ``%(event_name)s`` event now accepts the \n"
+ " arguments ``%(named_event_arguments)s%(has_kw_arguments)s``.\n"
+ " Listener functions which accept the previous argument \n"
+ " signature(s) listed above will be automatically \n"
+ " adapted to the new signature." % {
+ "since": since,
+ "event_name": self.__name__,
+ "named_event_arguments": ", ".join(self.arg_names),
+ "has_kw_arguments": ", **kw" if self.has_kw else ""
+ }
+ )
+
+ def _augment_fn_docs(self, parent_dispatch_cls, fn):
+ header = ".. container:: event_signatures\n\n"\
+ " Example argument forms::\n"\
+ "\n"
+
+ sample_target = getattr(parent_dispatch_cls, "_target_class_doc", "obj")
+ text = (
+ header +
+ self._indent(
+ self._standard_listen_example(sample_target, fn),
+ " " * 8)
+ )
+ if self.legacy_signatures:
+ text += self._indent(
+ self._legacy_listen_examples(sample_target, fn),
+ " " * 8)
+
+ text += self._version_signature_changes()
+
+ return util.inject_docstring_text(fn.__doc__,
+ text,
+ 1
+ )
+
def _contains(self, cls, evt):
return cls in self._clslevel and \
evt in self._clslevel[cls]
@@ -324,8 +496,11 @@ class _DispatchDescriptor(object):
obj.__dict__[self.__name__] = ret
return ret
+class _HasParentDispatchDescriptor(object):
+ def _adjust_fn_spec(self, fn, named):
+ return self.parent._adjust_fn_spec(fn, named)
-class _EmptyListener(object):
+class _EmptyListener(_HasParentDispatchDescriptor):
"""Serves as a class-level interface to the events
served by a _DispatchDescriptor, when there are no
instance-level events present.
@@ -337,12 +512,13 @@ class _EmptyListener(object):
def __init__(self, parent, target_cls):
if target_cls not in parent._clslevel:
parent.update_subclass(target_cls)
- self.parent = parent
+ self.parent = parent # _DispatchDescriptor
self.parent_listeners = parent._clslevel[target_cls]
self.name = parent.__name__
self.propagate = frozenset()
self.listeners = ()
+
def for_modify(self, obj):
"""Return an event collection which can be modified.
@@ -380,7 +556,7 @@ class _EmptyListener(object):
__nonzero__ = __bool__
-class _CompoundListener(object):
+class _CompoundListener(_HasParentDispatchDescriptor):
_exec_once = False
def exec_once(self, *args, **kw):
@@ -432,6 +608,7 @@ class _ListenerCollection(_CompoundListener):
if target_cls not in parent._clslevel:
parent.update_subclass(target_cls)
self.parent_listeners = parent._clslevel[target_cls]
+ self.parent = parent
self.name = parent.__name__
self.listeners = []
self.propagate = set()
@@ -520,6 +697,9 @@ class _JoinedListener(_CompoundListener):
# each time. less performant.
self.listeners = list(getattr(self.parent, self.name))
+ def _adjust_fn_spec(self, fn, named):
+ return self.local._adjust_fn_spec(fn, named)
+
def for_modify(self, obj):
self.local = self.parent_listeners = self.local.for_modify(obj)
return self