Skip to content

Commit 40dfd7b

Browse files
authored
Run build a second time when using --install-types --non-interactive (#10669)
If the first build finds missing stub packages, run the build a second time after installing types. Only show errors from the final build. Example output: ``` $ mypy --install-types --non-interactive t.py Installing missing stub packages: /Users/jukka/venv/mypy/bin/python3 -m pip install types-redis Collecting types-redis Using cached types_redis-3.5.2-py2.py3-none-any.whl (11 kB) Installing collected packages: types-redis Successfully installed types-redis-3.5.2 t.py:2: error: Unsupported operand types for + ("int" and "str") Found 1 error in 1 file (checked 1 source file) ``` Work on #10600.
1 parent bf231be commit 40dfd7b

File tree

2 files changed

+111
-55
lines changed

2 files changed

+111
-55
lines changed

mypy/main.py

Lines changed: 96 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -67,59 +67,34 @@ def main(script_path: Optional[str],
6767
sources, options = process_options(args, stdout=stdout, stderr=stderr,
6868
fscache=fscache)
6969

70-
messages = []
7170
formatter = util.FancyFormatter(stdout, stderr, options.show_error_codes)
7271

7372
if options.install_types and (stdout is not sys.stdout or stderr is not sys.stderr):
7473
# Since --install-types performs user input, we want regular stdout and stderr.
75-
fail("Error: --install-types not supported in this mode of running mypy", stderr, options)
74+
fail("error: --install-types not supported in this mode of running mypy", stderr, options)
7675

7776
if options.non_interactive and not options.install_types:
78-
fail("Error: --non-interactive is only supported with --install-types", stderr, options)
77+
fail("error: --non-interactive is only supported with --install-types", stderr, options)
7978

8079
if options.install_types and not options.incremental:
81-
fail("Error: --install-types not supported with incremental mode disabled",
80+
fail("error: --install-types not supported with incremental mode disabled",
8281
stderr, options)
8382

8483
if options.install_types and not sources:
8584
install_types(options.cache_dir, formatter, non_interactive=options.non_interactive)
8685
return
8786

88-
def flush_errors(new_messages: List[str], serious: bool) -> None:
89-
if options.non_interactive:
90-
return
91-
if options.pretty:
92-
new_messages = formatter.fit_in_terminal(new_messages)
93-
messages.extend(new_messages)
94-
f = stderr if serious else stdout
95-
for msg in new_messages:
96-
if options.color_output:
97-
msg = formatter.colorize(msg)
98-
f.write(msg + '\n')
99-
f.flush()
87+
res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
10088

101-
serious = False
102-
blockers = False
103-
res = None
104-
try:
105-
# Keep a dummy reference (res) for memory profiling below, as otherwise
106-
# the result could be freed.
107-
res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
108-
except CompileError as e:
109-
blockers = True
110-
if not e.use_stdout:
111-
serious = True
112-
if (options.warn_unused_configs
113-
and options.unused_configs
114-
and not options.incremental
115-
and not options.non_interactive):
116-
print("Warning: unused section(s) in %s: %s" %
117-
(options.config_file,
118-
get_config_module_names(options.config_file,
119-
[glob for glob in options.per_module_options.keys()
120-
if glob in options.unused_configs])),
121-
file=stderr)
122-
maybe_write_junit_xml(time.time() - t0, serious, messages, options)
89+
if options.non_interactive:
90+
missing_pkgs = read_types_packages_to_install(options.cache_dir, after_run=True)
91+
if missing_pkgs:
92+
# Install missing type packages and rerun build.
93+
install_types(options.cache_dir, formatter, after_run=True, non_interactive=True)
94+
fscache.flush()
95+
print()
96+
res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
97+
show_messages(messages, stderr, formatter, options)
12398

12499
if MEM_PROFILE:
125100
from mypy.memprofile import print_memory_profile
@@ -128,7 +103,7 @@ def flush_errors(new_messages: List[str], serious: bool) -> None:
128103
code = 0
129104
if messages:
130105
code = 2 if blockers else 1
131-
if options.error_summary and not options.non_interactive:
106+
if options.error_summary:
132107
if messages:
133108
n_errors, n_files = util.count_stats(messages)
134109
if n_errors:
@@ -141,10 +116,13 @@ def flush_errors(new_messages: List[str], serious: bool) -> None:
141116
stdout.write(formatter.format_success(len(sources), options.color_output) + '\n')
142117
stdout.flush()
143118

144-
if options.install_types:
145-
install_types(options.cache_dir, formatter, after_run=True,
146-
non_interactive=options.non_interactive)
147-
return
119+
if options.install_types and not options.non_interactive:
120+
result = install_types(options.cache_dir, formatter, after_run=True,
121+
non_interactive=False)
122+
if result:
123+
print()
124+
print("note: Run mypy again for up-to-date results with installed types")
125+
code = 2
148126

149127
if options.fast_exit:
150128
# Exit without freeing objects -- it's faster.
@@ -158,6 +136,62 @@ def flush_errors(new_messages: List[str], serious: bool) -> None:
158136
list([res])
159137

160138

139+
def run_build(sources: List[BuildSource],
140+
options: Options,
141+
fscache: FileSystemCache,
142+
t0: float,
143+
stdout: TextIO,
144+
stderr: TextIO) -> Tuple[Optional[build.BuildResult], List[str], bool]:
145+
formatter = util.FancyFormatter(stdout, stderr, options.show_error_codes)
146+
147+
messages = []
148+
149+
def flush_errors(new_messages: List[str], serious: bool) -> None:
150+
if options.pretty:
151+
new_messages = formatter.fit_in_terminal(new_messages)
152+
messages.extend(new_messages)
153+
if options.non_interactive:
154+
# Collect messages and possibly show them later.
155+
return
156+
f = stderr if serious else stdout
157+
show_messages(new_messages, f, formatter, options)
158+
159+
serious = False
160+
blockers = False
161+
res = None
162+
try:
163+
# Keep a dummy reference (res) for memory profiling afterwards, as otherwise
164+
# the result could be freed.
165+
res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
166+
except CompileError as e:
167+
blockers = True
168+
if not e.use_stdout:
169+
serious = True
170+
if (options.warn_unused_configs
171+
and options.unused_configs
172+
and not options.incremental
173+
and not options.non_interactive):
174+
print("Warning: unused section(s) in %s: %s" %
175+
(options.config_file,
176+
get_config_module_names(options.config_file,
177+
[glob for glob in options.per_module_options.keys()
178+
if glob in options.unused_configs])),
179+
file=stderr)
180+
maybe_write_junit_xml(time.time() - t0, serious, messages, options)
181+
return res, messages, blockers
182+
183+
184+
def show_messages(messages: List[str],
185+
f: TextIO,
186+
formatter: util.FancyFormatter,
187+
options: Options) -> None:
188+
for msg in messages:
189+
if options.color_output:
190+
msg = formatter.colorize(msg)
191+
f.write(msg + '\n')
192+
f.flush()
193+
194+
161195
# Make the help output a little less jarring.
162196
class AugmentedHelpFormatter(argparse.RawDescriptionHelpFormatter):
163197
def __init__(self, prog: str) -> None:
@@ -1087,29 +1121,36 @@ def fail(msg: str, stderr: TextIO, options: Options) -> None:
10871121
sys.exit(2)
10881122

10891123

1090-
def install_types(cache_dir: str,
1091-
formatter: util.FancyFormatter,
1092-
*,
1093-
after_run: bool = False,
1094-
non_interactive: bool = False) -> None:
1095-
"""Install stub packages using pip if some missing stubs were detected."""
1124+
def read_types_packages_to_install(cache_dir: str, after_run: bool) -> List[str]:
10961125
if not os.path.isdir(cache_dir):
10971126
if not after_run:
10981127
sys.stderr.write(
1099-
"Error: Can't determine which types to install with no files to check " +
1128+
"error: Can't determine which types to install with no files to check " +
11001129
"(and no cache from previous mypy run)\n"
11011130
)
11021131
else:
11031132
sys.stderr.write(
1104-
"Error: --install-types failed (no mypy cache directory)\n"
1133+
"error: --install-types failed (no mypy cache directory)\n"
11051134
)
11061135
sys.exit(2)
11071136
fnam = build.missing_stubs_file(cache_dir)
11081137
if not os.path.isfile(fnam):
1109-
# If there are no missing stubs, generate no output.
1110-
return
1138+
# No missing stubs.
1139+
return []
11111140
with open(fnam) as f:
1112-
packages = [line.strip() for line in f.readlines()]
1141+
return [line.strip() for line in f.readlines()]
1142+
1143+
1144+
def install_types(cache_dir: str,
1145+
formatter: util.FancyFormatter,
1146+
*,
1147+
after_run: bool = False,
1148+
non_interactive: bool = False) -> bool:
1149+
"""Install stub packages using pip if some missing stubs were detected."""
1150+
packages = read_types_packages_to_install(cache_dir, after_run)
1151+
if not packages:
1152+
# If there are no missing stubs, generate no output.
1153+
return False
11131154
if after_run and not non_interactive:
11141155
print()
11151156
print('Installing missing stub packages:')
@@ -1123,3 +1164,4 @@ def install_types(cache_dir: str,
11231164
sys.exit(2)
11241165
print()
11251166
subprocess.run(cmd)
1167+
return True

test-data/unit/cmdline.test

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1280,11 +1280,25 @@ pkg.py:1: error: Incompatible types in assignment (expression has type "int", va
12801280
[case testCmdlineNonInteractiveWithoutInstallTypes]
12811281
# cmd: mypy --non-interactive -m pkg
12821282
[out]
1283-
Error: --non-interactive is only supported with --install-types
1283+
error: --non-interactive is only supported with --install-types
12841284
== Return code: 2
12851285

12861286
[case testCmdlineNonInteractiveInstallTypesNothingToDo]
12871287
# cmd: mypy --install-types --non-interactive -m pkg
12881288
[file pkg.py]
12891289
1()
12901290
[out]
1291+
pkg.py:1: error: "int" not callable
1292+
1293+
[case testCmdlineNonInteractiveInstallTypesNothingToDoNoError]
1294+
# cmd: mypy --install-types --non-interactive -m pkg
1295+
[file pkg.py]
1296+
1 + 2
1297+
[out]
1298+
1299+
[case testCmdlineInteractiveInstallTypesNothingToDo]
1300+
# cmd: mypy --install-types -m pkg
1301+
[file pkg.py]
1302+
1()
1303+
[out]
1304+
pkg.py:1: error: "int" not callable

0 commit comments

Comments
 (0)