Skip to content

Commit 6b6ce1c

Browse files
committed
Namespace implementation (#1645)
1 parent e037806 commit 6b6ce1c

File tree

11 files changed

+443
-136
lines changed

11 files changed

+443
-136
lines changed

mypy/build.py

Lines changed: 196 additions & 121 deletions
Large diffs are not rendered by default.

mypy/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ def add_invertible_flag(flag: str,
323323
parser.add_argument('--show-traceback', '--tb', action='store_true',
324324
help="show traceback on fatal error")
325325
parser.add_argument('--stats', action='store_true', dest='dump_type_stats', help="dump stats")
326+
parser.add_argument('--namespace-packages', action='store_true', dest='namespace_packages',
327+
help='Allow implicit namespace packages (PEP420)')
326328
parser.add_argument('--inferstats', action='store_true', dest='dump_inference_stats',
327329
help="dump type inference stats")
328330
parser.add_argument('--custom-typing', metavar='MODULE', dest='custom_typing_module',
@@ -508,7 +510,8 @@ def add_invertible_flag(flag: str,
508510
.format(special_opts.package))
509511
options.build_type = BuildType.MODULE
510512
lib_path = [os.getcwd()] + build.mypy_path()
511-
targets = build.find_modules_recursive(special_opts.package, lib_path)
513+
mod_discovery = build.ModuleDiscovery(lib_path, options.namespace_packages)
514+
targets = mod_discovery.find_modules_recursive(special_opts.package)
512515
if not targets:
513516
fail("Can't find package '{}'".format(special_opts.package))
514517
return targets, options

mypy/options.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ def __init__(self) -> None:
161161
# Use stub builtins fixtures to speed up tests
162162
self.use_builtins_fixtures = False
163163

164+
# Allow implicit namespace packages (PEP420)
165+
self.namespace_packages = False
166+
164167
# -- experimental options --
165168
self.shadow_file = None # type: Optional[Tuple[str, str]]
166169
self.show_column_numbers = False # type: bool

mypy/semanal.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,6 @@ class SemanticAnalyzerPass2(NodeVisitor[None]):
182182
This is the second phase of semantic analysis.
183183
"""
184184

185-
# Library search paths
186-
lib_path = None # type: List[str]
187185
# Module name space
188186
modules = None # type: Dict[str, MypyFile]
189187
# Global name space for current module
@@ -229,13 +227,9 @@ class SemanticAnalyzerPass2(NodeVisitor[None]):
229227
def __init__(self,
230228
modules: Dict[str, MypyFile],
231229
missing_modules: Set[str],
232-
lib_path: List[str], errors: Errors,
230+
errors: Errors,
233231
plugin: Plugin) -> None:
234-
"""Construct semantic analyzer.
235-
236-
Use lib_path to search for modules, and report analysis errors
237-
using the Errors instance.
238-
"""
232+
"""Construct semantic analyzer."""
239233
self.locals = [None]
240234
self.imports = set()
241235
self.type = None
@@ -244,7 +238,6 @@ def __init__(self,
244238
self.function_stack = []
245239
self.block_depth = [0]
246240
self.loop_depth = 0
247-
self.lib_path = lib_path
248241
self.errors = errors
249242
self.modules = modules
250243
self.msg = MessageBuilder(errors, modules)

mypy/stubgen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ def find_module_path_and_all(module: str, pyversion: Tuple[int, int],
156156
module_all = getattr(mod, '__all__', None)
157157
else:
158158
# Find module by going through search path.
159-
module_path = mypy.build.find_module(module, ['.'] + search_path)
159+
md = mypy.build.ModuleDiscovery(['.'] + search_path)
160+
module_path = md.find_module(module)
160161
if not module_path:
161162
raise SystemExit(
162163
"Can't find module '{}' (consider using --search-path)".format(module))

mypy/test/testcheck.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
'check-incomplete-fixture.test',
8080
'check-custom-plugin.test',
8181
'check-default-plugin.test',
82+
'check-namespaces.test',
8283
]
8384

8485

@@ -328,7 +329,8 @@ def parse_module(self,
328329
module_names = m.group(1)
329330
out = []
330331
for module_name in module_names.split(' '):
331-
path = build.find_module(module_name, [test_temp_dir])
332+
md = build.ModuleDiscovery([test_temp_dir], namespaces_allowed=False)
333+
path = md.find_module(module_name)
332334
assert path is not None, "Can't find ad hoc case file"
333335
with open(path) as f:
334336
program_text = f.read()

mypy/test/testdmypy.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@ def parse_module(self,
270270
module_names = m.group(1)
271271
out = []
272272
for module_name in module_names.split(' '):
273-
path = build.find_module(module_name, [test_temp_dir])
273+
md = build.ModuleDiscovery([test_temp_dir])
274+
path = md.find_module(module_name)
274275
assert path is not None, "Can't find ad hoc case file"
275276
with open(path) as f:
276277
program_text = f.read()

mypy/test/testgraph.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import AbstractSet, Dict, Set, List
44

55
from mypy.myunit import Suite, assert_equal
6-
from mypy.build import BuildManager, State, BuildSourceSet
6+
from mypy.build import BuildManager, State, BuildSourceSet, ModuleDiscovery
77
from mypy.build import topsort, strongly_connected_components, sorted_components, order_ascc
88
from mypy.version import __version__
99
from mypy.options import Options
@@ -41,14 +41,14 @@ def _make_manager(self) -> BuildManager:
4141
options = Options()
4242
manager = BuildManager(
4343
data_dir='',
44-
lib_path=[],
4544
ignore_prefix='',
4645
source_set=BuildSourceSet([]),
4746
reports=Reports('', {}),
4847
options=options,
4948
version_id=__version__,
5049
plugin=Plugin(options),
5150
errors=errors,
51+
module_discovery=ModuleDiscovery([]),
5252
)
5353
return manager
5454

mypy/test/testmodulediscovery.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import os
2+
3+
from unittest import mock, TestCase
4+
from typing import List, Set
5+
6+
from mypy.build import ModuleDiscovery, find_module_clear_caches
7+
from mypy.myunit import Suite, assert_equal
8+
9+
10+
class ModuleDiscoveryTestCase(Suite):
11+
def set_up(self) -> None:
12+
self.files = set() # type: Set[str]
13+
14+
self._setup_mock_filesystem()
15+
16+
def tear_down(self) -> None:
17+
self._teardown_mock_filesystem()
18+
find_module_clear_caches()
19+
20+
def _list_dir(self, path: str) -> List[str]:
21+
res = set()
22+
23+
if not path.endswith(os.path.sep):
24+
path = path + os.path.sep
25+
26+
for item in self.files:
27+
if item.startswith(path):
28+
remnant = item.replace(path, '')
29+
segments = remnant.split(os.path.sep)
30+
if segments:
31+
res.add(segments[0])
32+
33+
return list(res)
34+
35+
def _is_file(self, path: str) -> bool:
36+
return path in self.files
37+
38+
def _is_dir(self, path: str) -> bool:
39+
for item in self.files:
40+
if not item.endswith('/'):
41+
item += '/'
42+
if item.startswith(path):
43+
return True
44+
return False
45+
46+
def _setup_mock_filesystem(self) -> None:
47+
self._listdir_patcher = mock.patch('os.listdir', side_effect=self._list_dir)
48+
self._listdir_mock = self._listdir_patcher.start()
49+
self._isfile_patcher = mock.patch('os.path.isfile', side_effect=self._is_file)
50+
self._isfile_mock = self._isfile_patcher.start()
51+
self._isdir_patcher = mock.patch('os.path.isdir', side_effect=self._is_dir)
52+
self._isdir_mock = self._isdir_patcher.start()
53+
54+
def _teardown_mock_filesystem(self) -> None:
55+
self._listdir_patcher.stop()
56+
self._isfile_patcher.stop()
57+
self._isdir_patcher.stop()
58+
59+
def test_module_vs_package(self) -> None:
60+
self.files = {
61+
os.path.join('dir1', 'mod.py'),
62+
os.path.join('dir2', 'mod', '__init__.py'),
63+
}
64+
m = ModuleDiscovery(['dir1', 'dir2'], namespaces_allowed=False)
65+
path = m.find_module('mod')
66+
assert_equal(path, os.path.join('dir1', 'mod.py'))
67+
68+
m = ModuleDiscovery(['dir2', 'dir1'], namespaces_allowed=False)
69+
path = m.find_module('mod')
70+
assert_equal(path, os.path.join('dir2', 'mod', '__init__.py'))
71+
72+
def test_package_in_different_directories(self) -> None:
73+
self.files = {
74+
os.path.join('dir1', 'mod', '__init__.py'),
75+
os.path.join('dir1', 'mod', 'a.py'),
76+
os.path.join('dir2', 'mod', '__init__.py'),
77+
os.path.join('dir2', 'mod', 'b.py'),
78+
}
79+
m = ModuleDiscovery(['./dir1', './dir2'], namespaces_allowed=False)
80+
path = m.find_module('mod.a')
81+
assert_equal(path, os.path.join('dir1', 'mod', 'a.py'))
82+
83+
path = m.find_module('mod.b')
84+
assert_equal(path, None)
85+
86+
def test_package_with_stubs(self) -> None:
87+
self.files = {
88+
os.path.join('dir1', 'mod', '__init__.py'),
89+
os.path.join('dir1', 'mod', 'a.py'),
90+
os.path.join('dir2', 'mod', '__init__.pyi'),
91+
os.path.join('dir2', 'mod', 'b.pyi'),
92+
}
93+
m = ModuleDiscovery(['dir1', 'dir2'], namespaces_allowed=False)
94+
path = m.find_module('mod.a')
95+
assert_equal(path, os.path.join('dir1', 'mod', 'a.py'))
96+
97+
path = m.find_module('mod.b')
98+
assert_equal(path, os.path.join('dir2', 'mod', 'b.pyi'))
99+
100+
def test_namespaces(self) -> None:
101+
self.files = {
102+
os.path.join('dir1', 'mod', 'a.py'),
103+
os.path.join('dir2', 'mod', 'b.py'),
104+
}
105+
m = ModuleDiscovery(['dir1', 'dir2'], namespaces_allowed=True)
106+
path = m.find_module('mod.a')
107+
assert_equal(path, os.path.join('dir1', 'mod', 'a.py'))
108+
109+
path = m.find_module('mod.b')
110+
assert_equal(path, os.path.join('dir2', 'mod', 'b.py'))
111+
112+
def test_find_modules_recursive(self) -> None:
113+
self.files = {
114+
os.path.join('dir1', 'mod', '__init__.py'),
115+
os.path.join('dir1', 'mod', 'a.py'),
116+
os.path.join('dir2', 'mod', '__init__.pyi'),
117+
os.path.join('dir2', 'mod', 'b.pyi'),
118+
}
119+
m = ModuleDiscovery(['dir1', 'dir2'], namespaces_allowed=True)
120+
srcs = m.find_modules_recursive('mod')
121+
assert_equal([s.module for s in srcs], ['mod', 'mod.a', 'mod.b'])
122+
123+
def test_find_modules_recursive_with_namespace(self) -> None:
124+
self.files = {
125+
os.path.join('dir1', 'mod', 'a.py'),
126+
os.path.join('dir2', 'mod', 'b.py'),
127+
}
128+
m = ModuleDiscovery(['dir1', 'dir2'], namespaces_allowed=True)
129+
srcs = m.find_modules_recursive('mod')
130+
assert_equal([s.module for s in srcs], ['mod.a', 'mod.b'])
131+
132+
def test_find_modules_recursive_with_stubs(self) -> None:
133+
self.files = {
134+
os.path.join('dir1', 'mod', '__init__.py'),
135+
os.path.join('dir1', 'mod', 'a.py'),
136+
os.path.join('dir2', 'mod', '__init__.pyi'),
137+
os.path.join('dir2', 'mod', 'a.pyi'),
138+
}
139+
m = ModuleDiscovery(['dir1', 'dir2'], namespaces_allowed=True)
140+
srcs = m.find_modules_recursive('mod')
141+
assert_equal([s.module for s in srcs], ['mod', 'mod.a'])

runtests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ def test_path(*names: str):
232232
'testsolve',
233233
'testsubtypes',
234234
'testtypes',
235+
'testmodulediscovery',
235236
)
236237

237238
for f in find_files('mypy', prefix='test', suffix='.py'):

test-data/unit/check-namespaces.test

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
-- Type checker test cases dealing with namespaces imports
2+
3+
[case testAccessModuleInsideNamespace]
4+
# flags: --namespace-packages
5+
from ns import a
6+
[file ns/a.py]
7+
class A: pass
8+
def f(a: A) -> None: pass
9+
10+
[case testAccessModuleInsideNamespaceNoNamespacePackages]
11+
from ns import a
12+
[file ns/a.py]
13+
class A: pass
14+
def f(a: A) -> None: pass
15+
[out]
16+
main:1: error: Cannot find module named 'ns'
17+
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
18+
19+
[case testAccessPackageInsideNamespace]
20+
# flags: --namespace-packages
21+
from ns import a
22+
[file ns/a/__init__.py]
23+
class A: pass
24+
def f(a: A) -> None: pass
25+
26+
[case testAccessPackageInsideNamespaceLocatedInSeparateDirectories]
27+
# flags: --config-file tmp/mypy.ini
28+
from ns import a, b
29+
[file mypy.ini]
30+
[[mypy]
31+
namespace_packages = True
32+
mypy_path = ./tmp/dir1:./tmp/dir2
33+
[file dir1/ns/a/__init__.py]
34+
class A: pass
35+
def f(a: A) -> None: pass
36+
[file dir2/ns/b.py]
37+
class B: pass
38+
def f(a: B) -> None: pass
39+
40+
[case testConflictingPackageAndNamespaceFromImport]
41+
# flags: --config-file tmp/mypy.ini
42+
from pkg_or_ns import a
43+
from pkg_or_ns import b # E: Module 'pkg_or_ns' has no attribute 'b'
44+
[file mypy.ini]
45+
[[mypy]
46+
namespace_packages = True
47+
mypy_path = ./tmp/dir:./tmp/other_dir
48+
[file dir/pkg_or_ns/__init__.py]
49+
[file dir/pkg_or_ns/a.py]
50+
[file other_dir/pkg_or_ns/b.py]
51+
52+
[case testConflictingPackageAndNamespaceImport]
53+
# flags: --config-file tmp/mypy.ini
54+
import pkg_or_ns.a
55+
import pkg_or_ns.b
56+
[file mypy.ini]
57+
[[mypy]
58+
namespace_packages = True
59+
mypy_path = ./tmp/dir:./tmp/other_dir
60+
[file dir/pkg_or_ns/__init__.py]
61+
[file dir/pkg_or_ns/a.py]
62+
[file other_dir/pkg_or_ns/b.py]
63+
[out]
64+
main:3: error: Cannot find module named 'pkg_or_ns.b'
65+
main:3: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
66+
67+
[case testConflictingModuleAndNamespace]
68+
# flags: --config-file tmp/mypy.ini
69+
from mod_or_ns import a
70+
from mod_or_ns import b # E: Module 'mod_or_ns' has no attribute 'b'
71+
[file mypy.ini]
72+
[[mypy]
73+
namespace_packages = True
74+
mypy_path = ./tmp/dir:./tmp/other_dir
75+
[file dir/mod_or_ns.py]
76+
a = None
77+
[file other_dir/mod_or_ns/b.py]
78+
79+
[case testeNamespaceInsidePackage]
80+
# flags: --config-file tmp/mypy.ini
81+
from pkg.ns import a
82+
[file mypy.ini]
83+
[[mypy]
84+
namespace_packages = True
85+
[file pkg/__init__.py]
86+
[file pkg/ns/a.py]
87+
[out]

0 commit comments

Comments
 (0)