]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-143535: Dispatch on the second argument if generic method is instance-bindable...
authorBartosz Sławecki <bartosz@ilikepython.com>
Tue, 24 Feb 2026 16:04:37 +0000 (17:04 +0100)
committerGitHub <noreply@github.com>
Tue, 24 Feb 2026 16:04:37 +0000 (17:04 +0100)
Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
Doc/whatsnew/3.15.rst
Lib/functools.py
Lib/test/test_functools.py
Misc/NEWS.d/next/Library/2026-02-09-02-16-36.gh-issue-144615.s04x4n.rst [new file with mode: 0644]

index cd1ec0e5c452d36b87fc1d503622251ee2c1f1b9..816d45e07568240eac7de707324a7c845eb75f3e 100644 (file)
@@ -744,6 +744,10 @@ functools
   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
 -------
index 59fc2a8fbf6219e15c5c12f5f0b24830e8cc4984..9bc2ee7e8c894c3f0232579a84e4041cd6762629 100644 (file)
@@ -19,7 +19,7 @@ from collections import namedtuple
 # 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
 
 ################################################################################
@@ -1060,6 +1060,11 @@ class _singledispatchmethod_get:
         # 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:
@@ -1088,9 +1093,22 @@ class _singledispatchmethod_get:
                                '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):
index 3801a82a6108914f611bdfa91b807ccfb025d9fb..86652b7fa4d6df60d061065d885e9c0c7b3f2c80 100644 (file)
@@ -3005,6 +3005,57 @@ class TestSingleDispatch(unittest.TestCase):
         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):
diff --git a/Misc/NEWS.d/next/Library/2026-02-09-02-16-36.gh-issue-144615.s04x4n.rst b/Misc/NEWS.d/next/Library/2026-02-09-02-16-36.gh-issue-144615.s04x4n.rst
new file mode 100644 (file)
index 0000000..1db257a
--- /dev/null
@@ -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.