Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run build a second time when using --install-types --non-interactive #10669

Merged
merged 8 commits into from Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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