Skip to content

Commit 5094460

Browse files
authored
Fix crash with generic class definition in function (#13678)
Fixes #12112. The reason why mypy was crashing with a "Must not defer during final iteration" error in the following snippet: from typing import TypeVar def test() -> None: T = TypeVar('T', bound='Foo') class Foo: def bar(self, foo: T) -> None: pass ...was because mypy did not seem to be updating the types of the `bar` callable on each pass: the `bind_function_type_variables` method in `typeanal.py` always returned the _old_ type variables instead of using the new updated ones we found by calling `self.lookup_qualified(...)`. This in turn prevented us from making any forward progress when mypy generated a CallableType containing a placedholder type variable. So, we repeated the semanal passes until we hit the limit and crashed. I opted to fix this by having the function always return the newly-bound TypeVarLikeType instead. (Hopefully this is safe -- the way mypy likes mutating types always makes it hard to reason about this sort of stuff). Interestingly, my fix for this bug introduced a regression in one of our existing tests: from typing import NamedTuple, TypeVar T = TypeVar("T") NT = NamedTuple("NT", [("key", int), ("value", T)]) # Test thinks the revealed type should be: # def [T] (key: builtins.int, value: T`-1) -> Tuple[builtins.int, T`-1, fallback=__main__.NT[T`-1]] # # ...but we started seeing: # def [T, _NT <: Tuple[builtins.int, T`-1]] (key: builtins.int, value: T`-1) -> Tuple[builtins.int, T`-1, fallback=test.WTF[T`-1]] reveal_type(NT) What seems to be happening here is that during the first pass, we add two type vars to the `tvar_scope` inside `bind_function_type_variables`: `T` with id -1 and `_NT` with id -2. But in the second pass, we lose track of the `T` typevar definition and/or introduce a fresh scope somewhere and infer `_NT` with id -1 instead? So now mypy thinks there are two type variables associated with this NamedTuple, which results in the screwed-up type definition. I wasn't really sure how to fix this, but I thought it was weird that: 1. We were using negative IDs instead of positive ones. (Class typevars are supposed to use the latter). 2. We weren't wrapping this whole thing in a new tvar scope frame, given we're nominally synthesizing a new class. So I did that, and the tests started passing? I wasn't able to repro this issue for TypedDicts, but opted to introduce a new tvar scope frame there as well for consistency.
1 parent a3a5d73 commit 5094460

File tree

6 files changed

+81
-47
lines changed

6 files changed

+81
-47
lines changed

mypy/semanal.py

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2752,30 +2752,32 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
27522752
return False
27532753
lvalue = s.lvalues[0]
27542754
name = lvalue.name
2755-
internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple(
2756-
s.rvalue, name, self.is_func_scope()
2757-
)
2758-
if internal_name is None:
2759-
return False
2760-
if isinstance(lvalue, MemberExpr):
2761-
self.fail("NamedTuple type as an attribute is not supported", lvalue)
2762-
return False
2763-
if internal_name != name:
2764-
self.fail(
2765-
'First argument to namedtuple() should be "{}", not "{}"'.format(
2766-
name, internal_name
2767-
),
2768-
s.rvalue,
2769-
code=codes.NAME_MATCH,
2755+
namespace = self.qualified_name(name)
2756+
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
2757+
internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple(
2758+
s.rvalue, name, self.is_func_scope()
27702759
)
2760+
if internal_name is None:
2761+
return False
2762+
if isinstance(lvalue, MemberExpr):
2763+
self.fail("NamedTuple type as an attribute is not supported", lvalue)
2764+
return False
2765+
if internal_name != name:
2766+
self.fail(
2767+
'First argument to namedtuple() should be "{}", not "{}"'.format(
2768+
name, internal_name
2769+
),
2770+
s.rvalue,
2771+
code=codes.NAME_MATCH,
2772+
)
2773+
return True
2774+
# Yes, it's a valid namedtuple, but defer if it is not ready.
2775+
if not info:
2776+
self.mark_incomplete(name, lvalue, becomes_typeinfo=True)
2777+
else:
2778+
self.setup_type_vars(info.defn, tvar_defs)
2779+
self.setup_alias_type_vars(info.defn)
27712780
return True
2772-
# Yes, it's a valid namedtuple, but defer if it is not ready.
2773-
if not info:
2774-
self.mark_incomplete(name, lvalue, becomes_typeinfo=True)
2775-
else:
2776-
self.setup_type_vars(info.defn, tvar_defs)
2777-
self.setup_alias_type_vars(info.defn)
2778-
return True
27792781

27802782
def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool:
27812783
"""Check if s defines a typed dict."""
@@ -2789,22 +2791,24 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool:
27892791
return False
27902792
lvalue = s.lvalues[0]
27912793
name = lvalue.name
2792-
is_typed_dict, info, tvar_defs = self.typed_dict_analyzer.check_typeddict(
2793-
s.rvalue, name, self.is_func_scope()
2794-
)
2795-
if not is_typed_dict:
2796-
return False
2797-
if isinstance(lvalue, MemberExpr):
2798-
self.fail("TypedDict type as attribute is not supported", lvalue)
2799-
return False
2800-
# Yes, it's a valid typed dict, but defer if it is not ready.
2801-
if not info:
2802-
self.mark_incomplete(name, lvalue, becomes_typeinfo=True)
2803-
else:
2804-
defn = info.defn
2805-
self.setup_type_vars(defn, tvar_defs)
2806-
self.setup_alias_type_vars(defn)
2807-
return True
2794+
namespace = self.qualified_name(name)
2795+
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
2796+
is_typed_dict, info, tvar_defs = self.typed_dict_analyzer.check_typeddict(
2797+
s.rvalue, name, self.is_func_scope()
2798+
)
2799+
if not is_typed_dict:
2800+
return False
2801+
if isinstance(lvalue, MemberExpr):
2802+
self.fail("TypedDict type as attribute is not supported", lvalue)
2803+
return False
2804+
# Yes, it's a valid typed dict, but defer if it is not ready.
2805+
if not info:
2806+
self.mark_incomplete(name, lvalue, becomes_typeinfo=True)
2807+
else:
2808+
defn = info.defn
2809+
self.setup_type_vars(defn, tvar_defs)
2810+
self.setup_alias_type_vars(defn)
2811+
return True
28082812

28092813
def analyze_lvalues(self, s: AssignmentStmt) -> None:
28102814
# We cannot use s.type, because analyze_simple_literal_type() will set it.

mypy/semanal_namedtuple.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,12 @@ def parse_namedtuple_args(
321321
) -> None | (tuple[list[str], list[Type], list[Expression], str, list[TypeVarLikeType], bool]):
322322
"""Parse a namedtuple() call into data needed to construct a type.
323323
324-
Returns a 5-tuple:
324+
Returns a 6-tuple:
325325
- List of argument names
326326
- List of argument types
327327
- List of default values
328328
- First argument of namedtuple
329+
- All typevars found in the field definition
329330
- Whether all types are ready.
330331
331332
Return None if the definition didn't typecheck.

mypy/typeanal.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,29 +1331,29 @@ def bind_function_type_variables(
13311331
) -> Sequence[TypeVarLikeType]:
13321332
"""Find the type variables of the function type and bind them in our tvar_scope"""
13331333
if fun_type.variables:
1334+
defs = []
13341335
for var in fun_type.variables:
13351336
var_node = self.lookup_qualified(var.name, defn)
13361337
assert var_node, "Binding for function type variable not found within function"
13371338
var_expr = var_node.node
13381339
assert isinstance(var_expr, TypeVarLikeExpr)
1339-
self.tvar_scope.bind_new(var.name, var_expr)
1340-
return fun_type.variables
1340+
binding = self.tvar_scope.bind_new(var.name, var_expr)
1341+
defs.append(binding)
1342+
return defs
13411343
typevars = self.infer_type_variables(fun_type)
13421344
# Do not define a new type variable if already defined in scope.
13431345
typevars = [
13441346
(name, tvar) for name, tvar in typevars if not self.is_defined_type_var(name, defn)
13451347
]
1346-
defs: list[TypeVarLikeType] = []
1348+
defs = []
13471349
for name, tvar in typevars:
13481350
if not self.tvar_scope.allow_binding(tvar.fullname):
13491351
self.fail(
13501352
f'Type variable "{name}" is bound by an outer class',
13511353
defn,
13521354
code=codes.VALID_TYPE,
13531355
)
1354-
self.tvar_scope.bind_new(name, tvar)
1355-
binding = self.tvar_scope.get_binding(tvar.fullname)
1356-
assert binding is not None
1356+
binding = self.tvar_scope.bind_new(name, tvar)
13571357
defs.append(binding)
13581358

13591359
return defs

test-data/unit/check-classes.test

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,35 @@ def f() -> None:
10601060
a.g(a) # E: Too many arguments for "g" of "A"
10611061
[targets __main__, __main__.f]
10621062

1063+
[case testGenericClassWithinFunction]
1064+
from typing import TypeVar
1065+
1066+
def test() -> None:
1067+
T = TypeVar('T', bound='Foo')
1068+
class Foo:
1069+
def returns_int(self) -> int:
1070+
return 0
1071+
1072+
def bar(self, foo: T) -> T:
1073+
x: T = foo
1074+
reveal_type(x) # N: Revealed type is "T`-1"
1075+
reveal_type(x.returns_int()) # N: Revealed type is "builtins.int"
1076+
return foo
1077+
reveal_type(Foo.bar) # N: Revealed type is "def [T <: __main__.Foo@5] (self: __main__.Foo@5, foo: T`-1) -> T`-1"
1078+
1079+
[case testGenericClassWithInvalidTypevarUseWithinFunction]
1080+
from typing import TypeVar
1081+
1082+
def test() -> None:
1083+
T = TypeVar('T', bound='Foo')
1084+
class Foo:
1085+
invalid: T # E: Type variable "T" is unbound \
1086+
# N: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class) \
1087+
# N: (Hint: Use "T" in function signature to bind "T" inside a function)
1088+
1089+
def bar(self, foo: T) -> T:
1090+
pass
1091+
10631092
[case testConstructNestedClass]
10641093
import typing
10651094
class A:

test-data/unit/check-namedtuple.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1284,7 +1284,7 @@ from typing import NamedTuple, TypeVar
12841284

12851285
T = TypeVar("T")
12861286
NT = NamedTuple("NT", [("key", int), ("value", T)])
1287-
reveal_type(NT) # N: Revealed type is "def [T] (key: builtins.int, value: T`-1) -> Tuple[builtins.int, T`-1, fallback=__main__.NT[T`-1]]"
1287+
reveal_type(NT) # N: Revealed type is "def [T] (key: builtins.int, value: T`1) -> Tuple[builtins.int, T`1, fallback=__main__.NT[T`1]]"
12881288

12891289
nts: NT[str]
12901290
reveal_type(nts) # N: Revealed type is "Tuple[builtins.int, builtins.str, fallback=__main__.NT[builtins.str]]"

test-data/unit/check-typeddict.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2550,7 +2550,7 @@ from typing import TypedDict, TypeVar
25502550

25512551
T = TypeVar("T")
25522552
TD = TypedDict("TD", {"key": int, "value": T})
2553-
reveal_type(TD) # N: Revealed type is "def [T] (*, key: builtins.int, value: T`-1) -> TypedDict('__main__.TD', {'key': builtins.int, 'value': T`-1})"
2553+
reveal_type(TD) # N: Revealed type is "def [T] (*, key: builtins.int, value: T`1) -> TypedDict('__main__.TD', {'key': builtins.int, 'value': T`1})"
25542554

25552555
tds: TD[str]
25562556
reveal_type(tds) # N: Revealed type is "TypedDict('__main__.TD', {'key': builtins.int, 'value': builtins.str})"

0 commit comments

Comments
 (0)