diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index fc596e4d90b210..54ab4b2d0983a6 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -390,6 +390,10 @@ def test_cannot_be_called(self): class TypeVarTupleTests(BaseTestCase): + def assertEndsWith(self, string, tail): + if not string.endswith(tail): + self.fail(f"String {string!r} does not end with {tail!r}") + def test_instance_is_equal_to_itself(self): Ts = TypeVarTuple('Ts') self.assertEqual(Ts, Ts) @@ -449,78 +453,74 @@ 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( + self.assertEndsWith(repr(A[()]), 'A[()]') + self.assertEndsWith(repr(A[float]), 'A[float]') + self.assertEndsWith(repr(A[float, str]), 'A[float, str]') + self.assertEndsWith( + repr( A[Unpack[tuple[int, ...]]]), 'A[*tuple[int, ...]]' - )) - self.assertTrue(repr( - A[float, Unpack[tuple[int, ...]]] - ).endswith( + ) + self.assertEndsWith( + repr(A[float, Unpack[tuple[int, ...]]]), 'A[float, *tuple[int, ...]]' - )) - self.assertTrue(repr( - A[Unpack[tuple[int, ...]], str] - ).endswith( + ) + self.assertEndsWith( + repr(A[Unpack[tuple[int, ...]], str]), 'A[*tuple[int, ...], str]' - )) - self.assertTrue(repr( - A[float, Unpack[tuple[int, ...]], str] - ).endswith( + ) + self.assertEndsWith( + repr(A[float, Unpack[tuple[int, ...]], str]), 'A[float, *tuple[int, ...], str]' - )) + ) - def test_variadic_class_alias_repr_is_correct(self): + def test_single_parameters_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] + self.assertEndsWith(repr(B), 'A[*Ts]') + self.assertEndsWith(repr(B[()]), 'A[*Ts][()]') + self.assertEndsWith(repr(B[float]), 'A[*Ts][float]') + self.assertEndsWith(repr(B[float, str]), 'A[*Ts][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] + self.assertEndsWith(repr(C), 'A[*Ts, int]') + self.assertEndsWith(repr(C[()]), 'A[*Ts, int][()]') + self.assertEndsWith(repr(C[float]), 'A[*Ts, int][float]') + self.assertEndsWith(repr(C[float, str]), 'A[*Ts, int][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] + self.assertEndsWith(repr(D), 'A[int, *Ts]') + self.assertEndsWith(repr(D[()]), 'A[int, *Ts][()]') + self.assertEndsWith(repr(D[float]), 'A[int, *Ts][float]') + self.assertEndsWith(repr(D[float, str]), 'A[int, *Ts][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] + self.assertEndsWith(repr(E), 'A[int, *Ts, str]') + self.assertEndsWith(repr(E[()]), 'A[int, *Ts, str][()]') + self.assertEndsWith(repr(E[float]), 'A[int, *Ts, str][float]') + self.assertEndsWith( + repr(E[float, bool]), + 'A[int, *Ts, str][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] + self.assertEndsWith(repr(F), 'A[*Ts, *tuple[str, ...]]') + self.assertEndsWith(repr(F[()]), 'A[*Ts, *tuple[str, ...]][()]') + self.assertEndsWith(repr(F[float]), 'A[*Ts, *tuple[str, ...]][float]') + self.assertEndsWith( + repr(F[float, int]), + 'A[*Ts, *tuple[str, ...]][float, int]' + ) + + G = A[T, Unpack[Ts]] + self.assertEndsWith(repr(G), 'A[~T, *Ts]') + self.assertEndsWith(repr(G[()]), 'A[~T, *Ts][()]') + self.assertEndsWith(repr(G[float]), 'A[~T, *Ts][float]') + self.assertEndsWith( + repr(G[float, int]), + 'A[~T, *Ts][float, int]' + ) def test_cannot_subclass_class(self): with self.assertRaises(TypeError): diff --git a/Lib/typing.py b/Lib/typing.py index e3015563b3e8ce..310f7623058be5 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1262,6 +1262,11 @@ def __getitem__(self, args): # complexity of typing.py). _check_generic(self, args, len(self.__parameters__)) + if any(isinstance(p, TypeVarTuple) for p in self.__parameters__): + # Determining the new type arguments is hard if `TypeVarTuple`s are + # involved, so we leave arguments unsubstituted. + return _UnsubstitutedGenericAlias(generic_alias=self, args=args) + new_args = self._determine_new_args(args) r = self.copy_with(new_args) return r @@ -1281,10 +1286,6 @@ 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)) @@ -1386,6 +1387,53 @@ def __iter__(self): yield Unpack[self] +class _UnsubstitutedGenericAlias: + """A generic alias whose type arguments have not been substituted. + + For example, suppose we defined a class `C` such that: + + >>> T1 = TypeVar('T1') + >>> T2 = TypeVar('T2') + >>> Ts = TypeVar('Ts') + >>> class C(Generic[T1, T2, *Ts]): ... + + We could then define a generic alias `A` using this class: + + >>> A = C[T1, int, *Ts] + >>> repr(A) + C[T1, int, *Ts] + + However, when we then do + + >>> B = A[str, float] + + we need to figure out that the new argument list to `C` is + [str, int, float], and for that we need to figure out how to substitute + the type arguments `(str, float)` into the remaining free type variables + `(T1, *Ts)`. + + This turns out to be rather complicated when `TypeVarTuple`s are involved, + so to reduce complexity in typing.py, we instead leave the expression + unsubstituted, returning an `_UnsubstitutedGenericAlias` whose repr() + is: + + >>> repr(B) + C[int, T2, *Ts][str, float] + """ + + def __init__(self, generic_alias, args): + self._generic_alias = generic_alias + self._args = args + + def __repr__(self): + if self._args: + args = ", ".join([_type_repr(a) for a in self._args]) + else: + # To ensure the repr is eval-able. + args = "()" + return '{}[{}]'.format(repr(self._generic_alias), args) + + # _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 # parameters are accepted (needs custom __getitem__).