Skip to content

Commit 828a9eb

Browse files
authored
Various fixes to following imports in mypy daemon (#8590)
Included fixes: - filter out bogus suppressed modules to speed things up - fix issue with import inside function - fix stub packages - also support `/` path separators on Windows - update search path based on paths given on the command line - support changes to files passed on the command line - add logging - minimal namespace package support Namespace package support is not complete, but the issue I found isn't specific to following imports in mypy daemon, so I didn't try to fix it in this PR. I'll create a follow-up issue.
1 parent 52c0a63 commit 828a9eb

File tree

5 files changed

+222
-19
lines changed

5 files changed

+222
-19
lines changed

mypy/dmypy_server.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ def initialize_fine_grained(self, sources: List[BuildSource],
423423
self.fine_grained_manager = FineGrainedBuildManager(result)
424424

425425
if self.following_imports():
426-
sources = find_all_sources_in_build(self.fine_grained_manager.graph)
426+
sources = find_all_sources_in_build(self.fine_grained_manager.graph, sources)
427427
self.update_sources(sources)
428428

429429
self.previous_sources = sources
@@ -523,13 +523,9 @@ def fine_grained_increment(self,
523523

524524
def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> List[str]:
525525
"""Like fine_grained_increment, but follow imports."""
526+
t0 = time.time()
526527

527-
# TODO:
528-
# - file events
529-
# - search path updates
530-
# - logging
531-
532-
changed_paths = self.fswatcher.find_changed()
528+
# TODO: Support file events
533529

534530
assert self.fine_grained_manager is not None
535531
fine_grained_manager = self.fine_grained_manager
@@ -538,9 +534,13 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
538534

539535
orig_modules = list(graph.keys())
540536

541-
# TODO: Are the differences from fine_grained_increment(), such as
542-
# updating sources after finding changed, necessary?
537+
# TODO: Are the differences from fine_grained_increment(), necessary?
543538
self.update_sources(sources)
539+
changed_paths = self.fswatcher.find_changed()
540+
manager.search_paths = compute_search_paths(sources, manager.options, manager.data_dir)
541+
542+
t1 = time.time()
543+
manager.log("fine-grained increment: find_changed: {:.3f}s".format(t1 - t0))
544544

545545
sources_set = {source.module for source in sources}
546546

@@ -570,10 +570,14 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
570570
# TODO: Removed?
571571
worklist.extend(changed)
572572

573+
t2 = time.time()
574+
573575
for module_id, state in graph.items():
574576
refresh_suppressed_submodules(module_id, state.path, fine_grained_manager.deps, graph,
575577
self.fscache)
576578

579+
t3 = time.time()
580+
577581
# There may be new files that became available, currently treated as
578582
# suppressed imports. Process them.
579583
seen_suppressed = set() # type: Set[str]
@@ -593,6 +597,8 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
593597
refresh_suppressed_submodules(module_id, path, fine_grained_manager.deps, graph,
594598
self.fscache)
595599

600+
t4 = time.time()
601+
596602
# Find all original modules in graph that were not reached -- they are deleted.
597603
to_delete = []
598604
for module_id in orig_modules:
@@ -612,6 +618,17 @@ def fine_grained_increment_follow_imports(self, sources: List[BuildSource]) -> L
612618

613619
self.previous_sources = find_all_sources_in_build(graph)
614620
self.update_sources(self.previous_sources)
621+
622+
t5 = time.time()
623+
624+
manager.log("fine-grained increment: update: {:.3f}s".format(t5 - t1))
625+
manager.add_stats(
626+
find_changes_time=t1 - t0,
627+
fg_update_time=t2 - t1,
628+
refresh_suppressed_time=t3 - t2,
629+
find_added_supressed_time=t4 - t3,
630+
cleanup_time=t5 - t4)
631+
615632
return messages
616633

617634
def follow_imports(self,
@@ -671,6 +688,10 @@ def find_added_suppressed(self,
671688
for module, state in graph.items():
672689
all_suppressed |= state.suppressed_set
673690

691+
# Filter out things that shouldn't actually be considered suppressed.
692+
# TODO: Figure out why these are treated as suppressed
693+
all_suppressed = {module for module in all_suppressed if module not in graph}
694+
674695
# TODO: Namespace packages
675696
# TODO: Handle seen?
676697

@@ -720,6 +741,9 @@ def pretty_messages(self, messages: List[str], n_sources: int,
720741

721742
def update_sources(self, sources: List[BuildSource]) -> None:
722743
paths = [source.path for source in sources if source.path is not None]
744+
if self.following_imports():
745+
# Filter out directories (used for namespace packages).
746+
paths = [path for path in paths if self.fscache.isfile(path)]
723747
self.fswatcher.add_watched_paths(paths)
724748

725749
def update_changed(self,
@@ -828,10 +852,13 @@ def get_meminfo() -> Dict[str, Any]:
828852
return res
829853

830854

831-
def find_all_sources_in_build(graph: mypy.build.Graph) -> List[BuildSource]:
832-
result = []
855+
def find_all_sources_in_build(graph: mypy.build.Graph,
856+
extra: Sequence[BuildSource] = ()) -> List[BuildSource]:
857+
result = list(extra)
858+
seen = set(source.module for source in result)
833859
for module, state in graph.items():
834-
result.append(BuildSource(state.path, module))
860+
if module not in seen:
861+
result.append(BuildSource(state.path, module))
835862
return result
836863

837864

mypy/modulefinder.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ def __init__(self, path: Optional[str], module: Optional[str],
7777
self.base_dir = base_dir # Directory where the package is rooted (e.g. 'xxx/yyy')
7878

7979
def __repr__(self) -> str:
80-
return '<BuildSource path=%r module=%r has_text=%s>' % (self.path,
81-
self.module,
82-
self.text is not None)
80+
return '<BuildSource path=%r module=%r has_text=%s base_dir=%s>' % (
81+
self.path,
82+
self.module,
83+
self.text is not None,
84+
self.base_dir)
8385

8486

8587
class FindModuleCache:

mypy/server/update.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"""
114114

115115
import os
116+
import sys
116117
import time
117118
from typing import (
118119
Dict, List, Set, Tuple, Union, Optional, NamedTuple, Sequence
@@ -1111,6 +1112,17 @@ def target_from_node(module: str,
11111112
return '%s.%s' % (module, node.name)
11121113

11131114

1115+
if sys.platform != 'win32':
1116+
INIT_SUFFIXES = ('/__init__.py', '/__init__.pyi') # type: Final
1117+
else:
1118+
INIT_SUFFIXES = (
1119+
os.sep + '__init__.py',
1120+
os.sep + '__init__.pyi',
1121+
os.altsep + '__init__.py',
1122+
os.altsep + '__init__.pyi',
1123+
) # type: Final
1124+
1125+
11141126
def refresh_suppressed_submodules(
11151127
module: str,
11161128
path: Optional[str],
@@ -1129,8 +1141,7 @@ def refresh_suppressed_submodules(
11291141
module: target package in which to look for submodules
11301142
path: path of the module
11311143
"""
1132-
# TODO: Windows paths
1133-
if path is None or not path.endswith(os.sep + '__init__.py'):
1144+
if path is None or not path.endswith(INIT_SUFFIXES):
11341145
# Only packages have submodules.
11351146
return
11361147
# Find any submodules present in the directory.
@@ -1145,8 +1156,13 @@ def refresh_suppressed_submodules(
11451156
trigger = make_trigger(submodule)
11461157
if trigger in deps:
11471158
for dep in deps[trigger]:
1148-
# TODO: <...> deps, imports in functions, etc.
1159+
# TODO: <...> deps, etc.
11491160
state = graph.get(dep)
1161+
if not state:
1162+
# Maybe it's a non-top-level target. We only care about the module.
1163+
dep_module = module_prefix(graph, dep)
1164+
if dep_module is not None:
1165+
state = graph.get(dep_module)
11501166
if state:
11511167
tree = state.tree
11521168
assert tree # TODO: What if doesn't exist?

mypy/test/testcheck.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def parse_module(self,
336336
cache = FindModuleCache(search_paths)
337337
for module_name in module_names.split(' '):
338338
path = cache.find_module(module_name)
339-
assert isinstance(path, str), "Can't find ad hoc case file"
339+
assert isinstance(path, str), "Can't find ad hoc case file: %s" % module_name
340340
with open(path, encoding='utf8') as f:
341341
program_text = f.read()
342342
out.append((module_name, path, program_text))

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

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,161 @@ main.py:3: error: Too few arguments for "f"
542542
==
543543
main.py:3: error: Too few arguments for "f"
544544
main.py:4: error: Too many arguments for "f"
545+
546+
[case testFollowImportsNormalPackageInitFile4-only_when_cache]
547+
# flags: --follow-imports=normal
548+
# cmd: mypy main.py
549+
550+
[file main.py]
551+
import p1.m # type: ignore
552+
from p2 import m # type: ignore
553+
554+
[file p1/__init__.py.2]
555+
1()
556+
557+
[file p1/m.py.2]
558+
''()
559+
560+
[file p2/__init__.py.3]
561+
''()
562+
563+
[file p2/m.py.3]
564+
565+
[out]
566+
==
567+
p1/__init__.py:1: error: "int" not callable
568+
p1/m.py:1: error: "str" not callable
569+
==
570+
p1/__init__.py:1: error: "int" not callable
571+
p1/m.py:1: error: "str" not callable
572+
p2/__init__.py:1: error: "str" not callable
573+
574+
[case testFollowImportsNormalSubmoduleCreatedWithImportInFunction]
575+
# flags: --follow-imports=normal
576+
# cmd: mypy main.py
577+
578+
[file main.py]
579+
def f() -> None:
580+
from p import m
581+
582+
[file p/__init__.py.2]
583+
1()
584+
585+
[file p/m.py.2]
586+
''()
587+
588+
[out]
589+
main.py:2: error: Cannot find implementation or library stub for module named 'p'
590+
main.py:2: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
591+
==
592+
p/m.py:1: error: "str" not callable
593+
p/__init__.py:1: error: "int" not callable
594+
595+
[case testFollowImportsNormalPackageInitFileStub]
596+
# flags: --follow-imports=normal
597+
# cmd: mypy main.py
598+
599+
[file main.py]
600+
from p import m
601+
602+
[file p/__init__.pyi.2]
603+
1()
604+
605+
[file p/m.pyi.2]
606+
''()
607+
608+
[file p/mm.pyi.3]
609+
x x x
610+
611+
[out]
612+
main.py:1: error: Cannot find implementation or library stub for module named 'p'
613+
main.py:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
614+
==
615+
p/m.pyi:1: error: "str" not callable
616+
p/__init__.pyi:1: error: "int" not callable
617+
==
618+
p/m.pyi:1: error: "str" not callable
619+
p/__init__.pyi:1: error: "int" not callable
620+
621+
[case testFollowImportsNormalNamespacePackages]
622+
# flags: --follow-imports=normal --namespace-packages
623+
# cmd: mypy main.py
624+
625+
[file main.py]
626+
import p1.m1
627+
import p2.m2
628+
629+
[file p1/m1.py]
630+
1()
631+
632+
[file p2/m2.py.2]
633+
''()
634+
635+
[delete p2/m2.py.3]
636+
637+
[out]
638+
p1/m1.py:1: error: "int" not callable
639+
main.py:2: error: Cannot find implementation or library stub for module named 'p2.m2'
640+
main.py:2: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
641+
==
642+
p2/m2.py:1: error: "str" not callable
643+
p1/m1.py:1: error: "int" not callable
644+
==
645+
main.py:2: error: Cannot find implementation or library stub for module named 'p2.m2'
646+
main.py:2: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
647+
p1/m1.py:1: error: "int" not callable
648+
649+
[case testFollowImportsNormalNewFileOnCommandLine]
650+
# flags: --follow-imports=normal
651+
# cmd: mypy main.py
652+
# cmd2: mypy main.py x.py
653+
654+
[file main.py]
655+
1()
656+
657+
[file x.py.2]
658+
''()
659+
660+
[out]
661+
main.py:1: error: "int" not callable
662+
==
663+
x.py:1: error: "str" not callable
664+
main.py:1: error: "int" not callable
665+
666+
[case testFollowImportsNormalSearchPathUpdate-only_when_nocache]
667+
# flags: --follow-imports=normal
668+
# cmd: mypy main.py
669+
# cmd2: mypy main.py src/foo.py
670+
671+
[file main.py]
672+
673+
[file src/foo.py.2]
674+
import bar
675+
''()
676+
677+
[file src/bar.py.2]
678+
1()
679+
680+
[out]
681+
==
682+
src/bar.py:1: error: "int" not callable
683+
src/foo.py:2: error: "str" not callable
684+
685+
[case testFollowImportsNormalSearchPathUpdate-only_when_cache]
686+
# flags: --follow-imports=normal
687+
# cmd: mypy main.py
688+
# cmd2: mypy main.py src/foo.py
689+
690+
[file main.py]
691+
692+
[file src/foo.py.2]
693+
import bar
694+
''()
695+
696+
[file src/bar.py.2]
697+
1()
698+
699+
[out]
700+
==
701+
src/foo.py:2: error: "str" not callable
702+
src/bar.py:1: error: "int" not callable

0 commit comments

Comments
 (0)