Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
10 changes: 10 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5396,6 +5396,16 @@ def set_info(node: SymbolNode, info: TypeInfo) -> None:
set_info(node.impl, info)


def func_scoped_name(name: str, line: int) -> str:
"""Mangled name to use when storing function-scoped symbols in global symbol tables."""
return f"{name}@{line}"


def inline_base(name: str, index: int) -> str:
"""Synthetic name to use when storing inlined base classes in symbol tables."""
return f"{name}@base{index + 1}"


# See docstring for mypy/cache.py for reserved tag ranges.
MYPY_FILE: Final[Tag] = 50
OVERLOADED_FUNC_DEF: Final[Tag] = 51
Expand Down
119 changes: 47 additions & 72 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,10 @@
WithStmt,
YieldExpr,
YieldFromExpr,
func_scoped_name,
get_member_expr_fullname,
implicit_module_attrs,
inline_base,
is_final_node,
type_aliases,
type_aliases_source_versions,
Expand Down Expand Up @@ -1992,7 +1994,7 @@ def analyze_class(self, defn: ClassDef) -> None:
return

self.analyze_class_keywords(defn)
bases_result = self.analyze_base_classes(bases)
bases_result = self.analyze_base_classes(defn.name, bases)
if bases_result is None or self.found_incomplete_ref(tag):
# Something was incomplete. Defer current target.
self.mark_incomplete(defn.name, defn)
Expand Down Expand Up @@ -2112,7 +2114,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 +2138,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 @@ -2512,51 +2514,26 @@ 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 class_fullname(self, name: str, line: int) -> str:
if not self.is_nested_within_func_scope():
return self.qualified_name(name)
name = func_scoped_name(name, line)
return f"{self.cur_mod_id}.{name}"

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
defn.fullname = self.class_fullname(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)
self.add_global_symbol(defn.name, defn, defn.info)

def make_empty_type_info(self, defn: ClassDef) -> TypeInfo:
if (
Expand Down Expand Up @@ -2587,7 +2564,7 @@ def get_name_repr_of_expr(self, expr: Expression) -> str | None:
return None

def analyze_base_classes(
self, base_type_exprs: list[Expression]
self, cls_name: str, base_type_exprs: list[Expression]
) -> tuple[list[tuple[ProperType, Expression]], bool] | None:
"""Analyze base class types.

Expand All @@ -2599,7 +2576,7 @@ def analyze_base_classes(
"""
is_error = False
bases = []
for base_expr in base_type_exprs:
for i, base_expr in enumerate(base_type_exprs):
if (
isinstance(base_expr, RefExpr)
and base_expr.fullname in TYPED_NAMEDTUPLE_NAMES + TPDICT_NAMES
Expand All @@ -2617,7 +2594,10 @@ def analyze_base_classes(

try:
base = self.expr_to_analyzed_type(
base_expr, allow_placeholder=True, allow_type_any=True
base_expr,
allow_placeholder=True,
allow_type_any=True,
unique_name=inline_base(cls_name, i),
)
except TypeTranslationError:
name = self.get_name_repr_of_expr(base_expr)
Expand Down Expand Up @@ -3594,7 +3574,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 @@ -3618,7 +3598,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 @@ -3655,7 +3635,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 @@ -5161,17 +5141,18 @@ 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)
else:
class_def.fullname = 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.
class_def.fullname = self.class_fullname(name, line)

info = TypeInfo(SymbolTable(), class_def, self.cur_mod_id)
class_def.info = info
Expand Down Expand Up @@ -7030,27 +7011,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, ctx: Context, 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[func_scoped_name(name, ctx.line)] = SymbolTableNode(GDEF, node)

def add_symbol_table_node(
self,
Expand Down Expand Up @@ -7111,8 +7083,10 @@ def add_symbol_table_node(
if isinstance(old, Var) and is_init_only(old):
if old.has_explicit_value:
self.fail("InitVar with default value cannot be redefined", context)
elif not (
isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new)
elif (
not (isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new))
# Avoid (additional) errors for internal symbols.
and "@" not in name
):
self.name_already_defined(name, context, existing)
elif type_param or (
Expand Down Expand Up @@ -7710,14 +7684,15 @@ def expr_to_analyzed_type(
allow_unbound_tvars: bool = False,
allow_param_spec_literals: bool = False,
allow_unpack: bool = False,
unique_name: str | None = None,
) -> Type | None:
if isinstance(expr, CallExpr):
if unique_name is not None and isinstance(expr, CallExpr):
# This is a legacy syntax intended mostly for Python 2, we keep it for
# 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()
expr, unique_name
)
if tvar_defs:
self.fail("Generic named tuples are not supported for legacy class 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(var_name, node, info)
call.analyzed = EnumCallExpr(info, items, values)
call.analyzed.set_line(call)
info.line = node.line
Expand Down
Loading
Loading