Skip to content

Commit eba2e25

Browse files
committed
PR feedback 1 (p-sawicki)
1 parent 1f638cc commit eba2e25

4 files changed

Lines changed: 57 additions & 53 deletions

File tree

mypy/build.py

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@
118118
MypyFile,
119119
OverloadedFuncDef,
120120
SymbolTable,
121-
TypeInfo,
122121
)
123122
from mypy.options import OPTIONS_AFFECTING_CACHE_NO_PLATFORM
124123
from mypy.partially_defined import PossiblyUndefinedVariableVisitor
@@ -3480,20 +3479,21 @@ def finish_passes(self) -> None:
34803479
if options.export_types:
34813480
manager.all_types.update(self.type_map())
34823481

3483-
# Possible sources of indirect dependencies:
3484-
# * Symbols not directly imported in this module but accessed via an attribute
3485-
# or via a re-export (vast majority of these recorded in semantic analysis).
3486-
# * For each expression type we need to record definitions of type components
3487-
# since "meaning" of the type may be updated when definitions are updated.
3488-
# * For mypyc-compiled modules only: modules defining MRO ancestors of
3489-
# classes defined here, since the generated C embeds each ancestor's
3490-
# method/attribute layout.
3491-
indirect_refs = self.tree.module_refs | self.type_checker().module_refs
3492-
if self.options.mypyc:
3493-
indirect_refs |= self.compiled_class_ancestor_refs()
34943482
# We should always patch indirect dependencies, even in full (non-incremental) builds,
34953483
# because the cache still may be written, and it must be correct.
3496-
self.patch_indirect_dependencies(indirect_refs, set(self.type_map().values()))
3484+
self.patch_indirect_dependencies(
3485+
# Three possible sources of indirect dependencies:
3486+
# * Symbols not directly imported in this module but accessed via an attribute
3487+
# or via a re-export (vast majority of these recorded in semantic analysis).
3488+
# * For each expression type we need to record definitions of type components
3489+
# since "meaning" of the type may be updated when definitions are updated.
3490+
# * Additional dependencies reported by plugins (e.g. mypyc, see
3491+
# MypycPlugin.get_additional_indirect_deps).
3492+
self.tree.module_refs
3493+
| self.type_checker().module_refs
3494+
| manager.plugin.get_additional_indirect_deps(self.tree),
3495+
set(self.type_map().values()),
3496+
)
34973497

34983498
if self.options.dump_inference_stats:
34993499
dump_type_stats(
@@ -3535,27 +3535,6 @@ def patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -
35353535
self.add_dependency(dep)
35363536
self.priorities[dep] = PRI_INDIRECT
35373537

3538-
def compiled_class_ancestor_refs(self) -> set[str]:
3539-
"""Modules defining MRO ancestors of classes defined in this module.
3540-
3541-
Only used for mypyc-compiled modules: the generated C for a class
3542-
(vtable arrays, getter/setter tables, object struct) references
3543-
every inherited method/attribute of every ancestor, including
3544-
ancestors defined in modules this module does not import directly.
3545-
Recording them as indirect dependencies makes an ancestor's
3546-
interface change re-trigger type checking (and hence C regeneration)
3547-
of this module. Only top-level classes are scanned: mypyc rejects
3548-
nested class definitions.
3549-
"""
3550-
assert self.tree is not None
3551-
mods: set[str] = set()
3552-
for sym in self.tree.names.values():
3553-
node = sym.node
3554-
if isinstance(node, TypeInfo) and node.module_name == self.id:
3555-
for ancestor in node.mro[1:]:
3556-
mods.add(ancestor.module_name)
3557-
return mods
3558-
35593538
def compute_fine_grained_deps(self) -> dict[str, set[str]]:
35603539
assert self.tree is not None
35613540
if self.id in ("builtins", "typing", "types", "sys", "_typeshed"):

mypy/plugin.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,17 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
587587
"""
588588
return []
589589

590+
def get_additional_indirect_deps(self, file: MypyFile) -> set[str]:
591+
"""Customize indirect dependencies for a module.
592+
593+
Unlike get_additional_deps(), this hook is called after the module
594+
has been type checked, so analyzed information (such as class MROs)
595+
is available. The returned module names are recorded as indirect
596+
dependencies: a change to their interfaces will invalidate this
597+
module's cache, but they are not treated as imports.
598+
"""
599+
return set()
600+
590601
def get_type_analyze_hook(self, fullname: str) -> Callable[[AnalyzeTypeContext], Type] | None:
591602
"""Customize behaviour of the type analyzer for given full names.
592603
@@ -846,6 +857,12 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
846857
deps.extend(plugin.get_additional_deps(file))
847858
return deps
848859

860+
def get_additional_indirect_deps(self, file: MypyFile) -> set[str]:
861+
deps: set[str] = set()
862+
for plugin in self._plugins:
863+
deps |= plugin.get_additional_indirect_deps(file)
864+
return deps
865+
849866
def get_type_analyze_hook(self, fullname: str) -> Callable[[AnalyzeTypeContext], Type] | None:
850867
# Micro-optimization: Inline iteration over plugins
851868
for plugin in self._plugins:

mypyc/codegen/emitmodule.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
)
2424
from mypy.errors import CompileError
2525
from mypy.fscache import FileSystemCache
26-
from mypy.nodes import MypyFile
26+
from mypy.nodes import MypyFile, TypeInfo
2727
from mypy.options import Options
2828
from mypy.plugin import Plugin, ReportConfigContext
2929
from mypy.util import hash_digest, json_dumps
@@ -200,6 +200,28 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
200200
# Report dependency on modules in the module's group
201201
return [(10, id, -1) for id in self.group_map.get(file.fullname, (None, []))[1]]
202202

203+
def get_additional_indirect_deps(self, file: MypyFile) -> set[str]:
204+
"""Modules defining MRO ancestors of classes defined in this module.
205+
206+
The generated C for a class (vtable arrays, getter/setter tables,
207+
object struct) references every inherited method/attribute of every
208+
ancestor, including ancestors defined in modules this module does
209+
not import directly. Recording them as indirect dependencies makes
210+
an ancestor's interface change re-trigger type checking (and hence
211+
C regeneration) of this module. Only top-level classes are scanned:
212+
mypyc rejects nested class definitions.
213+
"""
214+
if file.fullname not in self.group_map:
215+
return set()
216+
217+
mods: set[str] = set()
218+
for sym in file.names.values():
219+
node = sym.node
220+
if isinstance(node, TypeInfo) and node.module_name == file.fullname:
221+
for ancestor in node.mro[1:]:
222+
mods.add(ancestor.module_name)
223+
return mods
224+
203225

204226
def parse_and_typecheck(
205227
sources: list[BuildSource],

mypyc/test-data/run-multimodule.test

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,18 +1901,8 @@ from native import test
19011901
test()
19021902

19031903
[case testIncrementalCrossGroupInheritedMethodRemoved]
1904-
# Regression: under separate=True, a module whose only content is a subclass
1905-
# definition (`class Leaf(Mid): pass`) produces no expression types, so it
1906-
# recorded no dependency on the modules defining its transitive bases. When a
1907-
# Base method two hops up the inheritance chain was removed, staleness
1908-
# propagation stopped at other_mid (its own interface is unchanged) and
1909-
# other_leaf stayed fresh: its stale generated C still referenced the method
1910-
# in Leaf's vtable via exports_other_base.CPyDef_..., which no longer exists
1911-
# in the regenerated export table, and the build failed.
1912-
# compiled_class_ancestor_refs must record the MRO ancestors' modules as
1913-
# indirect deps so an ancestor's interface change recompiles the defining
1914-
# module too. (removed_method is declared after existing_method so the
1915-
# surviving call site's vtable slot is unaffected.)
1904+
# Regression: updating a transitive base class triggers recompilation of the
1905+
# subclass-defining module (mypy#21742).
19161906
import other_leaf
19171907

19181908
def test() -> int:
@@ -1955,12 +1945,8 @@ print(test())
19551945
6
19561946

19571947
[case testIncrementalCrossGroupInheritedMethodRemovedNonEmptyLeaf]
1958-
# Same regression as testIncrementalCrossGroupInheritedMethodRemoved, but the
1959-
# subclass body is NOT empty: a class constant and a method that never uses
1960-
# self. Neither produces an expression whose type reaches Leaf's MRO, so the
1961-
# pre-existing dependency sources still record nothing for other_leaf, while
1962-
# its generated vtable references the ancestors regardless of body shape.
1963-
# Guards against narrowing compiled_class_ancestor_refs to pass-only bodies.
1948+
# Regression: updating a transitive base class triggers recompilation of the
1949+
# module defining a subclass with a non-empty body (mypy#21742).
19641950
import other_leaf
19651951

19661952
def test() -> int:

0 commit comments

Comments
 (0)