[Python-checkins] bpo-44098: Drop ParamSpec from most ``__parameters__`` in typing generics (GH-26013) (#26091)

gvanrossum webhook-mailer at python.org
Thu May 13 13:19:32 EDT 2021


https://github.com/python/cpython/commit/c55ff1b352f8b82184f80d9dea220e832691acfc
commit: c55ff1b352f8b82184f80d9dea220e832691acfc
branch: 3.10
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: gvanrossum <gvanrossum at gmail.com>
date: 2021-05-13T10:19:24-07:00
summary:

bpo-44098: Drop ParamSpec from most ``__parameters__`` in typing generics (GH-26013) (#26091)

Added two new attributes to ``_GenericAlias``:
* ``_typevar_types``, a single type or tuple of types indicating what types are treated as a ``TypeVar``. Used for ``isinstance`` checks.
* ``_paramspec_tvars ``, a boolean flag which guards special behavior for dealing with ``ParamSpec``. Setting it to ``True`` means this  class deals with ``ParamSpec``.

Automerge-Triggered-By: GH:gvanrossum
(cherry picked from commit b2f3f8e3d81b0bb0ba18f563d82c28ba133c0790)

files:
A Misc/NEWS.d/next/Library/2021-05-10-17-45-00.bpo-44098._MoxuZ.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index a2a5d8f41019e..79c5c3a910407 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -4359,6 +4359,31 @@ def test_var_substitution(self):
         self.assertEqual(C1[int, str], Callable[[int], str])
         self.assertEqual(C1[[int, str, dict], float], Callable[[int, str, dict], float])
 
+    def test_no_paramspec_in__parameters__(self):
+        # ParamSpec should not be found in __parameters__
+        # of generics. Usages outside Callable, Concatenate
+        # and Generic are invalid.
+        T = TypeVar("T")
+        P = ParamSpec("P")
+        self.assertNotIn(P, List[P].__parameters__)
+        self.assertIn(T, Tuple[T, P].__parameters__)
+
+        # Test for consistency with builtin generics.
+        self.assertNotIn(P, list[P].__parameters__)
+        self.assertIn(T, tuple[T, P].__parameters__)
+
+    def test_paramspec_in_nested_generics(self):
+        # Although ParamSpec should not be found in __parameters__ of most
+        # generics, they probably should be found when nested in
+        # a valid location.
+        T = TypeVar("T")
+        P = ParamSpec("P")
+        C1 = Callable[P, T]
+        G1 = List[C1]
+        G2 = list[C1]
+        self.assertEqual(G1.__parameters__, (P, T))
+        self.assertEqual(G2.__parameters__, (P, T))
+
 
 class ConcatenateTests(BaseTestCase):
     def test_basics(self):
diff --git a/Lib/typing.py b/Lib/typing.py
index 26efe4a4600ac..639feeef99e75 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -195,15 +195,17 @@ def _type_repr(obj):
     return repr(obj)
 
 
-def _collect_type_vars(types):
-    """Collect all type variable-like variables contained
+def _collect_type_vars(types, typevar_types=None):
+    """Collect all type variable contained
     in types in order of first appearance (lexicographic order). For example::
 
         _collect_type_vars((T, List[S, T])) == (T, S)
     """
+    if typevar_types is None:
+        typevar_types = TypeVar
     tvars = []
     for t in types:
-        if isinstance(t, _TypeVarLike) and t not in tvars:
+        if isinstance(t, typevar_types) and t not in tvars:
             tvars.append(t)
         if isinstance(t, (_GenericAlias, GenericAlias)):
             tvars.extend([t for t in t.__parameters__ if t not in tvars])
@@ -932,7 +934,8 @@ def __getattr__(self, attr):
         raise AttributeError(attr)
 
     def __setattr__(self, attr, val):
-        if _is_dunder(attr) or attr in ('_name', '_inst', '_nparams'):
+        if _is_dunder(attr) or attr in {'_name', '_inst', '_nparams',
+                                        '_typevar_types', '_paramspec_tvars'}:
             super().__setattr__(attr, val)
         else:
             setattr(self.__origin__, attr, val)
@@ -957,14 +960,18 @@ def __subclasscheck__(self, cls):
 
 
 class _GenericAlias(_BaseGenericAlias, _root=True):
-    def __init__(self, origin, params, *, inst=True, name=None):
+    def __init__(self, origin, params, *, inst=True, name=None,
+                 _typevar_types=TypeVar,
+                 _paramspec_tvars=False):
         super().__init__(origin, inst=inst, name=name)
         if not isinstance(params, tuple):
             params = (params,)
         self.__args__ = tuple(... if a is _TypingEllipsis else
                               () if a is _TypingEmpty else
                               a for a in params)
-        self.__parameters__ = _collect_type_vars(params)
+        self.__parameters__ = _collect_type_vars(params, typevar_types=_typevar_types)
+        self._typevar_types = _typevar_types
+        self._paramspec_tvars = _paramspec_tvars
         if not name:
             self.__module__ = origin.__module__
 
@@ -991,14 +998,15 @@ def __getitem__(self, params):
         if not isinstance(params, tuple):
             params = (params,)
         params = tuple(_type_convert(p) for p in params)
-        if any(isinstance(t, ParamSpec) for t in self.__parameters__):
-            params = _prepare_paramspec_params(self, params)
+        if self._paramspec_tvars:
+            if any(isinstance(t, ParamSpec) for t in self.__parameters__):
+                params = _prepare_paramspec_params(self, params)
         _check_generic(self, params, len(self.__parameters__))
 
         subst = dict(zip(self.__parameters__, params))
         new_args = []
         for arg in self.__args__:
-            if isinstance(arg, _TypeVarLike):
+            if isinstance(arg, self._typevar_types):
                 arg = subst[arg]
             elif isinstance(arg, (_GenericAlias, GenericAlias)):
                 subparams = arg.__parameters__
@@ -1115,7 +1123,9 @@ def __reduce__(self):
 class _CallableType(_SpecialGenericAlias, _root=True):
     def copy_with(self, params):
         return _CallableGenericAlias(self.__origin__, params,
-                                     name=self._name, inst=self._inst)
+                                     name=self._name, inst=self._inst,
+                                     _typevar_types=(TypeVar, ParamSpec),
+                                     _paramspec_tvars=True)
 
     def __getitem__(self, params):
         if not isinstance(params, tuple) or len(params) != 2:
@@ -1208,7 +1218,10 @@ def __hash__(self):
 
 
 class _ConcatenateGenericAlias(_GenericAlias, _root=True):
-    pass
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs,
+                         _typevar_types=(TypeVar, ParamSpec),
+                         _paramspec_tvars=True)
 
 
 class Generic:
@@ -1244,7 +1257,7 @@ def __class_getitem__(cls, params):
         params = tuple(_type_convert(p) for p in params)
         if cls in (Generic, Protocol):
             # Generic and Protocol can only be subscripted with unique type variables.
-            if not all(isinstance(p, _TypeVarLike) for p in params):
+            if not all(isinstance(p, (TypeVar, ParamSpec)) for p in params):
                 raise TypeError(
                     f"Parameters to {cls.__name__}[...] must all be type variables "
                     f"or parameter specification variables.")
@@ -1256,7 +1269,9 @@ def __class_getitem__(cls, params):
             if any(isinstance(t, ParamSpec) for t in cls.__parameters__):
                 params = _prepare_paramspec_params(cls, params)
             _check_generic(cls, params, len(cls.__parameters__))
-        return _GenericAlias(cls, params)
+        return _GenericAlias(cls, params,
+                             _typevar_types=(TypeVar, ParamSpec),
+                             _paramspec_tvars=True)
 
     def __init_subclass__(cls, *args, **kwargs):
         super().__init_subclass__(*args, **kwargs)
@@ -1268,7 +1283,7 @@ def __init_subclass__(cls, *args, **kwargs):
         if error:
             raise TypeError("Cannot inherit from plain Generic")
         if '__orig_bases__' in cls.__dict__:
-            tvars = _collect_type_vars(cls.__orig_bases__)
+            tvars = _collect_type_vars(cls.__orig_bases__, (TypeVar, ParamSpec))
             # Look for Generic[T1, ..., Tn].
             # If found, tvars must be a subset of it.
             # If not found, tvars is it.
diff --git a/Misc/NEWS.d/next/Library/2021-05-10-17-45-00.bpo-44098._MoxuZ.rst b/Misc/NEWS.d/next/Library/2021-05-10-17-45-00.bpo-44098._MoxuZ.rst
new file mode 100644
index 0000000000000..2aaa8b695af46
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-05-10-17-45-00.bpo-44098._MoxuZ.rst
@@ -0,0 +1,5 @@
+``typing.ParamSpec`` will no longer be found in the ``__parameters__`` of
+most :mod:`typing` generics except in valid use locations specified by
+:pep:`612`. This prevents incorrect usage like ``typing.List[P][int]``. This
+change means incorrect usage which may have passed silently in 3.10 beta 1
+and earlier will now error.



More information about the Python-checkins mailing list