Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 6 additions & 2 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -3482,12 +3482,16 @@ def finish_passes(self) -> None:
# We should always patch indirect dependencies, even in full (non-incremental) builds,
# because the cache still may be written, and it must be correct.
self.patch_indirect_dependencies(
# Two possible sources of indirect dependencies:
# Three possible sources of indirect dependencies:
# * Symbols not directly imported in this module but accessed via an attribute
# or via a re-export (vast majority of these recorded in semantic analysis).
# * For each expression type we need to record definitions of type components
# since "meaning" of the type may be updated when definitions are updated.
self.tree.module_refs | self.type_checker().module_refs,
# * Additional dependencies reported by plugins (e.g. mypyc, see
# MypycPlugin.get_additional_indirect_deps).
self.tree.module_refs
| self.type_checker().module_refs
| manager.plugin.get_additional_indirect_deps(self.tree),
set(self.type_map().values()),
)

Expand Down
17 changes: 17 additions & 0 deletions mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,17 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
"""
return []

def get_additional_indirect_deps(self, file: MypyFile) -> set[str]:
Comment thread
geooo109 marked this conversation as resolved.
"""Customize indirect dependencies for a module.

This hook is called after the module has been type checked, so
analyzed information (such as class MROs) is available. The
returned module names are recorded as indirect dependencies:
a change to their interfaces will invalidate this module's
cache, but they are not treated as imports.
"""
return set()

def get_type_analyze_hook(self, fullname: str) -> Callable[[AnalyzeTypeContext], Type] | None:
"""Customize behaviour of the type analyzer for given full names.

Expand Down Expand Up @@ -846,6 +857,12 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
deps.extend(plugin.get_additional_deps(file))
return deps

def get_additional_indirect_deps(self, file: MypyFile) -> set[str]:
deps: set[str] = set()
for plugin in self._plugins:
deps |= plugin.get_additional_indirect_deps(file)
return deps

def get_type_analyze_hook(self, fullname: str) -> Callable[[AnalyzeTypeContext], Type] | None:
# Micro-optimization: Inline iteration over plugins
for plugin in self._plugins:
Expand Down
24 changes: 23 additions & 1 deletion mypyc/codegen/emitmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)
from mypy.errors import CompileError
from mypy.fscache import FileSystemCache
from mypy.nodes import MypyFile
from mypy.nodes import MypyFile, TypeInfo
from mypy.options import Options
from mypy.plugin import Plugin, ReportConfigContext
from mypy.util import hash_digest, json_dumps
Expand Down Expand Up @@ -200,6 +200,28 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
# Report dependency on modules in the module's group
return [(10, id, -1) for id in self.group_map.get(file.fullname, (None, []))[1]]

def get_additional_indirect_deps(self, file: MypyFile) -> set[str]:
"""Modules defining MRO ancestors of classes defined in this module.

The generated C for a class (vtable arrays, getter/setter tables,
object struct) references every inherited method/attribute of every
ancestor, including ancestors defined in modules this module does
not import directly. Recording them as indirect dependencies makes
an ancestor's interface change re-trigger type checking (and hence
C regeneration) of this module. Only top-level classes are scanned:
mypyc rejects nested class definitions.
"""
if file.fullname not in self.group_map:
return set()

mods: set[str] = set()
for sym in file.names.values():
node = sym.node
if isinstance(node, TypeInfo) and node.module_name == file.fullname:
for ancestor in node.mro[1:]:
mods.add(ancestor.module_name)
return mods


def parse_and_typecheck(
sources: list[BuildSource],
Expand Down
90 changes: 90 additions & 0 deletions mypyc/test-data/run-multimodule.test
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,96 @@ def _force_recompile() -> int:
from native import test
test()

[case testIncrementalCrossGroupInheritedMethodRemoved]
# Regression: updating a transitive base class triggers recompilation of the
# subclass-defining module (mypy#21742).
import other_leaf

def test() -> int:
c = other_leaf.Leaf()
return c.existing_method(5)

[file other_base.py]
class Base:
def existing_method(self, x: int) -> int:
return x + 1

def removed_method(self, x: int) -> int:
return x + 100

[file other_base.py.2]
class Base:
def existing_method(self, x: int) -> int:
return x + 1

[file other_mid.py]
from other_base import Base

class Mid(Base):
def mid_method(self, x: int) -> int:
return x + 2

[file other_leaf.py]
from other_mid import Mid

class Leaf(Mid):
pass

[file driver.py]
from native import test
print(test())

[out]
6
[out2]
6

[case testIncrementalCrossGroupInheritedMethodRemovedNonEmptyLeaf]
# Regression: updating a transitive base class triggers recompilation of the
# module defining a subclass with a non-empty body (mypy#21742).
import other_leaf

def test() -> int:
return other_leaf.Leaf().existing_method(5)

[file other_base.py]
class Base:
def existing_method(self, x: int) -> int:
return x + 1

def removed_method(self, x: int) -> int:
return x + 100

[file other_base.py.2]
class Base:
def existing_method(self, x: int) -> int:
return x + 1

[file other_mid.py]
from other_base import Base

class Mid(Base):
def mid_method(self, x: int) -> int:
return x + 2

[file other_leaf.py]
from other_mid import Mid

class Leaf(Mid):
FLAG = True

def foo(self) -> str:
return "foo"

[file driver.py]
from native import test
print(test())

[out]
6
[out2]
6

[case testCrossModuleInheritedAttrDefaultsSameGroup]
# separate: [(["native.py"], "grp1"), (["other_a.py", "other_b.py"], "grp2")]
# Regression: with the subclass (other_a) and base (other_b) in the same
Expand Down
Loading