Skip to content

Commit 2ab8849

Browse files
authored
Update semantic analyzer for TypeVar defaults (PEP 696) (#14873)
This PR updates the semantic analyzer to support most forms of TypeVars with defaults while also providing basic argument validation. Ref: #14851
1 parent 8409b88 commit 2ab8849

6 files changed

+224
-39
lines changed

mypy/message_registry.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
181181
INVALID_TYPEVAR_ARG_BOUND: Final = 'Type argument {} of "{}" must be a subtype of {}'
182182
INVALID_TYPEVAR_ARG_VALUE: Final = 'Invalid type argument value for "{}"'
183183
TYPEVAR_VARIANCE_DEF: Final = 'TypeVar "{}" may only be a literal bool'
184-
TYPEVAR_BOUND_MUST_BE_TYPE: Final = 'TypeVar "bound" must be a type'
184+
TYPEVAR_ARG_MUST_BE_TYPE: Final = '{} "{}" must be a type'
185185
TYPEVAR_UNEXPECTED_ARGUMENT: Final = 'Unexpected argument to "TypeVar()"'
186186
UNBOUND_TYPEVAR: Final = (
187187
"A function returning TypeVar should receive at least "

mypy/semanal.py

+124-28
Original file line numberDiff line numberDiff line change
@@ -4135,28 +4135,15 @@ def process_typevar_parameters(
41354135
if has_values:
41364136
self.fail("TypeVar cannot have both values and an upper bound", context)
41374137
return None
4138-
try:
4139-
# We want to use our custom error message below, so we suppress
4140-
# the default error message for invalid types here.
4141-
analyzed = self.expr_to_analyzed_type(
4142-
param_value, allow_placeholder=True, report_invalid_types=False
4143-
)
4144-
if analyzed is None:
4145-
# Type variables are special: we need to place them in the symbol table
4146-
# soon, even if upper bound is not ready yet. Otherwise avoiding
4147-
# a "deadlock" in this common pattern would be tricky:
4148-
# T = TypeVar('T', bound=Custom[Any])
4149-
# class Custom(Generic[T]):
4150-
# ...
4151-
analyzed = PlaceholderType(None, [], context.line)
4152-
upper_bound = get_proper_type(analyzed)
4153-
if isinstance(upper_bound, AnyType) and upper_bound.is_from_error:
4154-
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
4155-
# Note: we do not return 'None' here -- we want to continue
4156-
# using the AnyType as the upper bound.
4157-
except TypeTranslationError:
4158-
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
4138+
tv_arg = self.get_typevarlike_argument("TypeVar", param_name, param_value, context)
4139+
if tv_arg is None:
41594140
return None
4141+
upper_bound = tv_arg
4142+
elif param_name == "default":
4143+
tv_arg = self.get_typevarlike_argument(
4144+
"TypeVar", param_name, param_value, context, allow_unbound_tvars=True
4145+
)
4146+
default = tv_arg or AnyType(TypeOfAny.from_error)
41604147
elif param_name == "values":
41614148
# Probably using obsolete syntax with values=(...). Explain the current syntax.
41624149
self.fail('TypeVar "values" argument not supported', context)
@@ -4184,6 +4171,52 @@ def process_typevar_parameters(
41844171
variance = INVARIANT
41854172
return variance, upper_bound, default
41864173

4174+
def get_typevarlike_argument(
4175+
self,
4176+
typevarlike_name: str,
4177+
param_name: str,
4178+
param_value: Expression,
4179+
context: Context,
4180+
*,
4181+
allow_unbound_tvars: bool = False,
4182+
allow_param_spec_literals: bool = False,
4183+
report_invalid_typevar_arg: bool = True,
4184+
) -> ProperType | None:
4185+
try:
4186+
# We want to use our custom error message below, so we suppress
4187+
# the default error message for invalid types here.
4188+
analyzed = self.expr_to_analyzed_type(
4189+
param_value,
4190+
allow_placeholder=True,
4191+
report_invalid_types=False,
4192+
allow_unbound_tvars=allow_unbound_tvars,
4193+
allow_param_spec_literals=allow_param_spec_literals,
4194+
)
4195+
if analyzed is None:
4196+
# Type variables are special: we need to place them in the symbol table
4197+
# soon, even if upper bound is not ready yet. Otherwise avoiding
4198+
# a "deadlock" in this common pattern would be tricky:
4199+
# T = TypeVar('T', bound=Custom[Any])
4200+
# class Custom(Generic[T]):
4201+
# ...
4202+
analyzed = PlaceholderType(None, [], context.line)
4203+
typ = get_proper_type(analyzed)
4204+
if report_invalid_typevar_arg and isinstance(typ, AnyType) and typ.is_from_error:
4205+
self.fail(
4206+
message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name),
4207+
param_value,
4208+
)
4209+
# Note: we do not return 'None' here -- we want to continue
4210+
# using the AnyType.
4211+
return typ
4212+
except TypeTranslationError:
4213+
if report_invalid_typevar_arg:
4214+
self.fail(
4215+
message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name),
4216+
param_value,
4217+
)
4218+
return None
4219+
41874220
def extract_typevarlike_name(self, s: AssignmentStmt, call: CallExpr) -> str | None:
41884221
if not call:
41894222
return None
@@ -4216,13 +4249,50 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool:
42164249
if name is None:
42174250
return False
42184251

4219-
# ParamSpec is different from a regular TypeVar:
4220-
# arguments are not semantically valid. But, allowed in runtime.
4221-
# So, we need to warn users about possible invalid usage.
4222-
if len(call.args) > 1:
4223-
self.fail("Only the first argument to ParamSpec has defined semantics", s)
4252+
n_values = call.arg_kinds[1:].count(ARG_POS)
4253+
if n_values != 0:
4254+
self.fail('Too many positional arguments for "ParamSpec"', s)
42244255

42254256
default: Type = AnyType(TypeOfAny.from_omitted_generics)
4257+
for param_value, param_name in zip(
4258+
call.args[1 + n_values :], call.arg_names[1 + n_values :]
4259+
):
4260+
if param_name == "default":
4261+
tv_arg = self.get_typevarlike_argument(
4262+
"ParamSpec",
4263+
param_name,
4264+
param_value,
4265+
s,
4266+
allow_unbound_tvars=True,
4267+
allow_param_spec_literals=True,
4268+
report_invalid_typevar_arg=False,
4269+
)
4270+
default = tv_arg or AnyType(TypeOfAny.from_error)
4271+
if isinstance(tv_arg, Parameters):
4272+
for i, arg_type in enumerate(tv_arg.arg_types):
4273+
typ = get_proper_type(arg_type)
4274+
if isinstance(typ, AnyType) and typ.is_from_error:
4275+
self.fail(
4276+
f"Argument {i} of ParamSpec default must be a type", param_value
4277+
)
4278+
elif (
4279+
isinstance(default, AnyType)
4280+
and default.is_from_error
4281+
or not isinstance(default, (AnyType, UnboundType))
4282+
):
4283+
self.fail(
4284+
"The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec",
4285+
param_value,
4286+
)
4287+
default = AnyType(TypeOfAny.from_error)
4288+
else:
4289+
# ParamSpec is different from a regular TypeVar:
4290+
# arguments are not semantically valid. But, allowed in runtime.
4291+
# So, we need to warn users about possible invalid usage.
4292+
self.fail(
4293+
"The variance and bound arguments to ParamSpec do not have defined semantics yet",
4294+
s,
4295+
)
42264296

42274297
# PEP 612 reserves the right to define bound, covariant and contravariant arguments to
42284298
# ParamSpec in a later PEP. If and when that happens, we should do something
@@ -4256,10 +4326,32 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
42564326
if not call:
42574327
return False
42584328

4259-
if len(call.args) > 1:
4260-
self.fail("Only the first argument to TypeVarTuple has defined semantics", s)
4329+
n_values = call.arg_kinds[1:].count(ARG_POS)
4330+
if n_values != 0:
4331+
self.fail('Too many positional arguments for "TypeVarTuple"', s)
42614332

42624333
default: Type = AnyType(TypeOfAny.from_omitted_generics)
4334+
for param_value, param_name in zip(
4335+
call.args[1 + n_values :], call.arg_names[1 + n_values :]
4336+
):
4337+
if param_name == "default":
4338+
tv_arg = self.get_typevarlike_argument(
4339+
"TypeVarTuple",
4340+
param_name,
4341+
param_value,
4342+
s,
4343+
allow_unbound_tvars=True,
4344+
report_invalid_typevar_arg=False,
4345+
)
4346+
default = tv_arg or AnyType(TypeOfAny.from_error)
4347+
if not isinstance(default, UnpackType):
4348+
self.fail(
4349+
"The default argument to TypeVarTuple must be an Unpacked tuple",
4350+
param_value,
4351+
)
4352+
default = AnyType(TypeOfAny.from_error)
4353+
else:
4354+
self.fail(f'Unexpected keyword argument "{param_name}" for "TypeVarTuple"', s)
42634355

42644356
if not self.incomplete_feature_enabled(TYPE_VAR_TUPLE, s):
42654357
return False
@@ -6359,6 +6451,8 @@ def expr_to_analyzed_type(
63596451
report_invalid_types: bool = True,
63606452
allow_placeholder: bool = False,
63616453
allow_type_any: bool = False,
6454+
allow_unbound_tvars: bool = False,
6455+
allow_param_spec_literals: bool = False,
63626456
) -> Type | None:
63636457
if isinstance(expr, CallExpr):
63646458
# This is a legacy syntax intended mostly for Python 2, we keep it for
@@ -6387,6 +6481,8 @@ def expr_to_analyzed_type(
63876481
report_invalid_types=report_invalid_types,
63886482
allow_placeholder=allow_placeholder,
63896483
allow_type_any=allow_type_any,
6484+
allow_unbound_tvars=allow_unbound_tvars,
6485+
allow_param_spec_literals=allow_param_spec_literals,
63906486
)
63916487

63926488
def analyze_type_expr(self, expr: Expression) -> None:

mypy/types.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -3049,6 +3049,8 @@ def visit_type_var(self, t: TypeVarType) -> str:
30493049
s = f"{t.name}`{t.id}"
30503050
if self.id_mapper and t.upper_bound:
30513051
s += f"(upper_bound={t.upper_bound.accept(self)})"
3052+
if t.has_default():
3053+
s += f" = {t.default.accept(self)}"
30523054
return s
30533055

30543056
def visit_param_spec(self, t: ParamSpecType) -> str:
@@ -3064,6 +3066,8 @@ def visit_param_spec(self, t: ParamSpecType) -> str:
30643066
s += f"{t.name_with_suffix()}`{t.id}"
30653067
if t.prefix.arg_types:
30663068
s += "]"
3069+
if t.has_default():
3070+
s += f" = {t.default.accept(self)}"
30673071
return s
30683072

30693073
def visit_parameters(self, t: Parameters) -> str:
@@ -3102,6 +3106,8 @@ def visit_type_var_tuple(self, t: TypeVarTupleType) -> str:
31023106
else:
31033107
# Named type variable type.
31043108
s = f"{t.name}`{t.id}"
3109+
if t.has_default():
3110+
s += f" = {t.default.accept(self)}"
31053111
return s
31063112

31073113
def visit_callable_type(self, t: CallableType) -> str:
@@ -3138,6 +3144,8 @@ def visit_callable_type(self, t: CallableType) -> str:
31383144
if s:
31393145
s += ", "
31403146
s += f"*{n}.args, **{n}.kwargs"
3147+
if param_spec.has_default():
3148+
s += f" = {param_spec.default.accept(self)}"
31413149

31423150
s = f"({s})"
31433151

@@ -3156,12 +3164,18 @@ def visit_callable_type(self, t: CallableType) -> str:
31563164
vals = f"({', '.join(val.accept(self) for val in var.values)})"
31573165
vs.append(f"{var.name} in {vals}")
31583166
elif not is_named_instance(var.upper_bound, "builtins.object"):
3159-
vs.append(f"{var.name} <: {var.upper_bound.accept(self)}")
3167+
vs.append(
3168+
f"{var.name} <: {var.upper_bound.accept(self)}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
3169+
)
31603170
else:
3161-
vs.append(var.name)
3171+
vs.append(
3172+
f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
3173+
)
31623174
else:
3163-
# For other TypeVarLikeTypes, just use the name
3164-
vs.append(var.name)
3175+
# For other TypeVarLikeTypes, use the name and default
3176+
vs.append(
3177+
f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
3178+
)
31653179
s = f"[{', '.join(vs)}] {s}"
31663180

31673181
return f"def {s}"

test-data/unit/check-parameter-specification.test

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ P = ParamSpec('P')
66
[case testInvalidParamSpecDefinitions]
77
from typing import ParamSpec
88

9-
P1 = ParamSpec("P1", covariant=True) # E: Only the first argument to ParamSpec has defined semantics
10-
P2 = ParamSpec("P2", contravariant=True) # E: Only the first argument to ParamSpec has defined semantics
11-
P3 = ParamSpec("P3", bound=int) # E: Only the first argument to ParamSpec has defined semantics
12-
P4 = ParamSpec("P4", int, str) # E: Only the first argument to ParamSpec has defined semantics
13-
P5 = ParamSpec("P5", covariant=True, bound=int) # E: Only the first argument to ParamSpec has defined semantics
9+
P1 = ParamSpec("P1", covariant=True) # E: The variance and bound arguments to ParamSpec do not have defined semantics yet
10+
P2 = ParamSpec("P2", contravariant=True) # E: The variance and bound arguments to ParamSpec do not have defined semantics yet
11+
P3 = ParamSpec("P3", bound=int) # E: The variance and bound arguments to ParamSpec do not have defined semantics yet
12+
P4 = ParamSpec("P4", int, str) # E: Too many positional arguments for "ParamSpec"
13+
P5 = ParamSpec("P5", covariant=True, bound=int) # E: The variance and bound arguments to ParamSpec do not have defined semantics yet
1414
[builtins fixtures/paramspec.pyi]
1515

1616
[case testParamSpecLocations]
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
[case testTypeVarDefaultsBasic]
2+
import builtins
3+
from typing import Generic, TypeVar, ParamSpec, Callable, Tuple, List
4+
from typing_extensions import TypeVarTuple, Unpack
5+
6+
T1 = TypeVar("T1", default=int)
7+
P1 = ParamSpec("P1", default=[int, str])
8+
Ts1 = TypeVarTuple("Ts1", default=Unpack[Tuple[int, str]])
9+
10+
def f1(a: T1) -> List[T1]: ...
11+
reveal_type(f1) # N: Revealed type is "def [T1 = builtins.int] (a: T1`-1 = builtins.int) -> builtins.list[T1`-1 = builtins.int]"
12+
13+
def f2(a: Callable[P1, None] ) -> Callable[P1, None]: ...
14+
reveal_type(f2) # N: Revealed type is "def [P1 = [builtins.int, builtins.str]] (a: def (*P1.args, **P1.kwargs)) -> def (*P1.args, **P1.kwargs)"
15+
16+
def f3(a: Tuple[Unpack[Ts1]]) -> Tuple[Unpack[Ts1]]: ...
17+
reveal_type(f3) # N: Revealed type is "def [Ts1 = Unpack[Tuple[builtins.int, builtins.str]]] (a: Tuple[Unpack[Ts1`-1 = Unpack[Tuple[builtins.int, builtins.str]]]]) -> Tuple[Unpack[Ts1`-1 = Unpack[Tuple[builtins.int, builtins.str]]]]"
18+
19+
20+
class ClassA1(Generic[T1]): ...
21+
class ClassA2(Generic[P1]): ...
22+
class ClassA3(Generic[Unpack[Ts1]]): ...
23+
24+
reveal_type(ClassA1) # N: Revealed type is "def [T1 = builtins.int] () -> __main__.ClassA1[T1`1 = builtins.int]"
25+
reveal_type(ClassA2) # N: Revealed type is "def [P1 = [builtins.int, builtins.str]] () -> __main__.ClassA2[P1`1 = [builtins.int, builtins.str]]"
26+
reveal_type(ClassA3) # N: Revealed type is "def [Ts1 = Unpack[Tuple[builtins.int, builtins.str]]] () -> __main__.ClassA3[Unpack[Ts1`1 = Unpack[Tuple[builtins.int, builtins.str]]]]"
27+
[builtins fixtures/tuple.pyi]
28+
29+
[case testTypeVarDefaultsValid]
30+
from typing import TypeVar, ParamSpec, Any, List, Tuple
31+
from typing_extensions import TypeVarTuple, Unpack
32+
33+
S0 = TypeVar("S0")
34+
S1 = TypeVar("S1", bound=int)
35+
36+
P0 = ParamSpec("P0")
37+
Ts0 = TypeVarTuple("Ts0")
38+
39+
T1 = TypeVar("T1", default=int)
40+
T2 = TypeVar("T2", bound=float, default=int)
41+
T3 = TypeVar("T3", bound=List[Any], default=List[int])
42+
T4 = TypeVar("T4", int, str, default=int)
43+
T5 = TypeVar("T5", default=S0)
44+
T6 = TypeVar("T6", bound=float, default=S1)
45+
# T7 = TypeVar("T7", bound=List[Any], default=List[S0]) # TODO
46+
47+
P1 = ParamSpec("P1", default=[])
48+
P2 = ParamSpec("P2", default=...)
49+
P3 = ParamSpec("P3", default=[int, str])
50+
P4 = ParamSpec("P4", default=P0)
51+
52+
Ts1 = TypeVarTuple("Ts1", default=Unpack[Tuple[int]])
53+
Ts2 = TypeVarTuple("Ts2", default=Unpack[Tuple[int, ...]])
54+
# Ts3 = TypeVarTuple("Ts3", default=Unpack[Ts0]) # TODO
55+
[builtins fixtures/tuple.pyi]
56+
57+
[case testTypeVarDefaultsInvalid]
58+
from typing import TypeVar, ParamSpec, Tuple
59+
from typing_extensions import TypeVarTuple, Unpack
60+
61+
T1 = TypeVar("T1", default=2) # E: TypeVar "default" must be a type
62+
T2 = TypeVar("T2", default=[int, str]) # E: Bracketed expression "[...]" is not valid as a type \
63+
# N: Did you mean "List[...]"? \
64+
# E: TypeVar "default" must be a type
65+
66+
P1 = ParamSpec("P1", default=int) # E: The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec
67+
P2 = ParamSpec("P2", default=2) # E: The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec
68+
P3 = ParamSpec("P3", default=(2, int)) # E: The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec
69+
P4 = ParamSpec("P4", default=[2, int]) # E: Argument 0 of ParamSpec default must be a type
70+
71+
Ts1 = TypeVarTuple("Ts1", default=2) # E: The default argument to TypeVarTuple must be an Unpacked tuple
72+
Ts2 = TypeVarTuple("Ts2", default=int) # E: The default argument to TypeVarTuple must be an Unpacked tuple
73+
Ts3 = TypeVarTuple("Ts3", default=Tuple[int]) # E: The default argument to TypeVarTuple must be an Unpacked tuple
74+
[builtins fixtures/tuple.pyi]

test-data/unit/semanal-errors.test

+2-1
Original file line numberDiff line numberDiff line change
@@ -1465,8 +1465,9 @@ TVariadic2 = TypeVarTuple('TVariadic2')
14651465
TP = TypeVarTuple('?') # E: String argument 1 "?" to TypeVarTuple(...) does not match variable name "TP"
14661466
TP2: int = TypeVarTuple('TP2') # E: Cannot declare the type of a TypeVar or similar construct
14671467
TP3 = TypeVarTuple() # E: Too few arguments for TypeVarTuple()
1468-
TP4 = TypeVarTuple('TP4', 'TP4') # E: Only the first argument to TypeVarTuple has defined semantics
1468+
TP4 = TypeVarTuple('TP4', 'TP4') # E: Too many positional arguments for "TypeVarTuple"
14691469
TP5 = TypeVarTuple(t='TP5') # E: TypeVarTuple() expects a string literal as first argument
1470+
TP6 = TypeVarTuple('TP6', bound=int) # E: Unexpected keyword argument "bound" for "TypeVarTuple"
14701471

14711472
x: TVariadic # E: TypeVarTuple "TVariadic" is unbound
14721473
y: Unpack[TVariadic] # E: TypeVarTuple "TVariadic" is unbound

0 commit comments

Comments
 (0)