Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8577,6 +8577,9 @@ def is_func_scope(self) -> bool:
# message types are ignored.
return False

def is_nested_within_func_scope(self) -> bool:
return self._chk.scope.top_level_function() is not None

@property
def type(self) -> TypeInfo | None:
return self._chk.scope.current_class()
Expand Down
96 changes: 31 additions & 65 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2112,7 +2112,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:
if info is None:
self.mark_incomplete(defn.name, defn)
else:
self.prepare_class_def(defn, info, custom_names=True)
self.prepare_class_def(defn, info)
for decorator in defn.decorators:
decorator.accept(self)
if defn.info:
Expand All @@ -2136,13 +2136,13 @@ def analyze_namedtuple_classdef(
info: TypeInfo | None = defn.info
else:
is_named_tuple, info = self.named_tuple_analyzer.analyze_namedtuple_classdef(
defn, self.is_stub_file, self.is_func_scope()
defn, self.is_stub_file
)
if is_named_tuple:
if info is None:
self.mark_incomplete(defn.name, defn)
else:
self.prepare_class_def(defn, info, custom_names=True)
self.prepare_class_def(defn, info)
self.setup_type_vars(defn, tvar_defs)
self.setup_alias_type_vars(defn)
with self.scope.class_scope(defn.info):
Expand Down Expand Up @@ -2510,51 +2510,24 @@ def get_and_bind_all_tvars(self, type_exprs: list[Expression]) -> list[TypeVarLi
tvar_defs.append(tvar_def)
return tvar_defs

def prepare_class_def(
self, defn: ClassDef, info: TypeInfo | None = None, custom_names: bool = False
) -> None:
def prepare_class_def(self, defn: ClassDef, info: TypeInfo | None = None) -> None:
"""Prepare for the analysis of a class definition.

Create an empty TypeInfo and store it in a symbol table, or if the 'info'
argument is provided, store it instead (used for magic type definitions).
"""
if not defn.info:
defn.fullname = self.qualified_name(defn.name)
# TODO: Nested classes
if not self.is_nested_within_func_scope():
defn.fullname = self.qualified_name(defn.name)
else:
defn.fullname = f"{self.cur_mod_id}.{defn.name}@{defn.line}"
info = info or self.make_empty_type_info(defn)
defn.info = info
info.defn = defn
if not custom_names:
# Some special classes (in particular NamedTuples) use custom fullname logic.
# Don't override it here (also see comment below, this needs cleanup).
if not self.is_func_scope():
info._fullname = self.qualified_name(defn.name)
else:
info._fullname = info.name
local_name = defn.name
if "@" in local_name:
local_name = local_name.split("@")[0]
self.add_symbol(local_name, defn.info, defn)
self.add_symbol(defn.name, defn.info, defn)
if self.is_nested_within_func_scope():
# We need to preserve local classes, let's store them
# in globals under mangled unique names
#
# TODO: Putting local classes into globals breaks assumptions in fine-grained
# 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:
global_name = defn.info.name + "@" + str(defn.line)
defn.info._fullname = self.cur_mod_id + "." + global_name
else:
# Preserve name from previous fine-grained incremental run.
global_name = defn.info.name
defn.fullname = defn.info._fullname
if defn.info.is_named_tuple or defn.info.typeddict_type:
# Named tuples and Typed dicts nested within a class are stored
# in the class symbol table.
self.add_symbol_skip_local(global_name, defn.info)
else:
self.globals[global_name] = SymbolTableNode(GDEF, defn.info)
global_name = f"{defn.name}@{defn.line}"
self.globals[global_name] = SymbolTableNode(GDEF, defn.info)

def make_empty_type_info(self, defn: ClassDef) -> TypeInfo:
if (
Expand Down Expand Up @@ -3592,7 +3565,7 @@ def analyze_enum_assign(self, s: AssignmentStmt) -> bool:
# This is an analyzed enum definition.
# It is valid iff it can be stored correctly, failures were already reported.
return self._is_single_name_assignment(s)
return self.enum_call_analyzer.process_enum_call(s, self.is_func_scope())
return self.enum_call_analyzer.process_enum_call(s)

def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
"""Check if s defines a namedtuple."""
Expand All @@ -3616,7 +3589,7 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
namespace = self.qualified_name(name)
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple(
s.rvalue, name, self.is_func_scope()
s.rvalue, name
)
if internal_name is None:
return False
Expand Down Expand Up @@ -3653,7 +3626,7 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool:
namespace = self.qualified_name(name)
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
is_typed_dict, info, tvar_defs = self.typed_dict_analyzer.check_typeddict(
s.rvalue, name, self.is_func_scope()
s.rvalue, name
)
if not is_typed_dict:
return False
Expand Down Expand Up @@ -5159,15 +5132,19 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
return True

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
# even if they are nested in a function, since these classes will be (de-)serialized.
# (Note that the caller should append @line to the name to avoid collisions.)
# TODO: clean this up, see #6422.
class_def.fullname = self.cur_mod_id + "." + self.qualified_name(name)
# Ground rules for classes nested in functions:
# * Use is_nested_within_func_scope(), not is_func_scope(), to determine whether
# to use any special logic, because nothing inside top-level functions is serialized.
# * ClassDef.name is not mangled (i.e. @line suffix is not appended).
# * ClassDef.fullname, and thus TypeInfo.fullname are always pkg.mod.Name@line, any
# "intermediate" classes are not included in the fullname.
# * The caller is responsible for storing the generated TypeInfo twice: once as usual
# with add_symbol(), and once using add_global_symbol() using the mangled name.
# The second one is needed to properly serialize any classes nested in functions.
# TODO: make sure the daemon works well with these rules.
if self.is_nested_within_func_scope():
class_def.fullname = f"{self.cur_mod_id}.{name}@{line}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name generation logic is duplicated in a few places -- maybe move it to a helper function?

else:
class_def.fullname = self.qualified_name(name)

Expand Down Expand Up @@ -7028,27 +7005,18 @@ def add_symbol(
name, symbol, context, can_defer, escape_comprehensions, no_progress, type_param
)

def add_symbol_skip_local(self, name: str, node: SymbolNode) -> None:
"""Same as above, but skipping the local namespace.
def add_global_symbol(self, name: str, node: SymbolNode) -> None:
"""Add symbol to a global namespace.

This doesn't check for previous definition and is only used
for serialization of method-level classes.
for serialization of classes nested in functions/methods.

Classes defined within methods can be exposed through an
attribute type, but method-level symbol tables aren't serialized.
This method can be used to add such classes to an enclosing,
serialized symbol table.
"""
# TODO: currently this is only used by named tuples and typed dicts.
# Use this method also by normal classes, see issue #6422.
if self.type is not None:
names = self.type.names
kind = MDEF
else:
names = self.globals
kind = GDEF
symbol = SymbolTableNode(kind, node)
names[name] = symbol
self.globals[name] = SymbolTableNode(GDEF, node)

def add_symbol_table_node(
self,
Expand Down Expand Up @@ -7714,9 +7682,7 @@ def expr_to_analyzed_type(
# backwards compatibility, but new features like generic named tuples
# and recursive named tuples will be not supported.
expr.accept(self)
internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple(
expr, None, self.is_func_scope()
)
internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple(expr, None)
if tvar_defs:
self.fail("Generic named tuples are not supported for legacy class syntax", expr)
self.note("Use either Python 3 class syntax, or the assignment syntax", expr)
Expand Down
26 changes: 8 additions & 18 deletions mypy/semanal_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import annotations

from typing import Final, cast
from typing import Final

from mypy.nodes import (
ARG_NAMED,
Expand Down Expand Up @@ -60,7 +60,7 @@ def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
self.options = options
self.api = api

def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool:
def process_enum_call(self, s: AssignmentStmt) -> bool:
"""Check if s defines an Enum; if yes, store the definition in symbol table.

Return True if this looks like an Enum definition (but maybe with errors),
Expand All @@ -70,7 +70,7 @@ def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool:
return False
lvalue = s.lvalues[0]
name = lvalue.name
enum_call = self.check_enum_call(s.rvalue, name, is_func_scope)
enum_call = self.check_enum_call(s.rvalue, name)
if enum_call is None:
return False
if isinstance(lvalue, MemberExpr):
Expand All @@ -80,9 +80,7 @@ def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool:
self.api.add_symbol(name, enum_call, s)
return True

def check_enum_call(
self, node: Expression, var_name: str, is_func_scope: bool
) -> TypeInfo | None:
def check_enum_call(self, node: Expression, var_name: str) -> TypeInfo | None:
"""Check if a call defines an Enum.

Example:
Expand Down Expand Up @@ -110,23 +108,15 @@ class A(enum.Enum):
)
if not ok:
# Error. Construct dummy return value.
name = var_name
if is_func_scope:
name += "@" + str(call.line)
info = self.build_enum_call_typeinfo(name, [], fullname, node.line)
info = self.build_enum_call_typeinfo(var_name, [], fullname, node.line)
else:
if new_class_name != var_name:
msg = f'String argument 1 "{new_class_name}" to {fullname}(...) does not match variable name "{var_name}"'
self.fail(msg, call)

name = cast(StrExpr, 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, call.line)
info = self.build_enum_call_typeinfo(var_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)
if self.api.is_nested_within_func_scope():
self.api.add_global_symbol(f"{var_name}@{node.line}", info)
call.analyzed = EnumCallExpr(info, items, values)
call.analyzed.set_line(call)
info.line = node.line
Expand Down
40 changes: 16 additions & 24 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def __init__(
self.msg = msg

def analyze_namedtuple_classdef(
self, defn: ClassDef, is_stub_file: bool, is_func_scope: bool
self, defn: ClassDef, is_stub_file: bool
) -> tuple[bool, TypeInfo | None]:
"""Analyze if given class definition can be a named tuple definition.

Expand All @@ -122,8 +122,6 @@ def analyze_namedtuple_classdef(
# This is a valid named tuple, but some types are incomplete.
return True, None
items, types, default_items, statements = result
if is_func_scope and "@" not in defn.name:
defn.name += "@" + str(defn.line)
existing_info = None
if isinstance(defn.analyzed, NamedTupleExpr):
existing_info = defn.analyzed.info
Expand Down Expand Up @@ -221,7 +219,7 @@ def check_namedtuple_classdef(
return items, types, default_items, statements

def check_namedtuple(
self, node: Expression, var_name: str | None, is_func_scope: bool
self, node: Expression, var_name: str | None
) -> tuple[str | None, TypeInfo | None, list[TypeVarLikeType]]:
"""Check if a call defines a namedtuple.

Expand Down Expand Up @@ -256,15 +254,14 @@ def check_namedtuple(
# Error. Construct dummy return value.
if var_name:
name = var_name
if is_func_scope:
name += "@" + str(call.line)
else:
name = var_name = "namedtuple@" + str(call.line)
info = self.build_namedtuple_typeinfo(name, [], [], {}, node.line, None)
self.store_namedtuple_info(info, var_name, call, is_typed)
if name != var_name or is_func_scope:
# NOTE: we skip local namespaces since they are not serialized.
self.api.add_symbol_skip_local(name, info)
if self.api.is_nested_within_func_scope():
# NOTE: we always serialize in global namespace for convenience,
# because local namespaces are never serialized.
self.api.add_global_symbol(f"{name}@{call.line}", info)
return var_name, info, []
if not ok:
# This is a valid named tuple but some types are not ready.
Expand All @@ -280,16 +277,14 @@ def check_namedtuple(
else:
name = typename

if var_name is None or is_func_scope:
# There are two special cases where need to give it a unique name derived
# from the line number:
if var_name is None:
# There are two special cases where need to give it a unique name:
# * This is a base class expression, since it often matches the class name:
# class NT(NamedTuple('NT', [...])):
# ...
# * This is a local (function or method level) named tuple, since
# two methods of a class can define a named tuple with the same name,
# and they will be stored in the same namespace (see below).
name += "@" + str(call.line)
# * This is a local (function or method level) named tuple, this case is
# handled by basic_new_typeinfo().
name = "namedtuple@" + name
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in my other comment, I wonder if preserving the line number would be helpful here.

if defaults:
default_items = {
arg_name: default for arg_name, default in zip(items[-len(defaults) :], defaults)
Expand All @@ -312,21 +307,18 @@ def check_namedtuple(
else:
call.analyzed = NamedTupleExpr(info, is_typed=is_typed)
call.analyzed.set_line(call)
# There are three cases where we need to store the generated TypeInfo
# There are two cases where we need to store the generated TypeInfo
# second time (for the purpose of serialization):
# * If there is a name mismatch like One = NamedTuple('Other', [...])
# we also store the info under name 'Other@lineno', this is needed
# because classes are (de)serialized using their actual fullname, not
# the name of l.h.s.
# * If this is a method level named tuple. It can leak from the method
# via assignment to self attribute and therefore needs to be serialized
# (local namespaces are not serialized).
# * If it is a base class expression. It was not stored above, since
# there is no var_name (but it still needs to be serialized
# since it is in MRO of some class).
if name != var_name or is_func_scope:
# NOTE: we skip local namespaces since they are not serialized.
self.api.add_symbol_skip_local(name, info)
if var_name is None or self.api.is_nested_within_func_scope():
if self.api.is_nested_within_func_scope():
name = f"{name}@{call.line}"
self.api.add_global_symbol(name, info)
return typename, info, tvar_defs

def store_namedtuple_info(
Expand Down
7 changes: 2 additions & 5 deletions mypy/semanal_newtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
name = var_name
# OK, now we know this is a NewType. But the base type may be not ready yet,
# add placeholder as we do for ClassDef.

if self.api.is_func_scope():
name += "@" + str(s.line)
fullname = self.api.qualified_name(name)

if not call.analyzed or isinstance(call.analyzed, NewTypeExpr) and not call.analyzed.info:
Expand Down Expand Up @@ -134,8 +131,8 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
else:
call.analyzed.info.bases = newtype_class_info.bases
self.api.add_symbol(var_name, call.analyzed.info, s)
if self.api.is_func_scope():
self.api.add_symbol_skip_local(name, call.analyzed.info)
if self.api.is_nested_within_func_scope():
self.api.add_global_symbol(f"{var_name}@{s.line}", call.analyzed.info)
newtype_class_info.line = s.line
return True

Expand Down
Loading
Loading