[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@langa.pl>
This commit is contained in:
Alex Waygood 2021-11-04 19:34:14 +00:00 committed by GitHub
parent 9a4604bf03
commit effb72fa0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 150 additions and 2 deletions

View file

@ -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

View file

@ -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 = (

View file

@ -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.