Skip to content
51 changes: 39 additions & 12 deletions mypy/dmypy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ def initialize_fine_grained(self, sources: List[BuildSource],
self.fine_grained_manager = FineGrainedBuildManager(result)

if self.following_imports():
sources = find_all_sources_in_build(self.fine_grained_manager.graph)
sources = find_all_sources_in_build(self.fine_grained_manager.graph, sources)
self.update_sources(sources)

self.previous_sources = sources
Expand Down Expand Up @@ -523,13 +523,9 @@ def fine_grained_increment(self,

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

# TODO:
# - file events
# - search path updates
# - logging

changed_paths = self.fswatcher.find_changed()
# TODO: Support file events

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

orig_modules = list(graph.keys())

# TODO: Are the differences from fine_grained_increment(), such as
# updating sources after finding changed, necessary?
# TODO: Are the differences from fine_grained_increment(), necessary?
self.update_sources(sources)
changed_paths = self.fswatcher.find_changed()
manager.search_paths = compute_search_paths(sources, manager.options, manager.data_dir)

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

sources_set = {source.module for source in sources}

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

t2 = time.time()

for module_id, state in graph.items():
refresh_suppressed_submodules(module_id, state.path, fine_grained_manager.deps, graph,
self.fscache)

t3 = time.time()

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

t4 = time.time()

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

self.previous_sources = find_all_sources_in_build(graph)
self.update_sources(self.previous_sources)

t5 = time.time()

manager.log("fine-grained increment: update: {:.3f}s".format(t5 - t1))
manager.add_stats(
find_changes_time=t1 - t0,
fg_update_time=t2 - t1,
refresh_suppressed_time=t3 - t2,
find_added_supressed_time=t4 - t3,
cleanup_time=t5 - t4)

return messages

def follow_imports(self,
Expand Down Expand Up @@ -671,6 +688,10 @@ def find_added_suppressed(self,
for module, state in graph.items():
all_suppressed |= state.suppressed_set

# Filter out things that shouldn't actually be considered suppressed.
# TODO: Figure out why these are treated as suppressed
all_suppressed = {module for module in all_suppressed if module not in graph}

# TODO: Namespace packages
# TODO: Handle seen?

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

def update_sources(self, sources: List[BuildSource]) -> None:
paths = [source.path for source in sources if source.path is not None]
if self.following_imports():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow this really?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are directories in paths, we'll get exceptions inside fswatcher.

# Filter out directories (used for namespace packages).
paths = [path for path in paths if self.fscache.isfile(path)]
self.fswatcher.add_watched_paths(paths)

def update_changed(self,
Expand Down Expand Up @@ -828,10 +852,13 @@ def get_meminfo() -> Dict[str, Any]:
return res


def find_all_sources_in_build(graph: mypy.build.Graph) -> List[BuildSource]:
result = []
def find_all_sources_in_build(graph: mypy.build.Graph,
extra: Sequence[BuildSource] = ()) -> List[BuildSource]:
result = list(extra)
seen = set(source.module for source in result)
for module, state in graph.items():
result.append(BuildSource(state.path, module))
if module not in seen:
result.append(BuildSource(state.path, module))
return result


Expand Down
8 changes: 5 additions & 3 deletions mypy/modulefinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ def __init__(self, path: Optional[str], module: Optional[str],
self.base_dir = base_dir # Directory where the package is rooted (e.g. 'xxx/yyy')

def __repr__(self) -> str:
return '<BuildSource path=%r module=%r has_text=%s>' % (self.path,
self.module,
self.text is not None)
return '<BuildSource path=%r module=%r has_text=%s base_dir=%s>' % (
self.path,
self.module,
self.text is not None,
self.base_dir)


class FindModuleCache:
Expand Down
22 changes: 19 additions & 3 deletions mypy/server/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"""

import os
import sys
import time
from typing import (
Dict, List, Set, Tuple, Union, Optional, NamedTuple, Sequence
Expand Down Expand Up @@ -1111,6 +1112,17 @@ def target_from_node(module: str,
return '%s.%s' % (module, node.name)


if sys.platform != 'win32':
INIT_SUFFIXES = ('/__init__.py', '/__init__.pyi') # type: Final
else:
INIT_SUFFIXES = (
os.sep + '__init__.py',
os.sep + '__init__.pyi',
os.altsep + '__init__.py',
os.altsep + '__init__.pyi',
) # type: Final


def refresh_suppressed_submodules(
module: str,
path: Optional[str],
Expand All @@ -1129,8 +1141,7 @@ def refresh_suppressed_submodules(
module: target package in which to look for submodules
path: path of the module
"""
# TODO: Windows paths
if path is None or not path.endswith(os.sep + '__init__.py'):
if path is None or not path.endswith(INIT_SUFFIXES):
# Only packages have submodules.
return
# Find any submodules present in the directory.
Expand All @@ -1145,8 +1156,13 @@ def refresh_suppressed_submodules(
trigger = make_trigger(submodule)
if trigger in deps:
for dep in deps[trigger]:
# TODO: <...> deps, imports in functions, etc.
# TODO: <...> deps, etc.
state = graph.get(dep)
if not state:
# Maybe it's a non-top-level target. We only care about the module.
dep_module = module_prefix(graph, dep)
if dep_module is not None:
state = graph.get(dep_module)
if state:
tree = state.tree
assert tree # TODO: What if doesn't exist?
Expand Down
2 changes: 1 addition & 1 deletion mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ def parse_module(self,
cache = FindModuleCache(search_paths)
for module_name in module_names.split(' '):
path = cache.find_module(module_name)
assert isinstance(path, str), "Can't find ad hoc case file"
assert isinstance(path, str), "Can't find ad hoc case file: %s" % module_name
with open(path, encoding='utf8') as f:
program_text = f.read()
out.append((module_name, path, program_text))
Expand Down
158 changes: 158 additions & 0 deletions test-data/unit/fine-grained-follow-imports.test
Original file line number Diff line number Diff line change
Expand Up @@ -542,3 +542,161 @@ main.py:3: error: Too few arguments for "f"
==
main.py:3: error: Too few arguments for "f"
main.py:4: error: Too many arguments for "f"

[case testFollowImportsNormalPackageInitFile4-only_when_cache]
# flags: --follow-imports=normal
# cmd: mypy main.py

[file main.py]
import p1.m # type: ignore
from p2 import m # type: ignore

[file p1/__init__.py.2]
1()

[file p1/m.py.2]
''()

[file p2/__init__.py.3]
''()

[file p2/m.py.3]

[out]
==
p1/__init__.py:1: error: "int" not callable
p1/m.py:1: error: "str" not callable
==
p1/__init__.py:1: error: "int" not callable
p1/m.py:1: error: "str" not callable
p2/__init__.py:1: error: "str" not callable

[case testFollowImportsNormalSubmoduleCreatedWithImportInFunction]
# flags: --follow-imports=normal
# cmd: mypy main.py

[file main.py]
def f() -> None:
from p import m

[file p/__init__.py.2]
1()

[file p/m.py.2]
''()

[out]
main.py:2: error: Cannot find implementation or library stub for module named 'p'
main.py:2: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
==
p/m.py:1: error: "str" not callable
p/__init__.py:1: error: "int" not callable

[case testFollowImportsNormalPackageInitFileStub]
# flags: --follow-imports=normal
# cmd: mypy main.py

[file main.py]
from p import m

[file p/__init__.pyi.2]
1()

[file p/m.pyi.2]
''()

[file p/mm.pyi.3]
x x x

[out]
main.py:1: error: Cannot find implementation or library stub for module named 'p'
main.py:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
==
p/m.pyi:1: error: "str" not callable
p/__init__.pyi:1: error: "int" not callable
==
p/m.pyi:1: error: "str" not callable
p/__init__.pyi:1: error: "int" not callable

[case testFollowImportsNormalNamespacePackages]
# flags: --follow-imports=normal --namespace-packages
# cmd: mypy main.py

[file main.py]
import p1.m1
import p2.m2

[file p1/m1.py]
1()

[file p2/m2.py.2]
''()

[delete p2/m2.py.3]

[out]
p1/m1.py:1: error: "int" not callable
main.py:2: error: Cannot find implementation or library stub for module named 'p2.m2'
main.py:2: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
==
p2/m2.py:1: error: "str" not callable
p1/m1.py:1: error: "int" not callable
==
main.py:2: error: Cannot find implementation or library stub for module named 'p2.m2'
main.py:2: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
p1/m1.py:1: error: "int" not callable

[case testFollowImportsNormalNewFileOnCommandLine]
# flags: --follow-imports=normal
# cmd: mypy main.py
# cmd2: mypy main.py x.py

[file main.py]
1()

[file x.py.2]
''()

[out]
main.py:1: error: "int" not callable
==
x.py:1: error: "str" not callable
main.py:1: error: "int" not callable

[case testFollowImportsNormalSearchPathUpdate-only_when_nocache]
# flags: --follow-imports=normal
# cmd: mypy main.py
# cmd2: mypy main.py src/foo.py

[file main.py]

[file src/foo.py.2]
import bar
''()

[file src/bar.py.2]
1()

[out]
==
src/bar.py:1: error: "int" not callable
src/foo.py:2: error: "str" not callable

[case testFollowImportsNormalSearchPathUpdate-only_when_cache]
# flags: --follow-imports=normal
# cmd: mypy main.py
# cmd2: mypy main.py src/foo.py

[file main.py]

[file src/foo.py.2]
import bar
''()

[file src/bar.py.2]
1()

[out]
==
src/foo.py:2: error: "str" not callable
src/bar.py:1: error: "int" not callable