Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crash with nested NamedTuple in incremental mode #10431

Merged
merged 1 commit into from
May 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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]