Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e569753

Browse files
authored
bpo-32227: functools.singledispatch supports registering via type annotations (#4733)
1 parent 8874342 commit e569753

4 files changed

Lines changed: 108 additions & 8 deletions

File tree

Doc/library/functools.rst

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -281,23 +281,34 @@ The :mod:`functools` module defines the following functions:
281281
... print(arg)
282282

283283
To add overloaded implementations to the function, use the :func:`register`
284-
attribute of the generic function. It is a decorator, taking a type
285-
parameter and decorating a function implementing the operation for that
286-
type::
284+
attribute of the generic function. It is a decorator. For functions
285+
annotated with types, the decorator will infer the type of the first
286+
argument automatically::
287287

288-
>>> @fun.register(int)
289-
... def _(arg, verbose=False):
288+
>>> @fun.register
289+
... def _(arg: int, verbose=False):
290290
... if verbose:
291291
... print("Strength in numbers, eh?", end=" ")
292292
... print(arg)
293293
...
294-
>>> @fun.register(list)
295-
... def _(arg, verbose=False):
294+
>>> @fun.register
295+
... def _(arg: list, verbose=False):
296296
... if verbose:
297297
... print("Enumerate this:")
298298
... for i, elem in enumerate(arg):
299299
... print(i, elem)
300300

301+
For code which doesn't use type annotations, the appropriate type
302+
argument can be passed explicitly to the decorator itself::
303+
304+
>>> @fun.register(complex)
305+
... def _(arg, verbose=False):
306+
... if verbose:
307+
... print("Better than complicated.", end=" ")
308+
... print(arg.real, arg.imag)
309+
...
310+
311+
301312
To enable registering lambdas and pre-existing functions, the
302313
:func:`register` attribute can be used in a functional form::
303314

@@ -368,6 +379,9 @@ The :mod:`functools` module defines the following functions:
368379

369380
.. versionadded:: 3.4
370381

382+
.. versionchanged:: 3.7
383+
The :func:`register` attribute supports using type annotations.
384+
371385

372386
.. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
373387

Lib/functools.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,23 @@ def register(cls, func=None):
793793
"""
794794
nonlocal cache_token
795795
if func is None:
796-
return lambda f: register(cls, f)
796+
if isinstance(cls, type):
797+
return lambda f: register(cls, f)
798+
ann = getattr(cls, '__annotations__', {})
799+
if not ann:
800+
raise TypeError(
801+
f"Invalid first argument to `register()`: {cls!r}. "
802+
f"Use either `@register(some_class)` or plain `@register` "
803+
f"on an annotated function."
804+
)
805+
func = cls
806+
807+
# only import typing if annotation parsing is necessary
808+
from typing import get_type_hints
809+
argname, cls = next(iter(get_type_hints(func).items()))
810+
assert isinstance(cls, type), (
811+
f"Invalid annotation for {argname!r}. {cls!r} is not a class."
812+
)
797813
registry[cls] = func
798814
if cache_token is None and hasattr(cls, '__abstractmethods__'):
799815
cache_token = get_cache_token()

Lib/test/test_functools.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from test import support
1111
import threading
1212
import time
13+
import typing
1314
import unittest
1415
import unittest.mock
1516
from weakref import proxy
@@ -2119,6 +2120,73 @@ class X:
21192120
g._clear_cache()
21202121
self.assertEqual(len(td), 0)
21212122

2123+
def test_annotations(self):
2124+
@functools.singledispatch
2125+
def i(arg):
2126+
return "base"
2127+
@i.register
2128+
def _(arg: collections.abc.Mapping):
2129+
return "mapping"
2130+
@i.register
2131+
def _(arg: "collections.abc.Sequence"):
2132+
return "sequence"
2133+
self.assertEqual(i(None), "base")
2134+
self.assertEqual(i({"a": 1}), "mapping")
2135+
self.assertEqual(i([1, 2, 3]), "sequence")
2136+
self.assertEqual(i((1, 2, 3)), "sequence")
2137+
self.assertEqual(i("str"), "sequence")
2138+
2139+
# Registering classes as callables doesn't work with annotations,
2140+
# you need to pass the type explicitly.
2141+
@i.register(str)
2142+
class _:
2143+
def __init__(self, arg):
2144+
self.arg = arg
2145+
2146+
def __eq__(self, other):
2147+
return self.arg == other
2148+
self.assertEqual(i("str"), "str")
2149+
2150+
def test_invalid_registrations(self):
2151+
msg_prefix = "Invalid first argument to `register()`: "
2152+
msg_suffix = (
2153+
". Use either `@register(some_class)` or plain `@register` on an "
2154+
"annotated function."
2155+
)
2156+
@functools.singledispatch
2157+
def i(arg):
2158+
return "base"
2159+
with self.assertRaises(TypeError) as exc:
2160+
@i.register(42)
2161+
def _(arg):
2162+
return "I annotated with a non-type"
2163+
self.assertTrue(str(exc.exception).startswith(msg_prefix + "42"))
2164+
self.assertTrue(str(exc.exception).endswith(msg_suffix))
2165+
with self.assertRaises(TypeError) as exc:
2166+
@i.register
2167+
def _(arg):
2168+
return "I forgot to annotate"
2169+
self.assertTrue(str(exc.exception).startswith(msg_prefix +
2170+
"<function TestSingleDispatch.test_invalid_registrations.<locals>._"
2171+
))
2172+
self.assertTrue(str(exc.exception).endswith(msg_suffix))
2173+
2174+
# FIXME: The following will only work after PEP 560 is implemented.
2175+
return
2176+
2177+
with self.assertRaises(TypeError) as exc:
2178+
@i.register
2179+
def _(arg: typing.Iterable[str]):
2180+
# At runtime, dispatching on generics is impossible.
2181+
# When registering implementations with singledispatch, avoid
2182+
# types from `typing`. Instead, annotate with regular types
2183+
# or ABCs.
2184+
return "I annotated with a generic collection"
2185+
self.assertTrue(str(exc.exception).startswith(msg_prefix +
2186+
"<function TestSingleDispatch.test_invalid_registrations.<locals>._"
2187+
))
2188+
self.assertTrue(str(exc.exception).endswith(msg_suffix))
2189+
21222190

21232191
if __name__ == '__main__':
21242192
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
``functools.singledispatch`` now supports registering implementations using
2+
type annotations.

0 commit comments

Comments
 (0)