Skip to content

Commit 389a172

Browse files
authored
Fix crash with nested NamedTuple in incremental mode (#10431)
The name of the nested tuple type was inconsistent. Sometimes if was stored using the full name in the module symbol table. Also improve the internal API for creating classes to be less error-prone. Work on #7281.
1 parent 4a5e311 commit 389a172

7 files changed

+54
-20
lines changed

mypy/semanal.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,15 +1396,15 @@ def prepare_class_def(self, defn: ClassDef, info: Optional[TypeInfo] = None) ->
13961396
# incremental mode and we should avoid it. In general, this logic is too
13971397
# ad-hoc and needs to be removed/refactored.
13981398
if '@' not in defn.info._fullname:
1399-
local_name = defn.info._fullname + '@' + str(defn.line)
1399+
local_name = defn.info.name + '@' + str(defn.line)
14001400
if defn.info.is_named_tuple:
14011401
# Module is already correctly set in _fullname for named tuples.
14021402
defn.info._fullname += '@' + str(defn.line)
14031403
else:
14041404
defn.info._fullname = self.cur_mod_id + '.' + local_name
14051405
else:
14061406
# Preserve name from previous fine-grained incremental run.
1407-
local_name = defn.info._fullname
1407+
local_name = defn.info.name
14081408
defn.fullname = defn.info._fullname
14091409
self.globals[local_name] = SymbolTableNode(GDEF, defn.info)
14101410

@@ -3140,7 +3140,11 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool:
31403140
self.add_symbol(name, call.analyzed, s)
31413141
return True
31423142

3143-
def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance) -> TypeInfo:
3143+
def basic_new_typeinfo(self, name: str,
3144+
basetype_or_fallback: Instance,
3145+
line: int) -> TypeInfo:
3146+
if self.is_func_scope() and not self.type and '@' not in name:
3147+
name += '@' + str(line)
31443148
class_def = ClassDef(name, Block([]))
31453149
if self.is_func_scope() and not self.type:
31463150
# Full names of generated classes should always be prefixed with the module names

mypy/semanal_enum.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ class A(enum.Enum):
6767
items, values, ok = self.parse_enum_call_args(call, fullname.split('.')[-1])
6868
if not ok:
6969
# Error. Construct dummy return value.
70-
info = self.build_enum_call_typeinfo(var_name, [], fullname)
70+
info = self.build_enum_call_typeinfo(var_name, [], fullname, node.line)
7171
else:
7272
name = cast(Union[StrExpr, UnicodeExpr], call.args[0]).value
7373
if name != var_name or is_func_scope:
7474
# Give it a unique name derived from the line number.
7575
name += '@' + str(call.line)
76-
info = self.build_enum_call_typeinfo(name, items, fullname)
76+
info = self.build_enum_call_typeinfo(name, items, fullname, call.line)
7777
# Store generated TypeInfo under both names, see semanal_namedtuple for more details.
7878
if name != var_name or is_func_scope:
7979
self.api.add_symbol_skip_local(name, info)
@@ -82,10 +82,11 @@ class A(enum.Enum):
8282
info.line = node.line
8383
return info
8484

85-
def build_enum_call_typeinfo(self, name: str, items: List[str], fullname: str) -> TypeInfo:
85+
def build_enum_call_typeinfo(self, name: str, items: List[str], fullname: str,
86+
line: int) -> TypeInfo:
8687
base = self.api.named_type_or_none(fullname)
8788
assert base is not None
88-
info = self.api.basic_new_typeinfo(name, base)
89+
info = self.api.basic_new_typeinfo(name, base, line)
8990
info.metaclass_type = info.calculate_metaclass_type()
9091
info.is_enum = True
9192
for item in items:

mypy/semanal_namedtuple.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ def build_namedtuple_typeinfo(self,
382382
iterable_type = self.api.named_type_or_none('typing.Iterable', [implicit_any])
383383
function_type = self.api.named_type('__builtins__.function')
384384

385-
info = self.api.basic_new_typeinfo(name, fallback)
385+
info = self.api.basic_new_typeinfo(name, fallback, line)
386386
info.is_named_tuple = True
387387
tuple_base = TupleType(types, fallback)
388388
info.tuple_type = tuple_base

mypy/semanal_newtype.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,20 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
7272
# Create the corresponding class definition if the aliased type is subtypeable
7373
if isinstance(old_type, TupleType):
7474
newtype_class_info = self.build_newtype_typeinfo(name, old_type,
75-
old_type.partial_fallback)
75+
old_type.partial_fallback, s.line)
7676
newtype_class_info.tuple_type = old_type
7777
elif isinstance(old_type, Instance):
7878
if old_type.type.is_protocol:
7979
self.fail("NewType cannot be used with protocol classes", s)
80-
newtype_class_info = self.build_newtype_typeinfo(name, old_type, old_type)
80+
newtype_class_info = self.build_newtype_typeinfo(name, old_type, old_type, s.line)
8181
else:
8282
if old_type is not None:
8383
message = "Argument 2 to NewType(...) must be subclassable (got {})"
8484
self.fail(message.format(format_type(old_type)), s, code=codes.VALID_NEWTYPE)
8585
# Otherwise the error was already reported.
8686
old_type = AnyType(TypeOfAny.from_error)
8787
object_type = self.api.named_type('__builtins__.object')
88-
newtype_class_info = self.build_newtype_typeinfo(name, old_type, object_type)
88+
newtype_class_info = self.build_newtype_typeinfo(name, old_type, object_type, s.line)
8989
newtype_class_info.fallback_to_any = True
9090

9191
check_for_explicit_any(old_type, self.options, self.api.is_typeshed_stub_file, self.msg,
@@ -181,8 +181,9 @@ def check_newtype_args(self, name: str, call: CallExpr,
181181

182182
return None if has_failed else old_type, should_defer
183183

184-
def build_newtype_typeinfo(self, name: str, old_type: Type, base_type: Instance) -> TypeInfo:
185-
info = self.api.basic_new_typeinfo(name, base_type)
184+
def build_newtype_typeinfo(self, name: str, old_type: Type, base_type: Instance,
185+
line: int) -> TypeInfo:
186+
info = self.api.basic_new_typeinfo(name, base_type, line)
186187
info.is_newtype = True
187188

188189
# Add __init__ method

mypy/semanal_shared.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def anal_type(self, t: Type, *,
122122
raise NotImplementedError
123123

124124
@abstractmethod
125-
def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance) -> TypeInfo:
125+
def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance, line: int) -> TypeInfo:
126126
raise NotImplementedError
127127

128128
@abstractmethod

mypy/semanal_typeddict.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> Tuple[bool, Optional[Typ
6060
fields, types, required_keys = self.analyze_typeddict_classdef_fields(defn)
6161
if fields is None:
6262
return True, None # Defer
63-
info = self.build_typeddict_typeinfo(defn.name, fields, types, required_keys)
63+
info = self.build_typeddict_typeinfo(defn.name, fields, types, required_keys,
64+
defn.line)
6465
defn.analyzed = TypedDictExpr(info)
6566
defn.analyzed.line = defn.line
6667
defn.analyzed.column = defn.column
@@ -97,7 +98,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> Tuple[bool, Optional[Typ
9798
keys.extend(new_keys)
9899
types.extend(new_types)
99100
required_keys.update(new_required_keys)
100-
info = self.build_typeddict_typeinfo(defn.name, keys, types, required_keys)
101+
info = self.build_typeddict_typeinfo(defn.name, keys, types, required_keys, defn.line)
101102
defn.analyzed = TypedDictExpr(info)
102103
defn.analyzed.line = defn.line
103104
defn.analyzed.column = defn.column
@@ -196,7 +197,7 @@ def check_typeddict(self,
196197
name, items, types, total, ok = res
197198
if not ok:
198199
# Error. Construct dummy return value.
199-
info = self.build_typeddict_typeinfo('TypedDict', [], [], set())
200+
info = self.build_typeddict_typeinfo('TypedDict', [], [], set(), call.line)
200201
else:
201202
if var_name is not None and name != var_name:
202203
self.fail(
@@ -206,7 +207,7 @@ def check_typeddict(self,
206207
# Give it a unique name derived from the line number.
207208
name += '@' + str(call.line)
208209
required_keys = set(items) if total else set()
209-
info = self.build_typeddict_typeinfo(name, items, types, required_keys)
210+
info = self.build_typeddict_typeinfo(name, items, types, required_keys, call.line)
210211
info.line = node.line
211212
# Store generated TypeInfo under both names, see semanal_namedtuple for more details.
212213
if name != var_name or is_func_scope:
@@ -305,13 +306,14 @@ def fail_typeddict_arg(self, message: str,
305306

306307
def build_typeddict_typeinfo(self, name: str, items: List[str],
307308
types: List[Type],
308-
required_keys: Set[str]) -> TypeInfo:
309+
required_keys: Set[str],
310+
line: int) -> TypeInfo:
309311
# Prefer typing then typing_extensions if available.
310312
fallback = (self.api.named_type_or_none('typing._TypedDict', []) or
311313
self.api.named_type_or_none('typing_extensions._TypedDict', []) or
312314
self.api.named_type_or_none('mypy_extensions._TypedDict', []))
313315
assert fallback is not None
314-
info = self.api.basic_new_typeinfo(name, fallback)
316+
info = self.api.basic_new_typeinfo(name, fallback, line)
315317
info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), required_keys,
316318
fallback)
317319
return info

test-data/unit/check-incremental.test

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5506,3 +5506,29 @@ class Foo:
55065506
[delete c1.py.2]
55075507
[file c2.py.2]
55085508
class C: pass
5509+
5510+
[case testIncrementalNestedNamedTuple]
5511+
# flags: --python-version 3.6
5512+
import a
5513+
5514+
[file a.py]
5515+
import b
5516+
5517+
[file a.py.2]
5518+
import b # foo
5519+
5520+
[file b.py]
5521+
from typing import NamedTuple
5522+
5523+
def f() -> None:
5524+
class NT(NamedTuple):
5525+
x: int
5526+
5527+
n: NT = NT(x=2)
5528+
5529+
def g() -> None:
5530+
NT = NamedTuple('NT', [('y', str)])
5531+
5532+
n: NT = NT(y='x')
5533+
5534+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)