Skip to content

Commit 40fd841

Browse files
authored
modulefinder: make -p actually handle namespace packages correctly (#9683)
Once we were in a directory with __init__.py we only recursed into subdirectories if those subdirectories also contained an __init__.py. This is what #9616 should have been. Co-authored-by: hauntsaninja <>
1 parent 6e99a2d commit 40fd841

File tree

7 files changed

+66
-48
lines changed

7 files changed

+66
-48
lines changed

mypy/main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,7 @@ def set_strict_flags() -> None:
930930
())
931931
targets = []
932932
# TODO: use the same cache that the BuildManager will
933-
cache = FindModuleCache(search_paths, fscache, options, special_opts.packages)
933+
cache = FindModuleCache(search_paths, fscache, options)
934934
for p in special_opts.packages:
935935
if os.sep in p or os.altsep and os.altsep in p:
936936
fail("Package name '{}' cannot have a slash in it.".format(p),

mypy/modulefinder.py

+41-34
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,8 @@ class FindModuleCache:
105105

106106
def __init__(self,
107107
search_paths: SearchPaths,
108-
fscache: Optional[FileSystemCache] = None,
109-
options: Optional[Options] = None,
110-
ns_packages: Optional[List[str]] = None) -> None:
108+
fscache: Optional[FileSystemCache],
109+
options: Optional[Options]) -> None:
111110
self.search_paths = search_paths
112111
self.fscache = fscache or FileSystemCache()
113112
# Cache for get_toplevel_possibilities:
@@ -117,7 +116,6 @@ def __init__(self,
117116
self.results = {} # type: Dict[str, ModuleSearchResult]
118117
self.ns_ancestors = {} # type: Dict[str, str]
119118
self.options = options
120-
self.ns_packages = ns_packages or [] # type: List[str]
121119

122120
def clear(self) -> None:
123121
self.results.clear()
@@ -208,7 +206,7 @@ def _can_find_module_in_parent_dir(self, id: str) -> bool:
208206
of the current working directory.
209207
"""
210208
working_dir = os.getcwd()
211-
parent_search = FindModuleCache(SearchPaths((), (), (), ()))
209+
parent_search = FindModuleCache(SearchPaths((), (), (), ()), self.fscache, self.options)
212210
while any(file.endswith(("__init__.py", "__init__.pyi"))
213211
for file in os.listdir(working_dir)):
214212
working_dir = os.path.dirname(working_dir)
@@ -364,36 +362,45 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
364362
if isinstance(module_path, ModuleNotFoundReason):
365363
return []
366364
result = [BuildSource(module_path, module, None)]
365+
366+
package_path = None
367367
if module_path.endswith(('__init__.py', '__init__.pyi')):
368-
# Subtle: this code prefers the .pyi over the .py if both
369-
# exists, and also prefers packages over modules if both x/
370-
# and x.py* exist. How? We sort the directory items, so x
371-
# comes before x.py and x.pyi. But the preference for .pyi
372-
# over .py is encoded in find_module(); even though we see
373-
# x.py before x.pyi, find_module() will find x.pyi first. We
374-
# use hits to avoid adding it a second time when we see x.pyi.
375-
# This also avoids both x.py and x.pyi when x/ was seen first.
376-
hits = set() # type: Set[str]
377-
for item in sorted(self.fscache.listdir(os.path.dirname(module_path))):
378-
abs_path = os.path.join(os.path.dirname(module_path), item)
379-
if os.path.isdir(abs_path) and \
380-
(os.path.isfile(os.path.join(abs_path, '__init__.py')) or
381-
os.path.isfile(os.path.join(abs_path, '__init__.pyi'))):
382-
hits.add(item)
383-
result += self.find_modules_recursive(module + '.' + item)
384-
elif item != '__init__.py' and item != '__init__.pyi' and \
385-
item.endswith(('.py', '.pyi')):
386-
mod = item.split('.')[0]
387-
if mod not in hits:
388-
hits.add(mod)
389-
result += self.find_modules_recursive(module + '.' + mod)
390-
elif os.path.isdir(module_path):
391-
# Even subtler: handle recursive decent into PEP 420
392-
# namespace packages that are explicitly listed on the command
393-
# line with -p/--packages.
394-
for item in sorted(self.fscache.listdir(module_path)):
395-
item, _ = os.path.splitext(item)
396-
result += self.find_modules_recursive(module + '.' + item)
368+
package_path = os.path.dirname(module_path)
369+
elif self.fscache.isdir(module_path):
370+
package_path = module_path
371+
if package_path is None:
372+
return result
373+
374+
# This logic closely mirrors that in find_sources. One small but important difference is
375+
# that we do not sort names with keyfunc. The recursive call to find_modules_recursive
376+
# calls find_module, which will handle the preference between packages, pyi and py.
377+
# Another difference is it doesn't handle nested search paths / package roots.
378+
379+
seen = set() # type: Set[str]
380+
names = sorted(self.fscache.listdir(package_path))
381+
for name in names:
382+
# Skip certain names altogether
383+
if name == '__pycache__' or name.startswith('.') or name.endswith('~'):
384+
continue
385+
path = os.path.join(package_path, name)
386+
387+
if self.fscache.isdir(path):
388+
# Only recurse into packages
389+
if (self.options and self.options.namespace_packages) or (
390+
self.fscache.isfile(os.path.join(path, "__init__.py"))
391+
or self.fscache.isfile(os.path.join(path, "__init__.pyi"))
392+
):
393+
seen.add(name)
394+
result.extend(self.find_modules_recursive(module + '.' + name))
395+
else:
396+
stem, suffix = os.path.splitext(name)
397+
if stem == '__init__':
398+
continue
399+
if stem not in seen and '.' not in stem and suffix in PYTHON_EXTENSIONS:
400+
# (If we sorted names) we could probably just make the BuildSource ourselves,
401+
# but this ensures compatibility with find_module / the cache
402+
seen.add(stem)
403+
result.extend(self.find_modules_recursive(module + '.' + stem))
397404
return result
398405

399406

mypy/stubgen.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1308,7 +1308,7 @@ def find_module_paths_using_search(modules: List[str], packages: List[str],
13081308
result = [] # type: List[StubSource]
13091309
typeshed_path = default_lib_path(mypy.build.default_data_dir(), pyversion, None)
13101310
search_paths = SearchPaths(('.',) + tuple(search_path), (), (), tuple(typeshed_path))
1311-
cache = FindModuleCache(search_paths)
1311+
cache = FindModuleCache(search_paths, fscache=None, options=None)
13121312
for module in modules:
13131313
m_result = cache.find_module(module)
13141314
if isinstance(m_result, ModuleNotFoundReason):

mypy/stubtest.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -999,7 +999,9 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
999999
"""
10001000
data_dir = mypy.build.default_data_dir()
10011001
search_path = mypy.modulefinder.compute_search_paths([], options, data_dir)
1002-
find_module_cache = mypy.modulefinder.FindModuleCache(search_path)
1002+
find_module_cache = mypy.modulefinder.FindModuleCache(
1003+
search_path, fscache=None, options=options
1004+
)
10031005

10041006
all_modules = []
10051007
sources = []

mypy/test/testcheck.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def parse_module(self,
342342
module_names = m.group(1)
343343
out = []
344344
search_paths = SearchPaths((test_temp_dir,), (), (), ())
345-
cache = FindModuleCache(search_paths)
345+
cache = FindModuleCache(search_paths, fscache=None, options=None)
346346
for module_name in module_names.split(' '):
347347
path = cache.find_module(module_name)
348348
assert isinstance(path, str), "Can't find ad hoc case file: %s" % module_name

mypy/test/testmodulefinder.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ def setUp(self) -> None:
3232
)
3333
options = Options()
3434
options.namespace_packages = True
35-
self.fmc_ns = FindModuleCache(self.search_paths, options=options)
35+
self.fmc_ns = FindModuleCache(self.search_paths, fscache=None, options=options)
3636

3737
options = Options()
3838
options.namespace_packages = False
39-
self.fmc_nons = FindModuleCache(self.search_paths, options=options)
39+
self.fmc_nons = FindModuleCache(self.search_paths, fscache=None, options=options)
4040

4141
def test__no_namespace_packages__nsx(self) -> None:
4242
"""
@@ -159,11 +159,11 @@ def setUp(self) -> None:
159159
)
160160
options = Options()
161161
options.namespace_packages = True
162-
self.fmc_ns = FindModuleCache(self.search_paths, options=options)
162+
self.fmc_ns = FindModuleCache(self.search_paths, fscache=None, options=options)
163163

164164
options = Options()
165165
options.namespace_packages = False
166-
self.fmc_nons = FindModuleCache(self.search_paths, options=options)
166+
self.fmc_nons = FindModuleCache(self.search_paths, fscache=None, options=options)
167167

168168
def path(self, *parts: str) -> str:
169169
return os.path.join(self.package_dir, *parts)

test-data/unit/cmdline.test

+15-6
Original file line numberDiff line numberDiff line change
@@ -810,15 +810,24 @@ def bar(a: int, b: int) -> str:
810810
src/anamespace/foo/bar.py:2: error: Incompatible return value type (got "int", expected "str")
811811

812812
[case testNestedPEP420Packages]
813-
# cmd: mypy -p bottles --namespace-packages
814-
[file bottles/jars/secret/glitter.py]
813+
# cmd: mypy -p pkg --namespace-packages
814+
[file pkg/a1/b/c/d/e.py]
815815
x = 0 # type: str
816-
[file bottles/jars/sprinkle.py]
817-
from bottles.jars.secret.glitter import x
816+
[file pkg/a1/b/f.py]
817+
from pkg.a1.b.c.d.e import x
818+
x + 1
819+
820+
[file pkg/a2/__init__.py]
821+
[file pkg/a2/b/c/d/e.py]
822+
x = 0 # type: str
823+
[file pkg/a2/b/f.py]
824+
from pkg.a2.b.c.d.e import x
818825
x + 1
819826
[out]
820-
bottles/jars/secret/glitter.py:1: error: Incompatible types in assignment (expression has type "int", variable has type "str")
821-
bottles/jars/sprinkle.py:2: error: Unsupported operand types for + ("str" and "int")
827+
pkg/a2/b/c/d/e.py:1: error: Incompatible types in assignment (expression has type "int", variable has type "str")
828+
pkg/a1/b/c/d/e.py:1: error: Incompatible types in assignment (expression has type "int", variable has type "str")
829+
pkg/a2/b/f.py:2: error: Unsupported operand types for + ("str" and "int")
830+
pkg/a1/b/f.py:2: error: Unsupported operand types for + ("str" and "int")
822831

823832
[case testFollowImportStubs1]
824833
# cmd: mypy main.py

0 commit comments

Comments
 (0)