Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# import weakref # Deferred to single_dispatch()
from operator import itemgetter
from reprlib import recursive_repr
from types import GenericAlias, MethodType, MappingProxyType, UnionType
from types import FunctionType, GenericAlias, MethodType, MappingProxyType, UnionType
from _thread import RLock

################################################################################
Expand Down Expand Up @@ -1060,6 +1060,14 @@ def __init__(self, unbound, obj, cls):
# Set instance attributes which cannot be handled in __getattr__()
# because they conflict with type descriptors.
func = unbound.func

# Dispatch on the second argument if a generic method turns into
# a bound method on instance-level access. See GH-143535.
if obj is None and isinstance(func, FunctionType):
self._skip_bound_arg = True
else:
self._skip_bound_arg = False

try:
self.__module__ = func.__module__
except AttributeError:
Expand Down Expand Up @@ -1088,9 +1096,14 @@ def __call__(self, /, *args, **kwargs):
'singledispatchmethod method')
raise TypeError(f'{funcname} requires at least '
'1 positional argument')
method = self._dispatch(args[0].__class__)
if self._skip_bound_arg:
method = self._dispatch(args[1].__class__)
else:
method = self._dispatch(args[0].__class__)
if hasattr(method, "__get__"):
method = method.__get__(self._obj, self._cls)
if self._skip_bound_arg and isinstance(method, MethodType):
args = args[1:]
return method(*args, **kwargs)

def __getattr__(self, name):
Expand Down
51 changes: 51 additions & 0 deletions Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3005,6 +3005,57 @@ def static_func(arg: int) -> str:
self.assertEqual(A.static_func.__name__, 'static_func')
self.assertEqual(A().static_func.__name__, 'static_func')

def test_method_classlevel_calls(self):
"""Regression test for GH-143535."""
class C:
@functools.singledispatchmethod
def generic(self, x: object):
return "generic"

@generic.register
def special1(self, x: int):
return "special1"

@generic.register
@classmethod
def special2(self, x: float):
return "special2"

@generic.register
@staticmethod
def special3(self, x: complex):
return "special3"

def special4(self, x):
return "special4"

class D1:
def __get__(self, _, owner):
return lambda inst, x: owner.special4(inst, x)

generic.register(D1, D1())

def special5(self, x):
return "special5"

class D2:
def __get__(self, inst, owner):
# Different instance bound to the returned method
# doesn't cause it to receive the original instance
# as a separate argument.
# To work around this, wrap the returned bound method
# with `functools.partial`.
return C().special5

generic.register(D2, D2())

self.assertEqual(C.generic(C(), "foo"), "generic")
self.assertEqual(C.generic(C(), 1), "special1")
self.assertEqual(C.generic(C(), 2.0), "special2")
self.assertEqual(C.generic(C(), 3j), "special3")
self.assertEqual(C.generic(C(), C.D1()), "special4")
self.assertEqual(C.generic(C(), C.D2()), "special5")

def test_method_repr(self):
class Callable:
def __call__(self, *args):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Methods directly decorated with :deco:`functools.singledispatchmethod` now
dispatch on the second argument when called after being accessed as class
attributes. Patch by Bartosz Sławecki.
Loading