summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2013-07-08 13:39:56 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2013-07-08 13:39:56 -0400
commit02a81707dc8b7c4d69551cad195fb16ca6955df1 (patch)
treefeda05ad7e0ce7bef057b9ee9d55d3273d8d008a /lib/sqlalchemy
parentdb68ecff12f790fd129f03b8676b317fa17e5f28 (diff)
downloadsqlalchemy-02a81707dc8b7c4d69551cad195fb16ca6955df1.tar.gz
- create a new system where we can decorate an event method
with @_legacy_signature, will inspect incoming listener functions to see if they match an older signature, will wrap into a newer sig - add an event listen argument named=True, will send all args as kw args so that event listeners can be written with **kw, any combination of names - add a doc system to events that writes out the various calling styles for a given event, produces deprecation messages automatically. a little concerned that it's a bit verbose but will look at it up on RTD for awhile to get a feel. - change the calling signature for bulk update/delete events - we have the BulkUD object right there, and there's at least six or seven things people might want to see, so just send the whole BulkUD in [ticket:2775]
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/event.py198
-rw-r--r--lib/sqlalchemy/events.py8
-rw-r--r--lib/sqlalchemy/orm/events.py64
-rw-r--r--lib/sqlalchemy/orm/persistence.py10
-rw-r--r--lib/sqlalchemy/util/__init__.py7
-rw-r--r--lib/sqlalchemy/util/compat.py10
-rw-r--r--lib/sqlalchemy/util/deprecations.py40
-rw-r--r--lib/sqlalchemy/util/langhelpers.py16
8 files changed, 309 insertions, 44 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
diff --git a/lib/sqlalchemy/events.py b/lib/sqlalchemy/events.py
index 7f11232ac..4fb997b9c 100644
--- a/lib/sqlalchemy/events.py
+++ b/lib/sqlalchemy/events.py
@@ -70,6 +70,8 @@ class DDLEvents(event.Events):
"""
+ _target_class_doc = "SomeSchemaClassOrObject"
+
def before_create(self, target, connection, **kw):
"""Called before CREATE statments are emitted.
@@ -266,6 +268,8 @@ class PoolEvents(event.Events):
"""
+ _target_class_doc = "SomeEngineOrPool"
+
@classmethod
def _accept_with(cls, target):
if isinstance(target, type):
@@ -443,6 +447,8 @@ class ConnectionEvents(event.Events):
"""
+ _target_class_doc = "SomeEngine"
+
@classmethod
def _listen(cls, target, identifier, fn, retval=False):
target._has_events = True
@@ -753,7 +759,7 @@ class ConnectionEvents(event.Events):
:param conn: :class:`.Connection` object
"""
- def savepoint(self, conn, name=None):
+ def savepoint(self, conn, name):
"""Intercept savepoint() events.
:param conn: :class:`.Connection` object
diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py
index cea07bcf0..97019bb4e 100644
--- a/lib/sqlalchemy/orm/events.py
+++ b/lib/sqlalchemy/orm/events.py
@@ -42,6 +42,8 @@ class InstrumentationEvents(event.Events):
"""
+ _target_class_doc = "SomeBaseClass"
+
@classmethod
def _accept_with(cls, target):
# TODO: there's no coverage for this
@@ -151,6 +153,9 @@ class InstanceEvents(event.Events):
object, rather than the mapped instance itself.
"""
+
+ _target_class_doc = "SomeMappedClass"
+
@classmethod
def _accept_with(cls, target):
if isinstance(target, orm.instrumentation.ClassManager):
@@ -450,6 +455,8 @@ class MapperEvents(event.Events):
"""
+ _target_class_doc = "SomeMappedClass"
+
@classmethod
def _accept_with(cls, target):
if target is orm.mapper:
@@ -1083,6 +1090,9 @@ class SessionEvents(event.Events):
globally.
"""
+
+ _target_class_doc = "SomeSessionOrFactory"
+
@classmethod
def _accept_with(cls, target):
if isinstance(target, orm.scoped_session):
@@ -1382,31 +1392,55 @@ class SessionEvents(event.Events):
"""
- def after_bulk_update(self, session, query, query_context, result):
+ @event._legacy_signature("0.9",
+ ["session", "query", "query_context", "result"],
+ lambda update_context: (
+ update_context.session,
+ update_context.query,
+ update_context.context,
+ update_context.result))
+ def after_bulk_update(self, update_context):
"""Execute after a bulk update operation to the session.
This is called as a result of the :meth:`.Query.update` method.
- :param query: the :class:`.Query` object that this update operation was
- called upon.
- :param query_context: The :class:`.QueryContext` object, corresponding
- to the invocation of an ORM query.
- :param result: the :class:`.ResultProxy` returned as a result of the
- bulk UPDATE operation.
+ :param update_context: an "update context" object which contains
+ details about the update, including these attributes:
+
+ * ``session`` - the :class:`.Session` involved
+ * ``query`` -the :class:`.Query` object that this update operation was
+ called upon.
+ * ``context`` The :class:`.QueryContext` object, corresponding
+ to the invocation of an ORM query.
+ * ``result`` the :class:`.ResultProxy` returned as a result of the
+ bulk UPDATE operation.
+
"""
- def after_bulk_delete(self, session, query, query_context, result):
+ @event._legacy_signature("0.9",
+ ["session", "query", "query_context", "result"],
+ lambda delete_context: (
+ delete_context.session,
+ delete_context.query,
+ delete_context.context,
+ delete_context.result))
+ def after_bulk_delete(self, delete_context):
"""Execute after a bulk delete operation to the session.
This is called as a result of the :meth:`.Query.delete` method.
- :param query: the :class:`.Query` object that this update operation was
- called upon.
- :param query_context: The :class:`.QueryContext` object, corresponding
- to the invocation of an ORM query.
- :param result: the :class:`.ResultProxy` returned as a result of the
- bulk DELETE operation.
+ :param delete_context: a "delete context" object which contains
+ details about the update, including these attributes:
+
+ * ``session`` - the :class:`.Session` involved
+ * ``query`` -the :class:`.Query` object that this update operation was
+ called upon.
+ * ``context`` The :class:`.QueryContext` object, corresponding
+ to the invocation of an ORM query.
+ * ``result`` the :class:`.ResultProxy` returned as a result of the
+ bulk DELETE operation.
+
"""
@@ -1468,6 +1502,8 @@ class AttributeEvents(event.Events):
"""
+ _target_class_doc = "SomeClass.some_attribute"
+
@classmethod
def _accept_with(cls, target):
# TODO: coverage
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index 944623b07..44da88118 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -798,6 +798,10 @@ class BulkUD(object):
def __init__(self, query):
self.query = query.enable_eagerloads(False)
+ @property
+ def session(self):
+ return self.query.session
+
@classmethod
def _factory(cls, lookup, synchronize_session, *arg):
try:
@@ -915,8 +919,7 @@ class BulkUpdate(BulkUD):
def _do_post(self):
session = self.query.session
- session.dispatch.after_bulk_update(session, self.query,
- self.context, self.result)
+ session.dispatch.after_bulk_update(self)
class BulkDelete(BulkUD):
@@ -944,8 +947,7 @@ class BulkDelete(BulkUD):
def _do_post(self):
session = self.query.session
- session.dispatch.after_bulk_delete(session, self.query,
- self.context, self.result)
+ session.dispatch.after_bulk_delete(self)
class BulkUpdateEvaluate(BulkEvaluate, BulkUpdate):
diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py
index 739caefe0..5d9c4d49e 100644
--- a/lib/sqlalchemy/util/__init__.py
+++ b/lib/sqlalchemy/util/__init__.py
@@ -10,7 +10,7 @@ from .compat import callable, cmp, reduce, \
raise_from_cause, text_type, string_types, int_types, binary_type, \
quote_plus, with_metaclass, print_, itertools_filterfalse, u, ue, b,\
unquote_plus, b64decode, b64encode, byte_buffer, itertools_filter,\
- StringIO
+ StringIO, inspect_getargspec
from ._collections import KeyedTuple, ImmutableContainer, immutabledict, \
Properties, OrderedProperties, ImmutableProperties, OrderedDict, \
@@ -30,10 +30,11 @@ from .langhelpers import iterate_attributes, class_hierarchy, \
duck_type_collection, assert_arg_type, symbol, dictlike_iteritems,\
classproperty, set_creation_order, warn_exception, warn, NoneType,\
constructor_copy, methods_equivalent, chop_traceback, asint,\
- generic_repr, counter, PluginLoader, hybridmethod, safe_reraise
+ generic_repr, counter, PluginLoader, hybridmethod, safe_reraise,\
+ get_callable_argspec
from .deprecations import warn_deprecated, warn_pending_deprecation, \
- deprecated, pending_deprecation
+ deprecated, pending_deprecation, inject_docstring_text
# things that used to be not always available,
# but are now as of current support Python versions
diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py
index d866534ab..a89762b4e 100644
--- a/lib/sqlalchemy/util/compat.py
+++ b/lib/sqlalchemy/util/compat.py
@@ -22,7 +22,7 @@ pypy = hasattr(sys, 'pypy_version_info')
win32 = sys.platform.startswith('win')
cpython = not pypy and not jython # TODO: something better for this ?
-
+import collections
next = next
if py3k:
@@ -33,6 +33,9 @@ else:
except ImportError:
import pickle
+ArgSpec = collections.namedtuple("ArgSpec",
+ ["args", "varargs", "keywords", "defaults"])
+
if py3k:
import builtins
@@ -43,6 +46,10 @@ if py3k:
from io import BytesIO as byte_buffer
+ def inspect_getargspec(func):
+ return ArgSpec(
+ *inspect_getfullargspec(func)[0:4]
+ )
string_types = str,
binary_type = bytes
@@ -87,6 +94,7 @@ if py3k:
else:
from inspect import getargspec as inspect_getfullargspec
+ inspect_getargspec = inspect_getfullargspec
from urllib import quote_plus, unquote_plus
from urlparse import parse_qsl
import ConfigParser as configparser
diff --git a/lib/sqlalchemy/util/deprecations.py b/lib/sqlalchemy/util/deprecations.py
index e0dc168db..c315d2da6 100644
--- a/lib/sqlalchemy/util/deprecations.py
+++ b/lib/sqlalchemy/util/deprecations.py
@@ -107,17 +107,37 @@ def _decorate_with_warning(func, wtype, message, docstring_header=None):
doc = func.__doc__ is not None and func.__doc__ or ''
if docstring_header is not None:
docstring_header %= dict(func=func.__name__)
- docs = doc and doc.expandtabs().split('\n') or []
- indent = ''
- for line in docs[1:]:
- text = line.lstrip()
- if text:
- indent = line[0:len(line) - len(text)]
- break
- point = min(len(docs), 1)
- docs.insert(point, '\n' + indent + docstring_header.rstrip())
- doc = '\n'.join(docs)
+
+ doc = inject_docstring_text(doc, docstring_header, 1)
decorated = warned(func)
decorated.__doc__ = doc
return decorated
+
+import textwrap
+
+def _dedent_docstring(text):
+ split_text = text.split("\n", 1)
+ if len(split_text) == 1:
+ return text
+ else:
+ firstline, remaining = split_text
+ if not firstline.startswith(" "):
+ return firstline + "\n" + textwrap.dedent(remaining)
+ else:
+ return textwrap.dedent(text)
+
+def inject_docstring_text(doctext, injecttext, pos):
+ doctext = _dedent_docstring(doctext or "")
+ lines = doctext.split('\n')
+ injectlines = textwrap.dedent(injecttext).split("\n")
+ if injectlines[0]:
+ injectlines.insert(0, "")
+
+ blanks = [num for num, line in enumerate(lines) if not line.strip()]
+ blanks.insert(0, 0)
+
+ inject_pos = blanks[min(pos, len(blanks) - 1)]
+
+ lines = lines[0:inject_pos] + injectlines + lines[inject_pos:]
+ return "\n".join(lines)
diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py
index 1ff868e01..c91178a75 100644
--- a/lib/sqlalchemy/util/langhelpers.py
+++ b/lib/sqlalchemy/util/langhelpers.py
@@ -211,8 +211,20 @@ def get_func_kwargs(func):
"""
- return inspect.getargspec(func)[0]
-
+ return compat.inspect_getargspec(func)[0]
+
+def get_callable_argspec(fn, no_self=False):
+ if isinstance(fn, types.FunctionType):
+ return compat.inspect_getargspec(fn)
+ elif isinstance(fn, types.MethodType) and no_self:
+ spec = compat.inspect_getargspec(fn.__func__)
+ return compat.ArgSpec(spec.args[1:], spec.varargs, spec.keywords, spec.defaults)
+ elif hasattr(fn, '__func__'):
+ return compat.inspect_getargspec(fn.__func__)
+ elif hasattr(fn, '__call__'):
+ return get_callable_argspec(fn.__call__)
+ else:
+ raise ValueError("Can't inspect function: %s" % fn)
def format_argspec_plus(fn, grouped=True):
"""Returns a dictionary of formatted, introspected function arguments.