mirror of
https://github.com/python/cpython.git
synced 2025-12-23 09:19:18 +00:00
[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:
parent
9a4604bf03
commit
effb72fa0f
3 changed files with 150 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue