[Python-checkins] [3.9] bpo-45678: Fix `singledispatchmethod` `classmethod`/`staticmethod` bug (GH-29394)
ambv
webhook-mailer at python.org
Thu Nov 4 15:34:22 EDT 2021
https://github.com/python/cpython/commit/effb72fa0f6f8f8ec92687fc6a29d63bbdb7e98d
commit: effb72fa0f6f8f8ec92687fc6a29d63bbdb7e98d
branch: 3.9
author: Alex Waygood <Alex.Waygood at Gmail.com>
committer: ambv <lukasz at langa.pl>
date: 2021-11-04T20:34:14+01:00
summary:
[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 <lukasz at langa.pl>
files:
A Misc/NEWS.d/next/Library/2021-11-03-17-28-43.bpo-45678.Zj_O8j.rst
M Lib/functools.py
M Lib/test/test_functools.py
diff --git a/Lib/functools.py b/Lib/functools.py
index 5054e281ad281..1a290e1c02461 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -901,6 +901,13 @@ def __init__(self, func):
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 @@ def _method(*args, **kwargs):
_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 96e93ed8eab34..b2a7e5a88b422 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -2401,7 +2401,7 @@ def _(cls, arg):
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 @@ def add(self, x, y):
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 @@ def _(cls, arg: str):
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 0000000000000..f00707ca0099e
--- /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.
More information about the Python-checkins
mailing list