Skip to content

Commit 790e8a7

Browse files
authored
Error handling for recursive TypeVar defaults (PEP 696) (#16925)
This PR adds some additional error handling for recursive TypeVar defaults. Open issue for future PRs: - Expanding nested recursive defaults, e.g. `T2 = list[T1 = str]` - Scope binding, especially for TypeAliasTypes Ref: #14851
1 parent 46ebaca commit 790e8a7

File tree

5 files changed

+223
-12
lines changed

5 files changed

+223
-12
lines changed

mypy/messages.py

+9
Original file line numberDiff line numberDiff line change
@@ -2059,6 +2059,15 @@ def impossible_intersection(
20592059
template.format(formatted_base_class_list, reason), context, code=codes.UNREACHABLE
20602060
)
20612061

2062+
def tvar_without_default_type(
2063+
self, tvar_name: str, last_tvar_name_with_default: str, context: Context
2064+
) -> None:
2065+
self.fail(
2066+
f'"{tvar_name}" cannot appear after "{last_tvar_name_with_default}" '
2067+
"in type parameter list because it has no default type",
2068+
context,
2069+
)
2070+
20622071
def report_protocol_problems(
20632072
self,
20642073
subtype: Instance | TupleType | TypedDictType | TypeType | CallableType,

mypy/semanal.py

+38-9
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@
226226
SELF_TYPE_NAMES,
227227
FindTypeVarVisitor,
228228
TypeAnalyser,
229+
TypeVarDefaultTranslator,
229230
TypeVarLikeList,
230231
analyze_type_alias,
231232
check_for_explicit_any,
@@ -252,6 +253,7 @@
252253
TPDICT_NAMES,
253254
TYPE_ALIAS_NAMES,
254255
TYPE_CHECK_ONLY_NAMES,
256+
TYPE_VAR_LIKE_NAMES,
255257
TYPED_NAMEDTUPLE_NAMES,
256258
AnyType,
257259
CallableType,
@@ -1953,17 +1955,19 @@ class Foo(Bar, Generic[T]): ...
19531955
defn.removed_base_type_exprs.append(defn.base_type_exprs[i])
19541956
del base_type_exprs[i]
19551957
tvar_defs: list[TypeVarLikeType] = []
1958+
last_tvar_name_with_default: str | None = None
19561959
for name, tvar_expr in declared_tvars:
1957-
tvar_expr_default = tvar_expr.default
1958-
if isinstance(tvar_expr_default, UnboundType):
1959-
# TODO: - detect out of order and self-referencing TypeVars
1960-
# - nested default types, e.g. list[T1]
1961-
n = self.lookup_qualified(
1962-
tvar_expr_default.name, tvar_expr_default, suppress_errors=True
1963-
)
1964-
if n is not None and (default := self.tvar_scope.get_binding(n)) is not None:
1965-
tvar_expr.default = default
1960+
tvar_expr.default = tvar_expr.default.accept(
1961+
TypeVarDefaultTranslator(self, tvar_expr.name, context)
1962+
)
19661963
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
1964+
if last_tvar_name_with_default is not None and not tvar_def.has_default():
1965+
self.msg.tvar_without_default_type(
1966+
tvar_def.name, last_tvar_name_with_default, context
1967+
)
1968+
tvar_def.default = AnyType(TypeOfAny.from_error)
1969+
elif tvar_def.has_default():
1970+
last_tvar_name_with_default = tvar_def.name
19671971
tvar_defs.append(tvar_def)
19681972
return base_type_exprs, tvar_defs, is_protocol
19691973

@@ -2855,6 +2859,10 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
28552859
with self.allow_unbound_tvars_set():
28562860
s.rvalue.accept(self)
28572861
self.basic_type_applications = old_basic_type_applications
2862+
elif self.can_possibly_be_typevarlike_declaration(s):
2863+
# Allow unbound tvars inside TypeVarLike defaults to be evaluated later
2864+
with self.allow_unbound_tvars_set():
2865+
s.rvalue.accept(self)
28582866
else:
28592867
s.rvalue.accept(self)
28602868

@@ -3031,6 +3039,16 @@ def can_possibly_be_type_form(self, s: AssignmentStmt) -> bool:
30313039
# Something that looks like Foo = Bar[Baz, ...]
30323040
return True
30333041

3042+
def can_possibly_be_typevarlike_declaration(self, s: AssignmentStmt) -> bool:
3043+
"""Check if r.h.s. can be a TypeVarLike declaration."""
3044+
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
3045+
return False
3046+
if not isinstance(s.rvalue, CallExpr) or not isinstance(s.rvalue.callee, NameExpr):
3047+
return False
3048+
ref = s.rvalue.callee
3049+
ref.accept(self)
3050+
return ref.fullname in TYPE_VAR_LIKE_NAMES
3051+
30343052
def is_type_ref(self, rv: Expression, bare: bool = False) -> bool:
30353053
"""Does this expression refer to a type?
30363054
@@ -3515,9 +3533,20 @@ def analyze_alias(
35153533
found_type_vars = self.find_type_var_likes(typ)
35163534
tvar_defs: list[TypeVarLikeType] = []
35173535
namespace = self.qualified_name(name)
3536+
last_tvar_name_with_default: str | None = None
35183537
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
35193538
for name, tvar_expr in found_type_vars:
3539+
tvar_expr.default = tvar_expr.default.accept(
3540+
TypeVarDefaultTranslator(self, tvar_expr.name, typ)
3541+
)
35203542
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
3543+
if last_tvar_name_with_default is not None and not tvar_def.has_default():
3544+
self.msg.tvar_without_default_type(
3545+
tvar_def.name, last_tvar_name_with_default, typ
3546+
)
3547+
tvar_def.default = AnyType(TypeOfAny.from_error)
3548+
elif tvar_def.has_default():
3549+
last_tvar_name_with_default = tvar_def.name
35213550
tvar_defs.append(tvar_def)
35223551

35233552
analyzed, depends_on = analyze_type_alias(

mypy/typeanal.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@
3838
)
3939
from mypy.options import Options
4040
from mypy.plugin import AnalyzeTypeContext, Plugin, TypeAnalyzerPluginInterface
41-
from mypy.semanal_shared import SemanticAnalyzerCoreInterface, paramspec_args, paramspec_kwargs
41+
from mypy.semanal_shared import (
42+
SemanticAnalyzerCoreInterface,
43+
SemanticAnalyzerInterface,
44+
paramspec_args,
45+
paramspec_kwargs,
46+
)
4247
from mypy.state import state
4348
from mypy.tvar_scope import TypeVarLikeScope
4449
from mypy.types import (
@@ -2508,3 +2513,32 @@ def process_types(self, types: list[Type] | tuple[Type, ...]) -> None:
25082513
else:
25092514
for t in types:
25102515
t.accept(self)
2516+
2517+
2518+
class TypeVarDefaultTranslator(TrivialSyntheticTypeTranslator):
2519+
"""Type translate visitor that replaces UnboundTypes with in-scope TypeVars."""
2520+
2521+
def __init__(
2522+
self, api: SemanticAnalyzerInterface, tvar_expr_name: str, context: Context
2523+
) -> None:
2524+
self.api = api
2525+
self.tvar_expr_name = tvar_expr_name
2526+
self.context = context
2527+
2528+
def visit_unbound_type(self, t: UnboundType) -> Type:
2529+
sym = self.api.lookup_qualified(t.name, t, suppress_errors=True)
2530+
if sym is not None:
2531+
if type_var := self.api.tvar_scope.get_binding(sym):
2532+
return type_var
2533+
if isinstance(sym.node, TypeVarLikeExpr):
2534+
self.api.fail(
2535+
f'Type parameter "{self.tvar_expr_name}" has a default type '
2536+
"that refers to one or more type variables that are out of scope",
2537+
self.context,
2538+
)
2539+
return AnyType(TypeOfAny.from_error)
2540+
return super().visit_unbound_type(t)
2541+
2542+
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
2543+
# TypeAliasTypes are analyzed separately already, just return it
2544+
return t

mypy/types.py

+9
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@
8585
TypeVisitor as TypeVisitor,
8686
)
8787

88+
TYPE_VAR_LIKE_NAMES: Final = (
89+
"typing.TypeVar",
90+
"typing_extensions.TypeVar",
91+
"typing.ParamSpec",
92+
"typing_extensions.ParamSpec",
93+
"typing.TypeVarTuple",
94+
"typing_extensions.TypeVarTuple",
95+
)
96+
8897
TYPED_NAMEDTUPLE_NAMES: Final = ("typing.NamedTuple", "typing_extensions.NamedTuple")
8998

9099
# Supported names of TypedDict type constructors.

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

+132-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,74 @@ T3 = TypeVar("T3", int, str, default=bytes) # E: TypeVar default must be one of
8282
T4 = TypeVar("T4", int, str, default=Union[int, str]) # E: TypeVar default must be one of the constraint types
8383
T5 = TypeVar("T5", float, str, default=int) # E: TypeVar default must be one of the constraint types
8484

85+
[case testTypeVarDefaultsInvalid3]
86+
from typing import Dict, Generic, TypeVar
87+
88+
T1 = TypeVar("T1")
89+
T2 = TypeVar("T2", default=T3) # E: Name "T3" is used before definition
90+
T3 = TypeVar("T3", default=str)
91+
T4 = TypeVar("T4", default=T3)
92+
93+
class ClassError1(Generic[T3, T1]): ... # E: "T1" cannot appear after "T3" in type parameter list because it has no default type
94+
95+
def func_error1(
96+
a: ClassError1,
97+
b: ClassError1[int],
98+
c: ClassError1[int, float],
99+
) -> None:
100+
reveal_type(a) # N: Revealed type is "__main__.ClassError1[builtins.str, Any]"
101+
reveal_type(b) # N: Revealed type is "__main__.ClassError1[builtins.int, Any]"
102+
reveal_type(c) # N: Revealed type is "__main__.ClassError1[builtins.int, builtins.float]"
103+
104+
k = ClassError1()
105+
reveal_type(k) # N: Revealed type is "__main__.ClassError1[builtins.str, Any]"
106+
l = ClassError1[int]()
107+
reveal_type(l) # N: Revealed type is "__main__.ClassError1[builtins.int, Any]"
108+
m = ClassError1[int, float]()
109+
reveal_type(m) # N: Revealed type is "__main__.ClassError1[builtins.int, builtins.float]"
110+
111+
class ClassError2(Generic[T4, T3]): ... # E: Type parameter "T4" has a default type that refers to one or more type variables that are out of scope
112+
113+
def func_error2(
114+
a: ClassError2,
115+
b: ClassError2[int],
116+
c: ClassError2[int, float],
117+
) -> None:
118+
reveal_type(a) # N: Revealed type is "__main__.ClassError2[Any, builtins.str]"
119+
reveal_type(b) # N: Revealed type is "__main__.ClassError2[builtins.int, builtins.str]"
120+
reveal_type(c) # N: Revealed type is "__main__.ClassError2[builtins.int, builtins.float]"
121+
122+
k = ClassError2()
123+
reveal_type(k) # N: Revealed type is "__main__.ClassError2[Any, builtins.str]"
124+
l = ClassError2[int]()
125+
reveal_type(l) # N: Revealed type is "__main__.ClassError2[builtins.int, builtins.str]"
126+
m = ClassError2[int, float]()
127+
reveal_type(m) # N: Revealed type is "__main__.ClassError2[builtins.int, builtins.float]"
128+
129+
TERR1 = Dict[T3, T1] # E: "T1" cannot appear after "T3" in type parameter list because it has no default type
130+
131+
def func_error_alias1(
132+
a: TERR1,
133+
b: TERR1[int],
134+
c: TERR1[int, float],
135+
) -> None:
136+
reveal_type(a) # N: Revealed type is "builtins.dict[builtins.str, Any]"
137+
reveal_type(b) # N: Revealed type is "builtins.dict[builtins.int, Any]"
138+
reveal_type(c) # N: Revealed type is "builtins.dict[builtins.int, builtins.float]"
139+
140+
TERR2 = Dict[T4, T3] # TODO should be an error \
141+
# Type parameter "T4" has a default type that refers to one or more type variables that are out of scope
142+
143+
def func_error_alias2(
144+
a: TERR2,
145+
b: TERR2[int],
146+
c: TERR2[int, float],
147+
) -> None:
148+
reveal_type(a) # N: Revealed type is "builtins.dict[Any, builtins.str]"
149+
reveal_type(b) # N: Revealed type is "builtins.dict[builtins.int, builtins.str]"
150+
reveal_type(c) # N: Revealed type is "builtins.dict[builtins.int, builtins.float]"
151+
[builtins fixtures/dict.pyi]
152+
85153
[case testTypeVarDefaultsFunctions]
86154
from typing import TypeVar, ParamSpec, List, Union, Callable, Tuple
87155
from typing_extensions import TypeVarTuple, Unpack
@@ -351,11 +419,12 @@ def func_c4(
351419

352420
[case testTypeVarDefaultsClassRecursive1]
353421
# flags: --disallow-any-generics
354-
from typing import Generic, TypeVar
422+
from typing import Generic, TypeVar, List
355423

356424
T1 = TypeVar("T1", default=str)
357425
T2 = TypeVar("T2", default=T1)
358426
T3 = TypeVar("T3", default=T2)
427+
T4 = TypeVar("T4", default=List[T1])
359428

360429
class ClassD1(Generic[T1, T2]): ...
361430

@@ -397,12 +466,30 @@ def func_d2(
397466
n = ClassD2[int, float, str]()
398467
reveal_type(n) # N: Revealed type is "__main__.ClassD2[builtins.int, builtins.float, builtins.str]"
399468

469+
class ClassD3(Generic[T1, T4]): ...
470+
471+
def func_d3(
472+
a: ClassD3,
473+
b: ClassD3[int],
474+
c: ClassD3[int, float],
475+
) -> None:
476+
reveal_type(a) # N: Revealed type is "__main__.ClassD3[builtins.str, builtins.list[builtins.str]]"
477+
reveal_type(b) # N: Revealed type is "__main__.ClassD3[builtins.int, builtins.list[builtins.int]]"
478+
reveal_type(c) # N: Revealed type is "__main__.ClassD3[builtins.int, builtins.float]"
479+
480+
# k = ClassD3()
481+
# reveal_type(k) # Revealed type is "__main__.ClassD3[builtins.str, builtins.list[builtins.str]]" # TODO
482+
l = ClassD3[int]()
483+
reveal_type(l) # N: Revealed type is "__main__.ClassD3[builtins.int, builtins.list[builtins.int]]"
484+
m = ClassD3[int, float]()
485+
reveal_type(m) # N: Revealed type is "__main__.ClassD3[builtins.int, builtins.float]"
486+
400487
[case testTypeVarDefaultsClassRecursiveMultipleFiles]
401488
# flags: --disallow-any-generics
402489
from typing import Generic, TypeVar
403490
from file2 import T as T2
404491

405-
T = TypeVar('T', default=T2)
492+
T = TypeVar("T", default=T2)
406493

407494
class ClassG1(Generic[T2, T]):
408495
pass
@@ -587,3 +674,46 @@ def func_c4(
587674
# reveal_type(b) # Revealed type is "Tuple[builtins.int, builtins.str]" # TODO
588675
reveal_type(c) # N: Revealed type is "Tuple[builtins.int, builtins.float]"
589676
[builtins fixtures/tuple.pyi]
677+
678+
[case testTypeVarDefaultsTypeAliasRecursive1]
679+
# flags: --disallow-any-generics
680+
from typing import Dict, List, TypeVar
681+
682+
T1 = TypeVar("T1")
683+
T2 = TypeVar("T2", default=T1)
684+
685+
TD1 = Dict[T1, T2]
686+
687+
def func_d1(
688+
a: TD1, # E: Missing type parameters for generic type "TD1"
689+
b: TD1[int],
690+
c: TD1[int, float],
691+
) -> None:
692+
reveal_type(a) # N: Revealed type is "builtins.dict[Any, Any]"
693+
reveal_type(b) # N: Revealed type is "builtins.dict[builtins.int, builtins.int]"
694+
reveal_type(c) # N: Revealed type is "builtins.dict[builtins.int, builtins.float]"
695+
[builtins fixtures/dict.pyi]
696+
697+
[case testTypeVarDefaultsTypeAliasRecursive2]
698+
from typing import Any, Dict, Generic, TypeVar
699+
700+
T1 = TypeVar("T1", default=str)
701+
T2 = TypeVar("T2", default=T1)
702+
Alias1 = Dict[T1, T2]
703+
T3 = TypeVar("T3")
704+
class A(Generic[T3]): ...
705+
706+
T4 = TypeVar("T4", default=A[Alias1])
707+
class B(Generic[T4]): ...
708+
709+
def func_d3(
710+
a: B,
711+
b: B[A[Alias1[int]]],
712+
c: B[A[Alias1[int, float]]],
713+
d: B[int],
714+
) -> None:
715+
reveal_type(a) # N: Revealed type is "__main__.B[__main__.A[builtins.dict[builtins.str, builtins.str]]]"
716+
reveal_type(b) # N: Revealed type is "__main__.B[__main__.A[builtins.dict[builtins.int, builtins.int]]]"
717+
reveal_type(c) # N: Revealed type is "__main__.B[__main__.A[builtins.dict[builtins.int, builtins.float]]]"
718+
reveal_type(d) # N: Revealed type is "__main__.B[builtins.int]"
719+
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)