callables.
(Contributed by Serhiy Storchaka in :gh:`140873`.)
+* :func:`~functools.singledispatchmethod` now dispatches on the second argument
+ if it wraps a regular method and is called as a class attribute.
+ (Contributed by Bartosz Sławecki in :gh:`143535`.)
+
hashlib
-------
# 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
################################################################################
# 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.
+ self._dispatch_arg_index = 1 if obj is None and isinstance(func, FunctionType) else 0
+
try:
self.__module__ = func.__module__
except AttributeError:
'singledispatchmethod method')
raise TypeError(f'{funcname} requires at least '
'1 positional argument')
- method = self._dispatch(args[0].__class__)
+ method = self._dispatch(args[self._dispatch_arg_index].__class__)
+
if hasattr(method, "__get__"):
+ # If the method is a descriptor, it might be necessary
+ # to drop the first argument before calling
+ # as it can be no longer expected after descriptor access.
+ skip_bound_arg = False
+ if isinstance(method, staticmethod):
+ skip_bound_arg = self._dispatch_arg_index == 1
+
method = method.__get__(self._obj, self._cls)
+ if isinstance(method, MethodType):
+ skip_bound_arg = self._dispatch_arg_index == 1
+
+ if skip_bound_arg:
+ return method(*args[1:], **kwargs)
return method(*args, **kwargs)
def __getattr__(self, name):
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(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):
--- /dev/null
+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.