8000 Allow mixing ParamSpec and TypeVarTuple in Generic (#17450) · python/mypy@69042d3 · GitHub
[go: up one dir, main page]

Skip to content

Commit 69042d3

Browse files
Allow mixing ParamSpec and TypeVarTuple in Generic (#17450)
Fixes #16696 Fixes #16695 I think there are no good reasons to not allow this anymore. Also I am using this opportunity to tighten a bit invalid instances/aliases where a regular type variable is replaced with parameters and vice versa. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent b88fdbd commit 69042d3

File tree

6 files changed

+65
-31
lines changed

6 files changed

+65
-31
lines changed

mypy/erasetype.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
get_proper_type,
3535
get_proper_types,
3636
)
37+
from mypy.typevartuples import erased_vars
3738

3839

3940
def erase_type(typ: Type) -> ProperType:
@@ -77,17 +78,7 @@ def visit_deleted_type(self, t: DeletedType) -> ProperType:
7778
return t
7879

7980
def visit_instance(self, t: Instance) -> ProperType:
80-
args: list[Type] = []
81-
for tv in t.type.defn.type_vars:
82-
# Valid erasure for *Ts is *tuple[Any, ...], not just Any.
83-
if isinstance(tv, TypeVarTupleType):
84-
args.append(
85-
UnpackType(
86-
tv.tuple_fallback.copy_modified(args=[AnyType(TypeOfAny.special_form)])
87-
)
88-
)
89-
else:
90-
args.append(AnyType(TypeOfAny.special_form))
81+
args = erased_vars(t.type.defn.type_vars, TypeOfAny.special_form)
9182
return Instance(t.type, args, t.line)
9283

9384
def visit_type_var(self, t: TypeVarType) -> ProperType:

mypy/nodes.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3165,9 +3165,6 @@ def add_type_vars(self) -> None:
31653165
self.type_var_tuple_prefix = i
31663166
self.type_var_tuple_suffix = len(self.defn.type_vars) - i - 1
31673167
self.type_vars.append(vd.name)
3168-
assert not (
3169-
self.has_param_spec_type and self.has_type_var_tuple_type
3170-
), "Mixing type var tuples and param specs not supported yet"
31713168

31723169
@property
31733170
def name(self) -> str:

mypy/semanal_typeargs.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
get_proper_types,
4040
split_with_prefix_and_suffix,
4141
)
42+
from mypy.typevartuples import erased_vars
4243

4344

4445
class TypeArgumentAnalyzer(MixedTraverserVisitor):
@@ -89,7 +90,14 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None:
8990
return
9091
self.seen_aliases.add(t)
9192
assert t.alias is not None, f"Unfixed type alias {t.type_ref}"
92-
is_error = self.validate_args(t.alias.name, tuple(t.args), t.alias.alias_tvars, t)
93+
is_error, is_invalid = self.validate_args(
94+
t.alias.name, tuple(t.args), t.alias.alias_tvars, t
95+
)
96+
if is_invalid:
97+
# If there is an arity error (e.g. non-Parameters used for ParamSpec etc.),
98+
# then it is safer to erase the arguments completely, to avoid crashes later.
99+
# TODO: can we move this logic to typeanal.py?
100+
t.args = erased_vars(t.alias.alias_tvars, TypeOfAny.from_error)
93101
if not is_error:
94102
# If there was already an error for the alias itself, there is no point in checking
95103
# the expansion, most likely it will result in the same kind of error.
@@ -113,7 +121,9 @@ def visit_instance(self, t: Instance) -> None:
113121
info = t.type
114122
if isinstance(info, FakeInfo):
115123
return # https://github.com/python/mypy/issues/11079
116-
self.validate_args(info.name, t.args, info.defn.type_vars, t)
124+
_, is_invalid = self.validate_args(info.name, t.args, info.defn.type_vars, t)
125+
if is_invalid:
126+
t.args = tuple(erased_vars(info.defn.type_vars, TypeOfAny.from_error))
117127
if t.type.fullname == "builtins.tuple" and len(t.args) == 1:
118128
# Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...]
119129
arg = t.args[0]
@@ -125,7 +135,7 @@ def visit_instance(self, t: Instance) -> None:
125135

126136
def validate_args(
127137
self, name: str, args: tuple[Type, ...], type_vars: list[TypeVarLikeType], ctx: Context
128-
) -> bool:
138+
) -> tuple[bool, bool]:
129139
if any(isinstance(v, TypeVarTupleType) for v in type_vars):
130140
prefix = next(i for (i, v) in enumerate(type_vars) if isinstance(v, TypeVarTupleType))
131141
tvt = type_vars[prefix]
@@ -136,10 +146,11 @@ def validate_args(
136146
args = start + (TupleType(list(middle), tvt.tuple_fallback),) + end
137147

138148
is_error = False
149+
is_invalid = False
139150
for (i, arg), tvar in zip(enumerate(args), type_vars):
140151
if isinstance(tvar, TypeVarType):
141152
if isinstance(arg, ParamSpecType):
142-
is_error = True
153+
is_invalid = True
143154
self.fail(
144155
INVALID_PARAM_SPEC_LOCATION.format(format_type(arg, self.options)),
145156
ctx,
@@ -152,7 +163,7 @@ def validate_args(
152163
)
153164
continue
154165
if isinstance(arg, Parameters):
155-
is_error = True
166+
is_invalid = True
156167
self.fail(
157168
f"Cannot use {format_type(arg, self.options)} for regular type variable,"
158169
" only for ParamSpec",
@@ -205,13 +216,16 @@ def validate_args(
205216
if not isinstance(
206217
get_proper_type(arg), (ParamSpecType, Parameters, AnyType, UnboundType)
207218
):
219+
is_invalid = True
208220
self.fail(
209221
"Can only replace ParamSpec with a parameter types list or"
210222
f" another ParamSpec, got {format_type(arg, self.options)}",
211223
ctx,
212224
code=codes.VALID_TYPE,
213225
)
214-
return is_error
226+
if is_invalid:
227+
is_error = True
228+
return is_error, is_invalid
215229

216230
def visit_unpack_type(self, typ: UnpackType) -> None:
217231
super().visit_unpack_type(typ)

mypy/typevars.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from mypy.erasetype import erase_typevars
44
from mypy.nodes import TypeInfo
55
from mypy.types import (
6-
AnyType,
76
Instance,
87
ParamSpecType,
98
ProperType,
@@ -15,6 +14,7 @@
1514
TypeVarType,
1615
UnpackType,
1716
)
17+
from mypy.typevartuples import erased_vars
1818

1919

2020
def fill_typevars(typ: TypeInfo) -> Instance | TupleType:
@@ -64,16 +64,7 @@ def fill_typevars(typ: TypeInfo) -> Instance | TupleType:
6464

6565
def fill_typevars_with_any(typ: TypeInfo) -> Instance | TupleType:
6666
"""Apply a correct number of Any's as type arguments to a type."""
67-
args: list[Type] = []
68-
for tv in typ.defn.type_vars:
69-
# Valid erasure for *Ts is *tuple[Any, ...], not just Any.
70-
if isinstance(tv, TypeVarTupleType):
71-
args.append(
72-
UnpackType(tv.tuple_fallback.copy_modified(args=[AnyType(TypeOfAny.special_form)]))
73-
)
74-
else:
75-
args.append(AnyType(TypeOfAny.special_form))
76-
inst = Instance(typ, args)
67+
inst = Instance(typ, erased_vars(typ.defn.type_vars, TypeOfAny.special_form))
7768
if typ.tuple_type is None:
7869
return inst
7970
erased_tuple_type = erase_typevars(typ.tuple_type, {tv.id for tv in typ.defn.type_vars})

mypy/typevartuples.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
from typing import Sequence
66

77
from mypy.types import (
8+
AnyType,
89
Instance,
910
ProperType,
1011
Type,
12+
TypeVarLikeType,
13+
TypeVarTupleType,
1114
UnpackType,
1215
get_proper_type,
1316
split_with_prefix_and_suffix,
@@ -30,3 +33,14 @@ def extract_unpack(types: Sequence[Type]) -> ProperType | None:
3033
if isinstance(types[0], UnpackType):
3134
return get_proper_type(types[0].type)
3235
return None
36+
37+
38+
def erased_vars(type_vars: Sequence[TypeVarLikeType], type_of_any: int) -> list[Type]:
39+
args: list[Type] = []
40+
for tv in type_vars:
41+
# Valid erasure for *Ts is *tuple[Any, ...], not just Any.
42+
if isinstance(tv, TypeVarTupleType):
43+
args.append(UnpackType(tv.tuple_fallback.copy_modified(args=[AnyType(type_of_any)])))
44+
else:
45+
args.append(AnyType(type_of_any))
46+
return args

test-data/unit/check-typevar-tuple.test

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2460,3 +2460,30 @@ def test(x: T, *args: Unpack[Ts]) -> Tuple[T, Unpack[Ts]]: ...
24602460

24612461
reveal_type(test) # N: Revealed type is "def [T, Ts] (builtins.list[T`2], *args: Unpack[Ts`-2]) -> __main__.CM[Tuple[T`2, Unpack[Ts`-2]]]"
24622462
[builtins fixtures/tuple.pyi]
2463+
2464+
[case testMixingTypeVarTupleAndParamSpec]
2465+
from typing import Generic, ParamSpec, TypeVarTuple, Unpack, Callable, TypeVar
2466+
2467+
P = ParamSpec("P")
2468+
Ts = TypeVarTuple("Ts")
2469+
2470+
class A(Generic[P, Unpack[Ts]]): ...
2471+
class B(Generic[Unpack[Ts], P]): ...
2472+
2473+
a: A[[int, str], int, str]
2474+
reveal_type(a) # N: Revealed type is "__main__.A[[builtins.int, builtins.str], builtins.int, builtins.str]"
2475+
b: B[int, str, [int, str]]
2476+
reveal_type(b) # N: Revealed type is "__main__.B[builtins.int, builtins.str, [builtins.int, builtins.str]]"
2477+
2478+
x: A[int, str, [int, str]] # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "int"
2479+
reveal_type(x) # N: Revealed type is "__main__.A[Any, Unpack[builtins.tuple[Any, ...]]]"
2480+
y: B[[int, str], int, str] # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "str"
2481+
reveal_type(y) # N: Revealed type is "__main__.B[Unpack[builtins.tuple[Any, ...]], Any]"
2482+
2483+
R = TypeVar("R")
2484+
class C(Generic[P, R]):
2485+
fn: Callable[P, None]
2486+
2487+
c: C[int, str] # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "int"
2488+
reveal_type(c.fn) # N: Revealed type is "def (*Any, **Any)"
2489+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)
0