Skip to content

Commit 97388c2

Browse files
AlexWaygooduriyyoambv
authored
[3.9] bpo-39679: Fix singledispatchmethod classmethod/staticmethod bug (GH-29087)
This commit fixes a bug in the 3.9 branch where stacking `@functools.singledispatchmethod` on top of `@classmethod` or `@staticmethod` caused an exception to be raised if the method was registered using type-annotations rather than `@method.register(int)`. Tests for this scenario were added to the 3.11 and 3.10 branches in #29034 and #29072; this commit also backports those tests to the 3.9 branch. Co-authored-by: Yurii Karabas <[email protected]> Co-authored-by: Łukasz Langa <[email protected]>
1 parent 8365a5b commit 97388c2

File tree

3 files changed

+51
-0
lines changed

3 files changed

+51
-0
lines changed

Lib/functools.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,12 @@ def register(self, cls, method=None):
906906
907907
Registers a new implementation for the given *cls* on a *generic_method*.
908908
"""
909+
# bpo-39679: in Python <= 3.9, classmethods and staticmethods don't
910+
# inherit __annotations__ of the wrapped function (fixed in 3.10+ as
911+
# a side-effect of bpo-43682) but we need that for annotation-derived
912+
# singledispatches. So we add that just-in-time here.
913+
if isinstance(cls, (staticmethod, classmethod)):
914+
cls.__annotations__ = getattr(cls.__func__, '__annotations__', {})
909915
return self.dispatcher.register(cls, func=method)
910916

911917
def __get__(self, obj, cls=None):

Lib/test/test_functools.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2427,6 +2427,48 @@ def _(self, arg: str):
24272427
self.assertEqual(a.t(''), "str")
24282428
self.assertEqual(a.t(0.0), "base")
24292429

2430+
def test_staticmethod_type_ann_register(self):
2431+
class A:
2432+
@functools.singledispatchmethod
2433+
@staticmethod
2434+
def t(arg):
2435+
return arg
2436+
@t.register
2437+
@staticmethod
2438+
def _(arg: int):
2439+
return isinstance(arg, int)
2440+
@t.register
2441+
@staticmethod
2442+
def _(arg: str):
2443+
return isinstance(arg, str)
2444+
a = A()
2445+
2446+
self.assertTrue(A.t(0))
2447+
self.assertTrue(A.t(''))
2448+
self.assertEqual(A.t(0.0), 0.0)
2449+
2450+
def test_classmethod_type_ann_register(self):
2451+
class A:
2452+
def __init__(self, arg):
2453+
self.arg = arg
2454+
2455+
@functools.singledispatchmethod
2456+
@classmethod
2457+
def t(cls, arg):
2458+
return cls("base")
2459+
@t.register
2460+
@classmethod
2461+
def _(cls, arg: int):
2462+
return cls("int")
2463+
@t.register
2464+
@classmethod
2465+
def _(cls, arg: str):
2466+
return cls("str")
2467+
2468+
self.assertEqual(A.t(0).arg, "int")
2469+
self.assertEqual(A.t('').arg, "str")
2470+
self.assertEqual(A.t(0.0).arg, "base")
2471+
24302472
def test_invalid_registrations(self):
24312473
msg_prefix = "Invalid first argument to `register()`: "
24322474
msg_suffix = (
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix bug in :class:`functools.singledispatchmethod` that caused it to fail
2+
when attempting to register a :func:`classmethod` or :func:`staticmethod`
3+
using type annotations. Patch contributed by Alex Waygood.

0 commit comments

Comments
 (0)