Skip to content

Commit c651275

Browse files
emmatypingambv
authored andcommitted
bpo-32380: Create functools.singledispatchmethod (#6306)
1 parent 09c4a7d commit c651275

File tree

5 files changed

+205
-1
lines changed

5 files changed

+205
-1
lines changed

Doc/library/functools.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,52 @@ The :mod:`functools` module defines the following functions:
383383
The :func:`register` attribute supports using type annotations.
384384

385385

386+
.. class:: singledispatchmethod(func)
387+
388+
Transform a method into a :term:`single-dispatch <single
389+
dispatch>` :term:`generic function`.
390+
391+
To define a generic method, decorate it with the ``@singledispatchmethod``
392+
decorator. Note that the dispatch happens on the type of the first non-self
393+
or non-cls argument, create your function accordingly::
394+
395+
class Negator:
396+
@singledispatchmethod
397+
def neg(self, arg):
398+
raise NotImplementedError("Cannot negate a")
399+
400+
@neg.register
401+
def _(self, arg: int):
402+
return -arg
403+
404+
@neg.register
405+
def _(self, arg: bool):
406+
return not arg
407+
408+
``@singledispatchmethod`` supports nesting with other decorators such as
409+
``@classmethod``. Note that to allow for ``dispatcher.register``,
410+
``singledispatchmethod`` must be the *outer most* decorator. Here is the
411+
``Negator`` class with the ``neg`` methods being class bound::
412+
413+
class Negator:
414+
@singledispatchmethod
415+
@classmethod
416+
def neg(cls, arg):
417+
raise NotImplementedError("Cannot negate a")
418+
419+
@neg.register
420+
@classmethod
421+
def _(cls, arg: int):
422+
return -arg
423+
424+
@neg.register
425+
@classmethod
426+
def _(cls, arg: bool):
427+
return not arg
428+
429+
The same pattern can be used for other similar decorators: ``staticmethod``,
430+
``abstractmethod``, and others.
431+
386432
.. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
387433

388434
Update a *wrapper* function to look like the *wrapped* function. The optional

Lib/functools.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
1313
'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial',
14-
'partialmethod', 'singledispatch']
14+
'partialmethod', 'singledispatch', 'singledispatchmethod']
1515

1616
try:
1717
from _functools import reduce
@@ -826,3 +826,40 @@ def wrapper(*args, **kw):
826826
wrapper._clear_cache = dispatch_cache.clear
827827
update_wrapper(wrapper, func)
828828
return wrapper
829+
830+
831+
# Descriptor version
832+
class singledispatchmethod:
833+
"""Single-dispatch generic method descriptor.
834+
835+
Supports wrapping existing descriptors and handles non-descriptor
836+
callables as instance methods.
837+
"""
838+
839+
def __init__(self, func):
840+
if not callable(func) and not hasattr(func, "__get__"):
841+
raise TypeError(f"{func!r} is not callable or a descriptor")
842+
843+
self.dispatcher = singledispatch(func)
844+
self.func = func
845+
846+
def register(self, cls, method=None):
847+
"""generic_method.register(cls, func) -> func
848+
849+
Registers a new implementation for the given *cls* on a *generic_method*.
850+
"""
851+
return self.dispatcher.register(cls, func=method)
852+
853+
def __get__(self, obj, cls):
854+
def _method(*args, **kwargs):
855+
method = self.dispatcher.dispatch(args[0].__class__)
856+
return method.__get__(obj, cls)(*args, **kwargs)
857+
858+
_method.__isabstractmethod__ = self.__isabstractmethod__
859+
_method.register = self.register
860+
update_wrapper(_method, self.func)
861+
return _method
862+
863+
@property
864+
def __isabstractmethod__(self):
865+
return getattr(self.func, '__isabstractmethod__', False)

Lib/test/test_functools.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2147,6 +2147,124 @@ def __eq__(self, other):
21472147
return self.arg == other
21482148
self.assertEqual(i("str"), "str")
21492149

2150+
def test_method_register(self):
2151+
class A:
2152+
@functools.singledispatchmethod
2153+
def t(self, arg):
2154+
self.arg = "base"
2155+
@t.register(int)
2156+
def _(self, arg):
2157+
self.arg = "int"
2158+
@t.register(str)
2159+
def _(self, arg):
2160+
self.arg = "str"
2161+
a = A()
2162+
2163+
a.t(0)
2164+
self.assertEqual(a.arg, "int")
2165+
aa = A()
2166+
self.assertFalse(hasattr(aa, 'arg'))
2167+
a.t('')
2168+
self.assertEqual(a.arg, "str")
2169+
aa = A()
2170+
self.assertFalse(hasattr(aa, 'arg'))
2171+
a.t(0.0)
2172+
self.assertEqual(a.arg, "base")
2173+
aa = A()
2174+
self.assertFalse(hasattr(aa, 'arg'))
2175+
2176+
def test_staticmethod_register(self):
2177+
class A:
2178+
@functools.singledispatchmethod
2179+
@staticmethod
2180+
def t(arg):
2181+
return arg
2182+
@t.register(int)
2183+
@staticmethod
2184+
def _(arg):
2185+
return isinstance(arg, int)
2186+
@t.register(str)
2187+
@staticmethod
2188+
def _(arg):
2189+
return isinstance(arg, str)
2190+
a = A()
2191+
2192+
self.assertTrue(A.t(0))
2193+
self.assertTrue(A.t(''))
2194+
self.assertEqual(A.t(0.0), 0.0)
2195+
2196+
def test_classmethod_register(self):
2197+
class A:
2198+
def __init__(self, arg):
2199+
self.arg = arg
2200+
2201+
@functools.singledispatchmethod
2202+
@classmethod
2203+
def t(cls, arg):
2204+
return cls("base")
2205+
@t.register(int)
2206+
@classmethod
2207+
def _(cls, arg):
2208+
return cls("int")
2209+
@t.register(str)
2210+
@classmethod
2211+
def _(cls, arg):
2212+
return cls("str")
2213+
2214+
self.assertEqual(A.t(0).arg, "int")
2215+
self.assertEqual(A.t('').arg, "str")
2216+
self.assertEqual(A.t(0.0).arg, "base")
2217+
2218+
def test_callable_register(self):
2219+
class A:
2220+
def __init__(self, arg):
2221+
self.arg = arg
2222+
2223+
@functools.singledispatchmethod
2224+
@classmethod
2225+
def t(cls, arg):
2226+
return cls("base")
2227+
2228+
@A.t.register(int)
2229+
@classmethod
2230+
def _(cls, arg):
2231+
return cls("int")
2232+
@A.t.register(str)
2233+
@classmethod
2234+
def _(cls, arg):
2235+
return cls("str")
2236+
2237+
self.assertEqual(A.t(0).arg, "int")
2238+
self.assertEqual(A.t('').arg, "str")
2239+
self.assertEqual(A.t(0.0).arg, "base")
2240+
2241+
def test_abstractmethod_register(self):
2242+
class Abstract(abc.ABCMeta):
2243+
2244+
@functools.singledispatchmethod
2245+
@abc.abstractmethod
2246+
def add(self, x, y):
2247+
pass
2248+
2249+
self.assertTrue(Abstract.add.__isabstractmethod__)
2250+
2251+
def test_type_ann_register(self):
2252+
class A:
2253+
@functools.singledispatchmethod
2254+
def t(self, arg):
2255+
return "base"
2256+
@t.register
2257+
def _(self, arg: int):
2258+
return "int"
2259+
@t.register
2260+
def _(self, arg: str):
2261+
return "str"
2262+
a = A()
2263+
2264+
self.assertEqual(a.t(0), "int")
2265+
self.assertEqual(a.t(''), "str")
2266+
self.assertEqual(a.t(0.0), "base")
2267+
21502268
def test_invalid_registrations(self):
21512269
msg_prefix = "Invalid first argument to `register()`: "
21522270
msg_suffix = (

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,7 @@ Václav Šmilauer
15101510
Allen W. Smith
15111511
Christopher Smith
15121512
Eric V. Smith
1513+
Ethan H. Smith
15131514
Gregory P. Smith
15141515
Mark Smith
15151516
Nathaniel J. Smith
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Create functools.singledispatchmethod to support generic single dispatch on
2+
descriptors and methods.

0 commit comments

Comments
 (0)