Skip to content

Commit 03c7449

Browse files
authoredDec 25, 2021
[3.10] bpo-46032: Check types in singledispatch's register() at declaration time (GH-30050) (GH-30254)
The registry() method of functools.singledispatch() functions checks now the first argument or the first parameter annotation and raises a TypeError if it is not supported. Previously unsupported "types" were ignored (e.g. typing.List[int]) or caused an error at calling time (e.g. list[int]). (cherry picked from commit 078abb6)
1 parent a9e0b2b commit 03c7449

File tree

3 files changed

+87
-3
lines changed

3 files changed

+87
-3
lines changed
 

‎Lib/functools.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,7 @@ def _compose_mro(cls, types):
740740
# Remove entries which are already present in the __mro__ or unrelated.
741741
def is_related(typ):
742742
return (typ not in bases and hasattr(typ, '__mro__')
743+
and not isinstance(typ, GenericAlias)
743744
and issubclass(cls, typ))
744745
types = [n for n in types if is_related(n)]
745746
# Remove entries which are strict bases of other entries (they will end up
@@ -837,16 +838,25 @@ def dispatch(cls):
837838
dispatch_cache[cls] = impl
838839
return impl
839840

841+
def _is_valid_dispatch_type(cls):
842+
return isinstance(cls, type) and not isinstance(cls, GenericAlias)
843+
840844
def register(cls, func=None):
841845
"""generic_func.register(cls, func) -> func
842846
843847
Registers a new implementation for the given *cls* on a *generic_func*.
844848
845849
"""
846850
nonlocal cache_token
847-
if func is None:
848-
if isinstance(cls, type):
851+
if _is_valid_dispatch_type(cls):
852+
if func is None:
849853
return lambda f: register(cls, f)
854+
else:
855+
if func is not None:
856+
raise TypeError(
857+
f"Invalid first argument to `register()`. "
858+
f"{cls!r} is not a class."
859+
)
850860
ann = getattr(cls, '__annotations__', {})
851861
if not ann:
852862
raise TypeError(
@@ -859,11 +869,12 @@ def register(cls, func=None):
859869
# only import typing if annotation parsing is necessary
860870
from typing import get_type_hints
861871
argname, cls = next(iter(get_type_hints(func).items()))
862-
if not isinstance(cls, type):
872+
if not _is_valid_dispatch_type(cls):
863873
raise TypeError(
864874
f"Invalid annotation for {argname!r}. "
865875
f"{cls!r} is not a class."
866876
)
877+
867878
registry[cls] = func
868879
if cache_token is None and hasattr(cls, '__abstractmethods__'):
869880
cache_token = get_cache_token()

‎Lib/test/test_functools.py

+68
Original file line numberDiff line numberDiff line change
@@ -2665,6 +2665,74 @@ def f(*args):
26652665
with self.assertRaisesRegex(TypeError, msg):
26662666
f()
26672667

2668+
def test_register_genericalias(self):
2669+
@functools.singledispatch
2670+
def f(arg):
2671+
return "default"
2672+
2673+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2674+
f.register(list[int], lambda arg: "types.GenericAlias")
2675+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2676+
f.register(typing.List[int], lambda arg: "typing.GenericAlias")
2677+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2678+
f.register(list[int] | str, lambda arg: "types.UnionTypes(types.GenericAlias)")
2679+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2680+
f.register(typing.List[float] | bytes, lambda arg: "typing.Union[typing.GenericAlias]")
2681+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2682+
f.register(typing.Any, lambda arg: "typing.Any")
2683+
2684+
self.assertEqual(f([1]), "default")
2685+
self.assertEqual(f([1.0]), "default")
2686+
self.assertEqual(f(""), "default")
2687+
self.assertEqual(f(b""), "default")
2688+
2689+
def test_register_genericalias_decorator(self):
2690+
@functools.singledispatch
2691+
def f(arg):
2692+
return "default"
2693+
2694+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2695+
f.register(list[int])
2696+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2697+
f.register(typing.List[int])
2698+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2699+
f.register(list[int] | str)
2700+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2701+
f.register(typing.List[int] | str)
2702+
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2703+
f.register(typing.Any)
2704+
2705+
def test_register_genericalias_annotation(self):
2706+
@functools.singledispatch
2707+
def f(arg):
2708+
return "default"
2709+
2710+
with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
2711+
@f.register
2712+
def _(arg: list[int]):
2713+
return "types.GenericAlias"
2714+
with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
2715+
@f.register
2716+
def _(arg: typing.List[float]):
2717+
return "typing.GenericAlias"
2718+
with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
2719+
@f.register
2720+
def _(arg: list[int] | str):
2721+
return "types.UnionType(types.GenericAlias)"
2722+
with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
2723+
@f.register
2724+
def _(arg: typing.List[float] | bytes):
2725+
return "typing.Union[typing.GenericAlias]"
2726+
with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
2727+
@f.register
2728+
def _(arg: typing.Any):
2729+
return "typing.Any"
2730+
2731+
self.assertEqual(f([1]), "default")
2732+
self.assertEqual(f([1.0]), "default")
2733+
self.assertEqual(f(""), "default")
2734+
self.assertEqual(f(b""), "default")
2735+
26682736

26692737
class CachedCostItem:
26702738
_cost = 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The ``registry()`` method of :func:`functools.singledispatch` functions
2+
checks now the first argument or the first parameter annotation and raises a
3+
TypeError if it is not supported. Previously unsupported "types" were
4+
ignored (e.g. ``typing.List[int]``) or caused an error at calling time (e.g.
5+
``list[int]``).

0 commit comments

Comments
 (0)
Please sign in to comment.