Skip to content

Commit 0e6cfd4

Browse files
authored
Unify handling of self attributes in daemon (#21025)
There are two situations that require special handling in the daemon: * When we have an attribute _defined_ on `self` in a method. * When a variable defined in class body is _inferred_ using an assignment in a method. The first one is an old thing that is handled with so called "saved attributes", while the second is a recent addition handled by including relevant methods in the top-level target. Two different mechanisms with subtle differences for two similar situations make it hard to reason about what is going on in the daemon. And it will be even more complicated when we will add "staggered" interface/implementation type-checking in parallel checking, that will need to handle these situations as well. Although this PR is strictly speaking not required for parallel checking, I propose to unify all four quadrants in the (defined variable vs inferred variable type) x (parallel checking vs daemon) square under the same mechanism: have a _single_ flag on a function/method that specifies that this function/method may affect interface of the module, and thus should be _always_ processed as a part of the module top-level. This is a relatively significant change in the daemon, but IMO it is (much) cleaner, and we will probably need to rip off this band-aid sooner or later anyway. I didn't do this in my initial implementation of partial `None` type support, because I didn't want to touch the daemon more than strictly necessary (as I did in some of my other recent PRs), but now it seems to me that doing so creates too much tech-debt to address later.
1 parent 7ff36ef commit 0e6cfd4

File tree

6 files changed

+92
-142
lines changed

6 files changed

+92
-142
lines changed

mypy/checker.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
RefExpr,
131131
ReturnStmt,
132132
SetExpr,
133+
SplittingVisitor,
133134
StarExpr,
134135
Statement,
135136
StrExpr,
@@ -319,7 +320,7 @@ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> Literal
319320
return False
320321

321322

322-
class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi):
323+
class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi, SplittingVisitor):
323324
"""Mypy type checker.
324325
325326
Type check mypy source files that have been semantically analyzed.
@@ -454,9 +455,6 @@ def __init__(
454455
or self.path in self.msg.errors.ignored_files
455456
or (self.options.test_env and self.is_typeshed_stub)
456457
)
457-
458-
# If True, process function definitions. If False, don't. This is used
459-
# for processing module top levels in fine-grained incremental mode.
460458
self.recurse_into_functions = True
461459
# This internal flag is used to track whether we a currently type-checking
462460
# a final declaration (assignment), so that some errors should be suppressed.
@@ -719,23 +717,10 @@ def accept_loop(
719717
# Definitions
720718
#
721719

722-
@contextmanager
723-
def set_recurse_into_functions(self) -> Iterator[None]:
724-
"""Temporarily set recurse_into_functions to True.
725-
726-
This is used to process top-level functions/methods as a whole.
727-
"""
728-
old_recurse_into_functions = self.recurse_into_functions
729-
self.recurse_into_functions = True
730-
try:
731-
yield
732-
finally:
733-
self.recurse_into_functions = old_recurse_into_functions
734-
735720
def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
736721
# If a function/method can infer variable types, it should be processed as part
737722
# of the module top level (i.e. module interface).
738-
if not self.recurse_into_functions and not defn.can_infer_vars:
723+
if not self.recurse_into_functions and not defn.def_or_infer_vars:
739724
return
740725
with self.tscope.function_scope(defn), self.set_recurse_into_functions():
741726
self._visit_overloaded_func_def(defn)
@@ -1211,7 +1196,7 @@ def get_generator_return_type(self, return_type: Type, is_coroutine: bool) -> Ty
12111196
return NoneType()
12121197

12131198
def visit_func_def(self, defn: FuncDef) -> None:
1214-
if not self.recurse_into_functions and not defn.can_infer_vars:
1199+
if not self.recurse_into_functions and not defn.def_or_infer_vars:
12151200
return
12161201
with self.tscope.function_scope(defn), self.set_recurse_into_functions():
12171202
self.check_func_item(defn, name=defn.name)
@@ -1452,8 +1437,7 @@ def check_func_def(
14521437
not self.can_skip_diagnostics
14531438
or self.options.preserve_asts
14541439
or not isinstance(defn, FuncDef)
1455-
or defn.has_self_attr_def
1456-
or defn.can_infer_vars
1440+
or defn.def_or_infer_vars
14571441
):
14581442
self.accept(item.body)
14591443
unreachable = self.binder.is_unreachable()
@@ -5620,7 +5604,7 @@ def visit_decorator(self, e: Decorator) -> None:
56205604
def visit_decorator_inner(
56215605
self, e: Decorator, allow_empty: bool = False, skip_first_item: bool = False
56225606
) -> None:
5623-
if self.recurse_into_functions or e.func.can_infer_vars:
5607+
if self.recurse_into_functions or e.func.def_or_infer_vars:
56245608
with self.tscope.function_scope(e.func), self.set_recurse_into_functions():
56255609
self.check_func_item(e.func, name=e.func.name, allow_empty=allow_empty)
56265610

mypy/nodes.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from abc import abstractmethod
77
from collections import defaultdict
88
from collections.abc import Callable, Iterator, Sequence
9+
from contextlib import contextmanager
910
from enum import Enum, unique
1011
from typing import (
1112
TYPE_CHECKING,
@@ -659,7 +660,7 @@ class FuncBase(Node):
659660
"is_final", # Uses "@final"
660661
"is_explicit_override", # Uses "@override"
661662
"is_type_check_only", # Uses "@type_check_only"
662-
"can_infer_vars",
663+
"def_or_infer_vars",
663664
"_fullname",
664665
)
665666

@@ -680,8 +681,8 @@ def __init__(self) -> None:
680681
self.is_final = False
681682
self.is_explicit_override = False
682683
self.is_type_check_only = False
683-
# Can this function/method infer types of variables defined outside? Currently,
684-
# we only set this in cases like:
684+
# Can this function/method define variables or infer variables defined outside?
685+
# In particular, we set this in cases like:
685686
# x = None
686687
# def foo() -> None:
687688
# global x
@@ -691,7 +692,7 @@ def __init__(self) -> None:
691692
# x = None
692693
# def foo(self) -> None:
693694
# self.x = 1
694-
self.can_infer_vars = False
695+
self.def_or_infer_vars = False
695696
# Name with module prefix
696697
self._fullname = ""
697698

@@ -1035,7 +1036,6 @@ class FuncDef(FuncItem, SymbolNode, Statement):
10351036
"original_def",
10361037
"is_trivial_body",
10371038
"is_trivial_self",
1038-
"has_self_attr_def",
10391039
"is_mypy_only",
10401040
# Present only when a function is decorated with @typing.dataclass_transform or similar
10411041
"dataclass_transform_spec",
@@ -1074,8 +1074,6 @@ def __init__(
10741074
# the majority). In cases where self is not annotated and there are no Self
10751075
# in the signature we can simply drop the first argument.
10761076
self.is_trivial_self = False
1077-
# Keep track of functions where self attributes are defined.
1078-
self.has_self_attr_def = False
10791077
# This is needed because for positional-only arguments the name is set to None,
10801078
# but we sometimes still want to show it in error messages.
10811079
if arguments:
@@ -5089,6 +5087,26 @@ def read(cls, data: ReadBuffer) -> DataclassTransformSpec:
50895087
return ret
50905088

50915089

5090+
@trait
5091+
class SplittingVisitor:
5092+
# If True, process function definitions. If False, don't. This is used
5093+
# for processing module top levels in fine-grained incremental mode.
5094+
recurse_into_functions: bool
5095+
5096+
@contextmanager
5097+
def set_recurse_into_functions(self) -> Iterator[None]:
5098+
"""Temporarily set recurse_into_functions to True.
5099+
5100+
This is used to process top-level functions/methods as a whole.
5101+
"""
5102+
old_recurse_into_functions = self.recurse_into_functions
5103+
self.recurse_into_functions = True
5104+
try:
5105+
yield
5106+
finally:
5107+
self.recurse_into_functions = old_recurse_into_functions
5108+
5109+
50925110
def get_flags(node: Node, names: list[str]) -> list[str]:
50935111
return [name for name in names if getattr(node, name)]
50945112

mypy/semanal.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
SetComprehension,
160160
SetExpr,
161161
SliceExpr,
162+
SplittingVisitor,
162163
StarExpr,
163164
Statement,
164165
StrExpr,
@@ -372,7 +373,7 @@
372373

373374

374375
class SemanticAnalyzer(
375-
NodeVisitor[None], SemanticAnalyzerInterface, SemanticAnalyzerPluginInterface
376+
NodeVisitor[None], SemanticAnalyzerInterface, SemanticAnalyzerPluginInterface, SplittingVisitor
376377
):
377378
"""Semantically analyze parsed mypy files.
378379
@@ -497,8 +498,6 @@ def __init__(
497498
self.incomplete_namespaces = incomplete_namespaces
498499
self.all_exports: list[str] = []
499500
self.plugin = plugin
500-
# If True, process function definitions. If False, don't. This is used
501-
# for processing module top levels in fine-grained incremental mode.
502501
self.recurse_into_functions = True
503502
self.scope = Scope()
504503

@@ -981,10 +980,10 @@ def visit_func_def(self, defn: FuncDef) -> None:
981980
if not defn.is_decorated and not defn.is_overload:
982981
self.add_function_to_symbol_table(defn)
983982

984-
if not self.recurse_into_functions:
983+
if not self.recurse_into_functions and not defn.def_or_infer_vars:
985984
return
986985

987-
with self.scope.function_scope(defn):
986+
with self.scope.function_scope(defn), self.set_recurse_into_functions():
988987
with self.inside_except_star_block_set(value=False):
989988
self.analyze_func_def(defn)
990989

@@ -1272,14 +1271,14 @@ def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
12721271
self.statement = defn
12731272
self.add_function_to_symbol_table(defn)
12741273

1275-
if not self.recurse_into_functions:
1274+
if not self.recurse_into_functions and not defn.def_or_infer_vars:
12761275
return
12771276

12781277
# NB: Since _visit_overloaded_func_def will call accept on the
12791278
# underlying FuncDefs, the function might get entered twice.
12801279
# This is fine, though, because only the outermost function is
12811280
# used to compute targets.
1282-
with self.scope.function_scope(defn):
1281+
with self.scope.function_scope(defn), self.set_recurse_into_functions():
12831282
self.analyze_overloaded_func_def(defn)
12841283

12851284
@contextmanager
@@ -1809,8 +1808,9 @@ def visit_decorator(self, dec: Decorator) -> None:
18091808
dec.var.is_initialized_in_class = True
18101809
if no_type_check:
18111810
erase_func_annotations(dec.func)
1812-
if not no_type_check and self.recurse_into_functions:
1813-
dec.func.accept(self)
1811+
if not no_type_check and (self.recurse_into_functions or dec.func.def_or_infer_vars):
1812+
with self.set_recurse_into_functions():
1813+
dec.func.accept(self)
18141814
if could_be_decorated_property and dec.decorators and dec.var.is_property:
18151815
self.fail(
18161816
"Decorators on top of @property are not supported", dec, code=PROPERTY_DECORATOR
@@ -4602,7 +4602,7 @@ def make_name_lvalue_point_to_existing_def(
46024602
and original_def.node.is_inferred
46034603
):
46044604
for func in self.scope.functions:
4605-
func.can_infer_vars = True
4605+
func.def_or_infer_vars = True
46064606

46074607
def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, explicit_type: bool = False) -> None:
46084608
"""Analyze an lvalue or assignment target that is a list or tuple."""
@@ -4687,16 +4687,15 @@ def analyze_member_lvalue(
46874687
# TODO: should we also set lval.kind = MDEF?
46884688
self.type.names[lval.name] = SymbolTableNode(MDEF, v, implicit=True)
46894689
for func in self.scope.functions:
4690-
if isinstance(func, FuncDef):
4691-
func.has_self_attr_def = True
4690+
func.def_or_infer_vars = True
46924691
if (
46934692
cur_node
46944693
and isinstance(cur_node.node, Var)
46954694
and cur_node.node.is_inferred
46964695
and cur_node.node.is_initialized_in_class
46974696
):
46984697
for func in self.scope.functions:
4699-
func.can_infer_vars = True
4698+
func.def_or_infer_vars = True
47004699
self.check_lvalue_validity(lval.node, lval)
47014700

47024701
def is_self_member_ref(self, memberexpr: MemberExpr) -> bool:
@@ -7566,7 +7565,7 @@ def already_defined(
75667565

75677566
if isinstance(original_ctx, SymbolTableNode) and isinstance(original_ctx.node, MypyFile):
75687567
# Since this is an import, original_ctx.node points to the module definition.
7569-
# Therefore its line number is always 1, which is not useful for this
7568+
# Therefore, its line number is always 1, which is not useful for this
75707569
# error message.
75717570
extra_msg = " (by an import)"
75727571
elif node and node.line != -1 and self.is_local_name(node.fullname):

mypy/semanal_main.py

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import mypy.state
3535
from mypy.checker import FineGrainedDeferredNode
3636
from mypy.errors import Errors
37-
from mypy.nodes import Decorator, FuncDef, MypyFile, OverloadedFuncDef, TypeInfo, Var
37+
from mypy.nodes import Decorator, FuncDef, MypyFile, OverloadedFuncDef, TypeInfo
3838
from mypy.options import Options
3939
from mypy.plugin import ClassDefContext
4040
from mypy.plugins import dataclasses as dataclasses_plugin
@@ -52,7 +52,6 @@
5252
from mypy.semanal_infer import infer_decorator_signature_if_simple
5353
from mypy.semanal_shared import find_dataclass_transform_spec
5454
from mypy.semanal_typeargs import TypeArgumentAnalyzer
55-
from mypy.server.aststrip import SavedAttributes
5655

5756
if TYPE_CHECKING:
5857
from mypy.build import Graph, State
@@ -129,23 +128,18 @@ def cleanup_builtin_scc(state: State) -> None:
129128

130129

131130
def semantic_analysis_for_targets(
132-
state: State, nodes: list[FineGrainedDeferredNode], graph: Graph, saved_attrs: SavedAttributes
131+
state: State, nodes: list[FineGrainedDeferredNode], graph: Graph
133132
) -> None:
134133
"""Semantically analyze only selected nodes in a given module.
135134
136135
This essentially mirrors the logic of semantic_analysis_for_scc()
137-
except that we process only some targets. This is used in fine grained
136+
except that we process only some targets. This is used in fine-grained
138137
incremental mode, when propagating an update.
139-
140-
The saved_attrs are implicitly declared instance attributes (attributes
141-
defined on self) removed by AST stripper that may need to be reintroduced
142-
here. They must be added before any methods are analyzed.
143138
"""
144139
patches: Patches = []
145140
if any(isinstance(n.node, MypyFile) for n in nodes):
146141
# Process module top level first (if needed).
147142
process_top_levels(graph, [state.id], patches)
148-
restore_saved_attrs(saved_attrs)
149143
analyzer = state.manager.semantic_analyzer
150144
for n in nodes:
151145
if isinstance(n.node, MypyFile):
@@ -160,30 +154,6 @@ def semantic_analysis_for_targets(
160154
calculate_class_properties(graph, [state.id], state.manager.errors)
161155

162156

163-
def restore_saved_attrs(saved_attrs: SavedAttributes) -> None:
164-
"""Restore instance variables removed during AST strip that haven't been added yet."""
165-
for (cdef, name), sym in saved_attrs.items():
166-
info = cdef.info
167-
existing = info.get(name)
168-
defined_in_this_class = name in info.names
169-
assert isinstance(sym.node, Var)
170-
# This needs to mimic the logic in SemanticAnalyzer.analyze_member_lvalue()
171-
# regarding the existing variable in class body or in a superclass:
172-
# If the attribute of self is not defined in superclasses, create a new Var.
173-
if (
174-
existing is None
175-
or
176-
# (An abstract Var is considered as not defined.)
177-
(isinstance(existing.node, Var) and existing.node.is_abstract_var)
178-
or
179-
# Also an explicit declaration on self creates a new Var unless
180-
# there is already one defined in the class body.
181-
sym.node.explicit_self_type
182-
and not defined_in_this_class
183-
):
184-
info.names[name] = sym
185-
186-
187157
def process_top_levels(graph: Graph, scc: list[str], patches: Patches) -> None:
188158
# Process top levels until everything has been bound.
189159

@@ -240,6 +210,9 @@ def process_top_levels(graph: Graph, scc: list[str], patches: Patches) -> None:
240210
# processing the same target twice in a row, which is inefficient.
241211
worklist = list(reversed(all_deferred))
242212
final_iteration = not any_progress
213+
# Functions/methods that define/infer attributes are processed as part of top-levels.
214+
# We need to clear the locals for those between fine-grained iterations.
215+
analyzer.saved_locals.clear()
243216

244217

245218
def order_by_subclassing(targets: list[FullTargetInfo]) -> Iterator[FullTargetInfo]:

0 commit comments

Comments
 (0)