]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Fixed regression where using a ``functools.partial()`` with the event
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 4 Jan 2014 05:35:48 +0000 (00:35 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 4 Jan 2014 05:35:48 +0000 (00:35 -0500)
system would cause a recursion overflow due to usage of inspect.getargspec()
on it in order to detect a legacy calling signature for certain events,
and apparently there's no way to do this with a partial object.  Instead
we skip the legacy check and assume the modern style; the check itself
now only occurs for the SessionEvents.after_bulk_update and
SessionEvents.after_bulk_delete events.  Those two events will require
the new signature style if assigned to a "partial" event listener.
[ticket:2905]

doc/build/changelog/changelog_09.rst
lib/sqlalchemy/event/attr.py
lib/sqlalchemy/util/langhelpers.py
test/base/test_events.py
test/base/test_utils.py

index c72d853d9a48d57cb2d1a64da4979a2a413be478..c35da31202bb38ce9c1a2f0975be05aacce52fba 100644 (file)
 .. changelog::
     :version: 0.9.1
 
+    .. change::
+        :tags: bug, orm, events
+        :tickets: 2905
+
+        Fixed regression where using a ``functools.partial()`` with the event
+        system would cause a recursion overflow due to usage of inspect.getargspec()
+        on it in order to detect a legacy calling signature for certain events,
+        and apparently there's no way to do this with a partial object.  Instead
+        we skip the legacy check and assume the modern style; the check itself
+        now only occurs for the SessionEvents.after_bulk_update and
+        SessionEvents.after_bulk_delete events.  Those two events will require
+        the new signature style if assigned to a "partial" event listener.
+
     .. change::
         :tags: bug, orm, declarative
 
index 690ebce080f9c681fc1390b095629810a49edf27..f0fff5ca4f6489792131593280404ab331dd1577 100644 (file)
@@ -62,10 +62,15 @@ class _DispatchDescriptor(RefCollection):
         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 = legacy._wrap_fn_for_legacy(self, fn, argspec)
+        if self.legacy_signatures:
+            try:
+                argspec = util.get_callable_argspec(fn, no_self=True)
+            except ValueError:
+                pass
+            else:
+                fn = legacy._wrap_fn_for_legacy(self, fn, argspec)
         return fn
 
     def _wrap_fn_for_kw(self, fn):
index 7e261e38fe47f9d65d47cc3223f0e9f0f761ca55..105b64c6b43e1644f648e9fd997b9df0b7e16e68 100644 (file)
@@ -268,7 +268,9 @@ def get_callable_argspec(fn, no_self=False):
         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__'):
+    elif hasattr(fn, '__call__') and \
+        not hasattr(fn.__call__, '__call__'):  # functools.partial does this;
+                                               # not much we can do
         return get_callable_argspec(fn.__call__)
     else:
         raise ValueError("Can't inspect function: %s" % fn)
index 8673c9baf0c5c914524e9281a732e9fc957d72b4..e985f8d5b08432287301ae4f9615380a1cdf8e6f 100644 (file)
@@ -311,6 +311,26 @@ class LegacySignatureTest(fixtures.TestBase):
             canary(x, y, kw)
         self._test_legacy_accept_kw(inst, canary)
 
+    def test_legacy_accept_partial(self):
+        canary = Mock()
+        def evt(a, x, y, **kw):
+            canary(a, x, y, **kw)
+        from functools import partial
+        evt_partial = partial(evt, 5)
+        target = self.TargetOne()
+        event.listen(target, "event_four", evt_partial)
+        # can't do legacy accept on a partial; we can't inspect it
+        assert_raises(
+            TypeError,
+            target.dispatch.event_four, 4, 5, 6, 7, foo="bar"
+        )
+        target.dispatch.event_four(4, 5, foo="bar")
+        eq_(
+            canary.mock_calls,
+            [call(5, 4, 5, foo="bar")]
+        )
+
+
     def _test_legacy_accept_kw(self, target, canary):
         target.dispatch.event_four(4, 5, 6, 7, foo="bar")
 
index 1946bd7043abb9bb6703c1d69c60ee037ed1b17c..86e4b190a04de9488aafe3eefb3c698ddead2f38 100644 (file)
@@ -4,7 +4,7 @@ from sqlalchemy import util, sql, exc, testing
 from sqlalchemy.testing import assert_raises, assert_raises_message, fixtures
 from sqlalchemy.testing import eq_, is_, ne_, fails_if
 from sqlalchemy.testing.util import picklers, gc_collect
-from sqlalchemy.util import classproperty, WeakSequence
+from sqlalchemy.util import classproperty, WeakSequence, get_callable_argspec
 
 
 class KeyedTupleTest():
@@ -1184,6 +1184,33 @@ class ArgInspectionTest(fixtures.TestBase):
         test(f3)
         test(f4)
 
+    def test_callable_argspec_fn(self):
+        def foo(x, y, **kw):
+            pass
+        eq_(
+            get_callable_argspec(foo),
+            (['x', 'y'], None, 'kw', None)
+        )
+
+    def test_callable_argspec_method(self):
+        class Foo(object):
+            def foo(self, x, y, **kw):
+                pass
+        eq_(
+            get_callable_argspec(Foo.foo),
+            (['self', 'x', 'y'], None, 'kw', None)
+        )
+
+    def test_callable_argspec_partial(self):
+        from functools import partial
+        def foo(x, y, z, **kw):
+            pass
+        bar = partial(foo, 5)
+
+        assert_raises(
+            ValueError,
+            get_callable_argspec, bar
+        )
 
 class SymbolTest(fixtures.TestBase):
 
@@ -1665,3 +1692,5 @@ class TestClassProperty(fixtures.TestBase):
                 return d
 
         eq_(B.something, {'foo': 1, 'bazz': 2})
+
+