Skip to content

Commit 37ee432

Browse files
authored
Clean-up classes nested in functions (#21478)
Fixes #6422 Fixes #13024 TBH the current situation is embarrassing. I decided to _finally_ clean this all up and unify various cases. Now we have same simple rules used by all classes, both regular (first three) and magic: * Use the name mangling for everything inside a top-level function. * Do not mangle `defn.name` only `defn.fullname`. * Always store classes nested in functions in global symbol table (using enclosing class was only adding unnecessary complications) * In cases of name mismatch (like `One = NamedTuple("Other", ...)` etc) always use the `var_name` for all purposes. * In cases of inline base classes use different mangling mechanism (since those may be nested in functions as well).
1 parent 21e2859 commit 37ee432

15 files changed

Lines changed: 219 additions & 218 deletions

mypy/checker.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8577,6 +8577,9 @@ def is_func_scope(self) -> bool:
85778577
# message types are ignored.
85788578
return False
85798579

8580+
def is_nested_within_func_scope(self) -> bool:
8581+
return self._chk.scope.top_level_function() is not None
8582+
85808583
@property
85818584
def type(self) -> TypeInfo | None:
85828585
return self._chk.scope.current_class()

mypy/nodes.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5396,6 +5396,16 @@ def set_info(node: SymbolNode, info: TypeInfo) -> None:
53965396
set_info(node.impl, info)
53975397

53985398

5399+
def func_scoped_name(name: str, line: int) -> str:
5400+
"""Mangled name to use when storing function-scoped symbols in global symbol tables."""
5401+
return f"{name}@{line}"
5402+
5403+
5404+
def inline_base(name: str, index: int) -> str:
5405+
"""Synthetic name to use when storing inlined base classes in symbol tables."""
5406+
return f"{name}@base{index + 1}"
5407+
5408+
53995409
# See docstring for mypy/cache.py for reserved tag ranges.
54005410
MYPY_FILE: Final[Tag] = 50
54015411
OVERLOADED_FUNC_DEF: Final[Tag] = 51

mypy/semanal.py

Lines changed: 47 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,10 @@
188188
WithStmt,
189189
YieldExpr,
190190
YieldFromExpr,
191+
func_scoped_name,
191192
get_member_expr_fullname,
192193
implicit_module_attrs,
194+
inline_base,
193195
is_final_node,
194196
type_aliases,
195197
type_aliases_source_versions,
@@ -1992,7 +1994,7 @@ def analyze_class(self, defn: ClassDef) -> None:
19921994
return
19931995

19941996
self.analyze_class_keywords(defn)
1995-
bases_result = self.analyze_base_classes(bases)
1997+
bases_result = self.analyze_base_classes(defn.name, bases)
19961998
if bases_result is None or self.found_incomplete_ref(tag):
19971999
# Something was incomplete. Defer current target.
19982000
self.mark_incomplete(defn.name, defn)
@@ -2112,7 +2114,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:
21122114
if info is None:
21132115
self.mark_incomplete(defn.name, defn)
21142116
else:
2115-
self.prepare_class_def(defn, info, custom_names=True)
2117+
self.prepare_class_def(defn, info)
21162118
for decorator in defn.decorators:
21172119
decorator.accept(self)
21182120
if defn.info:
@@ -2136,13 +2138,13 @@ def analyze_namedtuple_classdef(
21362138
info: TypeInfo | None = defn.info
21372139
else:
21382140
is_named_tuple, info = self.named_tuple_analyzer.analyze_namedtuple_classdef(
2139-
defn, self.is_stub_file, self.is_func_scope()
2141+
defn, self.is_stub_file
21402142
)
21412143
if is_named_tuple:
21422144
if info is None:
21432145
self.mark_incomplete(defn.name, defn)
21442146
else:
2145-
self.prepare_class_def(defn, info, custom_names=True)
2147+
self.prepare_class_def(defn, info)
21462148
self.setup_type_vars(defn, tvar_defs)
21472149
self.setup_alias_type_vars(defn)
21482150
with self.scope.class_scope(defn.info):
@@ -2512,51 +2514,26 @@ def get_and_bind_all_tvars(self, type_exprs: list[Expression]) -> list[TypeVarLi
25122514
tvar_defs.append(tvar_def)
25132515
return tvar_defs
25142516

2515-
def prepare_class_def(
2516-
self, defn: ClassDef, info: TypeInfo | None = None, custom_names: bool = False
2517-
) -> None:
2517+
def class_fullname(self, name: str, line: int) -> str:
2518+
if not self.is_nested_within_func_scope():
2519+
return self.qualified_name(name)
2520+
name = func_scoped_name(name, line)
2521+
return f"{self.cur_mod_id}.{name}"
2522+
2523+
def prepare_class_def(self, defn: ClassDef, info: TypeInfo | None = None) -> None:
25182524
"""Prepare for the analysis of a class definition.
25192525
25202526
Create an empty TypeInfo and store it in a symbol table, or if the 'info'
25212527
argument is provided, store it instead (used for magic type definitions).
25222528
"""
25232529
if not defn.info:
2524-
defn.fullname = self.qualified_name(defn.name)
2525-
# TODO: Nested classes
2530+
defn.fullname = self.class_fullname(defn.name, defn.line)
25262531
info = info or self.make_empty_type_info(defn)
25272532
defn.info = info
25282533
info.defn = defn
2529-
if not custom_names:
2530-
# Some special classes (in particular NamedTuples) use custom fullname logic.
2531-
# Don't override it here (also see comment below, this needs cleanup).
2532-
if not self.is_func_scope():
2533-
info._fullname = self.qualified_name(defn.name)
2534-
else:
2535-
info._fullname = info.name
2536-
local_name = defn.name
2537-
if "@" in local_name:
2538-
local_name = local_name.split("@")[0]
2539-
self.add_symbol(local_name, defn.info, defn)
2534+
self.add_symbol(defn.name, defn.info, defn)
25402535
if self.is_nested_within_func_scope():
2541-
# We need to preserve local classes, let's store them
2542-
# in globals under mangled unique names
2543-
#
2544-
# TODO: Putting local classes into globals breaks assumptions in fine-grained
2545-
# incremental mode and we should avoid it. In general, this logic is too
2546-
# ad-hoc and needs to be removed/refactored.
2547-
if "@" not in defn.info._fullname:
2548-
global_name = defn.info.name + "@" + str(defn.line)
2549-
defn.info._fullname = self.cur_mod_id + "." + global_name
2550-
else:
2551-
# Preserve name from previous fine-grained incremental run.
2552-
global_name = defn.info.name
2553-
defn.fullname = defn.info._fullname
2554-
if defn.info.is_named_tuple or defn.info.typeddict_type:
2555-
# Named tuples and Typed dicts nested within a class are stored
2556-
# in the class symbol table.
2557-
self.add_symbol_skip_local(global_name, defn.info)
2558-
else:
2559-
self.globals[global_name] = SymbolTableNode(GDEF, defn.info)
2536+
self.add_global_symbol(defn.name, defn, defn.info)
25602537

25612538
def make_empty_type_info(self, defn: ClassDef) -> TypeInfo:
25622539
if (
@@ -2587,7 +2564,7 @@ def get_name_repr_of_expr(self, expr: Expression) -> str | None:
25872564
return None
25882565

25892566
def analyze_base_classes(
2590-
self, base_type_exprs: list[Expression]
2567+
self, cls_name: str, base_type_exprs: list[Expression]
25912568
) -> tuple[list[tuple[ProperType, Expression]], bool] | None:
25922569
"""Analyze base class types.
25932570
@@ -2599,7 +2576,7 @@ def analyze_base_classes(
25992576
"""
26002577
is_error = False
26012578
bases = []
2602-
for base_expr in base_type_exprs:
2579+
for i, base_expr in enumerate(base_type_exprs):
26032580
if (
26042581
isinstance(base_expr, RefExpr)
26052582
and base_expr.fullname in TYPED_NAMEDTUPLE_NAMES + TPDICT_NAMES
@@ -2617,7 +2594,10 @@ def analyze_base_classes(
26172594

26182595
try:
26192596
base = self.expr_to_analyzed_type(
2620-
base_expr, allow_placeholder=True, allow_type_any=True
2597+
base_expr,
2598+
allow_placeholder=True,
2599+
allow_type_any=True,
2600+
unique_name=inline_base(cls_name, i),
26212601
)
26222602
except TypeTranslationError:
26232603
name = self.get_name_repr_of_expr(base_expr)
@@ -3594,7 +3574,7 @@ def analyze_enum_assign(self, s: AssignmentStmt) -> bool:
35943574
# This is an analyzed enum definition.
35953575
# It is valid iff it can be stored correctly, failures were already reported.
35963576
return self._is_single_name_assignment(s)
3597-
return self.enum_call_analyzer.process_enum_call(s, self.is_func_scope())
3577+
return self.enum_call_analyzer.process_enum_call(s)
35983578

35993579
def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
36003580
"""Check if s defines a namedtuple."""
@@ -3618,7 +3598,7 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
36183598
namespace = self.qualified_name(name)
36193599
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
36203600
internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple(
3621-
s.rvalue, name, self.is_func_scope()
3601+
s.rvalue, name
36223602
)
36233603
if internal_name is None:
36243604
return False
@@ -3655,7 +3635,7 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool:
36553635
namespace = self.qualified_name(name)
36563636
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
36573637
is_typed_dict, info, tvar_defs = self.typed_dict_analyzer.check_typeddict(
3658-
s.rvalue, name, self.is_func_scope()
3638+
s.rvalue, name
36593639
)
36603640
if not is_typed_dict:
36613641
return False
@@ -5161,17 +5141,18 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
51615141
return True
51625142

51635143
def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance, line: int) -> TypeInfo:
5164-
if self.is_func_scope() and not self.type and "@" not in name:
5165-
name += "@" + str(line)
51665144
class_def = ClassDef(name, Block([]))
5167-
if self.is_func_scope() and not self.type:
5168-
# Full names of generated classes should always be prefixed with the module names
5169-
# even if they are nested in a function, since these classes will be (de-)serialized.
5170-
# (Note that the caller should append @line to the name to avoid collisions.)
5171-
# TODO: clean this up, see #6422.
5172-
class_def.fullname = self.cur_mod_id + "." + self.qualified_name(name)
5173-
else:
5174-
class_def.fullname = self.qualified_name(name)
5145+
# Ground rules for classes nested in functions:
5146+
# * Use is_nested_within_func_scope(), not is_func_scope(), to determine whether
5147+
# to use any special logic, because nothing inside top-level functions is serialized.
5148+
# * ClassDef.name is not mangled (i.e. @line suffix is not appended).
5149+
# * ClassDef.fullname, and thus TypeInfo.fullname are always pkg.mod.Name@line, any
5150+
# "intermediate" classes are not included in the fullname.
5151+
# * The caller is responsible for storing the generated TypeInfo twice: once as usual
5152+
# with add_symbol(), and once using add_global_symbol() using the mangled name.
5153+
# The second one is needed to properly serialize any classes nested in functions.
5154+
# TODO: make sure the daemon works well with these rules.
5155+
class_def.fullname = self.class_fullname(name, line)
51755156

51765157
info = TypeInfo(SymbolTable(), class_def, self.cur_mod_id)
51775158
class_def.info = info
@@ -7030,27 +7011,18 @@ def add_symbol(
70307011
name, symbol, context, can_defer, escape_comprehensions, no_progress, type_param
70317012
)
70327013

7033-
def add_symbol_skip_local(self, name: str, node: SymbolNode) -> None:
7034-
"""Same as above, but skipping the local namespace.
7014+
def add_global_symbol(self, name: str, ctx: Context, node: SymbolNode) -> None:
7015+
"""Add symbol to a global namespace.
70357016
70367017
This doesn't check for previous definition and is only used
7037-
for serialization of method-level classes.
7018+
for serialization of classes nested in functions/methods.
70387019
70397020
Classes defined within methods can be exposed through an
70407021
attribute type, but method-level symbol tables aren't serialized.
70417022
This method can be used to add such classes to an enclosing,
70427023
serialized symbol table.
70437024
"""
7044-
# TODO: currently this is only used by named tuples and typed dicts.
7045-
# Use this method also by normal classes, see issue #6422.
7046-
if self.type is not None:
7047-
names = self.type.names
7048-
kind = MDEF
7049-
else:
7050-
names = self.globals
7051-
kind = GDEF
7052-
symbol = SymbolTableNode(kind, node)
7053-
names[name] = symbol
7025+
self.globals[func_scoped_name(name, ctx.line)] = SymbolTableNode(GDEF, node)
70547026

70557027
def add_symbol_table_node(
70567028
self,
@@ -7111,8 +7083,10 @@ def add_symbol_table_node(
71117083
if isinstance(old, Var) and is_init_only(old):
71127084
if old.has_explicit_value:
71137085
self.fail("InitVar with default value cannot be redefined", context)
7114-
elif not (
7115-
isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new)
7086+
elif (
7087+
not (isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new))
7088+
# Avoid (additional) errors for internal symbols.
7089+
and "@" not in name
71167090
):
71177091
self.name_already_defined(name, context, existing)
71187092
elif type_param or (
@@ -7710,14 +7684,15 @@ def expr_to_analyzed_type(
77107684
allow_unbound_tvars: bool = False,
77117685
allow_param_spec_literals: bool = False,
77127686
allow_unpack: bool = False,
7687+
unique_name: str | None = None,
77137688
) -> Type | None:
7714-
if isinstance(expr, CallExpr):
7689+
if unique_name is not None and isinstance(expr, CallExpr):
77157690
# This is a legacy syntax intended mostly for Python 2, we keep it for
77167691
# backwards compatibility, but new features like generic named tuples
77177692
# and recursive named tuples will be not supported.
77187693
expr.accept(self)
77197694
internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple(
7720-
expr, None, self.is_func_scope()
7695+
expr, unique_name
77217696
)
77227697
if tvar_defs:
77237698
self.fail("Generic named tuples are not supported for legacy class syntax", expr)

mypy/semanal_enum.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from __future__ import annotations
77

8-
from typing import Final, cast
8+
from typing import Final
99

1010
from mypy.nodes import (
1111
ARG_NAMED,
@@ -60,7 +60,7 @@ def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
6060
self.options = options
6161
self.api = api
6262

63-
def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool:
63+
def process_enum_call(self, s: AssignmentStmt) -> bool:
6464
"""Check if s defines an Enum; if yes, store the definition in symbol table.
6565
6666
Return True if this looks like an Enum definition (but maybe with errors),
@@ -70,7 +70,7 @@ def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool:
7070
return False
7171
lvalue = s.lvalues[0]
7272
name = lvalue.name
73-
enum_call = self.check_enum_call(s.rvalue, name, is_func_scope)
73+
enum_call = self.check_enum_call(s.rvalue, name)
7474
if enum_call is None:
7575
return False
7676
if isinstance(lvalue, MemberExpr):
@@ -80,9 +80,7 @@ def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool:
8080
self.api.add_symbol(name, enum_call, s)
8181
return True
8282

83-
def check_enum_call(
84-
self, node: Expression, var_name: str, is_func_scope: bool
85-
) -> TypeInfo | None:
83+
def check_enum_call(self, node: Expression, var_name: str) -> TypeInfo | None:
8684
"""Check if a call defines an Enum.
8785
8886
Example:
@@ -110,23 +108,15 @@ class A(enum.Enum):
110108
)
111109
if not ok:
112110
# Error. Construct dummy return value.
113-
name = var_name
114-
if is_func_scope:
115-
name += "@" + str(call.line)
116-
info = self.build_enum_call_typeinfo(name, [], fullname, node.line)
111+
info = self.build_enum_call_typeinfo(var_name, [], fullname, node.line)
117112
else:
118113
if new_class_name != var_name:
119114
msg = f'String argument 1 "{new_class_name}" to {fullname}(...) does not match variable name "{var_name}"'
120115
self.fail(msg, call)
121-
122-
name = cast(StrExpr, call.args[0]).value
123-
if name != var_name or is_func_scope:
124-
# Give it a unique name derived from the line number.
125-
name += "@" + str(call.line)
126-
info = self.build_enum_call_typeinfo(name, items, fullname, call.line)
116+
info = self.build_enum_call_typeinfo(var_name, items, fullname, call.line)
127117
# Store generated TypeInfo under both names, see semanal_namedtuple for more details.
128-
if name != var_name or is_func_scope:
129-
self.api.add_symbol_skip_local(name, info)
118+
if self.api.is_nested_within_func_scope():
119+
self.api.add_global_symbol(var_name, node, info)
130120
call.analyzed = EnumCallExpr(info, items, values)
131121
call.analyzed.set_line(call)
132122
info.line = node.line

0 commit comments

Comments
 (0)