8000 Fix some edge cases of namespace package imports (#5762) · python/mypy@725247b · GitHub
[go: up one dir, main page]

Skip to content

Commit 725247b

Browse files
authored
Fix some edge cases of namespace package imports (#5762)
Fixes #5758. (Sadly there's no test for the second half of that issue; I've opened #5767 to track adding one.)
1 parent 1c95a49 commit 725247b

File tree

6 files changed

+69
-35
lines changed

6 files changed

+69
-35
lines changed

mypy/build.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,12 +572,20 @@ def correct_rel_imp(imp: Union[ImportFrom, ImportAll]) -> str:
572572
pri = import_priority(imp, PRI_MED)
573573
ancestor_pri = import_priority(imp, PRI_LOW)
574574
for id, _ in imp.ids:
575+
# We append the target (e.g. foo.bar.baz)
576+
# before the ancestors (e.g. foo and foo.bar)
577+
# so that, if FindModuleCache finds the target
578+
# module in a package marked with py.typed
579+
# underneath a namespace package installed in
580+
# site-packages, (gasp), that cache's
581+
# knowledge of the ancestors can be primed
582+
# when it is asked to find the target.
583+
res.append((pri, id, imp.line))
575584
ancestor_parts = id.split(".")[:-1]
576585
ancestors = []
577586
for part in ancestor_parts:
578587
ancestors.append(part)
579588
res.append((ancestor_pri, ".".join(ancestors), imp.line))
580-
res.append((pri, id, imp.line))
581589
elif isinstance(imp, ImportFrom):
582590
cur_id = correct_rel_imp(imp)
583591
pos = len(res)
@@ -1400,6 +1408,8 @@ def __init__(self,
14001408
self.ignore_all = True
14011409
self.path = path
14021410
self.xpath = path or '<string>'
1411+
if path and source is None and self.manager.fscache.isdir(path):
1412+
source = ''
14031413
self.source = source
14041414
if path and source is None and self.manager.cache_enabled:
14051415
self.meta = find_cache_meta(self.id, path, manager)

mypy/modulefinder.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,13 @@ def __init__(self,
7474
self.dirs = {} # type: Dict[Tuple[str, Tuple[str, ...]], PackageDirs]
7575
# Cache find_module: id -> result
7676
self.results = {} # type: Dict[str, Optional[str]]
77+
self.ns_ancestors = {} # type: Dict[str, str]
7778
self.options = options
7879

7980
def clear(self) -> None:
8081
self.results.clear()
8182
self.dirs.clear()
83+
self.ns_ancestors.clear()
8284

8385
def find_lib_path_dirs(self, dir_chain: str, lib_path: Tuple[str, ...]) -> PackageDirs:
8486
# Cache some repeated work within distinct find_module calls: finding which
@@ -115,6 +117,14 @@ def _find_module_non_stub_helper(self, components: List[str],
115117
return os.path.join(pkg_dir, *components[:-1]), index == 0
116118
return None
117119

120+
def _update_ns_ancestors(self, components: List[str], match: Tuple[str, bool]) -> None:
121+
path, verify = match
122+
for i in range(1, len(components)):
123+
pkg_id = '.'.join(components[:-i])
124+
if pkg_id not in self.ns_ancestors:
125+
self.ns_ancestors[pkg_id] = path
126+
path = os.path.dirname(path)
127+
118128
def _find_module(self, id: str) -> Optional[str]:
119129
fscache = self.fscache
120130

@@ -155,6 +165,7 @@ def _find_module(self, id: str) -> Optional[str]:
155165
non_stub_match = self._find_module_non_stub_helper(components, pkg_dir)
156166
if non_stub_match:
157167
third_party_inline_dirs.append(non_stub_match)
168+
self._update_ns_ancestors(components, non_stub_match)
158169
if self.options and self.options.use_builtins_fixtures:
159170
# Everything should be in fixtures.
160171
third_party_inline_dirs.clear()
@@ -187,6 +198,8 @@ def _find_module(self, id: str) -> Optional[str]:
187198
near_misses.append(path_stubs)
188199
continue
189200
return path_stubs
201+
elif self.options and self.options.namespace_packages and fscache.isdir(base_path):
202+
near_misses.append(base_path)
190203
# No package, look for module.
191204
for extension in PYTHON_EXTENSIONS:
192205
path = base_path + extension
@@ -222,7 +235,11 @@ def _find_module(self, id: str) -> Optional[str]:
222235
index = levels.index(max(levels))
223236
return near_misses[index]
224237

225-
return None
238+
# Finally, we may be asked to produce an ancestor for an
239+
# installed package with a py.typed marker that is a
240+
# subpackage of a namespace package. We only fess up to these
241+
# if we would otherwise return "not found".
242+
return self.ns_ancestors.get(id)
226243

227244
def find_modules_recursive(self, module: str) -> List[BuildSource]:
228245
module_path = self.find_module(module)

test-data/unit/check-modules.test

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2280,9 +2280,9 @@ def __getattr__(attr: str) -> Any: ...
22802280
# empty (i.e. complete subpackage)
22812281
[builtins fixtures/module.pyi]
22822282
[out]
2283-
main:1: error: Cannot find module named 'a.b.c'
2284-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
22852283
main:1: error: Cannot find module named 'a.b.c.d'
2284+
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
2285+
main:1: error: Cannot find module named 'a.b.c'
22862286

22872287
[case testModuleGetattrInit8a]
22882288
import a.b.c # Error
@@ -2620,3 +2620,10 @@ x = 0
26202620
[file mypy.ini]
26212621
[[mypy]
26222622
mypy_path = tmp/xx, tmp/yy
2623+
2624+
[case testNamespacePackagePlainImport]
2625+
# flags: --namespace-packages
2626+
import foo.bar.baz
2627+
reveal_type(foo.bar.baz.x) # E: Revealed type is 'builtins.int'
2628+
[file foo/bar/baz.py]
2629+
x = 0

test-data/unit/cmdline.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,9 @@ import a.b
486486
[file a.b.py]
487487
whatever
488488
[out]
489-
main.py:1: error: Cannot find module named 'a'
490-
main.py:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
491489
main.py:1: error: Cannot find module named 'a.b'
490+
main.py:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
491+
main.py:1: error: Cannot find module named 'a'
492492

493493
[case testPythonVersionTooOld10]
494494
# cmd: mypy -c pass

test-data/unit/fine-grained-modules.test

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ p.a.f(1)
189189
[file p/a.py.2]
190190
def f(x: str) -> None: pass
191191
[out]
192-
main:1: error: Cannot find module named 'p'
193-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
194192
main:1: error: Cannot find module named 'p.a'
193+
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
194+
main:1: error: Cannot find module named 'p'
195195
==
196196
main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
197197

@@ -215,9 +215,9 @@ p.a.f(1)
215215
[file p/a.py.3]
216216
def f(x: str) -> None: pass
217217
[out]
218-
main:1: error: Cannot find module named 'p'
219-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
220218
main:1: error: Cannot find module named 'p.a'
219+
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
220+
main:1: error: Cannot find module named 'p'
221221
==
222222
main:1: error: Cannot find module named 'p.a'
223223
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
@@ -232,13 +232,13 @@ p.a.f(1)
232232
def f(x: str) -> None: pass
233233
[file p/__init__.py.3]
234234
[out]
235-
main:1: error: Cannot find module named 'p'
236-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
237235
main:1: error: Cannot find module named 'p.a'
238-
==
239-
main:1: error: Cannot find module named 'p'
240236
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
237+
main:1: error: Cannot find module named 'p'
238+
==
241239
main:1: error: Cannot find module named 'p.a'
240+
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
241+
main:1: error: Cannot find module named 'p'
242242
==
243243
main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
244244

@@ -266,13 +266,13 @@ p.a.f(1)
266266
def f(x: str) -> None: pass
267267
[file p/__init__.py.3]
268268
[out]
269-
main:4: error: Cannot find module named 'p'
270-
main:4: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
271269
main:4: error: Cannot find module named 'p.a'
272-
==
273-
main:4: error: Cannot find module named 'p'
274270
main:4: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
271+
main:4: error: Cannot find module named 'p'
272+
==
275273
main:4: error: Cannot find module named 'p.a'
274+
main:4: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
275+
main:4: error: Cannot find module named 'p'
276276
==
277277
main:5: error: Argument 1 to "f" has incompatible type "int"; expected "str"
278278

@@ -839,9 +839,9 @@ def f(x: str) -> None: pass
839839
[out]
840840
main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
841841
==
842-
main:1: error: Cannot find module named 'p'
843-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
844842
main:1: error: Cannot find module named 'p.a'
843+
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
844+
main:1: error: Cannot find module named 'p'
845845

846846
[case testDeletePackage2]
847847
import p
@@ -873,9 +873,9 @@ main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
873873
main:1: error: Cannot find module named 'p.a'
874874
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
875875
==
876-
main:1: error: Cannot find module named 'p'
877-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
878876
main:1: error: Cannot find module named 'p.a'
877+
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
878+
main:1: error: Cannot find module named 'p'
879879

880880
[case testDeletePackage4]
881881
import p.a
@@ -888,13 +888,13 @@ def f(x: str) -> None: pass
888888
[out]
889889
main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
890890
==
891-
main:1: error: Cannot find module named 'p'
892-
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
893891
main:1: error: Cannot find module named 'p.a'
894-
==
895-
main:1: error: Cannot find module named 'p'
896892
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
893+
main:1: error: Cannot find module named 'p'
894+
==
897895
main:1: error: Cannot find module named 'p.a'
896+
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
897+
main:1: error: Cannot find module named 'p'
898898

899899
[case testDeletePackage5]
900900
# cmd1: mypy main p/a.py p/__init__.py
@@ -911,13 +911,13 @@ def f(x: str) -> None: pass
911911
[out]
912912
main:6: error: Argument 1 to "f" has incompatible type "int"; expected "str"
913913
==
914-
main:5: error: Cannot find module named 'p'
915-
main:5: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
916914
main:5: error: Cannot find module named 'p.a'
917-
==
918-
main:5: error: Cannot find module named 'p'
919915
main:5: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
916+
main:5: error: Cannot find module named 'p'
917+
==
920918
main:5: error: Cannot find module named 'p.a'
919+
main:5: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
920+
main:5: error: Cannot find module named 'p'
921921

922922

923923
[case testDeletePackage6]

test-data/unit/semanal-errors.test

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,11 @@ main:4: error: Name 'n' is not defined
279279
import typing
280280
import m.n
281281
[out]
282-
main:2: error: Cannot find module named 'm'
283-
main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
284282
main:2: error: Cannot find module named 'm.n'
283+
main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
284+
main:2: error: Cannot find module named 'm'
285285

286-
[case testMissingPackage]
286+
[case testMissingPackage2]
287287
import typing
288288
from m.n import x
289289
from a.b import *
@@ -318,9 +318,9 @@ m.n.x
318318
[file m/n.py]
319319
x = 1
320320
[out]
321-
main:2: error: Cannot find module named 'm'
322-
main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
323321
main:2: error: Cannot find module named 'm.n'
322+
main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
323+
main:2: error: Cannot find module named 'm'
324324

325325
[case testBreakOutsideLoop]
326326
break

0 commit comments

Comments
 (0)
0