Skip to content

Commit 20bc0b4

Browse files
authored
More improvements to following imports in mypy daemon (#8616)
This includes a bunch of changes to following imports: * Fix bug with cached modules without ASTs by loading the AST as needed * Improve docstrings and simplify code * Rename `follow_imports` * Merge `seen` and `sources_set` * Address some TODO items This may be easier to review by looking at individual commits. I think that following imports is ready to be enabled after this has been merged. We can always revert the change before the next release if we we encounter major issues. Major things I know about that still don't work (I'll try to address at least the first one before the next release): * `-m` and `-p` don't work correctly with dmypy (I think that happens also in other modes, but might be worse when following imports) * Suppressed modules are incorrect with namespace modules (happens also in other modes) * File events are not supported (but these are undocumented, so it's not critical?) * Performance with a huge number of files is unknown (other import modes have better performance and can be used as fallbacks if this is a problem) Work towards #5870. Continues progress made in #8590.
1 parent f4351ba commit 20bc0b4

File tree

3 files changed

+98
-50
lines changed

3 files changed

+98
-50
lines changed

mypy/dmypy_server.py

Lines changed: 55 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -534,25 +534,23 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
534534

535535
orig_modules = list(graph.keys())
536536

537-
# TODO: Are the differences from fine_grained_increment(), necessary?
538537
self.update_sources(sources)
539538
changed_paths = self.fswatcher.find_changed()
540539
manager.search_paths = compute_search_paths(sources, manager.options, manager.data_dir)
541540

542541
t1 = time.time()
543542
manager.log("fine-grained increment: find_changed: {:.3f}s".format(t1 - t0))
544543

545-
sources_set = {source.module for source in sources}
544+
seen = {source.module for source in sources}
546545

547546
# Find changed modules reachable from roots (or in roots) already in graph.
548-
seen = set() # type: Set[str]
549-
changed, removed, new_files = self.follow_imports(
550-
sources, graph, seen, changed_paths, sources_set
547+
changed, new_files = self.find_reachable_changed_modules(
548+
sources, graph, seen, changed_paths
551549
)
552550
sources.extend(new_files)
553551

554552
# Process changes directly reachable from roots.
555-
messages = fine_grained_manager.update(changed, removed)
553+
messages = fine_grained_manager.update(changed, [])
556554

557555
# Follow deps from changed modules (still within graph).
558556
worklist = changed[:]
@@ -561,31 +559,31 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
561559
if module[0] not in graph:
562560
continue
563561
sources2 = self.direct_imports(module, graph)
564-
changed, removed, new_files = self.follow_imports(
565-
sources2, graph, seen, changed_paths, sources_set
562+
changed, new_files = self.find_reachable_changed_modules(
563+
sources2, graph, seen, changed_paths
566564
)
567-
sources.extend(new_files)
568565
self.update_sources(new_files)
569-
messages = fine_grained_manager.update(changed, removed)
570-
# TODO: Removed?
566+
messages = fine_grained_manager.update(changed, [])
571567
worklist.extend(changed)
572568

573569
t2 = time.time()
574570

575-
for module_id, state in graph.items():
576-
refresh_suppressed_submodules(module_id, state.path, fine_grained_manager.deps, graph,
577-
self.fscache)
571+
def refresh_file(module: str, path: str) -> List[str]:
572+
return fine_grained_manager.update([(module, path)], [])
573+
574+
for module_id, state in list(graph.items()):
575+
new_messages = refresh_suppressed_submodules(
576+
module_id, state.path, fine_grained_manager.deps, graph, self.fscache, refresh_file
577+
)
578+
if new_messages is not None:
579+
messages = new_messages
578580

579581
t3 = time.time()
580582

581583
# There may be new files that became available, currently treated as
582584
# suppressed imports. Process them.
583-
seen_suppressed = set() # type: Set[str]
584585
while True:
585-
# TODO: Merge seen and seen_suppressed?
586-
new_unsuppressed, seen_suppressed = self.find_added_suppressed(
587-
graph, seen_suppressed, manager.search_paths
588-
)
586+
new_unsuppressed = self.find_added_suppressed(graph, seen, manager.search_paths)
589587
if not new_unsuppressed:
590588
break
591589
new_files = [BuildSource(mod[1], mod[0]) for mod in new_unsuppressed]
@@ -594,8 +592,15 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
594592
messages = fine_grained_manager.update(new_unsuppressed, [])
595593

596594
for module_id, path in new_unsuppressed:
597-
refresh_suppressed_submodules(module_id, path, fine_grained_manager.deps, graph,
598-
self.fscache)
595+
new_messages = refresh_suppressed_submodules(
596+
module_id, path,
597+
fine_grained_manager.deps,
598+
graph,
599+
self.fscache,
600+
refresh_file
601+
)
602+
if new_messages is not None:
603+
messages = new_messages
599604

600605
t4 = time.time()
601606

@@ -604,7 +609,7 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
604609
for module_id in orig_modules:
605610
if module_id not in graph:
606611
continue
607-
if module_id not in seen and module_id not in seen_suppressed:
612+
if module_id not in seen:
608613
module_path = graph[module_id].path
609614
assert module_path is not None
610615
to_delete.append((module_id, module_path))
@@ -631,33 +636,35 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
631636

632637
return messages
633638

634-
def follow_imports(self,
635-
sources: List[BuildSource],
636-
graph: mypy.build.Graph,
637-
seen: Set[str],
638-
changed_paths: AbstractSet[str],
639-
sources_set: Set[str]) -> Tuple[List[Tuple[str, str]],
640-
List[Tuple[str, str]],
641-
List[BuildSource]]:
642-
"""Follow imports within graph from given sources.
639+
def find_reachable_changed_modules(
640+
self,
641+
roots: List[BuildSource],
642+
graph: mypy.build.Graph,
643+
seen: Set[str],
644+
changed_paths: AbstractSet[str]) -> Tuple[List[Tuple[str, str]],
645+
List[BuildSource]]:
646+
"""Follow imports within graph from given sources until hitting changed modules.
647+
648+
If we find a changed module, we can't continue following imports as the imports
649+
may have changed.
643650
644651
Args:
645-
sources: roots of modules to search
652+
roots: modules where to start search from
646653
graph: module graph to use for the search
647-
seen: modules we've seen before that won't be visited (mutated here!)
654+
seen: modules we've seen before that won't be visited (mutated here!!)
648655
changed_paths: which paths have changed (stop search here and return any found)
649-
sources_set: set of sources (TODO: relationship with seen)
650656
651-
Return (reachable changed modules, removed modules, updated file list).
657+
Return (encountered reachable changed modules,
658+
unchanged files not in sources_set traversed).
652659
"""
653660
changed = []
654661
new_files = []
655-
worklist = sources[:]
662+
worklist = roots[:]
656663
seen.update(source.module for source in worklist)
657664
while worklist:
658665
nxt = worklist.pop()
659-
if nxt.module not in sources_set:
660-
sources_set.add(nxt.module)
666+
if nxt.module not in seen:
667+
seen.add(nxt.module)
661668
new_files.append(nxt)
662669
if nxt.path in changed_paths:
663670
assert nxt.path is not None # TODO
@@ -669,7 +676,7 @@ def follow_imports(self,
669676
seen.add(dep)
670677
worklist.append(BuildSource(graph[dep].path,
671678
graph[dep].id))
672-
return changed, [], new_files
679+
return changed, new_files
673680

674681
def direct_imports(self,
675682
module: Tuple[str, str],
@@ -682,8 +689,14 @@ def direct_imports(self,
682689
def find_added_suppressed(self,
683690
graph: mypy.build.Graph,
684691
seen: Set[str],
685-
search_paths: SearchPaths) -> Tuple[List[Tuple[str, str]], Set[str]]:
686-
"""Find suppressed modules that have been added (and not included in seen)."""
692+
search_paths: SearchPaths) -> List[Tuple[str, str]]:
693+
"""Find suppressed modules that have been added (and not included in seen).
694+
695+
Args:
696+
seen: reachable modules we've seen before (mutated here!!)
697+
698+
Return suppressed, added modules.
699+
"""
687700
all_suppressed = set()
688701
for module, state in graph.items():
689702
all_suppressed |= state.suppressed_set
@@ -693,7 +706,6 @@ def find_added_suppressed(self,
693706
all_suppressed = {module for module in all_suppressed if module not in graph}
694707

695708
# TODO: Namespace packages
696-
# TODO: Handle seen?
697709

698710
finder = FindModuleCache(search_paths, self.fscache, self.options)
699711

@@ -705,7 +717,7 @@ def find_added_suppressed(self,
705717
found.append((module, result))
706718
seen.add(module)
707719

708-
return found, seen
720+
return found
709721

710722
def increment_output(self,
711723
messages: List[str],

mypy/server/update.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
import sys
117117
import time
118118
from typing import (
119-
Dict, List, Set, Tuple, Union, Optional, NamedTuple, Sequence
119+
Dict, List, Set, Tuple, Union, Optional, NamedTuple, Sequence, Callable
120120
)
121121
from typing_extensions import Final
122122

@@ -1127,7 +1127,8 @@ def refresh_suppressed_submodules(
11271127
path: Optional[str],
11281128
deps: Dict[str, Set[str]],
11291129
graph: Graph,
1130-
fscache: FileSystemCache) -> None:
1130+
fscache: FileSystemCache,
1131+
refresh_file: Callable[[str, str], List[str]]) -> Optional[List[str]]:
11311132
"""Look for submodules that are now suppressed in target package.
11321133
11331134
If a submodule a.b gets added, we need to mark it as suppressed
@@ -1139,10 +1140,15 @@ def refresh_suppressed_submodules(
11391140
Args:
11401141
module: target package in which to look for submodules
11411142
path: path of the module
1143+
refresh_file: function that reads the AST of a module (returns error messages)
1144+
1145+
Return a list of errors from refresh_file() if it was called. If the
1146+
return value is None, we didn't call refresh_file().
11421147
"""
1148+
messages = None
11431149
if path is None or not path.endswith(INIT_SUFFIXES):
11441150
# Only packages have submodules.
1145-
return
1151+
return None
11461152
# Find any submodules present in the directory.
11471153
pkgdir = os.path.dirname(path)
11481154
for fnam in fscache.listdir(pkgdir):
@@ -1153,22 +1159,33 @@ def refresh_suppressed_submodules(
11531159
shortname = fnam.split('.')[0]
11541160
submodule = module + '.' + shortname
11551161
trigger = make_trigger(submodule)
1162+
1163+
# We may be missing the required fine-grained deps.
1164+
ensure_deps_loaded(module, deps, graph)
1165+
11561166
if trigger in deps:
11571167
for dep in deps[trigger]:
1158-
# TODO: <...> deps, etc.
1168+
# We can ignore <...> deps since a submodule can't trigger any.
11591169
state = graph.get(dep)
11601170
if not state:
11611171
# Maybe it's a non-top-level target. We only care about the module.
11621172
dep_module = module_prefix(graph, dep)
11631173
if dep_module is not None:
11641174
state = graph.get(dep_module)
11651175
if state:
1176+
# Is the file may missing an AST in case it's read from cache?
1177+
if state.tree is None:
1178+
# Create AST for the file. This may produce some new errors
1179+
# that we need to propagate.
1180+
assert state.path is not None
1181+
messages = refresh_file(state.id, state.path)
11661182
tree = state.tree
1167-
assert tree # TODO: What if doesn't exist?
1183+
assert tree # Will be fine, due to refresh_file() above
11681184
for imp in tree.imports:
11691185
if isinstance(imp, ImportFrom):
11701186
if (imp.id == module
1171-
and any(name == shortname for name, _ in imp.names)):
1172-
# TODO: Only if does not exist already
1187+
and any(name == shortname for name, _ in imp.names)
1188+
and submodule not in state.suppressed_set):
11731189
state.suppressed.append(submodule)
11741190
state.suppressed_set.add(submodule)
1191+
return messages

test-data/unit/fine-grained-follow-imports.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,3 +700,22 @@ import bar
700700
==
701701
src/foo.py:2: error: "str" not callable
702702
src/bar.py:1: error: "int" not callable
703+
704+
[case testFollowImportsNormalSuppressedAffectsCachedFile-only_when_cache]
705+
# flags: --follow-imports=normal
706+
# cmd: mypy main.py
707+
708+
[file main.py]
709+
from p import m # type: ignore
710+
m.f(1)
711+
712+
[file p/__init__.py]
713+
714+
[file p/m.py.2]
715+
# This change will trigger a cached file (main.py) through a supressed
716+
# submodule, resulting in additional errors in main.py.
717+
def f() -> None: pass
718+
719+
[out]
720+
==
721+
main.py:2: error: Too many arguments for "f"

0 commit comments

Comments
 (0)