Skip to content

Commit 1df0dd2

Browse files
cdce8phamdanal
authored andcommitted
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 1ee3e0b commit 1df0dd2

File tree

5 files changed

+223
-12
lines changed

5 files changed

+223
-12
lines changed

Diff for: 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,

Diff for: 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

@@ -2857,6 +2861,10 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
28572861
with self.allow_unbound_tvars_set():
28582862
s.rvalue.accept(self)
28592863
self.basic_type_applications = old_basic_type_applications
2864+
elif self.can_possibly_be_typevarlike_declaration(s):
2865+
# Allow unbound tvars inside TypeVarLike defaults to be evaluated later
2866+
with self.allow_unbound_tvars_set():
2867+
s.rvalue.accept(self)
28602868
else:
28612869
s.rvalue.accept(self)
28622870

@@ -3033,6 +3041,16 @@ def can_possibly_be_type_form(self, s: AssignmentStmt) -> bool:
30333041
# Something that looks like Foo = Bar[Baz, ...]
30343042
return True
30353043

3044+
def can_possibly_be_typevarlike_declaration(self, s: AssignmentStmt) -> bool:
3045+
"""Check if r.h.s. can be a TypeVarLike declaration."""
3046+
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
3047+
return False
3048+
if not isinstance(s.rvalue, CallExpr) or not isinstance(s.rvalue.callee, NameExpr):
3049+
return False
3050+
ref = s.rvalue.callee
3051+
ref.accept(self)
3052+
return ref.fullname in TYPE_VAR_LIKE_NAMES
3053+
30363054
def is_type_ref(self, rv: Expression, bare: bool = False) -> bool:
30373055
"""Does this expression refer to a type?
30383056
@@ -3522,9 +3540,20 @@ def analyze_alias(
35223540
tvar_defs: list[TypeVarLikeType] = []
35233541
namespace = self.qualified_name(name)
35243542
alias_type_vars = found_type_vars if declared_type_vars is None else declared_type_vars
3543+
last_tvar_name_with_default: str | None = None
35253544
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
35263545
for name, tvar_expr in alias_type_vars:
3546+
tvar_expr.default = tvar_expr.default.accept(
3547+
TypeVarDefaultTranslator(self, tvar_expr.name, typ)
3548+
)
35273549
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
3550+
if last_tvar_name_with_default is not None and not tvar_def.has_default():
3551+
self.msg.tvar_without_default_type(
3552+
tvar_def.name, last_tvar_name_with_default, typ
3553+
)
3554+
tvar_def.default = AnyType(TypeOfAny.from_error)
3555+
elif tvar_def.has_default():
3556+
last_tvar_name_with_default = tvar_def.name
35283557
tvar_defs.append(tvar_def)
35293558

35303559
analyzed, depends_on = analyze_type_alias(

Diff for: 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 (
@@ -2520,3 +2525,32 @@ def process_types(self, types: list[Type] | tuple[Type, ...]) -> None:
25202525
else:
25212526
for t in types:
25222527
t.accept(self)
2528+
2529+
2530+
class TypeVarDefaultTranslator(TrivialSyntheticTypeTranslator):
2531+
"""Type translate visitor that replaces UnboundTypes with in-scope TypeVars."""
2532+
2533+
def __init__(
2534+
self, api: SemanticAnalyzerInterface, tvar_expr_name: str, context: Context
2535+
) -> None:
2536+
self.api = api
2537+
self.tvar_expr_name = tvar_expr_name
2538+
self.context = context
2539+
2540+
def visit_unbound_type(self, t: UnboundType) -> Type:
2541+
sym = self.api.lookup_qualified(t.name, t, suppress_errors=True)
2542+
if sym is not None:
2543+
if type_var := self.api.tvar_scope.get_binding(sym):
2544+
return type_var
2545+
if isinstance(sym.node, TypeVarLikeExpr):
2546+
self.api.fail(
2547+
f'Type parameter "{self.tvar_expr_name}" has a default type '
2548+
"that refers to one or more type variables that are out of scope",
2549+
self.context,
2550+
)
2551+
return AnyType(TypeOfAny.from_error)
2552+
return super().visit_unbound_type(t)
2553+
2554+
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
2555+
# TypeAliasTypes are analyzed separately already, just return it
2556+
return t

Diff for: 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.

Diff for: 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)