Skip to content

Commit

Permalink
Run build a second time when using --install-types --non-interactive (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
JukkaL committed Jun 22, 2021
1 parent f5a3405 commit e8cf526
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 55 deletions.
150 changes: 96 additions & 54 deletions mypy/main.py
Expand Up @@ -67,59 +67,34 @@ def main(script_path: Optional[str],
sources, options = process_options(args, stdout=stdout, stderr=stderr,
fscache=fscache)

messages = []
formatter = util.FancyFormatter(stdout, stderr, options.show_error_codes)

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

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

if options.install_types and not options.incremental:
fail("Error: --install-types not supported with incremental mode disabled",
fail("error: --install-types not supported with incremental mode disabled",
stderr, options)

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

def flush_errors(new_messages: List[str], serious: bool) -> None:
if options.non_interactive:
return
if options.pretty:
new_messages = formatter.fit_in_terminal(new_messages)
messages.extend(new_messages)
f = stderr if serious else stdout
for msg in new_messages:
if options.color_output:
msg = formatter.colorize(msg)
f.write(msg + '\n')
f.flush()
res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)

serious = False
blockers = False
res = None
try:
# Keep a dummy reference (res) for memory profiling below, as otherwise
# the result could be freed.
res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
except CompileError as e:
blockers = True
if not e.use_stdout:
serious = True
if (options.warn_unused_configs
and options.unused_configs
and not options.incremental
and not options.non_interactive):
print("Warning: unused section(s) in %s: %s" %
(options.config_file,
get_config_module_names(options.config_file,
[glob for glob in options.per_module_options.keys()
if glob in options.unused_configs])),
file=stderr)
maybe_write_junit_xml(time.time() - t0, serious, messages, options)
if options.non_interactive:
missing_pkgs = read_types_packages_to_install(options.cache_dir, after_run=True)
if missing_pkgs:
# Install missing type packages and rerun build.
install_types(options.cache_dir, formatter, after_run=True, non_interactive=True)
fscache.flush()
print()
res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
show_messages(messages, stderr, formatter, options)

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

if options.install_types:
install_types(options.cache_dir, formatter, after_run=True,
non_interactive=options.non_interactive)
return
if options.install_types and not options.non_interactive:
result = install_types(options.cache_dir, formatter, after_run=True,
non_interactive=False)
if result:
print()
print("note: Run mypy again for up-to-date results with installed types")
code = 2

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


def run_build(sources: List[BuildSource],
options: Options,
fscache: FileSystemCache,
t0: float,
stdout: TextIO,
stderr: TextIO) -> Tuple[Optional[build.BuildResult], List[str], bool]:
formatter = util.FancyFormatter(stdout, stderr, options.show_error_codes)

messages = []

def flush_errors(new_messages: List[str], serious: bool) -> None:
if options.pretty:
new_messages = formatter.fit_in_terminal(new_messages)
messages.extend(new_messages)
if options.non_interactive:
# Collect messages and possibly show them later.
return
f = stderr if serious else stdout
show_messages(new_messages, f, formatter, options)

serious = False
blockers = False
res = None
try:
# Keep a dummy reference (res) for memory profiling afterwards, as otherwise
# the result could be freed.
res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
except CompileError as e:
blockers = True
if not e.use_stdout:
serious = True
if (options.warn_unused_configs
and options.unused_configs
and not options.incremental
and not options.non_interactive):
print("Warning: unused section(s) in %s: %s" %
(options.config_file,
get_config_module_names(options.config_file,
[glob for glob in options.per_module_options.keys()
if glob in options.unused_configs])),
file=stderr)
maybe_write_junit_xml(time.time() - t0, serious, messages, options)
return res, messages, blockers


def show_messages(messages: List[str],
f: TextIO,
formatter: util.FancyFormatter,
options: Options) -> None:
for msg in messages:
if options.color_output:
msg = formatter.colorize(msg)
f.write(msg + '\n')
f.flush()


# Make the help output a little less jarring.
class AugmentedHelpFormatter(argparse.RawDescriptionHelpFormatter):
def __init__(self, prog: str) -> None:
Expand Down Expand Up @@ -1087,29 +1121,36 @@ def fail(msg: str, stderr: TextIO, options: Options) -> None:
sys.exit(2)


def install_types(cache_dir: str,
formatter: util.FancyFormatter,
*,
after_run: bool = False,
non_interactive: bool = False) -> None:
"""Install stub packages using pip if some missing stubs were detected."""
def read_types_packages_to_install(cache_dir: str, after_run: bool) -> List[str]:
if not os.path.isdir(cache_dir):
if not after_run:
sys.stderr.write(
"Error: Can't determine which types to install with no files to check " +
"error: Can't determine which types to install with no files to check " +
"(and no cache from previous mypy run)\n"
)
else:
sys.stderr.write(
"Error: --install-types failed (no mypy cache directory)\n"
"error: --install-types failed (no mypy cache directory)\n"
)
sys.exit(2)
fnam = build.missing_stubs_file(cache_dir)
if not os.path.isfile(fnam):
# If there are no missing stubs, generate no output.
return
# No missing stubs.
return []
with open(fnam) as f:
packages = [line.strip() for line in f.readlines()]
return [line.strip() for line in f.readlines()]


def install_types(cache_dir: str,
formatter: util.FancyFormatter,
*,
after_run: bool = False,
non_interactive: bool = False) -> bool:
"""Install stub packages using pip if some missing stubs were detected."""
packages = read_types_packages_to_install(cache_dir, after_run)
if not packages:
# If there are no missing stubs, generate no output.
return False
if after_run and not non_interactive:
print()
print('Installing missing stub packages:')
Expand All @@ -1123,3 +1164,4 @@ def install_types(cache_dir: str,
sys.exit(2)
print()
subprocess.run(cmd)
return True
16 changes: 15 additions & 1 deletion test-data/unit/cmdline.test
Expand Up @@ -1280,11 +1280,25 @@ pkg.py:1: error: Incompatible types in assignment (expression has type "int", va
[case testCmdlineNonInteractiveWithoutInstallTypes]
# cmd: mypy --non-interactive -m pkg
[out]
Error: --non-interactive is only supported with --install-types
error: --non-interactive is only supported with --install-types
== Return code: 2

[case testCmdlineNonInteractiveInstallTypesNothingToDo]
# cmd: mypy --install-types --non-interactive -m pkg
[file pkg.py]
1()
[out]
pkg.py:1: error: "int" not callable

[case testCmdlineNonInteractiveInstallTypesNothingToDoNoError]
# cmd: mypy --install-types --non-interactive -m pkg
[file pkg.py]
1 + 2
[out]

[case testCmdlineInteractiveInstallTypesNothingToDo]
# cmd: mypy --install-types -m pkg
[file pkg.py]
1()
[out]
pkg.py:1: error: "int" not callable

0 comments on commit e8cf526

Please sign in to comment.