From 555865b835c4c51c82d3b808d28a9e47f9e9037f Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sat, 5 Oct 2024 19:21:39 +1000 Subject: [PATCH] Fix calling of callable object when binding was attempted. --- docs/changes.rst | 6 ++++++ src/wrapt/_wrappers.c | 6 ++---- src/wrapt/wrappers.py | 10 ++++++++- tests/test_decorators.py | 38 ++++++++++++++++++++++++++++++++++ tests/test_function_wrapper.py | 2 +- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f22053c..d28d108 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -16,6 +16,12 @@ Note that version 1.17.0 drops support for Python 3.6 and 3.7. Python version or instance. This was not the correct behaviour and the class or instance should not have been passed as the first argument. +* When an instance of a callable class object was wrapped which didn't not have + a `__get__()` method for binding, and it was called in context whhere binding + would be attempted, it would fail with error that `__get__()` did not exist + when instead it should have been called directly, ignoring that binding was + not possible. + Version 1.16.0 -------------- diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index a924d3b..cebb185 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -2488,10 +2488,8 @@ static PyObject *WraptFunctionWrapperBase_descr_get( } if (Py_TYPE(self->object_proxy.wrapped)->tp_descr_get == NULL) { - PyErr_Format(PyExc_AttributeError, - "'%s' object has no attribute '__get__'", - Py_TYPE(self->object_proxy.wrapped)->tp_name); - return NULL; + Py_INCREF(self); + return (PyObject *)self; } descriptor = (Py_TYPE(self->object_proxy.wrapped)->tp_descr_get)( diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index f785e44..8106a8a 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -528,13 +528,21 @@ def __get__(self, instance, owner): # extract the instance from the first argument of those passed in. if self._self_parent is None: + # Technically can probably just check for existence of __get__ on + # the wrapped object, but this is more explicit. + if self._self_binding == 'builtin': return self if self._self_binding == "class": return self - descriptor = self.__wrapped__.__get__(instance, owner) + binder = getattr(self.__wrapped__, '__get__', None) + + if binder is None: + return self + + descriptor = binder(instance, owner) return self.__bound_function_wrapper__(descriptor, instance, self._self_wrapper, self._self_enabled, diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 0af3851..80f05e9 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -297,5 +297,43 @@ class C13: C11.f1(c11) C12.f2(c12) + def test_call_semantics_for_assorted_wrapped_descriptor_use_cases(self): + class A: + def __call__(self): + print("A:__call__") + + a = A() + + class B: + def __call__(self): + print("B:__call__") + def __get__(self, obj, type): + print("B:__get__") + return self + + b = B() + + class C: + f1 = a + f2 = b + + c = C() + + c.f1() + c.f2() + + @wrapt.decorator + def wrapper(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + class D: + f1 = wrapper(a) + f2 = wrapper(b) + + d = D() + + d.f1() + d.f2() + if __name__ == '__main__': unittest.main() diff --git a/tests/test_function_wrapper.py b/tests/test_function_wrapper.py index 2ecde68..ac7df47 100644 --- a/tests/test_function_wrapper.py +++ b/tests/test_function_wrapper.py @@ -554,7 +554,7 @@ def _wrapper(wrapped, instance, args, kwargs): wrapper = wrapt.FunctionWrapper(None, _wrapper) wrapper.__get__(list(), list)() - self.assertRaises(AttributeError, run, ()) + self.assertRaises(TypeError, run, ()) class TestInvalidCalling(unittest.TestCase):