Skip to content

Commit c3d9b5c

Browse files
authored
Fix star import dependencies in mypy daemon (#21673)
Changes weren't propagated reliably when exporting names using `from <...> import *`. This could result in false negatives and false positives. I used coding agent assist with small incremental changes.
1 parent 0c6013d commit c3d9b5c

4 files changed

Lines changed: 179 additions & 21 deletions

File tree

mypy/server/deps.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,13 @@ def visit_member_expr(self, e: MemberExpr) -> None:
671671
if e.kind is not None:
672672
# Reference to a module attribute
673673
self.process_global_ref_expr(e)
674+
if isinstance(e.expr, RefExpr) and isinstance(e.expr.node, MypyFile):
675+
# Also depend on the name as accessed through this module. The
676+
# fullname above may point to the original definition (e.g. via
677+
# a re-export using "from ... import *"), but we must also
678+
# recheck if the name is removed from the accessed module's
679+
# namespace.
680+
self.add_dependency(make_trigger(e.expr.node.fullname + "." + e.name))
674681
else:
675682
# Reference to a non-module (or missing) attribute
676683
if e.expr not in self.type_map:

mypy/server/update.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -783,31 +783,37 @@ def calculate_active_triggers(
783783
else:
784784
snapshot2 = snapshot_symbol_table(id, new.names)
785785
diff = compare_symbol_table_snapshots(id, snapshot1, snapshot2)
786-
package_nesting_level = id.count(".")
787-
for item in diff.copy():
788-
if item.count(".") <= package_nesting_level + 1 and item.split(".")[-1] not in (
789-
"__builtins__",
790-
"__file__",
791-
"__name__",
792-
"__package__",
793-
"__doc__",
794-
):
795-
# Activate catch-all wildcard trigger for top-level module changes (used for
796-
# "from m import *"). This also gets triggered by changes to module-private
797-
# entries, but as these unneeded dependencies only result in extra processing,
798-
# it's a minor problem.
799-
#
800-
# TODO: Some __* names cause mistriggers. Fix the underlying issue instead of
801-
# special casing them here.
802-
diff.add(id + WILDCARD_TAG)
803-
if item.count(".") > package_nesting_level + 1:
804-
# These are for changes within classes, used by protocols.
805-
diff.add(item.rsplit(".", 1)[0] + WILDCARD_TAG)
806-
786+
diff |= wildcard_triggers_for_changes(id, diff)
807787
names |= diff
808788
return {make_trigger(name) for name in names}
809789

810790

791+
def wildcard_triggers_for_changes(module_id: str, diff: set[str]) -> set[str]:
792+
"""Return catch-all wildcard triggers activated by a set of changed names."""
793+
result: set[str] = set()
794+
package_nesting_level = module_id.count(".")
795+
for item in diff:
796+
if item.count(".") <= package_nesting_level + 1 and item.split(".")[-1] not in (
797+
"__builtins__",
798+
"__file__",
799+
"__name__",
800+
"__package__",
801+
"__doc__",
802+
):
803+
# Activate catch-all wildcard trigger for top-level module changes (used for
804+
# "from m import *"). This also gets triggered by changes to module-private
805+
# entries, but as these unneeded dependencies only result in extra processing,
806+
# it's a minor problem.
807+
#
808+
# TODO: Some __* names cause mistriggers. Fix the underlying issue instead of
809+
# special casing them here.
810+
result.add(module_id + WILDCARD_TAG)
811+
if item.count(".") > package_nesting_level + 1:
812+
# These are for changes within classes, used by protocols.
813+
result.add(item.rsplit(".", 1)[0] + WILDCARD_TAG)
814+
return result
815+
816+
811817
def replace_modules_with_new_variants(
812818
manager: BuildManager,
813819
graph: dict[str, State],
@@ -1044,6 +1050,7 @@ def key(node: FineGrainedDeferredNode) -> int:
10441050
changed = compare_symbol_table_snapshots(
10451051
file_node.fullname, old_symbols_snapshot, new_symbols_snapshot
10461052
)
1053+
changed |= wildcard_triggers_for_changes(module_id, changed)
10471054
new_triggered = {make_trigger(name) for name in changed}
10481055

10491056
# Dependencies may have changed.

test-data/unit/deps.test

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ x = 1
6969
<n.x> -> m.f
7070
<n> -> m, m.f
7171

72+
[case testAccessReexportedModuleAttribute]
73+
import pkg
74+
pkg.f()
75+
[file pkg/__init__.py]
76+
from pkg.sub import *
77+
[file pkg/sub.py]
78+
def f() -> None: pass
79+
[out]
80+
<pkg.f> -> m
81+
<pkg.sub.f> -> m
82+
<pkg.sub[wildcard]> -> pkg
83+
<pkg> -> m
84+
7285
[case testImport]
7386
import n
7487
[file n.py]

test-data/unit/fine-grained.test

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11839,3 +11839,134 @@ def bar() -> str: return "a"
1183911839
main:2: note: Revealed type is "None | builtins.int"
1184011840
==
1184111841
main:2: note: Revealed type is "None | builtins.str"
11842+
11843+
[case testStarExportedNameDeleted1]
11844+
import pkg
11845+
11846+
pkg.loads()
11847+
11848+
[file pkg/__init__.pyi]
11849+
from .sub import *
11850+
11851+
[file pkg/sub.pyi]
11852+
def loads() -> None: ...
11853+
11854+
[file pkg/sub.pyi.2]
11855+
[out]
11856+
==
11857+
main:3: error: "object" has no attribute "loads"
11858+
11859+
[case testStarExportedNameDeleted2]
11860+
import pkg
11861+
11862+
pkg.C()
11863+
11864+
[file pkg/__init__.pyi]
11865+
from .sub import *
11866+
11867+
[file pkg/sub.pyi]
11868+
class C: pass
11869+
11870+
[file pkg/sub.pyi.2]
11871+
[out]
11872+
==
11873+
main:3: error: "object" has no attribute "C"
11874+
11875+
[case testStarExportedNameDeleted3]
11876+
from pkg import loads
11877+
11878+
loads()
11879+
11880+
[file pkg/__init__.pyi]
11881+
from .sub import *
11882+
11883+
[file pkg/sub.pyi]
11884+
def loads() -> None: ...
11885+
11886+
[file pkg/sub.pyi.2]
11887+
[out]
11888+
==
11889+
main:1: error: Module "pkg" has no attribute "loads"
11890+
11891+
[case testStarExportedNameDeleted4]
11892+
from pkg import sub
11893+
11894+
a = sub.x
11895+
11896+
[file pkg/__init__.pyi]
11897+
from .sub import *
11898+
11899+
[file pkg/sub.pyi]
11900+
x = 1
11901+
11902+
[file pkg/sub.pyi.2]
11903+
[out]
11904+
==
11905+
main:3: error: "object" has no attribute "x"
11906+
11907+
[case testStarExportedNameDeleted5]
11908+
from pkg import sub
11909+
11910+
x: sub.N = 1
11911+
11912+
[file pkg/__init__.pyi]
11913+
from .sub import *
11914+
11915+
[file pkg/sub.pyi]
11916+
N = int
11917+
11918+
[file pkg/sub.pyi.2]
11919+
[out]
11920+
==
11921+
main:3: error: Name "sub.N" is not defined
11922+
11923+
[case testStarExportedNameDeleted6]
11924+
from pkg import *
11925+
11926+
loads()
11927+
11928+
[file pkg/__init__.pyi]
11929+
from .sub import *
11930+
11931+
[file pkg/sub.pyi]
11932+
def loads() -> None: ...
11933+
11934+
[file pkg/sub.pyi.2]
11935+
[out]
11936+
==
11937+
main:3: error: Name "loads" is not defined
11938+
11939+
[case testStarExportedNameDeleted7]
11940+
from pkg import *
11941+
11942+
C()
11943+
11944+
[file pkg/__init__.pyi]
11945+
from .sub import *
11946+
11947+
[file pkg/sub.pyi]
11948+
class C: pass
11949+
11950+
[file pkg/sub.pyi.2]
11951+
[out]
11952+
==
11953+
main:3: error: Name "C" is not defined
11954+
11955+
[case testStarExportedNameDeleted8]
11956+
from a import *
11957+
11958+
C()
11959+
11960+
[file a.py]
11961+
from b import *
11962+
11963+
[file b.py]
11964+
from c import *
11965+
11966+
[file c.py]
11967+
class C: pass
11968+
11969+
[file c.py.2]
11970+
[out]
11971+
==
11972+
main:3: error: Name "C" is not defined

0 commit comments

Comments
 (0)