[Python-checkins] [3.9] bpo-46032: Check types in singledispatch's register() at declaration time (GH-30050) (GH-30254) (GH-30255)
serhiy-storchaka
webhook-mailer at python.org
Sun Dec 26 07:23:32 EST 2021
https://github.com/python/cpython/commit/25a12aac4de819745dfc64664ba183a5784b5a81
commit: 25a12aac4de819745dfc64664ba183a5784b5a81
branch: 3.9
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: serhiy-storchaka <storchaka at gmail.com>
date: 2021-12-26T14:23:23+02:00
summary:
[3.9] bpo-46032: Check types in singledispatch's register() at declaration time (GH-30050) (GH-30254) (GH-30255)
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 078abb676cf759b1e960f78390b6e80f256f0255)
(cherry picked from commit 03c7449fbc7c57f5e0365f234a0b65c1dde763f2)
Co-authored-by: Serhiy Storchaka <storchaka at gmail.com>
files:
A Misc/NEWS.d/next/Library/2021-12-11-15-45-07.bpo-46032.HmciLT.rst
M Lib/functools.py
M Lib/test/test_functools.py
diff --git a/Lib/functools.py b/Lib/functools.py
index 1a290e1c02461..77e5035ebcc9d 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -739,6 +739,7 @@ def _compose_mro(cls, types):
# Remove entries which are already present in the __mro__ or unrelated.
def is_related(typ):
return (typ not in bases and hasattr(typ, '__mro__')
+ and not isinstance(typ, GenericAlias)
and issubclass(cls, typ))
types = [n for n in types if is_related(n)]
# Remove entries which are strict bases of other entries (they will end up
@@ -836,6 +837,9 @@ def dispatch(cls):
dispatch_cache[cls] = impl
return impl
+ def _is_valid_dispatch_type(cls):
+ return isinstance(cls, type) and not isinstance(cls, GenericAlias)
+
def register(cls, func=None):
"""generic_func.register(cls, func) -> func
@@ -843,9 +847,15 @@ def register(cls, func=None):
"""
nonlocal cache_token
- if func is None:
- if isinstance(cls, type):
+ if _is_valid_dispatch_type(cls):
+ if func is None:
return lambda f: register(cls, f)
+ else:
+ if func is not None:
+ raise TypeError(
+ f"Invalid first argument to `register()`. "
+ f"{cls!r} is not a class."
+ )
ann = getattr(cls, '__annotations__', {})
if not ann:
raise TypeError(
@@ -858,11 +868,12 @@ def register(cls, func=None):
# only import typing if annotation parsing is necessary
from typing import get_type_hints
argname, cls = next(iter(get_type_hints(func).items()))
- if not isinstance(cls, type):
+ if not _is_valid_dispatch_type(cls):
raise TypeError(
f"Invalid annotation for {argname!r}. "
f"{cls!r} is not a class."
)
+
registry[cls] = func
if cache_token is None and hasattr(cls, '__abstractmethods__'):
cache_token = get_cache_token()
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index b2a7e5a88b422..d7dec3f3d2756 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -2560,7 +2560,7 @@ def decorated_classmethod(cls, arg: int) -> str:
self.assertEqual(without_single_dispatch_foo, single_dispatch_foo)
self.assertEqual(single_dispatch_foo, '5')
-
+
self.assertEqual(
WithoutSingleDispatch.decorated_classmethod(5),
WithSingleDispatch.decorated_classmethod(5)
@@ -2655,6 +2655,74 @@ def f(*args):
with self.assertRaisesRegex(TypeError, msg):
f()
+ def test_register_genericalias(self):
+ @functools.singledispatch
+ def f(arg):
+ return "default"
+
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(list[int], lambda arg: "types.GenericAlias")
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(typing.List[int], lambda arg: "typing.GenericAlias")
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(typing.Union[list[int], str], lambda arg: "typing.Union[types.GenericAlias]")
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(typing.Union[typing.List[float], bytes], lambda arg: "typing.Union[typing.GenericAlias]")
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(typing.Any, lambda arg: "typing.Any")
+
+ self.assertEqual(f([1]), "default")
+ self.assertEqual(f([1.0]), "default")
+ self.assertEqual(f(""), "default")
+ self.assertEqual(f(b""), "default")
+
+ def test_register_genericalias_decorator(self):
+ @functools.singledispatch
+ def f(arg):
+ return "default"
+
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(list[int])
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(typing.List[int])
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(typing.Union[list[int], str])
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(typing.Union[typing.List[int], str])
+ with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
+ f.register(typing.Any)
+
+ def test_register_genericalias_annotation(self):
+ @functools.singledispatch
+ def f(arg):
+ return "default"
+
+ with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
+ @f.register
+ def _(arg: list[int]):
+ return "types.GenericAlias"
+ with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
+ @f.register
+ def _(arg: typing.List[float]):
+ return "typing.GenericAlias"
+ with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
+ @f.register
+ def _(arg: typing.Union[list[int], str]):
+ return "types.UnionType(types.GenericAlias)"
+ with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
+ @f.register
+ def _(arg: typing.Union[typing.List[float], bytes]):
+ return "typing.Union[typing.GenericAlias]"
+ with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
+ @f.register
+ def _(arg: typing.Any):
+ return "typing.Any"
+
+ self.assertEqual(f([1]), "default")
+ self.assertEqual(f([1.0]), "default")
+ self.assertEqual(f(""), "default")
+ self.assertEqual(f(b""), "default")
+
class CachedCostItem:
_cost = 1
diff --git a/Misc/NEWS.d/next/Library/2021-12-11-15-45-07.bpo-46032.HmciLT.rst b/Misc/NEWS.d/next/Library/2021-12-11-15-45-07.bpo-46032.HmciLT.rst
new file mode 100644
index 0000000000000..97a553d7ba29f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-12-11-15-45-07.bpo-46032.HmciLT.rst
@@ -0,0 +1,5 @@
+The ``registry()`` method of :func:`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]``).
More information about the Python-checkins
mailing list