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
5 changes: 4 additions & 1 deletion Doc/library/functools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down Expand Up @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,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
-------

Expand Down
5 changes: 4 additions & 1 deletion Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 34 additions & 1 deletion Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2785,7 +2785,7 @@ class Slot:
@functools.singledispatchmethod
@classmethod
def go(cls, item, arg):
pass
return item - arg

@go.register
@classmethod
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add support of non-:term:`descriptor` callables in
:func:`functools.singledispatchmethod`.
Loading