https://github.com/python/cpython/commit/b2b68d40f887c8a9583a9b48babc40f25bc5e0e2
commit: b2b68d40f887c8a9583a9b48babc40f25bc5e0e2
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2025-11-13T19:48:52+02:00
summary:

gh-140873: Add support of non-descriptor callables in 
functools.singledispatchmethod() (GH-140884)

files:
A Misc/NEWS.d/next/Library/2025-11-01-14-44-09.gh-issue-140873.kfuc9B.rst
M Doc/library/functools.rst
M Doc/whatsnew/3.15.rst
M Lib/functools.py
M Lib/test/test_functools.py

diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst
index 1d9ac328f32769..b2e2e11c0dc414 100644
--- a/Doc/library/functools.rst
+++ b/Doc/library/functools.rst
@@ -672,7 +672,7 @@ The :mod:`functools` module defines the following functions:
    dispatch>` :term:`generic function`.
 
    To define a generic method, decorate it with the ``@singledispatchmethod``
-   decorator. When defining a function using ``@singledispatchmethod``, note
+   decorator. When defining a method using ``@singledispatchmethod``, note
    that the dispatch happens on the type of the first non-*self* or non-*cls*
    argument::
 
@@ -716,6 +716,9 @@ The :mod:`functools` module defines the following functions:
 
    .. versionadded:: 3.8
 
+   .. versionchanged:: next
+      Added support of non-:term:`descriptor` callables.
+
 
 .. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, 
updated=WRAPPER_UPDATES)
 
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index b360ad964cf17f..895616e3049a50 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -498,6 +498,14 @@ difflib
   (Contributed by Jiahao Li in :gh:`134580`.)
 
 
+functools
+---------
+
+* :func:`~functools.singledispatchmethod` now supports non-:term:`descriptor`
+  callables.
+  (Contributed by Serhiy Storchaka in :gh:`140873`.)
+
+
 hashlib
 -------
 
diff --git a/Lib/functools.py b/Lib/functools.py
index a92844ba7227b0..8063eb5ffc3304 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -1083,7 +1083,10 @@ def __call__(self, /, *args, **kwargs):
                                'singledispatchmethod method')
             raise TypeError(f'{funcname} requires at least '
                             '1 positional argument')
-        return self._dispatch(args[0].__class__).__get__(self._obj, 
self._cls)(*args, **kwargs)
+        method = self._dispatch(args[0].__class__)
+        if hasattr(method, "__get__"):
+            method = method.__get__(self._obj, self._cls)
+        return method(*args, **kwargs)
 
     def __getattr__(self, name):
         # Resolve these attributes lazily to speed up creation of
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index ce9e7f6d57dd3c..090926fd8d8b61 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -2785,7 +2785,7 @@ class Slot:
             @functools.singledispatchmethod
             @classmethod
             def go(cls, item, arg):
-                pass
+                return item - arg
 
             @go.register
             @classmethod
@@ -2794,7 +2794,9 @@ def _(cls, item: int, arg):
 
         s = Slot()
         self.assertEqual(s.go(1, 1), 2)
+        self.assertEqual(s.go(1.5, 1), 0.5)
         self.assertEqual(Slot.go(1, 1), 2)
+        self.assertEqual(Slot.go(1.5, 1), 0.5)
 
     def test_staticmethod_slotted_class(self):
         class A:
@@ -3485,6 +3487,37 @@ def _(item, arg: bytes) -> str:
         self.assertEqual(str(Signature.from_callable(A.static_func)),
                          '(item, arg: int) -> str')
 
+    def test_method_non_descriptor(self):
+        class Callable:
+            def __init__(self, value):
+                self.value = value
+            def __call__(self, arg):
+                return self.value, arg
+
+        class A:
+            t = functools.singledispatchmethod(Callable('general'))
+            t.register(int, Callable('special'))
+
+            @functools.singledispatchmethod
+            def u(self, arg):
+                return 'general', arg
+            u.register(int, Callable('special'))
+
+            v = functools.singledispatchmethod(Callable('general'))
+            @v.register(int)
+            def _(self, arg):
+                return 'special', arg
+
+        a = A()
+        self.assertEqual(a.t(0), ('special', 0))
+        self.assertEqual(a.t(2.5), ('general', 2.5))
+        self.assertEqual(A.t(0), ('special', 0))
+        self.assertEqual(A.t(2.5), ('general', 2.5))
+        self.assertEqual(a.u(0), ('special', 0))
+        self.assertEqual(a.u(2.5), ('general', 2.5))
+        self.assertEqual(a.v(0), ('special', 0))
+        self.assertEqual(a.v(2.5), ('general', 2.5))
+
 
 class CachedCostItem:
     _cost = 1
diff --git 
a/Misc/NEWS.d/next/Library/2025-11-01-14-44-09.gh-issue-140873.kfuc9B.rst 
b/Misc/NEWS.d/next/Library/2025-11-01-14-44-09.gh-issue-140873.kfuc9B.rst
new file mode 100644
index 00000000000000..e15057640646d6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-01-14-44-09.gh-issue-140873.kfuc9B.rst
@@ -0,0 +1,2 @@
+Add support of non-:term:`descriptor` callables in
+:func:`functools.singledispatchmethod`.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to