diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index bd9920436223ce..81a42badf57825 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -10,7 +10,7 @@ from copy import copy, deepcopy from typing import Any, NoReturn, Never, assert_never -from typing import TypeVar, AnyStr +from typing import TypeVar, TypeVarTuple, Unpack, AnyStr from typing import T, KT, VT # Not in __all__. from typing import Union, Optional, Literal from typing import Tuple, List, Dict, MutableMapping @@ -369,6 +369,431 @@ def test_bad_var_substitution(self): list[T][arg] +class UnpackTests(BaseTestCase): + + def test_accepts_single_type(self): + Unpack[Tuple[int]] + + def test_rejects_multiple_types(self): + with self.assertRaises(TypeError): + Unpack[Tuple[int], Tuple[str]] + + def test_rejects_multiple_parameterization(self): + with self.assertRaises(TypeError): + Unpack[Tuple[int]][Tuple[int]] + + def test_cannot_be_called(self): + with self.assertRaises(TypeError): + Unpack() + + +class TypeVarTupleTests(BaseTestCase): + + def test_instance_is_equal_to_itself(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(Ts, Ts) + + def test_different_instances_are_different(self): + self.assertNotEqual(TypeVarTuple('Ts'), TypeVarTuple('Ts')) + + def test_instance_isinstance_of_typevartuple(self): + Ts = TypeVarTuple('Ts') + self.assertIsInstance(Ts, TypeVarTuple) + + def test_cannot_call_instance(self): + Ts = TypeVarTuple('Ts') + with self.assertRaises(TypeError): + Ts() + + def test_unpacked_typevartuple_is_equal_to_itself(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(Unpack[Ts], Unpack[Ts]) + + def test_parameterised_tuple_is_equal_to_itself(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(tuple[Unpack[Ts]], tuple[Unpack[Ts]]) + self.assertEqual(Tuple[Unpack[Ts]], Tuple[Unpack[Ts]]) + + def tests_tuple_arg_ordering_matters(self): + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') + self.assertNotEqual( + tuple[Unpack[Ts1], Unpack[Ts2]], + tuple[Unpack[Ts2], Unpack[Ts1]], + ) + self.assertNotEqual( + Tuple[Unpack[Ts1], Unpack[Ts2]], + Tuple[Unpack[Ts2], Unpack[Ts1]], + ) + + def test_tuple_args_and_parameters_are_correct(self): + Ts = TypeVarTuple('Ts') + t1 = tuple[Unpack[Ts]] + self.assertEqual(t1.__args__, (Unpack[Ts],)) + self.assertEqual(t1.__parameters__, (Ts,)) + t2 = Tuple[Unpack[Ts]] + self.assertEqual(t2.__args__, (Unpack[Ts],)) + self.assertEqual(t2.__parameters__, (Ts,)) + + def test_repr_is_correct(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(repr(Ts), 'Ts') + self.assertEqual(repr(Unpack[Ts]), '*Ts') + self.assertEqual(repr(tuple[Unpack[Ts]]), 'tuple[*Ts]') + self.assertEqual(repr(Tuple[Unpack[Ts]]), 'typing.Tuple[*Ts]') + self.assertEqual(repr(Unpack[tuple[Unpack[Ts]]]), '*tuple[*Ts]') + self.assertEqual(repr(Unpack[Tuple[Unpack[Ts]]]), '*typing.Tuple[*Ts]') + + def test_variadic_class_repr_is_correct(self): + Ts = TypeVarTuple('Ts') + class A(Generic[Unpack[Ts]]): pass + + self.assertTrue(repr(A[()]).endswith('A[()]')) + self.assertTrue(repr(A[float]).endswith('A[float]')) + self.assertTrue(repr(A[float, str]).endswith('A[float, str]')) + self.assertTrue(repr( + A[Unpack[tuple[int, ...]]] + ).endswith( + 'A[*tuple[int, ...]]' + )) + self.assertTrue(repr( + A[float, Unpack[tuple[int, ...]]] + ).endswith( + 'A[float, *tuple[int, ...]]' + )) + self.assertTrue(repr( + A[Unpack[tuple[int, ...]], str] + ).endswith( + 'A[*tuple[int, ...], str]' + )) + self.assertTrue(repr( + A[float, Unpack[tuple[int, ...]], str] + ).endswith( + 'A[float, *tuple[int, ...], str]' + )) + + def test_variadic_class_alias_repr_is_correct(self): + Ts = TypeVarTuple('Ts') + class A(Generic[Unpack[Ts]]): pass + + B = A[Unpack[Ts]] + self.assertTrue(repr(B).endswith('A[*Ts]')) + with self.assertRaises(NotImplementedError): + B[()] + with self.assertRaises(NotImplementedError): + B[float] + with self.assertRaises(NotImplementedError): + B[float, str] + + C = A[Unpack[Ts], int] + self.assertTrue(repr(C).endswith('A[*Ts, int]')) + with self.assertRaises(NotImplementedError): + C[()] + with self.assertRaises(NotImplementedError): + C[float] + with self.assertRaises(NotImplementedError): + C[float, str] + + D = A[int, Unpack[Ts]] + self.assertTrue(repr(D).endswith('A[int, *Ts]')) + with self.assertRaises(NotImplementedError): + D[()] + with self.assertRaises(NotImplementedError): + D[float] + with self.assertRaises(NotImplementedError): + D[float, str] + + E = A[int, Unpack[Ts], str] + self.assertTrue(repr(E).endswith('A[int, *Ts, str]')) + with self.assertRaises(NotImplementedError): + E[()] + with self.assertRaises(NotImplementedError): + E[float] + with self.assertRaises(NotImplementedError): + E[float, bool] + + F = A[Unpack[Ts], Unpack[tuple[str, ...]]] + self.assertTrue(repr(F).endswith('A[*Ts, *tuple[str, ...]]')) + with self.assertRaises(NotImplementedError): + F[()] + with self.assertRaises(NotImplementedError): + F[float] + with self.assertRaises(NotImplementedError): + F[float, int] + + def test_cannot_subclass_class(self): + with self.assertRaises(TypeError): + class C(TypeVarTuple): pass + + def test_cannot_subclass_instance(self): + Ts = TypeVarTuple('Ts') + with self.assertRaises(TypeError): + class C(Ts): pass + with self.assertRaises(TypeError): + class C(Unpack[Ts]): pass + + def test_variadic_class_args_are_correct(self): + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + class A(Generic[Unpack[Ts]]): pass + B = A[()] + self.assertEqual(B.__args__, ()) + C = A[int] + self.assertEqual(C.__args__, (int,)) + D = A[int, str] + self.assertEqual(D.__args__, (int, str)) + E = A[T] + self.assertEqual(E.__args__, (T,)) + F = A[Unpack[Ts]] + self.assertEqual(F.__args__, (Unpack[Ts],)) + G = A[T, Unpack[Ts]] + self.assertEqual(G.__args__, (T, Unpack[Ts])) + H = A[Unpack[Ts], T] + self.assertEqual(H.__args__, (Unpack[Ts], T)) + + def test_variadic_class_origin_is_correct(self): + Ts = TypeVarTuple('Ts') + class D(Generic[Unpack[Ts]]): pass + self.assertIs(D[int].__origin__, D) + self.assertIs(D[T].__origin__, D) + self.assertIs(D[Unpack[Ts]].__origin__, D) + + def test_tuple_args_are_correct(self): + Ts = TypeVarTuple('Ts') + + self.assertEqual(tuple[Unpack[Ts]].__args__, (Unpack[Ts],)) + self.assertEqual(Tuple[Unpack[Ts]].__args__, (Unpack[Ts],)) + + self.assertEqual(tuple[Unpack[Ts], int].__args__, (Unpack[Ts], int)) + self.assertEqual(Tuple[Unpack[Ts], int].__args__, (Unpack[Ts], int)) + + self.assertEqual(tuple[int, Unpack[Ts]].__args__, (int, Unpack[Ts])) + self.assertEqual(Tuple[int, Unpack[Ts]].__args__, (int, Unpack[Ts])) + + self.assertEqual(tuple[int, Unpack[Ts], str].__args__, + (int, Unpack[Ts], str)) + self.assertEqual(Tuple[int, Unpack[Ts], str].__args__, + (int, Unpack[Ts], str)) + + self.assertEqual(tuple[Unpack[Ts], int].__args__, (Unpack[Ts], int)) + self.assertEqual(Tuple[Unpack[Ts]].__args__, (Unpack[Ts],)) + + def test_callable_args_are_correct(self): + Ts = TypeVarTuple('Ts') + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') + + # TypeVarTuple in the arguments + + a = Callable[[Unpack[Ts]], None] + self.assertEqual(a.__args__, (Unpack[Ts], type(None))) + + b = Callable[[int, Unpack[Ts]], None] + self.assertEqual(b.__args__, (int, Unpack[Ts], type(None))) + + c = Callable[[Unpack[Ts], int], None] + self.assertEqual(c.__args__, (Unpack[Ts], int, type(None))) + + d = Callable[[str, Unpack[Ts], int], None] + self.assertEqual(d.__args__, (str, Unpack[Ts], int, type(None))) + + # TypeVarTuple as the return + + e = Callable[[None], Unpack[Ts]] + self.assertEqual(e.__args__, (type(None), Unpack[Ts])) + + f = Callable[[None], tuple[int, Unpack[Ts]]] + self.assertEqual(f.__args__, (type(None), tuple[int, Unpack[Ts]])) + + g = Callable[[None], tuple[Unpack[Ts], int]] + self.assertEqual(g.__args__, (type(None), tuple[Unpack[Ts], int])) + + h = Callable[[None], tuple[str, Unpack[Ts], int]] + self.assertEqual(h.__args__, (type(None), tuple[str, Unpack[Ts], int])) + + # TypeVarTuple in both + + i = Callable[[Unpack[Ts]], Unpack[Ts]] + self.assertEqual(i.__args__, (Unpack[Ts], Unpack[Ts])) + + j = Callable[[Unpack[Ts1]], Unpack[Ts2]] + self.assertEqual(j.__args__, (Unpack[Ts1], Unpack[Ts2])) + + def test_variadic_class_with_duplicate_typevartuples_fails(self): + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') + with self.assertRaises(TypeError): + class C(Generic[Unpack[Ts1], Unpack[Ts1]]): pass + with self.assertRaises(TypeError): + class C(Generic[Unpack[Ts1], Unpack[Ts2], Unpack[Ts1]]): pass + + def test_type_concatenation_in_variadic_class_argument_list_succeeds(self): + Ts = TypeVarTuple('Ts') + class C(Generic[Unpack[Ts]]): pass + C[int, Unpack[Ts]] + C[Unpack[Ts], int] + C[int, Unpack[Ts], str] + C[int, bool, Unpack[Ts], float, str] + + def test_type_concatenation_in_tuple_argument_list_succeeds(self): + Ts = TypeVarTuple('Ts') + + tuple[int, Unpack[Ts]] + tuple[Unpack[Ts], int] + tuple[int, Unpack[Ts], str] + tuple[int, bool, Unpack[Ts], float, str] + + Tuple[int, Unpack[Ts]] + Tuple[Unpack[Ts], int] + Tuple[int, Unpack[Ts], str] + Tuple[int, bool, Unpack[Ts], float, str] + + def test_variadic_class_definition_using_packed_typevartuple_fails(self): + Ts = TypeVarTuple('Ts') + with self.assertRaises(TypeError): + class C(Generic[Ts]): pass + + def test_variadic_class_definition_using_concrete_types_fails(self): + Ts = TypeVarTuple('Ts') + with self.assertRaises(TypeError): + class E(Generic[Unpack[Ts], int]): pass + + def test_variadic_class_with_2_typevars_accepts_2_or_more_args(self): + Ts = TypeVarTuple('Ts') + T1 = TypeVar('T1') + T2 = TypeVar('T2') + + class A(Generic[T1, T2, Unpack[Ts]]): pass + A[int, str] + A[int, str, float] + A[int, str, float, bool] + + class B(Generic[T1, Unpack[Ts], T2]): pass + B[int, str] + B[int, str, float] + B[int, str, float, bool] + + class C(Generic[Unpack[Ts], T1, T2]): pass + C[int, str] + C[int, str, float] + C[int, str, float, bool] + + def test_variadic_args_annotations_are_correct(self): + Ts = TypeVarTuple('Ts') + def f(*args: Unpack[Ts]): pass + self.assertEqual(f.__annotations__, {'args': Unpack[Ts]}) + + def test_variadic_args_with_ellipsis_annotations_are_correct(self): + Ts = TypeVarTuple('Ts') + + def a(*args: Unpack[tuple[int, ...]]): pass + self.assertEqual(a.__annotations__, + {'args': Unpack[tuple[int, ...]]}) + + def b(*args: Unpack[Tuple[int, ...]]): pass + self.assertEqual(b.__annotations__, + {'args': Unpack[Tuple[int, ...]]}) + + def test_concatenation_in_variadic_args_annotations_are_correct(self): + Ts = TypeVarTuple('Ts') + + # Unpacking using `Unpack`, native `tuple` type + + def a(*args: Unpack[tuple[int, Unpack[Ts]]]): pass + self.assertEqual( + a.__annotations__, + {'args': Unpack[tuple[int, Unpack[Ts]]]}, + ) + + def b(*args: Unpack[tuple[Unpack[Ts], int]]): pass + self.assertEqual( + b.__annotations__, + {'args': Unpack[tuple[Unpack[Ts], int]]}, + ) + + def c(*args: Unpack[tuple[str, Unpack[Ts], int]]): pass + self.assertEqual( + c.__annotations__, + {'args': Unpack[tuple[str, Unpack[Ts], int]]}, + ) + + def d(*args: Unpack[tuple[int, bool, Unpack[Ts], float, str]]): pass + self.assertEqual( + d.__annotations__, + {'args': Unpack[tuple[int, bool, Unpack[Ts], float, str]]}, + ) + + # Unpacking using `Unpack`, `Tuple` type from typing.py + + def e(*args: Unpack[Tuple[int, Unpack[Ts]]]): pass + self.assertEqual( + e.__annotations__, + {'args': Unpack[Tuple[int, Unpack[Ts]]]}, + ) + + def f(*args: Unpack[Tuple[Unpack[Ts], int]]): pass + self.assertEqual( + f.__annotations__, + {'args': Unpack[Tuple[Unpack[Ts], int]]}, + ) + + def g(*args: Unpack[Tuple[str, Unpack[Ts], int]]): pass + self.assertEqual( + g.__annotations__, + {'args': Unpack[Tuple[str, Unpack[Ts], int]]}, + ) + + def h(*args: Unpack[Tuple[int, bool, Unpack[Ts], float, str]]): pass + self.assertEqual( + h.__annotations__, + {'args': Unpack[Tuple[int, bool, Unpack[Ts], float, str]]}, + ) + + def test_variadic_class_same_args_results_in_equalty(self): + Ts = TypeVarTuple('Ts') + class C(Generic[Unpack[Ts]]): pass + + self.assertEqual(C[int], C[int]) + + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') + self.assertEqual( + C[Unpack[Ts1]], + C[Unpack[Ts1]], + ) + self.assertEqual( + C[Unpack[Ts1], Unpack[Ts2]], + C[Unpack[Ts1], Unpack[Ts2]], + ) + self.assertEqual( + C[int, Unpack[Ts1], Unpack[Ts2]], + C[int, Unpack[Ts1], Unpack[Ts2]], + ) + + def test_variadic_class_arg_ordering_matters(self): + Ts = TypeVarTuple('Ts') + class C(Generic[Unpack[Ts]]): pass + + self.assertNotEqual( + C[int, str], + C[str, int], + ) + + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') + self.assertNotEqual( + C[Unpack[Ts1], Unpack[Ts2]], + C[Unpack[Ts2], Unpack[Ts1]], + ) + + def test_variadic_class_arg_typevartuple_identity_matters(self): + Ts = TypeVarTuple('Ts') + class C(Generic[Unpack[Ts]]): pass + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') + self.assertNotEqual(C[Unpack[Ts1]], C[Unpack[Ts2]]) + + class UnionTests(BaseTestCase): def test_basics(self): @@ -1818,6 +2243,11 @@ class NewGeneric(Generic): ... class MyGeneric(Generic[T], Generic[S]): ... with self.assertRaises(TypeError): class MyGeneric(List[T], Generic[S]): ... + with self.assertRaises(TypeError): + Generic[()] + class C(Generic[T]): pass + with self.assertRaises(TypeError): + C[()] def test_init(self): T = TypeVar('T') diff --git a/Lib/typing.py b/Lib/typing.py index 6e0c68c842420b..59e2c672a8372e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -5,7 +5,7 @@ * Imports and exports, all public names should be explicitly added to __all__. * Internal helper functions: these should never be used in code outside this module. * _SpecialForm and its instances (special forms): - Any, NoReturn, Never, ClassVar, Union, Optional, Concatenate + Any, NoReturn, Never, ClassVar, Union, Optional, Concatenate, Unpack * Classes whose instances can be type arguments in addition to types: ForwardRef, TypeVar and ParamSpec * The core of internal generics API: _GenericAlias and _VariadicGenericAlias, the latter is @@ -56,6 +56,7 @@ def _idfunc(_, x): 'Tuple', 'Type', 'TypeVar', + 'TypeVarTuple', 'Union', # ABCs (from collections.abc). @@ -139,6 +140,7 @@ def _idfunc(_, x): 'TYPE_CHECKING', 'TypeAlias', 'TypeGuard', + 'Unpack', ] # The pseudo-submodules 're' and 'io' are part of the public @@ -182,7 +184,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms= 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.UnionType, ParamSpec, - ParamSpecArgs, ParamSpecKwargs)): + ParamSpecArgs, ParamSpecKwargs, TypeVarTuple)): return arg if not callable(arg): raise TypeError(f"{msg} Got {arg!r:.100}.") @@ -787,8 +789,28 @@ def __repr__(self): module_repr = f', module={self.__forward_module__!r}' return f'ForwardRef({self.__forward_arg__!r}{module_repr})' -class _TypeVarLike: - """Mixin for TypeVar-like types (TypeVar and ParamSpec).""" + +def _is_unpacked_typevartuple(x: Any) -> bool: + return ( + isinstance(x, _UnpackGenericAlias) + # If x is Unpack[tuple[...]], __parameters__ will be empty. + and x.__parameters__ + and isinstance(x.__parameters__[0], TypeVarTuple) + ) + + +def _is_typevar_like(x: Any) -> bool: + return isinstance(x, (TypeVar, ParamSpec)) or _is_unpacked_typevartuple(x) + + +class _BoundVarianceMixin: + """Mixin giving __init__ bound and variance arguments. + + This is used by TypeVar and ParamSpec, which both employ the notions of + a type 'bound' (restricting type arguments to be a subtype of some + specified type) and type 'variance' (determining subtype relations between + generic types). + """ def __init__(self, bound, covariant, contravariant): """Used to setup TypeVars and ParamSpec's bound, covariant and contravariant attributes. @@ -821,7 +843,7 @@ def __reduce__(self): return self.__name__ -class TypeVar( _Final, _Immutable, _TypeVarLike, _root=True): +class TypeVar(_Final, _Immutable, _BoundVarianceMixin, _root=True): """Type variable. Usage:: @@ -880,6 +902,39 @@ def __init__(self, name, *constraints, bound=None, self.__module__ = def_mod +class TypeVarTuple(_Final, _Immutable, _root=True): + """Type variable tuple. + + Usage: + + Ts = TypeVarTuple('Ts') # Can be given any name + + Just as a TypeVar (type variable) is a placeholder for a single type, + a TypeVarTuple is a placeholder for an *arbitrary* number of types. For + example, if we define a generic class using a TypeVarTuple: + + class C(Generic[*Ts]): ... + + Then we can parameterize that class with an arbitrary number of type + arguments: + + C[int] # Fine + C[int, str] # Also fine + C[()] # Even this is fine + + For more details, see PEP 646. + """ + + def __init__(self, name): + self._name = name + + def __iter__(self): + yield Unpack[self] + + def __repr__(self): + return self._name + + class ParamSpecArgs(_Final, _Immutable, _root=True): """The args for a ParamSpec object. @@ -928,7 +983,7 @@ def __eq__(self, other): return self.__origin__ == other.__origin__ -class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): +class ParamSpec(_Final, _Immutable, _BoundVarianceMixin, _root=True): """Parameter specification variable. Usage:: @@ -1059,6 +1114,45 @@ def __dir__(self): return list(set(super().__dir__() + [attr for attr in dir(self.__origin__) if not _is_dunder(attr)])) + +def _is_unpacked_tuple(x: Any) -> bool: + # Is `x` something like `*tuple[int]` or `*tuple[int, ...]`? + if not isinstance(x, _UnpackGenericAlias): + return False + # Alright, `x` is `Unpack[something]`. + + # `x` will always have `__args__`, because Unpack[] and Unpack[()] + # aren't legal. + unpacked_type = x.__args__[0] + + return getattr(unpacked_type, '__origin__', None) is tuple + + +def _is_unpacked_arbitrary_length_tuple(x: Any) -> bool: + if not _is_unpacked_tuple(x): + return False + unpacked_tuple = x.__args__[0] + + if not hasattr(unpacked_tuple, '__args__'): + # It's `Unpack[tuple]`. We can't make any assumptions about the length + # of the tuple, so it's effectively an arbitrary-length tuple. + return True + + tuple_args = unpacked_tuple.__args__ + if not tuple_args: + # It's `Unpack[tuple[()]]`. + return False + + last_arg = tuple_args[-1] + if last_arg is Ellipsis: + # It's `Unpack[tuple[something, ...]]`, which is arbitrary-length. + return True + + # If the arguments didn't end with an ellipsis, then it's not an + # arbitrary-length tuple. + return False + + # Special typing constructs Union, Optional, Generic, Callable and Tuple # use three special attributes for internal bookkeeping of generic types: # * __parameters__ is a tuple of unique free type parameters of a generic @@ -1097,7 +1191,7 @@ class _GenericAlias(_BaseGenericAlias, _root=True): # TypeVar[bool] def __init__(self, origin, args, *, inst=True, name=None, - _typevar_types=TypeVar, + _typevar_types=(TypeVar, TypeVarTuple), _paramspec_tvars=False): super().__init__(origin, inst=inst, name=name) if not isinstance(args, tuple): @@ -1154,7 +1248,10 @@ def __getitem__(self, args): if (self._paramspec_tvars and any(isinstance(t, ParamSpec) for t in self.__parameters__)): args = _prepare_paramspec_params(self, args) - else: + elif not any(isinstance(p, TypeVarTuple) for p in self.__parameters__): + # We only run this if there are no TypeVarTuples, because we + # don't check variadic generic arity at runtime (to reduce + # complexity of typing.py). _check_generic(self, args, len(self.__parameters__)) new_args = self._determine_new_args(args) @@ -1176,6 +1273,10 @@ def _determine_new_args(self, args): # anything more exotic than a plain `TypeVar`, we need to consider # edge cases. + if any(isinstance(p, TypeVarTuple) for p in self.__parameters__): + raise NotImplementedError( + "Type substitution for TypeVarTuples is not yet implemented" + ) # In the example above, this would be {T3: str} new_arg_by_param = dict(zip(self.__parameters__, args)) @@ -1189,6 +1290,10 @@ def _determine_new_args(self, args): f"ParamSpec, or Concatenate. Got {new_arg}") elif isinstance(old_arg, self._typevar_types): new_arg = new_arg_by_param[old_arg] + elif (TypeVarTuple in self._typevar_types + and _is_unpacked_typevartuple(old_arg)): + original_typevartuple = old_arg.__parameters__[0] + new_arg = new_arg_by_param[original_typevartuple] elif isinstance(old_arg, (_GenericAlias, GenericAlias, types.UnionType)): subparams = old_arg.__parameters__ if not subparams: @@ -1211,6 +1316,17 @@ def _determine_new_args(self, args): # ...we need to be careful; `new_args` should end up as # `(int, str, float)` rather than `([int, str], float)`. new_args.extend(new_arg) + elif _is_unpacked_typevartuple(old_arg): + # Consider the following `_GenericAlias`, `B`: + # class A(Generic[*Ts]): ... + # B = A[T, *Ts] + # If we then do: + # B[float, int, str] + # The `new_arg` corresponding to `T` will be `float`, and the + # `new_arg` corresponding to `*Ts` will be `(int, str)`. We + # should join all these types together in a flat list + # `(float, int, str)` - so again, we should `extend`. + new_args.extend(new_arg) else: new_args.append(new_arg) @@ -1224,7 +1340,11 @@ def __repr__(self): name = 'typing.' + self._name else: name = _type_repr(self.__origin__) - args = ", ".join([_type_repr(a) for a in self.__args__]) + if self.__args__: + args = ", ".join([_type_repr(a) for a in self.__args__]) + else: + # To ensure the repr is eval-able. + args = "()" return f'{name}[{args}]' def __reduce__(self): @@ -1252,6 +1372,9 @@ def __mro_entries__(self, bases): return () return (self.__origin__,) + def __iter__(self): + yield Unpack[self] + # _nparams is the number of accepted parameters, e.g. 0 for Hashable, # 1 for List and 2 for Dict. It may be -1 if variable number of @@ -1359,10 +1482,10 @@ def __getitem__(self, params): return self.copy_with((_TypingEmpty,)) if not isinstance(params, tuple): params = (params,) - if len(params) == 2 and params[1] is ...: + if len(params) >= 2 and params[-1] is ...: msg = "Tuple[t, ...]: t must be a type." - p = _type_check(params[0], msg) - return self.copy_with((p, _TypingEllipsis)) + params = tuple(_type_check(p, msg) for p in params[:-1]) + return self.copy_with((*params, _TypingEllipsis)) msg = "Tuple[t0, t1, ...]: each t must be a type." params = tuple(_type_check(p, msg) for p in params) return self.copy_with(params) @@ -1435,6 +1558,48 @@ def copy_with(self, params): return super().copy_with(params) +@_SpecialForm +def Unpack(self, parameters): + """Type unpack operator. + + The type unpack operator takes the child types from some container type, + such as `tuple[int, str]` or a `TypeVarTuple`, and 'pulls them out'. For + example: + + # For some generic class `Foo`: + Foo[Unpack[tuple[int, str]]] # Equivalent to Foo[int, str] + + Ts = TypeVarTuple('Ts') + # Specifies that `Bar` is generic in an arbitrary number of types. + # (Think of `Ts` as a tuple of an arbitrary number of individual + # `TypeVar`s, which the `Unpack` is 'pulling out' directly into the + # `Generic[]`.) + class Bar(Generic[Unpack[Ts]]): ... + Bar[int] # Valid + Bar[int, str] # Also valid + + From Python 3.11, this can also be done using the `*` operator: + + Foo[*tuple[int, str]] + class Bar(Generic[*Ts]): ... + + Note that there is only some runtime checking of this operator. Not + everything the runtime allows may be accepted by static type checkers. + + For more information, see PEP 646. + """ + item = _type_check(parameters, f'{self} accepts only single type.') + return _UnpackGenericAlias(origin=self, args=(item,)) + + +class _UnpackGenericAlias(_GenericAlias, _root=True): + + def __repr__(self): + # `Unpack` only takes one argument, so __args__ should contain only + # a single item. + return '*' + repr(self.__args__[0]) + + class Generic: """Abstract base class for generic types. @@ -1460,15 +1625,36 @@ def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT: @_tp_cache def __class_getitem__(cls, params): + """Parameterizes a generic class. + + At least, parameterizing a generic class is the *main* thing this method + does. For example, for some generic class `Foo`, this is called when we + do `Foo[int]` - there, with `cls=Foo` and `params=int`. + + However, note that this method is also called when defining generic + classes in the first place with `class Foo(Generic[T]): ...`. + """ if not isinstance(params, tuple): params = (params,) - if not params and cls is not Tuple: - raise TypeError( - f"Parameter list to {cls.__qualname__}[...] cannot be empty") + + if not params: + # We're only ok with `params` being empty if the class's only type + # parameter is a `TypeVarTuple` (which can contain zero types). + class_params = getattr(cls, "__parameters__", None) + only_class_parameter_is_typevartuple = ( + class_params is not None + and len(class_params) == 1 + and isinstance(class_params[0], TypeVarTuple) + ) + if not only_class_parameter_is_typevartuple: + raise TypeError( + f"Parameter list to {cls.__qualname__}[...] cannot be empty" + ) + 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, (TypeVar, ParamSpec)) for p in params): + if not all(_is_typevar_like(p) for p in params): raise TypeError( f"Parameters to {cls.__name__}[...] must all be type variables " f"or parameter specification variables.") @@ -1479,11 +1665,16 @@ 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) - else: + elif not any(isinstance(p, TypeVarTuple) for p in cls.__parameters__): + # We only run this if there are no TypeVarTuples, because we + # don't check variadic generic arity at runtime (to reduce + # complexity of typing.py). _check_generic(cls, params, len(cls.__parameters__)) - return _GenericAlias(cls, params, - _typevar_types=(TypeVar, ParamSpec), - _paramspec_tvars=True) + return _GenericAlias( + cls, params, + _typevar_types=(TypeVar, TypeVarTuple, ParamSpec), + _paramspec_tvars=True, + ) def __init_subclass__(cls, *args, **kwargs): super().__init_subclass__(*args, **kwargs) @@ -1495,7 +1686,9 @@ 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__, (TypeVar, ParamSpec)) + tvars = _collect_type_vars( + cls.__orig_bases__, (TypeVar, TypeVarTuple, 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/2022-01-30-22-05-53.bpo-43224.E-eT22.rst b/Misc/NEWS.d/next/Library/2022-01-30-22-05-53.bpo-43224.E-eT22.rst new file mode 100644 index 00000000000000..c248dd7b28778c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-30-22-05-53.bpo-43224.E-eT22.rst @@ -0,0 +1 @@ +Implement support for PEP 646 in typing.py.