diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index fd024dcec8208b..815ae4e1fb3258 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -40,10 +40,11 @@ from weakref import WeakSet, ReferenceType, ref import typing -from typing import TypeVar +from typing import TypeVar, TypeVarTuple T = TypeVar('T') K = TypeVar('K') V = TypeVar('V') +Ts = TypeVarTuple('Ts)') class BaseTest(unittest.TestCase): """Test basics.""" @@ -170,7 +171,7 @@ def test_exposed_type(self): self.assertEqual(a.__args__, (int,)) self.assertEqual(a.__parameters__, ()) - def test_parameters(self): + def test_parameters_typevar(self): from typing import List, Dict, Callable D0 = dict[str, int] self.assertEqual(D0.__args__, (str, int)) @@ -209,6 +210,34 @@ def test_parameters(self): self.assertEqual(L5.__args__, (Callable[[K, V], K],)) self.assertEqual(L5.__parameters__, (K, V)) + def test_parameters_typevartuple(self): + from typing import List, Dict, Callable + T0 = tuple[*Ts] + self.assertEqual(T0.__args__, (Ts._unpacked,)) + self.assertEqual(T0.__parameters__, (T,)) + + L0 = list[str] + self.assertEqual(L0.__args__, (str,)) + self.assertEqual(L0.__parameters__, ()) + L1 = list[T] + self.assertEqual(L1.__args__, (T,)) + self.assertEqual(L1.__parameters__, (T,)) + L2 = list[list[T]] + self.assertEqual(L2.__args__, (list[T],)) + self.assertEqual(L2.__parameters__, (T,)) + L3 = list[List[T]] + self.assertEqual(L3.__args__, (List[T],)) + self.assertEqual(L3.__parameters__, (T,)) + L4a = list[Dict[K, V]] + self.assertEqual(L4a.__args__, (Dict[K, V],)) + self.assertEqual(L4a.__parameters__, (K, V)) + L4b = list[Dict[T, int]] + self.assertEqual(L4b.__args__, (Dict[T, int],)) + self.assertEqual(L4b.__parameters__, (T,)) + L5 = list[Callable[[K, V], K]] + self.assertEqual(L5.__args__, (Callable[[K, V], K],)) + self.assertEqual(L5.__parameters__, (K, V)) + def test_parameter_chaining(self): from typing import List, Dict, Union, Callable self.assertEqual(list[T][int], list[int]) @@ -391,5 +420,9 @@ def __call__(self): self.assertEqual(repr(C1), "collections.abc.Callable" "[typing.Concatenate[int, ~P], int]") + with self.subTest("Testing TypeVarTuple uses"): + Ts = typing.TypeVarTuple('Ts') + tuple[*Ts] + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3b8efe16c6e238..fa1dfffe8445fe 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -7,7 +7,7 @@ from copy import copy, deepcopy from typing import Any, NoReturn -from typing import TypeVar, AnyStr +from typing import TypeVar, TypeVarTuple, Unpack, _UnpackedTypeVarTuple, AnyStr from typing import T, KT, VT # Not in __all__. from typing import Union, Optional, Literal from typing import Tuple, List, Dict, MutableMapping @@ -236,6 +236,153 @@ def test_no_bivariant(self): TypeVar('T', covariant=True, contravariant=True) +class TypeVarTupleTests(BaseTestCase): + + def test_basic_plain(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(Ts, Ts) + self.assertEqual(Unpack[Ts], Unpack[Ts]) + self.assertIsInstance(Ts, TypeVarTuple) + self.assertIsInstance(Unpack[Ts], _UnpackedTypeVarTuple) + + def test_repr(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(repr(Ts), 'Ts') + self.assertEqual(repr(Unpack[Ts]), '*Ts') + + def test_no_redefinition(self): + self.assertNotEqual(TypeVar('Ts'), TypeVar('Ts')) + + def test_cannot_subclass_vars(self): + with self.assertRaises(TypeError): + class V(TypeVarTuple('Ts')): + pass + with self.assertRaises(TypeError): + class V(Unpack[TypeVarTuple('Ts')]): + pass + + def test_cannot_subclass_var_itself(self): + with self.assertRaises(TypeError): + class V(TypeVarTuple): + pass + + def test_cannot_instantiate_vars(self): + Ts = TypeVarTuple('Ts') + with self.assertRaises(TypeError): + Ts() + + def test_unpack(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(Unpack[Ts], Unpack[Ts]) + self.assertIsInstance(Unpack[Ts], _UnpackedTypeVarTuple) + with self.assertRaises(TypeError): + Unpack() + with self.assertRaises(TypeError): + Unpack[Tuple[int, str]] + with self.assertRaises(TypeError): + Unpack[List[int]] + + def test_tuple(self): + Ts = TypeVarTuple('Ts') + Tuple[Unpack[Ts]] + with self.assertRaises(TypeError): + Tuple[Ts] + + def test_list(self): + Ts = TypeVarTuple('Ts') + with self.assertRaises(TypeError): + List[Ts] + with self.assertRaises(TypeError): + List[Unpack[Ts]] + + def test_union(self): + Xs = TypeVarTuple('Xs') + Ys = TypeVarTuple('Ys') + self.assertNotEqual(Xs, Ys) + with self.assertRaises(TypeError): + Union[Xs] + Union[Xs, int] + self.assertEqual( + Union[Unpack[Xs]], + Unpack[Xs] + ) + self.assertNotEqual( + Union[Unpack[Xs]], + Union[Unpack[Xs], Unpack[Ys]] + ) + self.assertEqual( + Union[Unpack[Xs], Unpack[Xs]], + Unpack[Xs] + ) + self.assertNotEqual( + Union[Unpack[Xs], int], + Union[Unpack[Xs]] + ) + self.assertNotEqual( + Union[Unpack[Xs], int], + Union[int] + ) + self.assertEqual( + Union[Unpack[Xs], int].__args__, + (Unpack[Xs], int) + ) + self.assertEqual( + Union[Unpack[Xs], int].__parameters__, + (Unpack[Xs],) + ) + self.assertIs( + Union[Unpack[Xs], int].__origin__, + Union + ) + + def test_concatenation(self): + Xs = TypeVarTuple('Xs') + Tuple[int, Unpack[Xs]] + Tuple[Unpack[Xs], int] + Tuple[int, Unpack[Xs], str] + class C(Generic[Unpack[Xs]]): pass + C[int, Unpack[Xs]] + C[Unpack[Xs], int] + C[int, Unpack[Xs], str] + + with self.assertRaises(TypeError): + Tuple[Unpack[Xs], Unpack[Xs]] + with self.assertRaises(TypeError): + C[Unpack[Xs], Unpack[Xs]] + Ys = TypeVarTuple('Ys') + with self.assertRaises(TypeError): + Tuple[Unpack[Xs], Unpack[Ys]] + with self.assertRaises(TypeError): + C[Unpack[Xs], Unpack[Ys]] + + def test_class(self): + Ts = TypeVarTuple('Ts') + + class C(Generic[Unpack[Ts]]): pass + C[int] + C[int, str] + + with self.assertRaises(TypeError): + class C(Generic[Ts]): pass + with self.assertRaises(TypeError): + class C(Generic[Unpack[Ts], int]): pass + + T1 = TypeVar('T') + T2 = TypeVar('T') + class C(Generic[T1, T2, Unpack[Ts]]): pass + C[int, str] + C[int, str, float] + with self.assertRaises(TypeError): + C[int] + + def test_args_and_parameters(self): + Ts = TypeVarTuple('Ts') + + t = Tuple[*Ts] + self.assertEqual(t.__args__, (Ts._unpacked,)) + self.assertEqual(t.__parameters__, (Ts,)) + + class UnionTests(BaseTestCase): def test_basics(self): @@ -1433,12 +1580,20 @@ def test_basics(self): def test_generic_errors(self): T = TypeVar('T') S = TypeVar('S') + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') with self.assertRaises(TypeError): Generic[T][T] + with self.assertRaises(TypeError): + Generic[Unpack[Ts1]][Unpack[Ts1]] with self.assertRaises(TypeError): Generic[T][S] + with self.assertRaises(TypeError): + Generic[Unpack[Ts1]][Unpack[Ts2]] with self.assertRaises(TypeError): class C(Generic[T], Generic[T]): ... + with self.assertRaises(TypeError): + class C(Generic[Ts1], Generic[Ts1]): ... with self.assertRaises(TypeError): isinstance([], List[int]) with self.assertRaises(TypeError): @@ -1447,14 +1602,22 @@ class C(Generic[T], Generic[T]): ... class NewGeneric(Generic): ... with self.assertRaises(TypeError): class MyGeneric(Generic[T], Generic[S]): ... + with self.assertRaises(TypeError): + class MyGeneric(Generic[Ts1], Generic[Ts2]): ... with self.assertRaises(TypeError): class MyGeneric(List[T], Generic[S]): ... def test_init(self): T = TypeVar('T') S = TypeVar('S') + Ts1 = TypeVar('Ts1') + Ts2 = TypeVar('Ts2') with self.assertRaises(TypeError): Generic[T, T] + with self.assertRaises(TypeError): + Generic[Unpack[Ts1], Unpack[Ts1]] + with self.assertRaises(TypeError): + Generic[Unpack[Ts1], Unpack[Ts2]] with self.assertRaises(TypeError): Generic[T, S, T] @@ -1508,14 +1671,40 @@ class C(Generic[T]): self.assertTrue(str(Z).endswith( '.C[typing.Tuple[str, int]]')) + def test_chain_repr_typevartuple(self): + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + + class C(Generic[Unpack[Ts]]): + pass + + X = C[Tuple[Unpack[Ts], T]] + self.assertEqual(X, C[Tuple[Unpack[Ts], T]]) + self.assertNotEqual(X, C[Tuple[T, Unpack[Ts]]]) + + Y = X[T, int] + self.assertEqual(Y, X[T, int]) + self.assertNotEqual(Y, X[Unpack[Ts], int]) + self.assertNotEqual(Y, X[T, str]) + + Z = Y[str] + self.assertEqual(Z, Y[str]) + self.assertNotEqual(Z, Y[int]) + self.assertNotEqual(Z, Y[T]) + + self.assertTrue(str(Z).endswith( + '.C[typing.Tuple[str, int]]')) + def test_new_repr(self): T = TypeVar('T') U = TypeVar('U', covariant=True) S = TypeVar('S') + Ts = TypeVarTuple('Ts') self.assertEqual(repr(List), 'typing.List') self.assertEqual(repr(List[T]), 'typing.List[~T]') self.assertEqual(repr(List[U]), 'typing.List[+U]') + self.assertEqual(repr(Tuple[Unpack[Ts]]), 'typing.Tuple[*Ts]') self.assertEqual(repr(List[S][T][int]), 'typing.List[int]') self.assertEqual(repr(List[int]), 'typing.List[int]') @@ -1535,6 +1724,8 @@ def test_new_repr_bare(self): T = TypeVar('T') self.assertEqual(repr(Generic[T]), 'typing.Generic[~T]') self.assertEqual(repr(typing.Protocol[T]), 'typing.Protocol[~T]') + Ts = TypeVarTuple('Ts') + self.assertEqual(repr(Generic[Unpack[Ts]]), 'typing.Generic[*Ts]') class C(typing.Dict[Any, Any]): ... # this line should just work repr(C.__mro__) diff --git a/Lib/typing.py b/Lib/typing.py index 6224930c3b0275..0960caae2198b5 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -7,7 +7,7 @@ * _SpecialForm and its instances (special forms): Any, NoReturn, ClassVar, Union, Optional, Concatenate * Classes whose instances can be type arguments in addition to types: - ForwardRef, TypeVar and ParamSpec + ForwardRef, TypeVar, TypeVarTuple and ParamSpec * The core of internal generics API: _GenericAlias and _VariadicGenericAlias, the latter is currently only used by Tuple and Callable. All subscripted types like X[int], Union[int, str], etc., are instances of either of these classes. @@ -49,7 +49,10 @@ 'Tuple', 'Type', 'TypeVar', + 'TypeVarTuple', 'Union', + 'Unpack', + '_UnpackedTypeVarTuple', # ABCs (from collections.abc). 'AbstractSet', # collections.abc.Set. @@ -158,7 +161,11 @@ def _type_check(arg, msg, is_argument=True): return arg if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol): raise TypeError(f"Plain {arg} is not valid as type argument") - if isinstance(arg, (type, TypeVar, ForwardRef, types.Union, ParamSpec)): + if isinstance(arg, TypeVarTuple): + raise TypeError("Type variable tuple must be unpacked before being " + "used as a type argument") + if isinstance(arg, (type, TypeVar, _UnpackedTypeVarTuple, ForwardRef, + types.Union, ParamSpec)): return arg if not callable(arg): raise TypeError(f"{msg} Got {arg!r:.100}.") @@ -194,23 +201,82 @@ def _collect_type_vars(types): """ tvars = [] for t in types: - if isinstance(t, _TypeVarLike) and t not in tvars: + if isinstance(t, _TypeVarTypes) 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]) return tuple(tvars) -def _check_generic(cls, parameters, elen): - """Check correct count for parameters of a generic cls (internal helper). - This gives a nice error message in case of count mismatch. +def _check_type_parameter_count(cls, + type_params, expected_num_type_params=None): + """Checks for correct count of type parameters of a generic class. + + Gives a nice error message in case of count mismatch. + + Args: + cls: The generic class in question. + type_parameters: A tuple of the type parameters the user is attempting + to use with to the generic class. For example, for 'Tuple[int, str]', + `type_parameters` would be `(int, str)`. + expected_num_type_params: The exact number of type parameters expected. + If `None`, this is determined by inspecting `cls.__parameters__`. """ - if not elen: - raise TypeError(f"{cls} is not a generic class") - alen = len(parameters) - if alen != elen: - raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};" - f" actual {alen}, expected {elen}") + actual_num_type_params = len(type_params) + + # These should have been checked elsewhere. + assert not any(isinstance(p, TypeVarTuple) for p in type_params) + if expected_num_type_params is not None: + assert expected_num_type_params > 0 + + if ((expected_num_type_params is not None) and + (any(isinstance(p, _UnpackedTypeVarTuple) for p in type_params))): + msg = ("Cannot use an unpacked type variable tuple, which can " + "represent an arbitrary number of types, as a type " + f"parameter for {cls}, " + f"which expects exactly {expected_num_type_params} " + "type ") + if expected_num_type_params > 1: + msg += "parameters" + else: + msg += "parameter" + raise TypeError(msg) + + # If all type variables are `TypeVar` or `ParamSpec`, the number + # of them should exactly match the number of type variables. + if (expected_num_type_params is None and + all(isinstance(p, (TypeVar, ParamSpec)) + for p in cls.__parameters__)): + expected_num_type_params = len(cls.__parameters__) + + # Case 1: we know exactly how many type parameters to expect. + if expected_num_type_params is not None: + if actual_num_type_params > expected_num_type_params: + msg = f"Too many parameters for {cls}; " + elif actual_num_type_params < expected_num_type_params: + msg = f"Too few parameters for {cls}; " + else: + return + msg2 = (f"actual {actual_num_type_params}, " + f"expected {expected_num_type_params}") + raise TypeError(msg + msg2) + + # Case 2: some of the type variables are `TypeVarTuple`, so we only know + # the *minimum* number of type variables to expect. + min_num_type_params = len([p for p in cls.__parameters__ + if isinstance(p, (TypeVar, ParamSpec))]) + if actual_num_type_params < min_num_type_params: + raise TypeError(f"Too few parameters for {cls}; " + f"actual {actual_num_type_params}, " + f"expected at least {min_num_type_params}") + + +def _check_num_type_variable_tuples(params): + if len([p for p in params if isinstance(p, _UnpackedTypeVarTuple)]) > 1: + raise TypeError( + "Only one unpacked type variable tuple may appear in a " + "type parameter list") + def _prepare_paramspec_params(cls, params): """Prepares the parameters for a Generic containing ParamSpec @@ -628,12 +694,11 @@ def __hash__(self): def __repr__(self): return f'ForwardRef({self.__forward_arg__!r})' -class _TypeVarLike: - """Mixin for TypeVar-like types (TypeVar and ParamSpec).""" + +class _BoundVarianceMixin: + """Handles bounds, covariance and contravariance for TypeVar/ParamSpec.""" + def __init__(self, bound, covariant, contravariant): - """Used to setup TypeVars and ParamSpec's bound, covariant and - contravariant attributes. - """ if covariant and contravariant: raise ValueError("Bivariant types are not supported.") self.__covariant__ = bool(covariant) @@ -662,7 +727,7 @@ def __reduce__(self): return self.__name__ -class TypeVar( _Final, _Immutable, _TypeVarLike, _root=True): +class TypeVar(_Final, _Immutable, _BoundVarianceMixin, _root=True): """Type variable. Usage:: @@ -727,7 +792,42 @@ def __init__(self, name, *constraints, bound=None, self.__module__ = def_mod -class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): +class TypeVarTuple(_Final, _root=True): + + def __init__(self, name): + self.__name__ = name + # All unpacked usages of this TypeVarTuple instance should have + # the same identity, so instantiate this here. + self._unpacked = _UnpackedTypeVarTuple(self) + + def __iter__(self): + yield self._unpacked + + def __repr__(self): + return self.__name__ + + +class _UnpackedTypeVarTuple: + + def __init__(self, type_var_tuple: TypeVarTuple): + self._type_var_tuple = type_var_tuple + + def __repr__(self): + return '*' + self._type_var_tuple.__name__ + + +class Unpack: + + def __new__(cls, *args, **kwargs): + raise TypeError("Cannot instantiate Unpack") + + def __class_getitem__(self, type_var_tuple: TypeVarTuple): + if not isinstance(type_var_tuple, TypeVarTuple): + raise TypeError("Parameter to Unpack must be a TypeVarTuple") + return type_var_tuple._unpacked + + +class ParamSpec(_Final, _Immutable, _BoundVarianceMixin, _root=True): """Parameter specification variable. Usage:: @@ -789,6 +889,7 @@ def __init__(self, name, *, bound=None, covariant=False, contravariant=False): if def_mod != 'typing': self.__module__ = def_mod +_TypeVarTypes = (TypeVar, _UnpackedTypeVarTuple, ParamSpec) def _is_dunder(attr): return attr.startswith('__') and attr.endswith('__') @@ -900,12 +1001,12 @@ def __getitem__(self, 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) - _check_generic(self, params, len(self.__parameters__)) + _check_type_parameter_count(self, params) subst = dict(zip(self.__parameters__, params)) new_args = [] for arg in self.__args__: - if isinstance(arg, _TypeVarLike): + if isinstance(arg, _TypeVarTypes): arg = subst[arg] elif isinstance(arg, (_GenericAlias, GenericAlias)): subparams = arg.__parameters__ @@ -974,7 +1075,7 @@ def __getitem__(self, params): params = (params,) msg = "Parameters to generic types must be types." params = tuple(_type_check(p, msg) for p in params) - _check_generic(self, params, self._nparams) + _check_type_parameter_count(self, params, self._nparams) return self.copy_with(params) def copy_with(self, params): @@ -1065,6 +1166,7 @@ def __getitem__(self, params): return self.copy_with((p, _TypingEllipsis)) msg = "Tuple[t0, t1, ...]: each t must be a type." params = tuple(_type_check(p, msg) for p in params) + _check_num_type_variable_tuples(params) return self.copy_with(params) @@ -1149,12 +1251,18 @@ def __class_getitem__(cls, params): raise TypeError( f"Parameter list to {cls.__qualname__}[...] cannot be empty") params = tuple(_type_convert(p) for p in params) + _check_num_type_variable_tuples(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): + # Generic and Protocol can only be subscripted with unique type + # variables or unpacked type variable tuples. + if any(isinstance(p, TypeVarTuple) for p in params): + raise TypeError( + "Type variable tuples must be unpacked before being used " + f"as parameters to {cls.__name__}[...].") + if not all(isinstance(p, _TypeVarTypes) for p in params): raise TypeError( - f"Parameters to {cls.__name__}[...] must all be type variables " - f"or parameter specification variables.") + f"Parameters to {cls.__name__}[...] must all " + "be type variables or parameter specification variables.") if len(set(params)) != len(params): raise TypeError( f"Parameters to {cls.__name__}[...] must all be unique") @@ -1162,7 +1270,7 @@ def __class_getitem__(cls, params): # Subscripting a regular Generic subclass. if any(isinstance(t, ParamSpec) for t in cls.__parameters__): params = _prepare_paramspec_params(cls, params) - _check_generic(cls, params, len(cls.__parameters__)) + _check_type_parameter_count(cls, params) return _GenericAlias(cls, params) def __init_subclass__(cls, *args, **kwargs): diff --git a/Misc/NEWS.d/next/Library/2021-02-14-17-22-52.bpo-43224.WDihrT.rst b/Misc/NEWS.d/next/Library/2021-02-14-17-22-52.bpo-43224.WDihrT.rst new file mode 100644 index 00000000000000..dfdedf25443b64 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-02-14-17-22-52.bpo-43224.WDihrT.rst @@ -0,0 +1 @@ +Add support for PEP 646. \ No newline at end of file