mirror of
https://github.com/python/cpython.git
synced 2025-10-15 11:22:18 +00:00
bpo-32380: Create functools.singledispatchmethod (#6306)
This commit is contained in:
parent
09c4a7dee2
commit
c651275afe
5 changed files with 205 additions and 1 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Create functools.singledispatchmethod to support generic single dispatch on
|
||||||
|
descriptors and methods.
|
Loading…
Add table
Add a link
Reference in a new issue