diff options
-rw-r--r-- | Lib/test/test_typing.py | 25 | ||||
-rw-r--r-- | Lib/typing.py | 43 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2021-05-10-17-45-00.bpo-44098._MoxuZ.rst | 5 |
3 files changed, 59 insertions, 14 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a2a5d8f..79c5c3a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4359,6 +4359,31 @@ class ParamSpecTests(BaseTestCase): 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 26efe4a..639feee 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 @@ class _BaseGenericAlias(_Final, _root=True): 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 @@ class _BaseGenericAlias(_Final, _root=True): 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 @@ class _GenericAlias(_BaseGenericAlias, _root=True): 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 @@ class _CallableGenericAlias(_GenericAlias, _root=True): 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 @@ class _LiteralGenericAlias(_GenericAlias, _root=True): 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 @@ class Generic: 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 @@ class Generic: 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 @@ class Generic: 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 0000000..2aaa8b6 --- /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. |