Skip to content

Commit

Permalink
bpo-44098: Drop ParamSpec from most __parameters__ in typing gene…
Browse files Browse the repository at this point in the history
…rics (GH-26013)

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
  • Loading branch information
Fidget-Spinner committed May 13, 2021
1 parent 7565586 commit b2f3f8e
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 14 deletions.
25 changes: 25 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
43 changes: 29 additions & 14 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Expand All @@ -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__

Expand All @@ -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__
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.")
Expand All @@ -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)
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit b2f3f8e

Please sign in to comment.