bpo-32380: Create functools.singledispatchmethod (#6306)

This commit is contained in:
Ethan Smith 2018-05-26 16:38:33 -04:00 committed by Łukasz Langa
parent 09c4a7dee2
commit c651275afe
5 changed files with 205 additions and 1 deletions

View file

@ -383,6 +383,52 @@ The :mod:`functools` module defines the following functions:
The :func:`register` attribute supports using type annotations. The :func:`register` attribute supports using type annotations.
.. class:: singledispatchmethod(func)
Transform a method into a :term:`single-dispatch <single
dispatch>` :term:`generic function`.
To define a generic method, decorate it with the ``@singledispatchmethod``
decorator. Note that the dispatch happens on the type of the first non-self
or non-cls argument, create your function accordingly::
class Negator:
@singledispatchmethod
def neg(self, arg):
raise NotImplementedError("Cannot negate a")
@neg.register
def _(self, arg: int):
return -arg
@neg.register
def _(self, arg: bool):
return not arg
``@singledispatchmethod`` supports nesting with other decorators such as
``@classmethod``. Note that to allow for ``dispatcher.register``,
``singledispatchmethod`` must be the *outer most* decorator. Here is the
``Negator`` class with the ``neg`` methods being class bound::
class Negator:
@singledispatchmethod
@classmethod
def neg(cls, arg):
raise NotImplementedError("Cannot negate a")
@neg.register
@classmethod
def _(cls, arg: int):
return -arg
@neg.register
@classmethod
def _(cls, arg: bool):
return not arg
The same pattern can be used for other similar decorators: ``staticmethod``,
``abstractmethod``, and others.
.. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) .. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
Update a *wrapper* function to look like the *wrapped* function. The optional Update a *wrapper* function to look like the *wrapped* function. The optional

View file

@ -11,7 +11,7 @@
__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES', __all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial', 'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial',
'partialmethod', 'singledispatch'] 'partialmethod', 'singledispatch', 'singledispatchmethod']
try: try:
from _functools import reduce from _functools import reduce
@ -826,3 +826,40 @@ def singledispatch(func):
wrapper._clear_cache = dispatch_cache.clear wrapper._clear_cache = dispatch_cache.clear
update_wrapper(wrapper, func) update_wrapper(wrapper, func)
return wrapper return wrapper
# Descriptor version
class singledispatchmethod:
"""Single-dispatch generic method descriptor.
Supports wrapping existing descriptors and handles non-descriptor
callables as instance methods.
"""
def __init__(self, func):
if not callable(func) and not hasattr(func, "__get__"):
raise TypeError(f"{func!r} is not callable or a descriptor")
self.dispatcher = singledispatch(func)
self.func = func
def register(self, cls, method=None):
"""generic_method.register(cls, func) -> func
Registers a new implementation for the given *cls* on a *generic_method*.
"""
return self.dispatcher.register(cls, func=method)
def __get__(self, obj, cls):
def _method(*args, **kwargs):
method = self.dispatcher.dispatch(args[0].__class__)
return method.__get__(obj, cls)(*args, **kwargs)
_method.__isabstractmethod__ = self.__isabstractmethod__
_method.register = self.register
update_wrapper(_method, self.func)
return _method
@property
def __isabstractmethod__(self):
return getattr(self.func, '__isabstractmethod__', False)

View file

@ -2147,6 +2147,124 @@ class TestSingleDispatch(unittest.TestCase):
return self.arg == other return self.arg == other
self.assertEqual(i("str"), "str") self.assertEqual(i("str"), "str")
def test_method_register(self):
class A:
@functools.singledispatchmethod
def t(self, arg):
self.arg = "base"
@t.register(int)
def _(self, arg):
self.arg = "int"
@t.register(str)
def _(self, arg):
self.arg = "str"
a = A()
a.t(0)
self.assertEqual(a.arg, "int")
aa = A()
self.assertFalse(hasattr(aa, 'arg'))
a.t('')
self.assertEqual(a.arg, "str")
aa = A()
self.assertFalse(hasattr(aa, 'arg'))
a.t(0.0)
self.assertEqual(a.arg, "base")
aa = A()
self.assertFalse(hasattr(aa, 'arg'))
def test_staticmethod_register(self):
class A:
@functools.singledispatchmethod
@staticmethod
def t(arg):
return arg
@t.register(int)
@staticmethod
def _(arg):
return isinstance(arg, int)
@t.register(str)
@staticmethod
def _(arg):
return isinstance(arg, str)
a = A()
self.assertTrue(A.t(0))
self.assertTrue(A.t(''))
self.assertEqual(A.t(0.0), 0.0)
def test_classmethod_register(self):
class A:
def __init__(self, arg):
self.arg = arg
@functools.singledispatchmethod
@classmethod
def t(cls, arg):
return cls("base")
@t.register(int)
@classmethod
def _(cls, arg):
return cls("int")
@t.register(str)
@classmethod
def _(cls, arg):
return cls("str")
self.assertEqual(A.t(0).arg, "int")
self.assertEqual(A.t('').arg, "str")
self.assertEqual(A.t(0.0).arg, "base")
def test_callable_register(self):
class A:
def __init__(self, arg):
self.arg = arg
@functools.singledispatchmethod
@classmethod
def t(cls, arg):
return cls("base")
@A.t.register(int)
@classmethod
def _(cls, arg):
return cls("int")
@A.t.register(str)
@classmethod
def _(cls, arg):
return cls("str")
self.assertEqual(A.t(0).arg, "int")
self.assertEqual(A.t('').arg, "str")
self.assertEqual(A.t(0.0).arg, "base")
def test_abstractmethod_register(self):
class Abstract(abc.ABCMeta):
@functools.singledispatchmethod
@abc.abstractmethod
def add(self, x, y):
pass
self.assertTrue(Abstract.add.__isabstractmethod__)
def test_type_ann_register(self):
class A:
@functools.singledispatchmethod
def t(self, arg):
return "base"
@t.register
def _(self, arg: int):
return "int"
@t.register
def _(self, arg: str):
return "str"
a = A()
self.assertEqual(a.t(0), "int")
self.assertEqual(a.t(''), "str")
self.assertEqual(a.t(0.0), "base")
def test_invalid_registrations(self): def test_invalid_registrations(self):
msg_prefix = "Invalid first argument to `register()`: " msg_prefix = "Invalid first argument to `register()`: "
msg_suffix = ( msg_suffix = (

View file

@ -1510,6 +1510,7 @@ Václav Šmilauer
Allen W. Smith Allen W. Smith
Christopher Smith Christopher Smith
Eric V. Smith Eric V. Smith
Ethan H. Smith
Gregory P. Smith Gregory P. Smith
Mark Smith Mark Smith
Nathaniel J. Smith Nathaniel J. Smith

View file

@ -0,0 +1,2 @@
Create functools.singledispatchmethod to support generic single dispatch on
descriptors and methods.