Skip to content

Commit 18a5107

Browse files
authored
Fix nested namedtuple crash in incremental mode (#12803)
Make sure the fullname of a named tuple defined within a method matches the nesting of the definition in the symbol table. Otherwise we'll have a crash during deserialization. In particular, a named tuple defined within a method will now be always stored in the symbol table of the surrounding class, instead of the global symbol table. Previously there was an inconsistency between old-style and new-style syntax. Fix #10913.
1 parent 85c2159 commit 18a5107

File tree

3 files changed

+70
-11
lines changed

3 files changed

+70
-11
lines changed

mypy/semanal.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool:
12201220
is_named_tuple, info = True, defn.info # type: bool, Optional[TypeInfo]
12211221
else:
12221222
is_named_tuple, info = self.named_tuple_analyzer.analyze_namedtuple_classdef(
1223-
defn, self.is_stub_file)
1223+
defn, self.is_stub_file, self.is_func_scope())
12241224
if is_named_tuple:
12251225
if info is None:
12261226
self.mark_incomplete(defn.name, defn)
@@ -1462,7 +1462,10 @@ def prepare_class_def(self, defn: ClassDef, info: Optional[TypeInfo] = None) ->
14621462
info._fullname = self.qualified_name(defn.name)
14631463
else:
14641464
info._fullname = info.name
1465-
self.add_symbol(defn.name, defn.info, defn)
1465+
local_name = defn.name
1466+
if '@' in local_name:
1467+
local_name = local_name.split('@')[0]
1468+
self.add_symbol(local_name, defn.info, defn)
14661469
if self.is_nested_within_func_scope():
14671470
# We need to preserve local classes, let's store them
14681471
# in globals under mangled unique names
@@ -1471,17 +1474,17 @@ def prepare_class_def(self, defn: ClassDef, info: Optional[TypeInfo] = None) ->
14711474
# incremental mode and we should avoid it. In general, this logic is too
14721475
# ad-hoc and needs to be removed/refactored.
14731476
if '@' not in defn.info._fullname:
1474-
local_name = defn.info.name + '@' + str(defn.line)
1475-
if defn.info.is_named_tuple:
1476-
# Module is already correctly set in _fullname for named tuples.
1477-
defn.info._fullname += '@' + str(defn.line)
1478-
else:
1479-
defn.info._fullname = self.cur_mod_id + '.' + local_name
1477+
global_name = defn.info.name + '@' + str(defn.line)
1478+
defn.info._fullname = self.cur_mod_id + '.' + global_name
14801479
else:
14811480
# Preserve name from previous fine-grained incremental run.
1482-
local_name = defn.info.name
1481+
global_name = defn.info.name
14831482
defn.fullname = defn.info._fullname
1484-
self.globals[local_name] = SymbolTableNode(GDEF, defn.info)
1483+
if defn.info.is_named_tuple:
1484+
# Named tuple nested within a class is stored in the class symbol table.
1485+
self.add_symbol_skip_local(global_name, defn.info)
1486+
else:
1487+
self.globals[global_name] = SymbolTableNode(GDEF, defn.info)
14851488

14861489
def make_empty_type_info(self, defn: ClassDef) -> TypeInfo:
14871490
if (self.is_module_scope()

mypy/semanal_namedtuple.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
5353
self.options = options
5454
self.api = api
5555

56-
def analyze_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool
56+
def analyze_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool,
57+
is_func_scope: bool
5758
) -> Tuple[bool, Optional[TypeInfo]]:
5859
"""Analyze if given class definition can be a named tuple definition.
5960
@@ -70,6 +71,8 @@ def analyze_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool
7071
# This is a valid named tuple, but some types are incomplete.
7172
return True, None
7273
items, types, default_items = result
74+
if is_func_scope and '@' not in defn.name:
75+
defn.name += '@' + str(defn.line)
7376
info = self.build_namedtuple_typeinfo(
7477
defn.name, items, types, default_items, defn.line)
7578
defn.info = info

test-data/unit/check-incremental.test

+53
Original file line numberDiff line numberDiff line change
@@ -5658,3 +5658,56 @@ class D(C):
56585658
[out]
56595659
[out2]
56605660
tmp/a.py:9: error: Trying to assign name "z" that is not in "__slots__" of type "a.D"
5661+
5662+
[case testIncrementalWithDifferentKindsOfNestedTypesWithinMethod]
5663+
# flags: --python-version 3.7
5664+
5665+
import a
5666+
5667+
[file a.py]
5668+
import b
5669+
5670+
[file a.py.2]
5671+
import b
5672+
b.xyz
5673+
5674+
[file b.py]
5675+
from typing import NamedTuple, NewType
5676+
from typing_extensions import TypedDict, TypeAlias
5677+
from enum import Enum
5678+
from dataclasses import dataclass
5679+
5680+
class C:
5681+
def f(self) -> None:
5682+
class C:
5683+
c: int
5684+
class NT1(NamedTuple):
5685+
c: int
5686+
NT2 = NamedTuple("NT2", [("c", int)])
5687+
class NT3(NT1):
5688+
pass
5689+
class TD(TypedDict):
5690+
c: int
5691+
TD2 = TypedDict("TD2", {"c": int})
5692+
class E(Enum):
5693+
X = 1
5694+
@dataclass
5695+
class DC:
5696+
c: int
5697+
Alias: TypeAlias = NT1
5698+
N = NewType("N", NT1)
5699+
5700+
c: C = C()
5701+
nt1: NT1 = NT1(c=1)
5702+
nt2: NT2 = NT2(c=1)
5703+
nt3: NT3 = NT3(c=1)
5704+
td: TD = TD(c=1)
5705+
td2: TD2 = TD2(c=1)
5706+
e: E = E.X
5707+
dc: DC = DC(c=1)
5708+
al: Alias = Alias(c=1)
5709+
n: N = N(NT1(c=1))
5710+
5711+
[builtins fixtures/dict.pyi]
5712+
[out2]
5713+
tmp/a.py:2: error: "object" has no attribute "xyz"

0 commit comments

Comments
 (0)