From: Alex Waygood Date: Thu, 4 Nov 2021 19:34:14 +0000 (+0000) Subject: [3.9] bpo-45678: Fix `singledispatchmethod` `classmethod`/`staticmethod` bug (GH... X-Git-Tag: v3.9.8~6 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=effb72fa0f6f8f8ec92687fc6a29d63bbdb7e98d;p=thirdparty%2FPython%2Fcpython.git [3.9] bpo-45678: Fix `singledispatchmethod` `classmethod`/`staticmethod` bug (GH-29394) This PR fixes a bug in the 3.9 branch where ``functools.singledispatchmethod`` did not properly wrap attributes such as ``__name__``, ``__doc__`` and ``__module__`` of the target method. It also backports tests already merged into the 3.11 and 3.10 branches in #29328 and #29390. Co-authored-by: Ɓukasz Langa --- diff --git a/Lib/functools.py b/Lib/functools.py index 5054e281ad28..1a290e1c0246 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -901,6 +901,13 @@ class singledispatchmethod: self.dispatcher = singledispatch(func) self.func = func + # bpo-45678: special-casing for classmethod/staticmethod in Python <=3.9, + # as functools.update_wrapper doesn't work properly in singledispatchmethod.__get__ + # if it is applied to an unbound classmethod/staticmethod + if isinstance(func, (staticmethod, classmethod)): + self._wrapped_func = func.__func__ + else: + self._wrapped_func = func def register(self, cls, method=None): """generic_method.register(cls, func) -> func @@ -921,7 +928,7 @@ class singledispatchmethod: _method.__isabstractmethod__ = self.__isabstractmethod__ _method.register = self.register - update_wrapper(_method, self.func) + update_wrapper(_method, self._wrapped_func) return _method @property diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 96e93ed8eab3..b2a7e5a88b42 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -2401,7 +2401,7 @@ class TestSingleDispatch(unittest.TestCase): self.assertEqual(A.t(0.0).arg, "base") def test_abstractmethod_register(self): - class Abstract(abc.ABCMeta): + class Abstract(metaclass=abc.ABCMeta): @functools.singledispatchmethod @abc.abstractmethod @@ -2409,6 +2409,10 @@ class TestSingleDispatch(unittest.TestCase): pass self.assertTrue(Abstract.add.__isabstractmethod__) + self.assertTrue(Abstract.__dict__['add'].__isabstractmethod__) + + with self.assertRaises(TypeError): + Abstract() def test_type_ann_register(self): class A: @@ -2469,6 +2473,141 @@ class TestSingleDispatch(unittest.TestCase): self.assertEqual(A.t('').arg, "str") self.assertEqual(A.t(0.0).arg, "base") + def test_method_wrapping_attributes(self): + class A: + @functools.singledispatchmethod + def func(self, arg: int) -> str: + """My function docstring""" + return str(arg) + @functools.singledispatchmethod + @classmethod + def cls_func(cls, arg: int) -> str: + """My function docstring""" + return str(arg) + @functools.singledispatchmethod + @staticmethod + def static_func(arg: int) -> str: + """My function docstring""" + return str(arg) + + for meth in ( + A.func, + A().func, + A.cls_func, + A().cls_func, + A.static_func, + A().static_func + ): + with self.subTest(meth=meth): + self.assertEqual(meth.__doc__, 'My function docstring') + self.assertEqual(meth.__annotations__['arg'], int) + + self.assertEqual(A.func.__name__, 'func') + self.assertEqual(A().func.__name__, 'func') + self.assertEqual(A.cls_func.__name__, 'cls_func') + self.assertEqual(A().cls_func.__name__, 'cls_func') + self.assertEqual(A.static_func.__name__, 'static_func') + self.assertEqual(A().static_func.__name__, 'static_func') + + def test_double_wrapped_methods(self): + def classmethod_friendly_decorator(func): + wrapped = func.__func__ + @classmethod + @functools.wraps(wrapped) + def wrapper(*args, **kwargs): + return wrapped(*args, **kwargs) + return wrapper + + class WithoutSingleDispatch: + @classmethod + @contextlib.contextmanager + def cls_context_manager(cls, arg: int) -> str: + try: + yield str(arg) + finally: + return 'Done' + + @classmethod_friendly_decorator + @classmethod + def decorated_classmethod(cls, arg: int) -> str: + return str(arg) + + class WithSingleDispatch: + @functools.singledispatchmethod + @classmethod + @contextlib.contextmanager + def cls_context_manager(cls, arg: int) -> str: + """My function docstring""" + try: + yield str(arg) + finally: + return 'Done' + + @functools.singledispatchmethod + @classmethod_friendly_decorator + @classmethod + def decorated_classmethod(cls, arg: int) -> str: + """My function docstring""" + return str(arg) + + # These are sanity checks + # to test the test itself is working as expected + with WithoutSingleDispatch.cls_context_manager(5) as foo: + without_single_dispatch_foo = foo + + with WithSingleDispatch.cls_context_manager(5) as foo: + single_dispatch_foo = foo + + self.assertEqual(without_single_dispatch_foo, single_dispatch_foo) + self.assertEqual(single_dispatch_foo, '5') + + self.assertEqual( + WithoutSingleDispatch.decorated_classmethod(5), + WithSingleDispatch.decorated_classmethod(5) + ) + + self.assertEqual(WithSingleDispatch.decorated_classmethod(5), '5') + + # Behavioural checks now follow + for method_name in ('cls_context_manager', 'decorated_classmethod'): + with self.subTest(method=method_name): + self.assertEqual( + getattr(WithSingleDispatch, method_name).__name__, + getattr(WithoutSingleDispatch, method_name).__name__ + ) + + self.assertEqual( + getattr(WithSingleDispatch(), method_name).__name__, + getattr(WithoutSingleDispatch(), method_name).__name__ + ) + + for meth in ( + WithSingleDispatch.cls_context_manager, + WithSingleDispatch().cls_context_manager, + WithSingleDispatch.decorated_classmethod, + WithSingleDispatch().decorated_classmethod + ): + with self.subTest(meth=meth): + self.assertEqual(meth.__doc__, 'My function docstring') + self.assertEqual(meth.__annotations__['arg'], int) + + self.assertEqual( + WithSingleDispatch.cls_context_manager.__name__, + 'cls_context_manager' + ) + self.assertEqual( + WithSingleDispatch().cls_context_manager.__name__, + 'cls_context_manager' + ) + self.assertEqual( + WithSingleDispatch.decorated_classmethod.__name__, + 'decorated_classmethod' + ) + self.assertEqual( + WithSingleDispatch().decorated_classmethod.__name__, + 'decorated_classmethod' + ) + def test_invalid_registrations(self): msg_prefix = "Invalid first argument to `register()`: " msg_suffix = ( diff --git a/Misc/NEWS.d/next/Library/2021-11-03-17-28-43.bpo-45678.Zj_O8j.rst b/Misc/NEWS.d/next/Library/2021-11-03-17-28-43.bpo-45678.Zj_O8j.rst new file mode 100644 index 000000000000..f00707ca0099 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-11-03-17-28-43.bpo-45678.Zj_O8j.rst @@ -0,0 +1,2 @@ +Fix bug in Python 3.9 that meant ``functools.singledispatchmethod`` failed +to properly wrap the attributes of the target method. Patch by Alex Waygood.