diff --git a/mypy/server/deps.py b/mypy/server/deps.py index b2c91d8db4888..29cd8ac8b4635 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -671,6 +671,13 @@ def visit_member_expr(self, e: MemberExpr) -> None: if e.kind is not None: # Reference to a module attribute self.process_global_ref_expr(e) + if isinstance(e.expr, RefExpr) and isinstance(e.expr.node, MypyFile): + # Also depend on the name as accessed through this module. The + # fullname above may point to the original definition (e.g. via + # a re-export using "from ... import *"), but we must also + # recheck if the name is removed from the accessed module's + # namespace. + self.add_dependency(make_trigger(e.expr.node.fullname + "." + e.name)) else: # Reference to a non-module (or missing) attribute if e.expr not in self.type_map: diff --git a/mypy/server/update.py b/mypy/server/update.py index 64ce0fb5d3f8a..cb7ff60e14cba 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -783,31 +783,37 @@ def calculate_active_triggers( else: snapshot2 = snapshot_symbol_table(id, new.names) diff = compare_symbol_table_snapshots(id, snapshot1, snapshot2) - package_nesting_level = id.count(".") - for item in diff.copy(): - if item.count(".") <= package_nesting_level + 1 and item.split(".")[-1] not in ( - "__builtins__", - "__file__", - "__name__", - "__package__", - "__doc__", - ): - # Activate catch-all wildcard trigger for top-level module changes (used for - # "from m import *"). This also gets triggered by changes to module-private - # entries, but as these unneeded dependencies only result in extra processing, - # it's a minor problem. - # - # TODO: Some __* names cause mistriggers. Fix the underlying issue instead of - # special casing them here. - diff.add(id + WILDCARD_TAG) - if item.count(".") > package_nesting_level + 1: - # These are for changes within classes, used by protocols. - diff.add(item.rsplit(".", 1)[0] + WILDCARD_TAG) - + diff |= wildcard_triggers_for_changes(id, diff) names |= diff return {make_trigger(name) for name in names} +def wildcard_triggers_for_changes(module_id: str, diff: set[str]) -> set[str]: + """Return catch-all wildcard triggers activated by a set of changed names.""" + result: set[str] = set() + package_nesting_level = module_id.count(".") + for item in diff: + if item.count(".") <= package_nesting_level + 1 and item.split(".")[-1] not in ( + "__builtins__", + "__file__", + "__name__", + "__package__", + "__doc__", + ): + # Activate catch-all wildcard trigger for top-level module changes (used for + # "from m import *"). This also gets triggered by changes to module-private + # entries, but as these unneeded dependencies only result in extra processing, + # it's a minor problem. + # + # TODO: Some __* names cause mistriggers. Fix the underlying issue instead of + # special casing them here. + result.add(module_id + WILDCARD_TAG) + if item.count(".") > package_nesting_level + 1: + # These are for changes within classes, used by protocols. + result.add(item.rsplit(".", 1)[0] + WILDCARD_TAG) + return result + + def replace_modules_with_new_variants( manager: BuildManager, graph: dict[str, State], @@ -1044,6 +1050,7 @@ def key(node: FineGrainedDeferredNode) -> int: changed = compare_symbol_table_snapshots( file_node.fullname, old_symbols_snapshot, new_symbols_snapshot ) + changed |= wildcard_triggers_for_changes(module_id, changed) new_triggered = {make_trigger(name) for name in changed} # Dependencies may have changed. diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index ab2c5f1c43e74..6850eabf09b6a 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -69,6 +69,19 @@ x = 1 -> m.f -> m, m.f +[case testAccessReexportedModuleAttribute] +import pkg +pkg.f() +[file pkg/__init__.py] +from pkg.sub import * +[file pkg/sub.py] +def f() -> None: pass +[out] + -> m + -> m + -> pkg + -> m + [case testImport] import n [file n.py] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index bc02f49e5d835..ef2e8c0b343bf 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -11839,3 +11839,134 @@ def bar() -> str: return "a" main:2: note: Revealed type is "None | builtins.int" == main:2: note: Revealed type is "None | builtins.str" + +[case testStarExportedNameDeleted1] +import pkg + +pkg.loads() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +def loads() -> None: ... + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: "object" has no attribute "loads" + +[case testStarExportedNameDeleted2] +import pkg + +pkg.C() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +class C: pass + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: "object" has no attribute "C" + +[case testStarExportedNameDeleted3] +from pkg import loads + +loads() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +def loads() -> None: ... + +[file pkg/sub.pyi.2] +[out] +== +main:1: error: Module "pkg" has no attribute "loads" + +[case testStarExportedNameDeleted4] +from pkg import sub + +a = sub.x + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +x = 1 + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: "object" has no attribute "x" + +[case testStarExportedNameDeleted5] +from pkg import sub + +x: sub.N = 1 + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +N = int + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: Name "sub.N" is not defined + +[case testStarExportedNameDeleted6] +from pkg import * + +loads() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +def loads() -> None: ... + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: Name "loads" is not defined + +[case testStarExportedNameDeleted7] +from pkg import * + +C() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +class C: pass + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: Name "C" is not defined + +[case testStarExportedNameDeleted8] +from a import * + +C() + +[file a.py] +from b import * + +[file b.py] +from c import * + +[file c.py] +class C: pass + +[file c.py.2] +[out] +== +main:3: error: Name "C" is not defined