Skip to content

Commit

Permalink
Fix crash with nested NamedTuple in incremental mode (#10431)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JukkaL committed May 6, 2021
1 parent 4a5e311 commit 389a172
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 20 deletions.
10 changes: 7 additions & 3 deletions mypy/semanal.py
Expand Up @@ -1396,15 +1396,15 @@ def prepare_class_def(self, defn: ClassDef, info: Optional[TypeInfo] = None) ->
# incremental mode and we should avoid it. In general, this logic is too
# ad-hoc and needs to be removed/refactored.
if '@' not in defn.info._fullname:
local_name = defn.info._fullname + '@' + str(defn.line)
local_name = defn.info.name + '@' + str(defn.line)
if defn.info.is_named_tuple:
# Module is already correctly set in _fullname for named tuples.
defn.info._fullname += '@' + str(defn.line)
else:
defn.info._fullname = self.cur_mod_id + '.' + local_name
else:
# Preserve name from previous fine-grained incremental run.
local_name = defn.info._fullname
local_name = defn.info.name
defn.fullname = defn.info._fullname
self.globals[local_name] = SymbolTableNode(GDEF, defn.info)

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

def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance) -> TypeInfo:
def basic_new_typeinfo(self, name: str,
basetype_or_fallback: Instance,
line: int) -> TypeInfo:
if self.is_func_scope() and not self.type and '@' not in name:
name += '@' + str(line)
class_def = ClassDef(name, Block([]))
if self.is_func_scope() and not self.type:
# Full names of generated classes should always be prefixed with the module names
Expand Down
9 changes: 5 additions & 4 deletions mypy/semanal_enum.py
Expand Up @@ -67,13 +67,13 @@ class A(enum.Enum):
items, values, ok = self.parse_enum_call_args(call, fullname.split('.')[-1])
if not ok:
# Error. Construct dummy return value.
info = self.build_enum_call_typeinfo(var_name, [], fullname)
info = self.build_enum_call_typeinfo(var_name, [], fullname, node.line)
else:
name = cast(Union[StrExpr, UnicodeExpr], call.args[0]).value
if name != var_name or is_func_scope:
# Give it a unique name derived from the line number.
name += '@' + str(call.line)
info = self.build_enum_call_typeinfo(name, items, fullname)
info = self.build_enum_call_typeinfo(name, items, fullname, call.line)
# Store generated TypeInfo under both names, see semanal_namedtuple for more details.
if name != var_name or is_func_scope:
self.api.add_symbol_skip_local(name, info)
Expand All @@ -82,10 +82,11 @@ class A(enum.Enum):
info.line = node.line
return info

def build_enum_call_typeinfo(self, name: str, items: List[str], fullname: str) -> TypeInfo:
def build_enum_call_typeinfo(self, name: str, items: List[str], fullname: str,
line: int) -> TypeInfo:
base = self.api.named_type_or_none(fullname)
assert base is not None
info = self.api.basic_new_typeinfo(name, base)
info = self.api.basic_new_typeinfo(name, base, line)
info.metaclass_type = info.calculate_metaclass_type()
info.is_enum = True
for item in items:
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal_namedtuple.py
Expand Up @@ -382,7 +382,7 @@ def build_namedtuple_typeinfo(self,
iterable_type = self.api.named_type_or_none('typing.Iterable', [implicit_any])
function_type = self.api.named_type('__builtins__.function')

info = self.api.basic_new_typeinfo(name, fallback)
info = self.api.basic_new_typeinfo(name, fallback, line)
info.is_named_tuple = True
tuple_base = TupleType(types, fallback)
info.tuple_type = tuple_base
Expand Down
11 changes: 6 additions & 5 deletions mypy/semanal_newtype.py
Expand Up @@ -72,20 +72,20 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
# Create the corresponding class definition if the aliased type is subtypeable
if isinstance(old_type, TupleType):
newtype_class_info = self.build_newtype_typeinfo(name, old_type,
old_type.partial_fallback)
old_type.partial_fallback, s.line)
newtype_class_info.tuple_type = old_type
elif isinstance(old_type, Instance):
if old_type.type.is_protocol:
self.fail("NewType cannot be used with protocol classes", s)
newtype_class_info = self.build_newtype_typeinfo(name, old_type, old_type)
newtype_class_info = self.build_newtype_typeinfo(name, old_type, old_type, s.line)
else:
if old_type is not None:
message = "Argument 2 to NewType(...) must be subclassable (got {})"
self.fail(message.format(format_type(old_type)), s, code=codes.VALID_NEWTYPE)
# Otherwise the error was already reported.
old_type = AnyType(TypeOfAny.from_error)
object_type = self.api.named_type('__builtins__.object')
newtype_class_info = self.build_newtype_typeinfo(name, old_type, object_type)
newtype_class_info = self.build_newtype_typeinfo(name, old_type, object_type, s.line)
newtype_class_info.fallback_to_any = True

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

return None if has_failed else old_type, should_defer

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

# Add __init__ method
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal_shared.py
Expand Up @@ -122,7 +122,7 @@ def anal_type(self, t: Type, *,
raise NotImplementedError

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

@abstractmethod
Expand Down
14 changes: 8 additions & 6 deletions mypy/semanal_typeddict.py
Expand Up @@ -60,7 +60,8 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> Tuple[bool, Optional[Typ
fields, types, required_keys = self.analyze_typeddict_classdef_fields(defn)
if fields is None:
return True, None # Defer
info = self.build_typeddict_typeinfo(defn.name, fields, types, required_keys)
info = self.build_typeddict_typeinfo(defn.name, fields, types, required_keys,
defn.line)
defn.analyzed = TypedDictExpr(info)
defn.analyzed.line = defn.line
defn.analyzed.column = defn.column
Expand Down Expand Up @@ -97,7 +98,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> Tuple[bool, Optional[Typ
keys.extend(new_keys)
types.extend(new_types)
required_keys.update(new_required_keys)
info = self.build_typeddict_typeinfo(defn.name, keys, types, required_keys)
info = self.build_typeddict_typeinfo(defn.name, keys, types, required_keys, defn.line)
defn.analyzed = TypedDictExpr(info)
defn.analyzed.line = defn.line
defn.analyzed.column = defn.column
Expand Down Expand Up @@ -196,7 +197,7 @@ def check_typeddict(self,
name, items, types, total, ok = res
if not ok:
# Error. Construct dummy return value.
info = self.build_typeddict_typeinfo('TypedDict', [], [], set())
info = self.build_typeddict_typeinfo('TypedDict', [], [], set(), call.line)
else:
if var_name is not None and name != var_name:
self.fail(
Expand All @@ -206,7 +207,7 @@ def check_typeddict(self,
# Give it a unique name derived from the line number.
name += '@' + str(call.line)
required_keys = set(items) if total else set()
info = self.build_typeddict_typeinfo(name, items, types, required_keys)
info = self.build_typeddict_typeinfo(name, items, types, required_keys, call.line)
info.line = node.line
# Store generated TypeInfo under both names, see semanal_namedtuple for more details.
if name != var_name or is_func_scope:
Expand Down Expand Up @@ -305,13 +306,14 @@ def fail_typeddict_arg(self, message: str,

def build_typeddict_typeinfo(self, name: str, items: List[str],
types: List[Type],
required_keys: Set[str]) -> TypeInfo:
required_keys: Set[str],
line: int) -> TypeInfo:
# Prefer typing then typing_extensions if available.
fallback = (self.api.named_type_or_none('typing._TypedDict', []) or
self.api.named_type_or_none('typing_extensions._TypedDict', []) or
self.api.named_type_or_none('mypy_extensions._TypedDict', []))
assert fallback is not None
info = self.api.basic_new_typeinfo(name, fallback)
info = self.api.basic_new_typeinfo(name, fallback, line)
info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), required_keys,
fallback)
return info
Expand Down
26 changes: 26 additions & 0 deletions test-data/unit/check-incremental.test
Expand Up @@ -5506,3 +5506,29 @@ class Foo:
[delete c1.py.2]
[file c2.py.2]
class C: pass

[case testIncrementalNestedNamedTuple]
# flags: --python-version 3.6
import a

[file a.py]
import b

[file a.py.2]
import b # foo

[file b.py]
from typing import NamedTuple

def f() -> None:
class NT(NamedTuple):
x: int

n: NT = NT(x=2)

def g() -> None:
NT = NamedTuple('NT', [('y', str)])

n: NT = NT(y='x')

[builtins fixtures/tuple.pyi]

0 comments on commit 389a172

Please sign in to comment.