Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-43224: Work around substitution of unpacked TypeVarTuple #31804

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
110 changes: 55 additions & 55 deletions Lib/test/test_typing.py
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
56 changes: 52 additions & 4 deletions Lib/typing.py
Expand Up @@ -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
Expand All @@ -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))

Expand Down Expand Up @@ -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__).
Expand Down