[Python-checkins] bpo-44801: Check arguments in substitution of ParamSpec in Callable (GH-27585)

ambv webhook-mailer at python.org
Wed Aug 4 14:07:09 EDT 2021


https://github.com/python/cpython/commit/3875a6954741065b136650db67ac533bc70a3eac
commit: 3875a6954741065b136650db67ac533bc70a3eac
branch: main
author: Serhiy Storchaka <storchaka at gmail.com>
committer: ambv <lukasz at langa.pl>
date: 2021-08-04T20:07:01+02:00
summary:

bpo-44801: Check arguments in substitution of ParamSpec in Callable (GH-27585)

files:
A Misc/NEWS.d/next/Library/2021-08-03-20-37-45.bpo-44801.i49Aug.rst
M Lib/_collections_abc.py
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py
index 33db9b259817c..87a9cd2d46de9 100644
--- a/Lib/_collections_abc.py
+++ b/Lib/_collections_abc.py
@@ -426,22 +426,16 @@ class _CallableGenericAlias(GenericAlias):
     __slots__ = ()
 
     def __new__(cls, origin, args):
-        return cls.__create_ga(origin, args)
-
-    @classmethod
-    def __create_ga(cls, origin, args):
-        if not isinstance(args, tuple) or len(args) != 2:
+        if not (isinstance(args, tuple) and len(args) == 2):
             raise TypeError(
                 "Callable must be used as Callable[[arg, ...], result].")
         t_args, t_result = args
-        if isinstance(t_args, (list, tuple)):
-            ga_args = tuple(t_args) + (t_result,)
-        # This relaxes what t_args can be on purpose to allow things like
-        # PEP 612 ParamSpec.  Responsibility for whether a user is using
-        # Callable[...] properly is deferred to static type checkers.
-        else:
-            ga_args = args
-        return super().__new__(cls, origin, ga_args)
+        if isinstance(t_args, list):
+            args = (*t_args, t_result)
+        elif not _is_param_expr(t_args):
+            raise TypeError(f"Expected a list of types, an ellipsis, "
+                            f"ParamSpec, or Concatenate. Got {t_args}")
+        return super().__new__(cls, origin, args)
 
     @property
     def __parameters__(self):
@@ -456,7 +450,7 @@ def __parameters__(self):
         return tuple(dict.fromkeys(params))
 
     def __repr__(self):
-        if _has_special_args(self.__args__):
+        if len(self.__args__) == 2 and _is_param_expr(self.__args__[0]):
             return super().__repr__()
         return (f'collections.abc.Callable'
                 f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], '
@@ -464,7 +458,7 @@ def __repr__(self):
 
     def __reduce__(self):
         args = self.__args__
-        if not _has_special_args(args):
+        if not (len(args) == 2 and _is_param_expr(args[0])):
             args = list(args[:-1]), args[-1]
         return _CallableGenericAlias, (Callable, args)
 
@@ -479,10 +473,11 @@ def __getitem__(self, item):
         param_len = len(self.__parameters__)
         if param_len == 0:
             raise TypeError(f'{self} is not a generic class')
-        if (param_len == 1
-                and isinstance(item, (tuple, list))
-                and len(item) > 1) or not isinstance(item, tuple):
+        if not isinstance(item, tuple):
             item = (item,)
+        if (param_len == 1 and _is_param_expr(self.__parameters__[0])
+                and item and not _is_param_expr(item[0])):
+            item = (list(item),)
         item_len = len(item)
         if item_len != param_len:
             raise TypeError(f'Too {"many" if item_len > param_len else "few"}'
@@ -492,7 +487,13 @@ def __getitem__(self, item):
         new_args = []
         for arg in self.__args__:
             if _is_typevarlike(arg):
-                arg = subst[arg]
+                if _is_param_expr(arg):
+                    arg = subst[arg]
+                    if not _is_param_expr(arg):
+                        raise TypeError(f"Expected a list of types, an ellipsis, "
+                                        f"ParamSpec, or Concatenate. Got {arg}")
+                else:
+                    arg = subst[arg]
             # Looks like a GenericAlias
             elif hasattr(arg, '__parameters__') and isinstance(arg.__parameters__, tuple):
                 subparams = arg.__parameters__
@@ -502,32 +503,31 @@ def __getitem__(self, item):
             new_args.append(arg)
 
         # args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
-        if not isinstance(new_args[0], (tuple, list)):
+        if not isinstance(new_args[0], list):
             t_result = new_args[-1]
             t_args = new_args[:-1]
             new_args = (t_args, t_result)
         return _CallableGenericAlias(Callable, tuple(new_args))
 
+
 def _is_typevarlike(arg):
     obj = type(arg)
     # looks like a TypeVar/ParamSpec
     return (obj.__module__ == 'typing'
             and obj.__name__ in {'ParamSpec', 'TypeVar'})
 
-def _has_special_args(args):
-    """Checks if args[0] matches either ``...``, ``ParamSpec`` or
+def _is_param_expr(obj):
+    """Checks if obj matches either a list of types, ``...``, ``ParamSpec`` or
     ``_ConcatenateGenericAlias`` from typing.py
     """
-    if len(args) != 2:
-        return False
-    obj = args[0]
     if obj is Ellipsis:
         return True
+    if isinstance(obj, list):
+        return True
     obj = type(obj)
     names = ('ParamSpec', '_ConcatenateGenericAlias')
     return obj.__module__ == 'typing' and any(obj.__name__ == name for name in names)
 
-
 def _type_repr(obj):
     """Return the repr() of an object, special-casing types (internal helper).
 
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 1a172a06705de..439d963af5bd4 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -581,17 +581,33 @@ def test_paramspec(self):
         Callable = self.Callable
         fullname = f"{Callable.__module__}.Callable"
         P = ParamSpec('P')
+        P2 = ParamSpec('P2')
         C1 = Callable[P, T]
         # substitution
-        self.assertEqual(C1[int, str], Callable[[int], str])
+        self.assertEqual(C1[[int], str], Callable[[int], str])
         self.assertEqual(C1[[int, str], str], Callable[[int, str], str])
+        self.assertEqual(C1[[], str], Callable[[], str])
+        self.assertEqual(C1[..., str], Callable[..., str])
+        self.assertEqual(C1[P2, str], Callable[P2, str])
+        self.assertEqual(C1[Concatenate[int, P2], str],
+                         Callable[Concatenate[int, P2], str])
         self.assertEqual(repr(C1), f"{fullname}[~P, ~T]")
-        self.assertEqual(repr(C1[int, str]), f"{fullname}[[int], str]")
+        self.assertEqual(repr(C1[[int, str], str]), f"{fullname}[[int, str], str]")
+        with self.assertRaises(TypeError):
+            C1[int, str]
 
         C2 = Callable[P, int]
+        self.assertEqual(C2[[int]], Callable[[int], int])
+        self.assertEqual(C2[[int, str]], Callable[[int, str], int])
+        self.assertEqual(C2[[]], Callable[[], int])
+        self.assertEqual(C2[...], Callable[..., int])
+        self.assertEqual(C2[P2], Callable[P2, int])
+        self.assertEqual(C2[Concatenate[int, P2]],
+                         Callable[Concatenate[int, P2], int])
         # special case in PEP 612 where
         # X[int, str, float] == X[[int, str, float]]
-        self.assertEqual(C2[int, str, float], C2[[int, str, float]])
+        self.assertEqual(C2[int], Callable[[int], int])
+        self.assertEqual(C2[int, str], Callable[[int, str], int])
         self.assertEqual(repr(C2), f"{fullname}[~P, int]")
         self.assertEqual(repr(C2[int, str]), f"{fullname}[[int, str], int]")
 
@@ -4656,6 +4672,29 @@ class Z(Generic[P]):
         self.assertEqual(G5.__parameters__, G6.__parameters__)
         self.assertEqual(G5, G6)
 
+        G7 = Z[int]
+        self.assertEqual(G7.__args__, ((int,),))
+        self.assertEqual(G7.__parameters__, ())
+
+        with self.assertRaisesRegex(TypeError, "many arguments for"):
+            Z[[int, str], bool]
+        with self.assertRaisesRegex(TypeError, "many arguments for"):
+            Z[P_2, bool]
+
+    def test_multiple_paramspecs_in_user_generics(self):
+        P = ParamSpec("P")
+        P2 = ParamSpec("P2")
+
+        class X(Generic[P, P2]):
+            f: Callable[P, int]
+            g: Callable[P2, str]
+
+        G1 = X[[int, str], [bytes]]
+        G2 = X[[int], [str, bytes]]
+        self.assertNotEqual(G1, G2)
+        self.assertEqual(G1.__args__, ((int, str), (bytes,)))
+        self.assertEqual(G2.__args__, ((int,), (str, bytes)))
+
     def test_no_paramspec_in__parameters__(self):
         # ParamSpec should not be found in __parameters__
         # of generics. Usages outside Callable, Concatenate
diff --git a/Lib/typing.py b/Lib/typing.py
index 7a12d31f2b35e..9c595ae541aa1 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -182,6 +182,11 @@ def _type_check(arg, msg, is_argument=True, module=None):
     return arg
 
 
+def _is_param_expr(arg):
+    return arg is ... or isinstance(arg,
+            (tuple, list, ParamSpec, _ConcatenateGenericAlias))
+
+
 def _type_repr(obj):
     """Return the repr() of an object, special-casing types (internal helper).
 
@@ -236,7 +241,9 @@ def _prepare_paramspec_params(cls, params):
     variables (internal helper).
     """
     # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
-    if len(cls.__parameters__) == 1 and len(params) > 1:
+    if (len(cls.__parameters__) == 1
+            and params and not _is_param_expr(params[0])):
+        assert isinstance(cls.__parameters__[0], ParamSpec)
         return (params,)
     else:
         _check_generic(cls, params, len(cls.__parameters__))
@@ -1033,7 +1040,13 @@ def __getitem__(self, params):
         new_args = []
         for arg in self.__args__:
             if isinstance(arg, self._typevar_types):
-                arg = subst[arg]
+                if isinstance(arg, ParamSpec):
+                    arg = subst[arg]
+                    if not _is_param_expr(arg):
+                        raise TypeError(f"Expected a list of types, an ellipsis, "
+                                        f"ParamSpec, or Concatenate. Got {arg}")
+                else:
+                    arg = subst[arg]
             elif isinstance(arg, (_GenericAlias, GenericAlias, types.UnionType)):
                 subparams = arg.__parameters__
                 if subparams:
@@ -1131,8 +1144,7 @@ class _CallableGenericAlias(_GenericAlias, _root=True):
     def __repr__(self):
         assert self._name == 'Callable'
         args = self.__args__
-        if len(args) == 2 and (args[0] is Ellipsis
-                               or isinstance(args[0], (ParamSpec, _ConcatenateGenericAlias))):
+        if len(args) == 2 and _is_param_expr(args[0]):
             return super().__repr__()
         return (f'typing.Callable'
                 f'[[{", ".join([_type_repr(a) for a in args[:-1]])}], '
@@ -1140,8 +1152,7 @@ def __repr__(self):
 
     def __reduce__(self):
         args = self.__args__
-        if not (len(args) == 2 and (args[0] is Ellipsis
-                                    or isinstance(args[0], (ParamSpec, _ConcatenateGenericAlias)))):
+        if not (len(args) == 2 and _is_param_expr(args[0])):
             args = list(args[:-1]), args[-1]
         return operator.getitem, (Callable, args)
 
@@ -1864,8 +1875,7 @@ def get_args(tp):
     if isinstance(tp, (_GenericAlias, GenericAlias)):
         res = tp.__args__
         if (tp.__origin__ is collections.abc.Callable
-                and not (res[0] is Ellipsis
-                         or isinstance(res[0], (ParamSpec, _ConcatenateGenericAlias)))):
+                and not (len(res) == 2 and _is_param_expr(res[0]))):
             res = (list(res[:-1]), res[-1])
         return res
     if isinstance(tp, types.UnionType):
diff --git a/Misc/NEWS.d/next/Library/2021-08-03-20-37-45.bpo-44801.i49Aug.rst b/Misc/NEWS.d/next/Library/2021-08-03-20-37-45.bpo-44801.i49Aug.rst
new file mode 100644
index 0000000000000..05e372a5fabb0
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-08-03-20-37-45.bpo-44801.i49Aug.rst
@@ -0,0 +1,3 @@
+Ensure that the :class:`~typing.ParamSpec` variable in Callable
+can only be substituted with a parameters expression (a list of types,
+an ellipsis, ParamSpec or Concatenate).



More information about the Python-checkins mailing list