Skip to content

Commit fd99544

Browse files
authored
Improve missing module error for subdirectories (#8927)
Fixes #7405
1 parent 21e160e commit fd99544

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

mypy/modulefinder.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,18 @@ class ModuleNotFoundReason(Enum):
4949
# corresponding *-stubs package.
5050
FOUND_WITHOUT_TYPE_HINTS = 1
5151

52+
# The module was not found in the current working directory, but
53+
# was able to be found in the parent directory.
54+
WRONG_WORKING_DIRECTORY = 2
55+
5256
def error_message_templates(self) -> Tuple[str, str]:
5357
if self is ModuleNotFoundReason.NOT_FOUND:
5458
msg = "Cannot find implementation or library stub for module named '{}'"
5559
note = "See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports"
60+
elif self is ModuleNotFoundReason.WRONG_WORKING_DIRECTORY:
61+
msg = "Cannot find implementation or library stub for module named '{}'"
62+
note = ("You may be running mypy in a subpackage, "
63+
"mypy should be run on the package root")
5664
elif self is ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS:
5765
msg = "Skipping analyzing '{}': found module but no type hints or library stubs"
5866
note = "See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports"
@@ -166,6 +174,9 @@ def find_module(self, id: str) -> ModuleSearchResult:
166174
"""Return the path of the module source file or why it wasn't found."""
167175
if id not in self.results:
168176
self.results[id] = self._find_module(id)
177+
if (self.results[id] is ModuleNotFoundReason.NOT_FOUND
178+
and self._can_find_module_in_parent_dir(id)):
179+
self.results[id] = ModuleNotFoundReason.WRONG_WORKING_DIRECTORY
169180
return self.results[id]
170181

171182
def _find_module_non_stub_helper(self, components: List[str],
@@ -192,6 +203,20 @@ def _update_ns_ancestors(self, components: List[str], match: Tuple[str, bool]) -
192203
self.ns_ancestors[pkg_id] = path
193204
path = os.path.dirname(path)
194205

206+
def _can_find_module_in_parent_dir(self, id: str) -> bool:
207+
"""Test if a module can be found by checking the parent directories
208+
of the current working directory.
209+
"""
210+
working_dir = os.getcwd()
211+
parent_search = FindModuleCache(SearchPaths((), (), (), ()))
212+
while any(file.endswith(("__init__.py", "__init__.pyi"))
213+
for file in os.listdir(working_dir)):
214+
working_dir = os.path.dirname(working_dir)
215+
parent_search.search_paths = SearchPaths((working_dir,), (), (), ())
216+
if not isinstance(parent_search._find_module(id), ModuleNotFoundReason):
217+
return True
218+
return False
219+
195220
def _find_module(self, id: str) -> ModuleSearchResult:
196221
fscache = self.fscache
197222

mypy/test/testcmdline.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import sys
1111

1212
from typing import List
13+
from typing import Optional
1314

1415
from mypy.test.config import test_temp_dir, PREFIX
1516
from mypy.test.data import DataDrivenTestCase, DataSuite
@@ -45,6 +46,7 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None:
4546
for s in testcase.input:
4647
file.write('{}\n'.format(s))
4748
args = parse_args(testcase.input[0])
49+
custom_cwd = parse_cwd(testcase.input[1]) if len(testcase.input) > 1 else None
4850
args.append('--show-traceback')
4951
args.append('--no-site-packages')
5052
if '--error-summary' not in args:
@@ -56,7 +58,10 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None:
5658
process = subprocess.Popen(fixed + args,
5759
stdout=subprocess.PIPE,
5860
stderr=subprocess.PIPE,
59-
cwd=test_temp_dir,
61+
cwd=os.path.join(
62+
test_temp_dir,
63+
custom_cwd or ""
64+
),
6065
env=env)
6166
outb, errb = process.communicate()
6267
result = process.returncode
@@ -112,3 +117,18 @@ def parse_args(line: str) -> List[str]:
112117
if not m:
113118
return [] # No args; mypy will spit out an error.
114119
return m.group(1).split()
120+
121+
122+
def parse_cwd(line: str) -> Optional[str]:
123+
"""Parse the second line of the program for the command line.
124+
125+
This should have the form
126+
127+
# cwd: <directory>
128+
129+
For example:
130+
131+
# cwd: main/subdir
132+
"""
133+
m = re.match('# cwd: (.*)$', line)
134+
return m.group(1) if m else None

test-data/unit/cmdline.test

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,37 @@ ignore_missing_imports = True
486486
[out]
487487
main.py:2: note: Revealed type is 'Any'
488488

489+
490+
[case testFailedImportOnWrongCWD]
491+
# cmd: mypy main.py
492+
# cwd: main/subdir1/subdir2
493+
[file main/subdir1/subdir2/main.py]
494+
import parent
495+
import grandparent
496+
import missing
497+
[file main/subdir1/subdir2/__init__.py]
498+
[file main/subdir1/parent.py]
499+
[file main/subdir1/__init__.py]
500+
[file main/grandparent.py]
501+
[file main/__init__.py]
502+
[out]
503+
main.py:1: error: Cannot find implementation or library stub for module named 'parent'
504+
main.py:1: note: You may be running mypy in a subpackage, mypy should be run on the package root
505+
main.py:2: error: Cannot find implementation or library stub for module named 'grandparent'
506+
main.py:3: error: Cannot find implementation or library stub for module named 'missing'
507+
main.py:3: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
508+
509+
[case testImportInParentButNoInit]
510+
# cmd: mypy main.py
511+
# cwd: main/not_a_package
512+
[file main/not_a_package/main.py]
513+
import needs_init
514+
[file main/needs_init.py]
515+
[file main/__init__.py]
516+
[out]
517+
main.py:1: error: Cannot find implementation or library stub for module named 'needs_init'
518+
main.py:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
519+
489520
[case testConfigNoErrorForUnknownXFlagInSubsection]
490521
# cmd: mypy -c pass
491522
[file mypy.ini]

0 commit comments

Comments
 (0)