Commit e5697535 authored by Łukasz Langa's avatar Łukasz Langa Committed by GitHub

bpo-32227: functools.singledispatch supports registering via type annotations (#4733)

parent 8874342c
...@@ -281,23 +281,34 @@ The :mod:`functools` module defines the following functions: ...@@ -281,23 +281,34 @@ The :mod:`functools` module defines the following functions:
... print(arg) ... print(arg)
To add overloaded implementations to the function, use the :func:`register` To add overloaded implementations to the function, use the :func:`register`
attribute of the generic function. It is a decorator, taking a type attribute of the generic function. It is a decorator. For functions
parameter and decorating a function implementing the operation for that annotated with types, the decorator will infer the type of the first
type:: argument automatically::
>>> @fun.register(int) >>> @fun.register
... def _(arg, verbose=False): ... def _(arg: int, verbose=False):
... if verbose: ... if verbose:
... print("Strength in numbers, eh?", end=" ") ... print("Strength in numbers, eh?", end=" ")
... print(arg) ... print(arg)
... ...
>>> @fun.register(list) >>> @fun.register
... def _(arg, verbose=False): ... def _(arg: list, verbose=False):
... if verbose: ... if verbose:
... print("Enumerate this:") ... print("Enumerate this:")
... for i, elem in enumerate(arg): ... for i, elem in enumerate(arg):
... print(i, elem) ... print(i, elem)
For code which doesn't use type annotations, the appropriate type
argument can be passed explicitly to the decorator itself::
>>> @fun.register(complex)
... def _(arg, verbose=False):
... if verbose:
... print("Better than complicated.", end=" ")
... print(arg.real, arg.imag)
...
To enable registering lambdas and pre-existing functions, the To enable registering lambdas and pre-existing functions, the
:func:`register` attribute can be used in a functional form:: :func:`register` attribute can be used in a functional form::
...@@ -368,6 +379,9 @@ The :mod:`functools` module defines the following functions: ...@@ -368,6 +379,9 @@ The :mod:`functools` module defines the following functions:
.. versionadded:: 3.4 .. versionadded:: 3.4
.. versionchanged:: 3.7
The :func:`register` attribute supports using type annotations.
.. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) .. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
......
...@@ -793,7 +793,23 @@ def singledispatch(func): ...@@ -793,7 +793,23 @@ def singledispatch(func):
""" """
nonlocal cache_token nonlocal cache_token
if func is None: if func is None:
if isinstance(cls, type):
return lambda f: register(cls, f) return lambda f: register(cls, f)
ann = getattr(cls, '__annotations__', {})
if not ann:
raise TypeError(
f"Invalid first argument to `register()`: {cls!r}. "
f"Use either `@register(some_class)` or plain `@register` "
f"on an annotated function."
)
func = cls
# only import typing if annotation parsing is necessary
from typing import get_type_hints
argname, cls = next(iter(get_type_hints(func).items()))
assert isinstance(cls, type), (
f"Invalid annotation for {argname!r}. {cls!r} is not a class."
)
registry[cls] = func registry[cls] = func
if cache_token is None and hasattr(cls, '__abstractmethods__'): if cache_token is None and hasattr(cls, '__abstractmethods__'):
cache_token = get_cache_token() cache_token = get_cache_token()
......
...@@ -10,6 +10,7 @@ import sys ...@@ -10,6 +10,7 @@ import sys
from test import support from test import support
import threading import threading
import time import time
import typing
import unittest import unittest
import unittest.mock import unittest.mock
from weakref import proxy from weakref import proxy
...@@ -2119,6 +2120,73 @@ class TestSingleDispatch(unittest.TestCase): ...@@ -2119,6 +2120,73 @@ class TestSingleDispatch(unittest.TestCase):
g._clear_cache() g._clear_cache()
self.assertEqual(len(td), 0) self.assertEqual(len(td), 0)
def test_annotations(self):
@functools.singledispatch
def i(arg):
return "base"
@i.register
def _(arg: collections.abc.Mapping):
return "mapping"
@i.register
def _(arg: "collections.abc.Sequence"):
return "sequence"
self.assertEqual(i(None), "base")
self.assertEqual(i({"a": 1}), "mapping")
self.assertEqual(i([1, 2, 3]), "sequence")
self.assertEqual(i((1, 2, 3)), "sequence")
self.assertEqual(i("str"), "sequence")
# Registering classes as callables doesn't work with annotations,
# you need to pass the type explicitly.
@i.register(str)
class _:
def __init__(self, arg):
self.arg = arg
def __eq__(self, other):
return self.arg == other
self.assertEqual(i("str"), "str")
def test_invalid_registrations(self):
msg_prefix = "Invalid first argument to `register()`: "
msg_suffix = (
". Use either `@register(some_class)` or plain `@register` on an "
"annotated function."
)
@functools.singledispatch
def i(arg):
return "base"
with self.assertRaises(TypeError) as exc:
@i.register(42)
def _(arg):
return "I annotated with a non-type"
self.assertTrue(str(exc.exception).startswith(msg_prefix + "42"))
self.assertTrue(str(exc.exception).endswith(msg_suffix))
with self.assertRaises(TypeError) as exc:
@i.register
def _(arg):
return "I forgot to annotate"
self.assertTrue(str(exc.exception).startswith(msg_prefix +
"<function TestSingleDispatch.test_invalid_registrations.<locals>._"
))
self.assertTrue(str(exc.exception).endswith(msg_suffix))
# FIXME: The following will only work after PEP 560 is implemented.
return
with self.assertRaises(TypeError) as exc:
@i.register
def _(arg: typing.Iterable[str]):
# At runtime, dispatching on generics is impossible.
# When registering implementations with singledispatch, avoid
# types from `typing`. Instead, annotate with regular types
# or ABCs.
return "I annotated with a generic collection"
self.assertTrue(str(exc.exception).startswith(msg_prefix +
"<function TestSingleDispatch.test_invalid_registrations.<locals>._"
))
self.assertTrue(str(exc.exception).endswith(msg_suffix))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
``functools.singledispatch`` now supports registering implementations using
type annotations.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment