Skip to content

Commit 3e128fa

Browse files
committed
improve test coverage and documentation
1 parent 55eb815 commit 3e128fa

File tree

5 files changed

+173
-4
lines changed

5 files changed

+173
-4
lines changed

docs/source/running_mypy.rst

+26
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,32 @@ same directory on the search path, only the stub file is used.
516516
(However, if the files are in different directories, the one found
517517
in the earlier directory is used.)
518518

519+
If a namespace package is spread across many distinct folders, for
520+
instance::
521+
522+
foo/
523+
company/
524+
foo/
525+
a.py
526+
bar/
527+
company/
528+
bar/
529+
b.py
530+
baz/
531+
company/
532+
baz/
533+
c.py
534+
...
535+
536+
Then the default logic used to scan through search paths to resolve
537+
imports can become very slow. Specifically it becomes quadratic in
538+
the number of folders sharing the top-level ``company`` namespace.
539+
To work around this, it is possible to enable an experimental fast path
540+
that can more efficiently resolve imports within the set of input files
541+
to be typechecked. This is controlled by setting the :option:`--fast-module-lookup`
542+
option.
543+
544+
519545
Other advice and best practices
520546
*******************************
521547

mypy/modulefinder.py

+3
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ def clear(self) -> None:
189189
self.ns_ancestors.clear()
190190

191191
def find_module_via_source_set(self, id: str) -> Optional[ModuleSearchResult]:
192+
"""Fast path to find modules by looking through the input sources
193+
194+
This is only used when --fast-module-lookup is passed on the command line."""
192195
if not self.source_set:
193196
return None
194197

mypy/test/helpers.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,8 @@ def assert_type(typ: type, value: object) -> None:
365365

366366

367367
def parse_options(program_text: str, testcase: DataDrivenTestCase,
368-
incremental_step: int) -> Options:
368+
incremental_step: int,
369+
extra_flags: List[str] = []) -> Options:
369370
"""Parse comments like '# flags: --foo' in a test case."""
370371
options = Options()
371372
flags = re.search('# flags: (.*)$', program_text, flags=re.MULTILINE)
@@ -378,12 +379,13 @@ def parse_options(program_text: str, testcase: DataDrivenTestCase,
378379
if flags:
379380
flag_list = flags.group(1).split()
380381
flag_list.append('--no-site-packages') # the tests shouldn't need an installed Python
382+
flag_list.extend(extra_flags)
381383
targets, options = process_options(flag_list, require_targets=False)
382384
if targets:
383385
# TODO: support specifying targets via the flags pragma
384386
raise RuntimeError('Specifying targets via the flags pragma is not supported.')
385387
else:
386-
flag_list = []
388+
flag_list = extra_flags
387389
options = Options()
388390
# TODO: Enable strict optional in test cases by default (requires *many* test case changes)
389391
options.strict_optional = False

mypy/test/testcheck.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
'check-multiple-inheritance.test',
4141
'check-super.test',
4242
'check-modules.test',
43+
'check-modules-fast.test',
4344
'check-typevar-values.test',
4445
'check-unsupported.test',
4546
'check-unreachable-code.test',
@@ -111,6 +112,13 @@
111112
if sys.platform in ('darwin', 'win32'):
112113
typecheck_files.extend(['check-modules-case.test'])
113114

115+
# some test cases are run multiple times with various combinations of extra flags
116+
EXTRA_FLAGS = {
117+
'check-modules.test': [['--fast-module-lookup']],
118+
'check-modules-fast.test': [['--fast-module-lookup']],
119+
'check-modules-case.test': [['--fast-module-lookup']],
120+
}
121+
114122

115123
class TypeCheckSuite(DataSuite):
116124
files = typecheck_files
@@ -138,10 +146,13 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
138146
self.run_case_once(testcase, ops, step)
139147
else:
140148
self.run_case_once(testcase)
149+
for extra_flags in EXTRA_FLAGS.get(os.path.basename(testcase.file), []):
150+
self.run_case_once(testcase, extra_flags=extra_flags)
141151

142152
def run_case_once(self, testcase: DataDrivenTestCase,
143153
operations: List[FileOperation] = [],
144-
incremental_step: int = 0) -> None:
154+
incremental_step: int = 0,
155+
extra_flags: List[str] = []) -> None:
145156
original_program_text = '\n'.join(testcase.input)
146157
module_data = self.parse_module(original_program_text, incremental_step)
147158

@@ -162,7 +173,8 @@ def run_case_once(self, testcase: DataDrivenTestCase,
162173
perform_file_operations(operations)
163174

164175
# Parse options after moving files (in case mypy.ini is being moved).
165-
options = parse_options(original_program_text, testcase, incremental_step)
176+
options = parse_options(original_program_text, testcase, incremental_step,
177+
extra_flags=extra_flags)
166178
options.use_builtins_fixtures = True
167179
options.show_traceback = True
168180

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
-- Type checker test cases dealing with module lookup edge cases
2+
-- to ensure that --fast-module-lookup matches regular lookup behavior
3+
4+
[case testModuleLookup]
5+
import m
6+
reveal_type(m.a) # N: Revealed type is "m.A"
7+
8+
[file m.py]
9+
class A: pass
10+
a = A()
11+
12+
[case testModuleLookupStub]
13+
import m
14+
reveal_type(m.a) # N: Revealed type is "m.A"
15+
16+
[file m.pyi]
17+
class A: pass
18+
a = A()
19+
20+
[case testModuleLookupFromImport]
21+
from m import a
22+
reveal_type(a) # N: Revealed type is "m.A"
23+
24+
[file m.py]
25+
class A: pass
26+
a = A()
27+
28+
[case testModuleLookupStubFromImport]
29+
from m import a
30+
reveal_type(a) # N: Revealed type is "m.A"
31+
32+
[file m.pyi]
33+
class A: pass
34+
a = A()
35+
36+
37+
[case testModuleLookupWeird]
38+
from m import a
39+
reveal_type(a) # N: Revealed type is "builtins.object"
40+
reveal_type(a.b) # N: Revealed type is "m.a.B"
41+
42+
[file m.py]
43+
class A: pass
44+
a = A()
45+
46+
[file m/__init__.py]
47+
[file m/a.py]
48+
class B: pass
49+
b = B()
50+
51+
52+
[case testModuleLookupWeird2]
53+
from m.a import b
54+
reveal_type(b) # N: Revealed type is "m.a.B"
55+
56+
[file m.py]
57+
class A: pass
58+
a = A()
59+
60+
[file m/__init__.py]
61+
[file m/a.py]
62+
class B: pass
63+
b = B()
64+
65+
66+
[case testModuleLookupWeird3]
67+
from m.a import b
68+
reveal_type(b) # N: Revealed type is "m.a.B"
69+
70+
[file m.py]
71+
class A: pass
72+
a = A()
73+
[file m/__init__.py]
74+
class B: pass
75+
a = B()
76+
[file m/a.py]
77+
class B: pass
78+
b = B()
79+
80+
81+
[case testModuleLookupWeird4]
82+
import m.a
83+
m.a.b # E: "str" has no attribute "b"
84+
85+
[file m.py]
86+
class A: pass
87+
a = A()
88+
[file m/__init__.py]
89+
class B: pass
90+
a = 'foo'
91+
b = B()
92+
[file m/a.py]
93+
class C: pass
94+
b = C()
95+
96+
97+
[case testModuleLookupWeird5]
98+
import m.a as ma
99+
reveal_type(ma.b) # N: Revealed type is "m.a.C"
100+
101+
[file m.py]
102+
class A: pass
103+
a = A()
104+
[file m/__init__.py]
105+
class B: pass
106+
a = 'foo'
107+
b = B()
108+
[file m/a.py]
109+
class C: pass
110+
b = C()
111+
112+
113+
[case testModuleLookupWeird6]
114+
from m.a import b
115+
reveal_type(b) # N: Revealed type is "m.a.C"
116+
117+
[file m.py]
118+
class A: pass
119+
a = A()
120+
[file m/__init__.py]
121+
class B: pass
122+
a = 'foo'
123+
b = B()
124+
[file m/a.py]
125+
class C: pass
126+
b = C()

0 commit comments

Comments
 (0)