1558 lines
56 KiB
Python
1558 lines
56 KiB
Python
|
"""Mypy type checker command line tool."""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
import argparse
|
||
|
import os
|
||
|
import subprocess
|
||
|
import sys
|
||
|
import time
|
||
|
from gettext import gettext
|
||
|
from typing import IO, Any, Final, NoReturn, Sequence, TextIO
|
||
|
|
||
|
from mypy import build, defaults, state, util
|
||
|
from mypy.config_parser import (
|
||
|
get_config_module_names,
|
||
|
parse_config_file,
|
||
|
parse_version,
|
||
|
validate_package_allow_list,
|
||
|
)
|
||
|
from mypy.errorcodes import error_codes
|
||
|
from mypy.errors import CompileError
|
||
|
from mypy.find_sources import InvalidSourceList, create_source_list
|
||
|
from mypy.fscache import FileSystemCache
|
||
|
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths, get_search_dirs, mypy_path
|
||
|
from mypy.options import INCOMPLETE_FEATURES, BuildType, Options
|
||
|
from mypy.split_namespace import SplitNamespace
|
||
|
from mypy.version import __version__
|
||
|
|
||
|
orig_stat: Final = os.stat
|
||
|
MEM_PROFILE: Final = False # If True, dump memory profile
|
||
|
|
||
|
|
||
|
def stat_proxy(path: str) -> os.stat_result:
|
||
|
try:
|
||
|
st = orig_stat(path)
|
||
|
except os.error as err:
|
||
|
print(f"stat({path!r}) -> {err}")
|
||
|
raise
|
||
|
else:
|
||
|
print(
|
||
|
"stat(%r) -> (st_mode=%o, st_mtime=%d, st_size=%d)"
|
||
|
% (path, st.st_mode, st.st_mtime, st.st_size)
|
||
|
)
|
||
|
return st
|
||
|
|
||
|
|
||
|
def main(
|
||
|
*,
|
||
|
args: list[str] | None = None,
|
||
|
stdout: TextIO = sys.stdout,
|
||
|
stderr: TextIO = sys.stderr,
|
||
|
clean_exit: bool = False,
|
||
|
) -> None:
|
||
|
"""Main entry point to the type checker.
|
||
|
|
||
|
Args:
|
||
|
args: Custom command-line arguments. If not given, sys.argv[1:] will
|
||
|
be used.
|
||
|
clean_exit: Don't hard kill the process on exit. This allows catching
|
||
|
SystemExit.
|
||
|
"""
|
||
|
util.check_python_version("mypy")
|
||
|
t0 = time.time()
|
||
|
# To log stat() calls: os.stat = stat_proxy
|
||
|
sys.setrecursionlimit(2**14)
|
||
|
if args is None:
|
||
|
args = sys.argv[1:]
|
||
|
|
||
|
fscache = FileSystemCache()
|
||
|
sources, options = process_options(args, stdout=stdout, stderr=stderr, fscache=fscache)
|
||
|
if clean_exit:
|
||
|
options.fast_exit = False
|
||
|
|
||
|
formatter = util.FancyFormatter(stdout, stderr, options.hide_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)
|
||
|
|
||
|
if options.non_interactive and not options.install_types:
|
||
|
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", stderr, options
|
||
|
)
|
||
|
|
||
|
if options.install_types and options.python_executable is None:
|
||
|
fail(
|
||
|
"error: --install-types not supported without python executable or site packages",
|
||
|
stderr,
|
||
|
options,
|
||
|
)
|
||
|
|
||
|
if options.install_types and not sources:
|
||
|
install_types(formatter, options, non_interactive=options.non_interactive)
|
||
|
return
|
||
|
|
||
|
res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
|
||
|
|
||
|
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(formatter, options, 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
|
||
|
|
||
|
print_memory_profile()
|
||
|
|
||
|
code = 0
|
||
|
n_errors, n_notes, n_files = util.count_stats(messages)
|
||
|
if messages and n_notes < len(messages):
|
||
|
code = 2 if blockers else 1
|
||
|
if options.error_summary:
|
||
|
if n_errors:
|
||
|
summary = formatter.format_error(
|
||
|
n_errors, n_files, len(sources), blockers=blockers, use_color=options.color_output
|
||
|
)
|
||
|
stdout.write(summary + "\n")
|
||
|
# Only notes should also output success
|
||
|
elif not messages or n_notes == len(messages):
|
||
|
stdout.write(formatter.format_success(len(sources), options.color_output) + "\n")
|
||
|
stdout.flush()
|
||
|
|
||
|
if options.install_types and not options.non_interactive:
|
||
|
result = install_types(formatter, options, 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.
|
||
|
#
|
||
|
# NOTE: We don't flush all open files on exit (or run other destructors)!
|
||
|
util.hard_exit(code)
|
||
|
elif code:
|
||
|
sys.exit(code)
|
||
|
|
||
|
# HACK: keep res alive so that mypyc won't free it before the hard_exit
|
||
|
list([res])
|
||
|
|
||
|
|
||
|
def run_build(
|
||
|
sources: list[BuildSource],
|
||
|
options: Options,
|
||
|
fscache: FileSystemCache,
|
||
|
t0: float,
|
||
|
stdout: TextIO,
|
||
|
stderr: TextIO,
|
||
|
) -> tuple[build.BuildResult | None, list[str], bool]:
|
||
|
formatter = util.FancyFormatter(stdout, stderr, options.hide_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:
|
||
|
super().__init__(prog=prog, max_help_position=28)
|
||
|
|
||
|
def _fill_text(self, text: str, width: int, indent: str) -> str:
|
||
|
if "\n" in text:
|
||
|
# Assume we want to manually format the text
|
||
|
return super()._fill_text(text, width, indent)
|
||
|
else:
|
||
|
# Assume we want argparse to manage wrapping, indenting, and
|
||
|
# formatting the text for us.
|
||
|
return argparse.HelpFormatter._fill_text(self, text, width, indent)
|
||
|
|
||
|
|
||
|
# Define pairs of flag prefixes with inverse meaning.
|
||
|
flag_prefix_pairs: Final = [("allow", "disallow"), ("show", "hide")]
|
||
|
flag_prefix_map: Final[dict[str, str]] = {}
|
||
|
for a, b in flag_prefix_pairs:
|
||
|
flag_prefix_map[a] = b
|
||
|
flag_prefix_map[b] = a
|
||
|
|
||
|
|
||
|
def invert_flag_name(flag: str) -> str:
|
||
|
split = flag[2:].split("-", 1)
|
||
|
if len(split) == 2:
|
||
|
prefix, rest = split
|
||
|
if prefix in flag_prefix_map:
|
||
|
return f"--{flag_prefix_map[prefix]}-{rest}"
|
||
|
elif prefix == "no":
|
||
|
return f"--{rest}"
|
||
|
|
||
|
return f"--no-{flag[2:]}"
|
||
|
|
||
|
|
||
|
class PythonExecutableInferenceError(Exception):
|
||
|
"""Represents a failure to infer the version or executable while searching."""
|
||
|
|
||
|
|
||
|
def python_executable_prefix(v: str) -> list[str]:
|
||
|
if sys.platform == "win32":
|
||
|
# on Windows, all Python executables are named `python`. To handle this, there
|
||
|
# is the `py` launcher, which can be passed a version e.g. `py -3.8`, and it will
|
||
|
# execute an installed Python 3.8 interpreter. See also:
|
||
|
# https://docs.python.org/3/using/windows.html#python-launcher-for-windows
|
||
|
return ["py", f"-{v}"]
|
||
|
else:
|
||
|
return [f"python{v}"]
|
||
|
|
||
|
|
||
|
def _python_executable_from_version(python_version: tuple[int, int]) -> str:
|
||
|
if sys.version_info[:2] == python_version:
|
||
|
return sys.executable
|
||
|
str_ver = ".".join(map(str, python_version))
|
||
|
try:
|
||
|
sys_exe = (
|
||
|
subprocess.check_output(
|
||
|
python_executable_prefix(str_ver) + ["-c", "import sys; print(sys.executable)"],
|
||
|
stderr=subprocess.STDOUT,
|
||
|
)
|
||
|
.decode()
|
||
|
.strip()
|
||
|
)
|
||
|
return sys_exe
|
||
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||
|
raise PythonExecutableInferenceError(
|
||
|
"failed to find a Python executable matching version {},"
|
||
|
" perhaps try --python-executable, or --no-site-packages?".format(python_version)
|
||
|
) from e
|
||
|
|
||
|
|
||
|
def infer_python_executable(options: Options, special_opts: argparse.Namespace) -> None:
|
||
|
"""Infer the Python executable from the given version.
|
||
|
|
||
|
This function mutates options based on special_opts to infer the correct Python executable
|
||
|
to use.
|
||
|
"""
|
||
|
# TODO: (ethanhs) Look at folding these checks and the site packages subprocess calls into
|
||
|
# one subprocess call for speed.
|
||
|
|
||
|
# Use the command line specified executable, or fall back to one set in the
|
||
|
# config file. If an executable is not specified, infer it from the version
|
||
|
# (unless no_executable is set)
|
||
|
python_executable = special_opts.python_executable or options.python_executable
|
||
|
|
||
|
if python_executable is None:
|
||
|
if not special_opts.no_executable and not options.no_site_packages:
|
||
|
python_executable = _python_executable_from_version(options.python_version)
|
||
|
options.python_executable = python_executable
|
||
|
|
||
|
|
||
|
HEADER: Final = """%(prog)s [-h] [-v] [-V] [more options; see below]
|
||
|
[-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]"""
|
||
|
|
||
|
|
||
|
DESCRIPTION: Final = """
|
||
|
Mypy is a program that will type check your Python code.
|
||
|
|
||
|
Pass in any files or folders you want to type check. Mypy will
|
||
|
recursively traverse any provided folders to find .py files:
|
||
|
|
||
|
$ mypy my_program.py my_src_folder
|
||
|
|
||
|
For more information on getting started, see:
|
||
|
|
||
|
- https://mypy.readthedocs.io/en/stable/getting_started.html
|
||
|
|
||
|
For more details on both running mypy and using the flags below, see:
|
||
|
|
||
|
- https://mypy.readthedocs.io/en/stable/running_mypy.html
|
||
|
- https://mypy.readthedocs.io/en/stable/command_line.html
|
||
|
|
||
|
You can also use a config file to configure mypy instead of using
|
||
|
command line flags. For more details, see:
|
||
|
|
||
|
- https://mypy.readthedocs.io/en/stable/config_file.html
|
||
|
"""
|
||
|
|
||
|
FOOTER: Final = """Environment variables:
|
||
|
Define MYPYPATH for additional module search path entries.
|
||
|
Define MYPY_CACHE_DIR to override configuration cache_dir path."""
|
||
|
|
||
|
|
||
|
class CapturableArgumentParser(argparse.ArgumentParser):
|
||
|
|
||
|
"""Override ArgumentParser methods that use sys.stdout/sys.stderr directly.
|
||
|
|
||
|
This is needed because hijacking sys.std* is not thread-safe,
|
||
|
yet output must be captured to properly support mypy.api.run.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, *args: Any, **kwargs: Any):
|
||
|
self.stdout = kwargs.pop("stdout", sys.stdout)
|
||
|
self.stderr = kwargs.pop("stderr", sys.stderr)
|
||
|
super().__init__(*args, **kwargs)
|
||
|
|
||
|
# =====================
|
||
|
# Help-printing methods
|
||
|
# =====================
|
||
|
def print_usage(self, file: IO[str] | None = None) -> None:
|
||
|
if file is None:
|
||
|
file = self.stdout
|
||
|
self._print_message(self.format_usage(), file)
|
||
|
|
||
|
def print_help(self, file: IO[str] | None = None) -> None:
|
||
|
if file is None:
|
||
|
file = self.stdout
|
||
|
self._print_message(self.format_help(), file)
|
||
|
|
||
|
def _print_message(self, message: str, file: IO[str] | None = None) -> None:
|
||
|
if message:
|
||
|
if file is None:
|
||
|
file = self.stderr
|
||
|
file.write(message)
|
||
|
|
||
|
# ===============
|
||
|
# Exiting methods
|
||
|
# ===============
|
||
|
def exit(self, status: int = 0, message: str | None = None) -> NoReturn:
|
||
|
if message:
|
||
|
self._print_message(message, self.stderr)
|
||
|
sys.exit(status)
|
||
|
|
||
|
def error(self, message: str) -> NoReturn:
|
||
|
"""error(message: string)
|
||
|
|
||
|
Prints a usage message incorporating the message to stderr and
|
||
|
exits.
|
||
|
|
||
|
If you override this in a subclass, it should not return -- it
|
||
|
should either exit or raise an exception.
|
||
|
"""
|
||
|
self.print_usage(self.stderr)
|
||
|
args = {"prog": self.prog, "message": message}
|
||
|
self.exit(2, gettext("%(prog)s: error: %(message)s\n") % args)
|
||
|
|
||
|
|
||
|
class CapturableVersionAction(argparse.Action):
|
||
|
|
||
|
"""Supplement CapturableArgumentParser to handle --version.
|
||
|
|
||
|
This is nearly identical to argparse._VersionAction except,
|
||
|
like CapturableArgumentParser, it allows output to be captured.
|
||
|
|
||
|
Another notable difference is that version is mandatory.
|
||
|
This allows removing a line in __call__ that falls back to parser.version
|
||
|
(which does not appear to exist).
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
option_strings: Sequence[str],
|
||
|
version: str,
|
||
|
dest: str = argparse.SUPPRESS,
|
||
|
default: str = argparse.SUPPRESS,
|
||
|
help: str = "show program's version number and exit",
|
||
|
stdout: IO[str] | None = None,
|
||
|
):
|
||
|
super().__init__(
|
||
|
option_strings=option_strings, dest=dest, default=default, nargs=0, help=help
|
||
|
)
|
||
|
self.version = version
|
||
|
self.stdout = stdout or sys.stdout
|
||
|
|
||
|
def __call__(
|
||
|
self,
|
||
|
parser: argparse.ArgumentParser,
|
||
|
namespace: argparse.Namespace,
|
||
|
values: str | Sequence[Any] | None,
|
||
|
option_string: str | None = None,
|
||
|
) -> NoReturn:
|
||
|
formatter = parser._get_formatter()
|
||
|
formatter.add_text(self.version)
|
||
|
parser._print_message(formatter.format_help(), self.stdout)
|
||
|
parser.exit()
|
||
|
|
||
|
|
||
|
def process_options(
|
||
|
args: list[str],
|
||
|
stdout: TextIO | None = None,
|
||
|
stderr: TextIO | None = None,
|
||
|
require_targets: bool = True,
|
||
|
server_options: bool = False,
|
||
|
fscache: FileSystemCache | None = None,
|
||
|
program: str = "mypy",
|
||
|
header: str = HEADER,
|
||
|
) -> tuple[list[BuildSource], Options]:
|
||
|
"""Parse command line arguments.
|
||
|
|
||
|
If a FileSystemCache is passed in, and package_root options are given,
|
||
|
call fscache.set_package_root() to set the cache's package root.
|
||
|
"""
|
||
|
stdout = stdout or sys.stdout
|
||
|
stderr = stderr or sys.stderr
|
||
|
|
||
|
parser = CapturableArgumentParser(
|
||
|
prog=program,
|
||
|
usage=header,
|
||
|
description=DESCRIPTION,
|
||
|
epilog=FOOTER,
|
||
|
fromfile_prefix_chars="@",
|
||
|
formatter_class=AugmentedHelpFormatter,
|
||
|
add_help=False,
|
||
|
stdout=stdout,
|
||
|
stderr=stderr,
|
||
|
)
|
||
|
|
||
|
strict_flag_names: list[str] = []
|
||
|
strict_flag_assignments: list[tuple[str, bool]] = []
|
||
|
|
||
|
def add_invertible_flag(
|
||
|
flag: str,
|
||
|
*,
|
||
|
inverse: str | None = None,
|
||
|
default: bool,
|
||
|
dest: str | None = None,
|
||
|
help: str,
|
||
|
strict_flag: bool = False,
|
||
|
group: argparse._ActionsContainer | None = None,
|
||
|
) -> None:
|
||
|
if inverse is None:
|
||
|
inverse = invert_flag_name(flag)
|
||
|
if group is None:
|
||
|
group = parser
|
||
|
|
||
|
if help is not argparse.SUPPRESS:
|
||
|
help += f" (inverse: {inverse})"
|
||
|
|
||
|
arg = group.add_argument(
|
||
|
flag, action="store_false" if default else "store_true", dest=dest, help=help
|
||
|
)
|
||
|
dest = arg.dest
|
||
|
group.add_argument(
|
||
|
inverse,
|
||
|
action="store_true" if default else "store_false",
|
||
|
dest=dest,
|
||
|
help=argparse.SUPPRESS,
|
||
|
)
|
||
|
if strict_flag:
|
||
|
assert dest is not None
|
||
|
strict_flag_names.append(flag)
|
||
|
strict_flag_assignments.append((dest, not default))
|
||
|
|
||
|
# Unless otherwise specified, arguments will be parsed directly onto an
|
||
|
# Options object. Options that require further processing should have
|
||
|
# their `dest` prefixed with `special-opts:`, which will cause them to be
|
||
|
# parsed into the separate special_opts namespace object.
|
||
|
|
||
|
# Note: we have a style guide for formatting the mypy --help text. See
|
||
|
# https://github.com/python/mypy/wiki/Documentation-Conventions
|
||
|
|
||
|
general_group = parser.add_argument_group(title="Optional arguments")
|
||
|
general_group.add_argument(
|
||
|
"-h", "--help", action="help", help="Show this help message and exit"
|
||
|
)
|
||
|
general_group.add_argument(
|
||
|
"-v", "--verbose", action="count", dest="verbosity", help="More verbose messages"
|
||
|
)
|
||
|
|
||
|
compilation_status = "no" if __file__.endswith(".py") else "yes"
|
||
|
general_group.add_argument(
|
||
|
"-V",
|
||
|
"--version",
|
||
|
action=CapturableVersionAction,
|
||
|
version="%(prog)s " + __version__ + f" (compiled: {compilation_status})",
|
||
|
help="Show program's version number and exit",
|
||
|
stdout=stdout,
|
||
|
)
|
||
|
|
||
|
config_group = parser.add_argument_group(
|
||
|
title="Config file",
|
||
|
description="Use a config file instead of command line arguments. "
|
||
|
"This is useful if you are using many flags or want "
|
||
|
"to set different options per each module.",
|
||
|
)
|
||
|
config_group.add_argument(
|
||
|
"--config-file",
|
||
|
help="Configuration file, must have a [mypy] section "
|
||
|
"(defaults to {})".format(", ".join(defaults.CONFIG_FILES)),
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--warn-unused-configs",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Warn about unused '[mypy-<pattern>]' or '[[tool.mypy.overrides]]' "
|
||
|
"config sections",
|
||
|
group=config_group,
|
||
|
)
|
||
|
|
||
|
imports_group = parser.add_argument_group(
|
||
|
title="Import discovery", description="Configure how imports are discovered and followed."
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--no-namespace-packages",
|
||
|
dest="namespace_packages",
|
||
|
default=True,
|
||
|
help="Support namespace packages (PEP 420, __init__.py-less)",
|
||
|
group=imports_group,
|
||
|
)
|
||
|
imports_group.add_argument(
|
||
|
"--ignore-missing-imports",
|
||
|
action="store_true",
|
||
|
help="Silently ignore imports of missing modules",
|
||
|
)
|
||
|
imports_group.add_argument(
|
||
|
"--follow-imports",
|
||
|
choices=["normal", "silent", "skip", "error"],
|
||
|
default="normal",
|
||
|
help="How to treat imports (default normal)",
|
||
|
)
|
||
|
imports_group.add_argument(
|
||
|
"--python-executable",
|
||
|
action="store",
|
||
|
metavar="EXECUTABLE",
|
||
|
help="Python executable used for finding PEP 561 compliant installed"
|
||
|
" packages and stubs",
|
||
|
dest="special-opts:python_executable",
|
||
|
)
|
||
|
imports_group.add_argument(
|
||
|
"--no-site-packages",
|
||
|
action="store_true",
|
||
|
dest="special-opts:no_executable",
|
||
|
help="Do not search for installed PEP 561 compliant packages",
|
||
|
)
|
||
|
imports_group.add_argument(
|
||
|
"--no-silence-site-packages",
|
||
|
action="store_true",
|
||
|
help="Do not silence errors in PEP 561 compliant installed packages",
|
||
|
)
|
||
|
|
||
|
platform_group = parser.add_argument_group(
|
||
|
title="Platform configuration",
|
||
|
description="Type check code assuming it will be run under certain "
|
||
|
"runtime conditions. By default, mypy assumes your code "
|
||
|
"will be run using the same operating system and Python "
|
||
|
"version you are using to run mypy itself.",
|
||
|
)
|
||
|
platform_group.add_argument(
|
||
|
"--python-version",
|
||
|
type=parse_version,
|
||
|
metavar="x.y",
|
||
|
help="Type check code assuming it will be running on Python x.y",
|
||
|
dest="special-opts:python_version",
|
||
|
)
|
||
|
platform_group.add_argument(
|
||
|
"--platform",
|
||
|
action="store",
|
||
|
metavar="PLATFORM",
|
||
|
help="Type check special-cased code for the given OS platform "
|
||
|
"(defaults to sys.platform)",
|
||
|
)
|
||
|
platform_group.add_argument(
|
||
|
"--always-true",
|
||
|
metavar="NAME",
|
||
|
action="append",
|
||
|
default=[],
|
||
|
help="Additional variable to be considered True (may be repeated)",
|
||
|
)
|
||
|
platform_group.add_argument(
|
||
|
"--always-false",
|
||
|
metavar="NAME",
|
||
|
action="append",
|
||
|
default=[],
|
||
|
help="Additional variable to be considered False (may be repeated)",
|
||
|
)
|
||
|
|
||
|
disallow_any_group = parser.add_argument_group(
|
||
|
title="Disallow dynamic typing",
|
||
|
description="Disallow the use of the dynamic 'Any' type under certain conditions.",
|
||
|
)
|
||
|
disallow_any_group.add_argument(
|
||
|
"--disallow-any-unimported",
|
||
|
default=False,
|
||
|
action="store_true",
|
||
|
help="Disallow Any types resulting from unfollowed imports",
|
||
|
)
|
||
|
disallow_any_group.add_argument(
|
||
|
"--disallow-any-expr",
|
||
|
default=False,
|
||
|
action="store_true",
|
||
|
help="Disallow all expressions that have type Any",
|
||
|
)
|
||
|
disallow_any_group.add_argument(
|
||
|
"--disallow-any-decorated",
|
||
|
default=False,
|
||
|
action="store_true",
|
||
|
help="Disallow functions that have Any in their signature "
|
||
|
"after decorator transformation",
|
||
|
)
|
||
|
disallow_any_group.add_argument(
|
||
|
"--disallow-any-explicit",
|
||
|
default=False,
|
||
|
action="store_true",
|
||
|
help="Disallow explicit Any in type positions",
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--disallow-any-generics",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Disallow usage of generic types that do not specify explicit type parameters",
|
||
|
group=disallow_any_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--disallow-subclassing-any",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Disallow subclassing values of type 'Any' when defining classes",
|
||
|
group=disallow_any_group,
|
||
|
)
|
||
|
|
||
|
untyped_group = parser.add_argument_group(
|
||
|
title="Untyped definitions and calls",
|
||
|
description="Configure how untyped definitions and calls are handled. "
|
||
|
"Note: by default, mypy ignores any untyped function definitions "
|
||
|
"and assumes any calls to such functions have a return "
|
||
|
"type of 'Any'.",
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--disallow-untyped-calls",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Disallow calling functions without type annotations"
|
||
|
" from functions with type annotations",
|
||
|
group=untyped_group,
|
||
|
)
|
||
|
untyped_group.add_argument(
|
||
|
"--untyped-calls-exclude",
|
||
|
metavar="MODULE",
|
||
|
action="append",
|
||
|
default=[],
|
||
|
help="Disable --disallow-untyped-calls for functions/methods coming"
|
||
|
" from specific package, module, or class",
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--disallow-untyped-defs",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Disallow defining functions without type annotations"
|
||
|
" or with incomplete type annotations",
|
||
|
group=untyped_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--disallow-incomplete-defs",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Disallow defining functions with incomplete type annotations "
|
||
|
"(while still allowing entirely unannotated definitions)",
|
||
|
group=untyped_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--check-untyped-defs",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Type check the interior of functions without type annotations",
|
||
|
group=untyped_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--disallow-untyped-decorators",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Disallow decorating typed functions with untyped decorators",
|
||
|
group=untyped_group,
|
||
|
)
|
||
|
|
||
|
none_group = parser.add_argument_group(
|
||
|
title="None and Optional handling",
|
||
|
description="Adjust how values of type 'None' are handled. For more context on "
|
||
|
"how mypy handles values of type 'None', see: "
|
||
|
"https://mypy.readthedocs.io/en/stable/kinds_of_types.html#no-strict-optional",
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--implicit-optional",
|
||
|
default=False,
|
||
|
help="Assume arguments with default values of None are Optional",
|
||
|
group=none_group,
|
||
|
)
|
||
|
none_group.add_argument("--strict-optional", action="store_true", help=argparse.SUPPRESS)
|
||
|
none_group.add_argument(
|
||
|
"--no-strict-optional",
|
||
|
action="store_false",
|
||
|
dest="strict_optional",
|
||
|
help="Disable strict Optional checks (inverse: --strict-optional)",
|
||
|
)
|
||
|
|
||
|
add_invertible_flag(
|
||
|
"--force-uppercase-builtins", default=False, help=argparse.SUPPRESS, group=none_group
|
||
|
)
|
||
|
|
||
|
add_invertible_flag(
|
||
|
"--force-union-syntax", default=False, help=argparse.SUPPRESS, group=none_group
|
||
|
)
|
||
|
|
||
|
lint_group = parser.add_argument_group(
|
||
|
title="Configuring warnings",
|
||
|
description="Detect code that is sound but redundant or problematic.",
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--warn-redundant-casts",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Warn about casting an expression to its inferred type",
|
||
|
group=lint_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--warn-unused-ignores",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Warn about unneeded '# type: ignore' comments",
|
||
|
group=lint_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--no-warn-no-return",
|
||
|
dest="warn_no_return",
|
||
|
default=True,
|
||
|
help="Do not warn about functions that end without returning",
|
||
|
group=lint_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--warn-return-any",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Warn about returning values of type Any from non-Any typed functions",
|
||
|
group=lint_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--warn-unreachable",
|
||
|
default=False,
|
||
|
strict_flag=False,
|
||
|
help="Warn about statements or expressions inferred to be unreachable",
|
||
|
group=lint_group,
|
||
|
)
|
||
|
|
||
|
# Note: this group is intentionally added here even though we don't add
|
||
|
# --strict to this group near the end.
|
||
|
#
|
||
|
# That way, this group will appear after the various strictness groups
|
||
|
# but before the remaining flags.
|
||
|
# We add `--strict` near the end so we don't accidentally miss any strictness
|
||
|
# flags that are added after this group.
|
||
|
strictness_group = parser.add_argument_group(title="Miscellaneous strictness flags")
|
||
|
|
||
|
add_invertible_flag(
|
||
|
"--allow-untyped-globals",
|
||
|
default=False,
|
||
|
strict_flag=False,
|
||
|
help="Suppress toplevel errors caused by missing annotations",
|
||
|
group=strictness_group,
|
||
|
)
|
||
|
|
||
|
add_invertible_flag(
|
||
|
"--allow-redefinition",
|
||
|
default=False,
|
||
|
strict_flag=False,
|
||
|
help="Allow unconditional variable redefinition with a new type",
|
||
|
group=strictness_group,
|
||
|
)
|
||
|
|
||
|
add_invertible_flag(
|
||
|
"--no-implicit-reexport",
|
||
|
default=True,
|
||
|
strict_flag=True,
|
||
|
dest="implicit_reexport",
|
||
|
help="Treat imports as private unless aliased",
|
||
|
group=strictness_group,
|
||
|
)
|
||
|
|
||
|
add_invertible_flag(
|
||
|
"--strict-equality",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Prohibit equality, identity, and container checks for non-overlapping types",
|
||
|
group=strictness_group,
|
||
|
)
|
||
|
|
||
|
add_invertible_flag(
|
||
|
"--extra-checks",
|
||
|
default=False,
|
||
|
strict_flag=True,
|
||
|
help="Enable additional checks that are technically correct but may be impractical "
|
||
|
"in real code. For example, this prohibits partial overlap in TypedDict updates, "
|
||
|
"and makes arguments prepended via Concatenate positional-only",
|
||
|
group=strictness_group,
|
||
|
)
|
||
|
|
||
|
strict_help = "Strict mode; enables the following flags: {}".format(
|
||
|
", ".join(strict_flag_names)
|
||
|
)
|
||
|
strictness_group.add_argument(
|
||
|
"--strict", action="store_true", dest="special-opts:strict", help=strict_help
|
||
|
)
|
||
|
|
||
|
strictness_group.add_argument(
|
||
|
"--disable-error-code",
|
||
|
metavar="NAME",
|
||
|
action="append",
|
||
|
default=[],
|
||
|
help="Disable a specific error code",
|
||
|
)
|
||
|
strictness_group.add_argument(
|
||
|
"--enable-error-code",
|
||
|
metavar="NAME",
|
||
|
action="append",
|
||
|
default=[],
|
||
|
help="Enable a specific error code",
|
||
|
)
|
||
|
|
||
|
error_group = parser.add_argument_group(
|
||
|
title="Configuring error messages",
|
||
|
description="Adjust the amount of detail shown in error messages.",
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--show-error-context",
|
||
|
default=False,
|
||
|
dest="show_error_context",
|
||
|
help='Precede errors with "note:" messages explaining context',
|
||
|
group=error_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--show-column-numbers",
|
||
|
default=False,
|
||
|
help="Show column numbers in error messages",
|
||
|
group=error_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--show-error-end",
|
||
|
default=False,
|
||
|
help="Show end line/end column numbers in error messages."
|
||
|
" This implies --show-column-numbers",
|
||
|
group=error_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--hide-error-codes",
|
||
|
default=False,
|
||
|
help="Hide error codes in error messages",
|
||
|
group=error_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--show-error-code-links",
|
||
|
default=False,
|
||
|
help="Show links to error code documentation",
|
||
|
group=error_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--pretty",
|
||
|
default=False,
|
||
|
help="Use visually nicer output in error messages:"
|
||
|
" Use soft word wrap, show source code snippets,"
|
||
|
" and show error location markers",
|
||
|
group=error_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--no-color-output",
|
||
|
dest="color_output",
|
||
|
default=True,
|
||
|
help="Do not colorize error messages",
|
||
|
group=error_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--no-error-summary",
|
||
|
dest="error_summary",
|
||
|
default=True,
|
||
|
help="Do not show error stats summary",
|
||
|
group=error_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--show-absolute-path",
|
||
|
default=False,
|
||
|
help="Show absolute paths to files",
|
||
|
group=error_group,
|
||
|
)
|
||
|
error_group.add_argument(
|
||
|
"--soft-error-limit",
|
||
|
default=defaults.MANY_ERRORS_THRESHOLD,
|
||
|
type=int,
|
||
|
dest="many_errors_threshold",
|
||
|
help=argparse.SUPPRESS,
|
||
|
)
|
||
|
|
||
|
incremental_group = parser.add_argument_group(
|
||
|
title="Incremental mode",
|
||
|
description="Adjust how mypy incrementally type checks and caches modules. "
|
||
|
"Mypy caches type information about modules into a cache to "
|
||
|
"let you speed up future invocations of mypy. Also see "
|
||
|
"mypy's daemon mode: "
|
||
|
"mypy.readthedocs.io/en/stable/mypy_daemon.html#mypy-daemon",
|
||
|
)
|
||
|
incremental_group.add_argument(
|
||
|
"-i", "--incremental", action="store_true", help=argparse.SUPPRESS
|
||
|
)
|
||
|
incremental_group.add_argument(
|
||
|
"--no-incremental",
|
||
|
action="store_false",
|
||
|
dest="incremental",
|
||
|
help="Disable module cache (inverse: --incremental)",
|
||
|
)
|
||
|
incremental_group.add_argument(
|
||
|
"--cache-dir",
|
||
|
action="store",
|
||
|
metavar="DIR",
|
||
|
help="Store module cache info in the given folder in incremental mode "
|
||
|
"(defaults to '{}')".format(defaults.CACHE_DIR),
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--sqlite-cache",
|
||
|
default=False,
|
||
|
help="Use a sqlite database to store the cache",
|
||
|
group=incremental_group,
|
||
|
)
|
||
|
incremental_group.add_argument(
|
||
|
"--cache-fine-grained",
|
||
|
action="store_true",
|
||
|
help="Include fine-grained dependency information in the cache for the mypy daemon",
|
||
|
)
|
||
|
incremental_group.add_argument(
|
||
|
"--skip-version-check",
|
||
|
action="store_true",
|
||
|
help="Allow using cache written by older mypy version",
|
||
|
)
|
||
|
incremental_group.add_argument(
|
||
|
"--skip-cache-mtime-checks",
|
||
|
action="store_true",
|
||
|
help="Skip cache internal consistency checks based on mtime",
|
||
|
)
|
||
|
|
||
|
internals_group = parser.add_argument_group(
|
||
|
title="Advanced options", description="Debug and customize mypy internals."
|
||
|
)
|
||
|
internals_group.add_argument("--pdb", action="store_true", help="Invoke pdb on fatal error")
|
||
|
internals_group.add_argument(
|
||
|
"--show-traceback", "--tb", action="store_true", help="Show traceback on fatal error"
|
||
|
)
|
||
|
internals_group.add_argument(
|
||
|
"--raise-exceptions", action="store_true", help="Raise exception on fatal error"
|
||
|
)
|
||
|
internals_group.add_argument(
|
||
|
"--custom-typing-module",
|
||
|
metavar="MODULE",
|
||
|
dest="custom_typing_module",
|
||
|
help="Use a custom typing module",
|
||
|
)
|
||
|
internals_group.add_argument(
|
||
|
"--new-type-inference",
|
||
|
action="store_true",
|
||
|
help="Enable new experimental type inference algorithm",
|
||
|
)
|
||
|
internals_group.add_argument(
|
||
|
"--disable-recursive-aliases",
|
||
|
action="store_true",
|
||
|
help="Disable experimental support for recursive type aliases",
|
||
|
)
|
||
|
# Deprecated reverse variant of the above.
|
||
|
internals_group.add_argument(
|
||
|
"--enable-recursive-aliases", action="store_true", help=argparse.SUPPRESS
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
"--enable-incomplete-feature",
|
||
|
action="append",
|
||
|
metavar="FEATURE",
|
||
|
help="Enable support of incomplete/experimental features for early preview",
|
||
|
)
|
||
|
internals_group.add_argument(
|
||
|
"--custom-typeshed-dir", metavar="DIR", help="Use the custom typeshed in DIR"
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--warn-incomplete-stub",
|
||
|
default=False,
|
||
|
help="Warn if missing type annotation in typeshed, only relevant with"
|
||
|
" --disallow-untyped-defs or --disallow-incomplete-defs enabled",
|
||
|
group=internals_group,
|
||
|
)
|
||
|
internals_group.add_argument(
|
||
|
"--shadow-file",
|
||
|
nargs=2,
|
||
|
metavar=("SOURCE_FILE", "SHADOW_FILE"),
|
||
|
dest="shadow_file",
|
||
|
action="append",
|
||
|
help="When encountering SOURCE_FILE, read and type check "
|
||
|
"the contents of SHADOW_FILE instead.",
|
||
|
)
|
||
|
internals_group.add_argument("--fast-exit", action="store_true", help=argparse.SUPPRESS)
|
||
|
internals_group.add_argument(
|
||
|
"--no-fast-exit", action="store_false", dest="fast_exit", help=argparse.SUPPRESS
|
||
|
)
|
||
|
# This flag is useful for mypy tests, where function bodies may be omitted. Plugin developers
|
||
|
# may want to use this as well in their tests.
|
||
|
add_invertible_flag(
|
||
|
"--allow-empty-bodies", default=False, help=argparse.SUPPRESS, group=internals_group
|
||
|
)
|
||
|
# This undocumented feature exports limited line-level dependency information.
|
||
|
internals_group.add_argument("--export-ref-info", action="store_true", help=argparse.SUPPRESS)
|
||
|
|
||
|
report_group = parser.add_argument_group(
|
||
|
title="Report generation", description="Generate a report in the specified format."
|
||
|
)
|
||
|
for report_type in sorted(defaults.REPORTER_NAMES):
|
||
|
if report_type not in {"memory-xml"}:
|
||
|
report_group.add_argument(
|
||
|
f"--{report_type.replace('_', '-')}-report",
|
||
|
metavar="DIR",
|
||
|
dest=f"special-opts:{report_type}_report",
|
||
|
)
|
||
|
|
||
|
other_group = parser.add_argument_group(title="Miscellaneous")
|
||
|
other_group.add_argument("--quickstart-file", help=argparse.SUPPRESS)
|
||
|
other_group.add_argument("--junit-xml", help="Write junit.xml to the given file")
|
||
|
other_group.add_argument(
|
||
|
"--find-occurrences",
|
||
|
metavar="CLASS.MEMBER",
|
||
|
dest="special-opts:find_occurrences",
|
||
|
help="Print out all usages of a class member (experimental)",
|
||
|
)
|
||
|
other_group.add_argument(
|
||
|
"--scripts-are-modules",
|
||
|
action="store_true",
|
||
|
help="Script x becomes module x instead of __main__",
|
||
|
)
|
||
|
|
||
|
add_invertible_flag(
|
||
|
"--install-types",
|
||
|
default=False,
|
||
|
strict_flag=False,
|
||
|
help="Install detected missing library stub packages using pip",
|
||
|
group=other_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--non-interactive",
|
||
|
default=False,
|
||
|
strict_flag=False,
|
||
|
help=(
|
||
|
"Install stubs without asking for confirmation and hide "
|
||
|
+ "errors, with --install-types"
|
||
|
),
|
||
|
group=other_group,
|
||
|
inverse="--interactive",
|
||
|
)
|
||
|
|
||
|
if server_options:
|
||
|
# TODO: This flag is superfluous; remove after a short transition (2018-03-16)
|
||
|
other_group.add_argument(
|
||
|
"--experimental",
|
||
|
action="store_true",
|
||
|
dest="fine_grained_incremental",
|
||
|
help="Enable fine-grained incremental mode",
|
||
|
)
|
||
|
other_group.add_argument(
|
||
|
"--use-fine-grained-cache",
|
||
|
action="store_true",
|
||
|
help="Use the cache in fine-grained incremental mode",
|
||
|
)
|
||
|
|
||
|
# hidden options
|
||
|
parser.add_argument(
|
||
|
"--stats", action="store_true", dest="dump_type_stats", help=argparse.SUPPRESS
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
"--inferstats", action="store_true", dest="dump_inference_stats", help=argparse.SUPPRESS
|
||
|
)
|
||
|
parser.add_argument("--dump-build-stats", action="store_true", help=argparse.SUPPRESS)
|
||
|
# Dump timing stats for each processed file into the given output file
|
||
|
parser.add_argument("--timing-stats", dest="timing_stats", help=argparse.SUPPRESS)
|
||
|
# Dump per line type checking timing stats for each processed file into the given
|
||
|
# output file. Only total time spent in each top level expression will be shown.
|
||
|
# Times are show in microseconds.
|
||
|
parser.add_argument(
|
||
|
"--line-checking-stats", dest="line_checking_stats", help=argparse.SUPPRESS
|
||
|
)
|
||
|
# --debug-cache will disable any cache-related compressions/optimizations,
|
||
|
# which will make the cache writing process output pretty-printed JSON (which
|
||
|
# is easier to debug).
|
||
|
parser.add_argument("--debug-cache", action="store_true", help=argparse.SUPPRESS)
|
||
|
# --dump-deps will dump all fine-grained dependencies to stdout
|
||
|
parser.add_argument("--dump-deps", action="store_true", help=argparse.SUPPRESS)
|
||
|
# --dump-graph will dump the contents of the graph of SCCs and exit.
|
||
|
parser.add_argument("--dump-graph", action="store_true", help=argparse.SUPPRESS)
|
||
|
# --semantic-analysis-only does exactly that.
|
||
|
parser.add_argument("--semantic-analysis-only", action="store_true", help=argparse.SUPPRESS)
|
||
|
# --local-partial-types disallows partial types spanning module top level and a function
|
||
|
# (implicitly defined in fine-grained incremental mode)
|
||
|
parser.add_argument("--local-partial-types", action="store_true", help=argparse.SUPPRESS)
|
||
|
# --logical-deps adds some more dependencies that are not semantically needed, but
|
||
|
# may be helpful to determine relative importance of classes and functions for overall
|
||
|
# type precision in a code base. It also _removes_ some deps, so this flag should be never
|
||
|
# used except for generating code stats. This also automatically enables --cache-fine-grained.
|
||
|
# NOTE: This is an experimental option that may be modified or removed at any time.
|
||
|
parser.add_argument("--logical-deps", action="store_true", help=argparse.SUPPRESS)
|
||
|
# --bazel changes some behaviors for use with Bazel (https://bazel.build).
|
||
|
parser.add_argument("--bazel", action="store_true", help=argparse.SUPPRESS)
|
||
|
# --package-root adds a directory below which directories are considered
|
||
|
# packages even without __init__.py. May be repeated.
|
||
|
parser.add_argument(
|
||
|
"--package-root", metavar="ROOT", action="append", default=[], help=argparse.SUPPRESS
|
||
|
)
|
||
|
# --cache-map FILE ... gives a mapping from source files to cache files.
|
||
|
# Each triple of arguments is a source file, a cache meta file, and a cache data file.
|
||
|
# Modules not mentioned in the file will go through cache_dir.
|
||
|
# Must be followed by another flag or by '--' (and then only file args may follow).
|
||
|
parser.add_argument(
|
||
|
"--cache-map", nargs="+", dest="special-opts:cache_map", help=argparse.SUPPRESS
|
||
|
)
|
||
|
# --debug-serialize will run tree.serialize() even if cache generation is disabled.
|
||
|
# Useful for mypy_primer to detect serialize errors earlier.
|
||
|
parser.add_argument("--debug-serialize", action="store_true", help=argparse.SUPPRESS)
|
||
|
# This one is deprecated, but we will keep it for few releases.
|
||
|
parser.add_argument(
|
||
|
"--enable-incomplete-features", action="store_true", help=argparse.SUPPRESS
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
"--disable-bytearray-promotion", action="store_true", help=argparse.SUPPRESS
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
"--disable-memoryview-promotion", action="store_true", help=argparse.SUPPRESS
|
||
|
)
|
||
|
# This flag is deprecated, it has been moved to --extra-checks
|
||
|
parser.add_argument("--strict-concatenate", action="store_true", help=argparse.SUPPRESS)
|
||
|
|
||
|
# options specifying code to check
|
||
|
code_group = parser.add_argument_group(
|
||
|
title="Running code",
|
||
|
description="Specify the code you want to type check. For more details, see "
|
||
|
"mypy.readthedocs.io/en/stable/running_mypy.html#running-mypy",
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--explicit-package-bases",
|
||
|
default=False,
|
||
|
help="Use current directory and MYPYPATH to determine module names of files passed",
|
||
|
group=code_group,
|
||
|
)
|
||
|
add_invertible_flag(
|
||
|
"--fast-module-lookup", default=False, help=argparse.SUPPRESS, group=code_group
|
||
|
)
|
||
|
code_group.add_argument(
|
||
|
"--exclude",
|
||
|
action="append",
|
||
|
metavar="PATTERN",
|
||
|
default=[],
|
||
|
help=(
|
||
|
"Regular expression to match file names, directory names or paths which mypy should "
|
||
|
"ignore while recursively discovering files to check, e.g. --exclude '/setup\\.py$'. "
|
||
|
"May be specified more than once, eg. --exclude a --exclude b"
|
||
|
),
|
||
|
)
|
||
|
code_group.add_argument(
|
||
|
"-m",
|
||
|
"--module",
|
||
|
action="append",
|
||
|
metavar="MODULE",
|
||
|
default=[],
|
||
|
dest="special-opts:modules",
|
||
|
help="Type-check module; can repeat for more modules",
|
||
|
)
|
||
|
code_group.add_argument(
|
||
|
"-p",
|
||
|
"--package",
|
||
|
action="append",
|
||
|
metavar="PACKAGE",
|
||
|
default=[],
|
||
|
dest="special-opts:packages",
|
||
|
help="Type-check package recursively; can be repeated",
|
||
|
)
|
||
|
code_group.add_argument(
|
||
|
"-c",
|
||
|
"--command",
|
||
|
action="append",
|
||
|
metavar="PROGRAM_TEXT",
|
||
|
dest="special-opts:command",
|
||
|
help="Type-check program passed in as string",
|
||
|
)
|
||
|
code_group.add_argument(
|
||
|
metavar="files",
|
||
|
nargs="*",
|
||
|
dest="special-opts:files",
|
||
|
help="Type-check given files or directories",
|
||
|
)
|
||
|
|
||
|
# Parse arguments once into a dummy namespace so we can get the
|
||
|
# filename for the config file and know if the user requested all strict options.
|
||
|
dummy = argparse.Namespace()
|
||
|
parser.parse_args(args, dummy)
|
||
|
config_file = dummy.config_file
|
||
|
# Don't explicitly test if "config_file is not None" for this check.
|
||
|
# This lets `--config-file=` (an empty string) be used to disable all config files.
|
||
|
if config_file and not os.path.exists(config_file):
|
||
|
parser.error(f"Cannot find config file '{config_file}'")
|
||
|
|
||
|
options = Options()
|
||
|
strict_option_set = False
|
||
|
|
||
|
def set_strict_flags() -> None:
|
||
|
nonlocal strict_option_set
|
||
|
strict_option_set = True
|
||
|
for dest, value in strict_flag_assignments:
|
||
|
setattr(options, dest, value)
|
||
|
|
||
|
# Parse config file first, so command line can override.
|
||
|
parse_config_file(options, set_strict_flags, config_file, stdout, stderr)
|
||
|
|
||
|
# Set strict flags before parsing (if strict mode enabled), so other command
|
||
|
# line options can override.
|
||
|
if getattr(dummy, "special-opts:strict"):
|
||
|
set_strict_flags()
|
||
|
|
||
|
# Override cache_dir if provided in the environment
|
||
|
environ_cache_dir = os.getenv("MYPY_CACHE_DIR", "")
|
||
|
if environ_cache_dir.strip():
|
||
|
options.cache_dir = environ_cache_dir
|
||
|
options.cache_dir = os.path.expanduser(options.cache_dir)
|
||
|
|
||
|
# Parse command line for real, using a split namespace.
|
||
|
special_opts = argparse.Namespace()
|
||
|
parser.parse_args(args, SplitNamespace(options, special_opts, "special-opts:"))
|
||
|
|
||
|
# The python_version is either the default, which can be overridden via a config file,
|
||
|
# or stored in special_opts and is passed via the command line.
|
||
|
options.python_version = special_opts.python_version or options.python_version
|
||
|
if options.python_version < (3,):
|
||
|
parser.error(
|
||
|
"Mypy no longer supports checking Python 2 code. "
|
||
|
"Consider pinning to mypy<0.980 if you need to check Python 2 code."
|
||
|
)
|
||
|
try:
|
||
|
infer_python_executable(options, special_opts)
|
||
|
except PythonExecutableInferenceError as e:
|
||
|
parser.error(str(e))
|
||
|
|
||
|
if special_opts.no_executable or options.no_site_packages:
|
||
|
options.python_executable = None
|
||
|
|
||
|
# Paths listed in the config file will be ignored if any paths, modules or packages
|
||
|
# are passed on the command line.
|
||
|
if not (special_opts.files or special_opts.packages or special_opts.modules):
|
||
|
if options.files:
|
||
|
special_opts.files = options.files
|
||
|
if options.packages:
|
||
|
special_opts.packages = options.packages
|
||
|
if options.modules:
|
||
|
special_opts.modules = options.modules
|
||
|
|
||
|
# Check for invalid argument combinations.
|
||
|
if require_targets:
|
||
|
code_methods = sum(
|
||
|
bool(c)
|
||
|
for c in [
|
||
|
special_opts.modules + special_opts.packages,
|
||
|
special_opts.command,
|
||
|
special_opts.files,
|
||
|
]
|
||
|
)
|
||
|
if code_methods == 0 and not options.install_types:
|
||
|
parser.error("Missing target module, package, files, or command.")
|
||
|
elif code_methods > 1:
|
||
|
parser.error("May only specify one of: module/package, files, or command.")
|
||
|
if options.explicit_package_bases and not options.namespace_packages:
|
||
|
parser.error(
|
||
|
"Can only use --explicit-package-bases with --namespace-packages, since otherwise "
|
||
|
"examining __init__.py's is sufficient to determine module names for files"
|
||
|
)
|
||
|
|
||
|
# Check for overlapping `--always-true` and `--always-false` flags.
|
||
|
overlap = set(options.always_true) & set(options.always_false)
|
||
|
if overlap:
|
||
|
parser.error(
|
||
|
"You can't make a variable always true and always false (%s)"
|
||
|
% ", ".join(sorted(overlap))
|
||
|
)
|
||
|
|
||
|
validate_package_allow_list(options.untyped_calls_exclude)
|
||
|
|
||
|
# Process `--enable-error-code` and `--disable-error-code` flags
|
||
|
disabled_codes = set(options.disable_error_code)
|
||
|
enabled_codes = set(options.enable_error_code)
|
||
|
|
||
|
valid_error_codes = set(error_codes.keys())
|
||
|
|
||
|
invalid_codes = (enabled_codes | disabled_codes) - valid_error_codes
|
||
|
if invalid_codes:
|
||
|
parser.error(f"Invalid error code(s): {', '.join(sorted(invalid_codes))}")
|
||
|
|
||
|
options.disabled_error_codes |= {error_codes[code] for code in disabled_codes}
|
||
|
options.enabled_error_codes |= {error_codes[code] for code in enabled_codes}
|
||
|
|
||
|
# Enabling an error code always overrides disabling
|
||
|
options.disabled_error_codes -= options.enabled_error_codes
|
||
|
|
||
|
# Validate incomplete features.
|
||
|
for feature in options.enable_incomplete_feature:
|
||
|
if feature not in INCOMPLETE_FEATURES:
|
||
|
parser.error(f"Unknown incomplete feature: {feature}")
|
||
|
if options.enable_incomplete_features:
|
||
|
print(
|
||
|
"Warning: --enable-incomplete-features is deprecated, use"
|
||
|
" --enable-incomplete-feature=FEATURE instead"
|
||
|
)
|
||
|
options.enable_incomplete_feature = list(INCOMPLETE_FEATURES)
|
||
|
|
||
|
# Compute absolute path for custom typeshed (if present).
|
||
|
if options.custom_typeshed_dir is not None:
|
||
|
options.abs_custom_typeshed_dir = os.path.abspath(options.custom_typeshed_dir)
|
||
|
|
||
|
# Set build flags.
|
||
|
if special_opts.find_occurrences:
|
||
|
_find_occurrences = tuple(special_opts.find_occurrences.split("."))
|
||
|
if len(_find_occurrences) < 2:
|
||
|
parser.error("Can only find occurrences of class members.")
|
||
|
if len(_find_occurrences) != 2:
|
||
|
parser.error("Can only find occurrences of non-nested class members.")
|
||
|
state.find_occurrences = _find_occurrences # type: ignore[assignment]
|
||
|
|
||
|
# Set reports.
|
||
|
for flag, val in vars(special_opts).items():
|
||
|
if flag.endswith("_report") and val is not None:
|
||
|
report_type = flag[:-7].replace("_", "-")
|
||
|
report_dir = val
|
||
|
options.report_dirs[report_type] = report_dir
|
||
|
|
||
|
# Process --package-root.
|
||
|
if options.package_root:
|
||
|
process_package_roots(fscache, parser, options)
|
||
|
|
||
|
# Process --cache-map.
|
||
|
if special_opts.cache_map:
|
||
|
if options.sqlite_cache:
|
||
|
parser.error("--cache-map is incompatible with --sqlite-cache")
|
||
|
|
||
|
process_cache_map(parser, special_opts, options)
|
||
|
|
||
|
# An explicitly specified cache_fine_grained implies local_partial_types
|
||
|
# (because otherwise the cache is not compatible with dmypy)
|
||
|
if options.cache_fine_grained:
|
||
|
options.local_partial_types = True
|
||
|
|
||
|
# Implicitly show column numbers if error location end is shown
|
||
|
if options.show_error_end:
|
||
|
options.show_column_numbers = True
|
||
|
|
||
|
# Let logical_deps imply cache_fine_grained (otherwise the former is useless).
|
||
|
if options.logical_deps:
|
||
|
options.cache_fine_grained = True
|
||
|
|
||
|
if options.enable_recursive_aliases:
|
||
|
print(
|
||
|
"Warning: --enable-recursive-aliases is deprecated;"
|
||
|
" recursive types are enabled by default"
|
||
|
)
|
||
|
if options.strict_concatenate and not strict_option_set:
|
||
|
print("Warning: --strict-concatenate is deprecated; use --extra-checks instead")
|
||
|
|
||
|
# Set target.
|
||
|
if special_opts.modules + special_opts.packages:
|
||
|
options.build_type = BuildType.MODULE
|
||
|
sys_path, _ = get_search_dirs(options.python_executable)
|
||
|
search_paths = SearchPaths(
|
||
|
(os.getcwd(),), tuple(mypy_path() + options.mypy_path), tuple(sys_path), ()
|
||
|
)
|
||
|
targets = []
|
||
|
# TODO: use the same cache that the BuildManager will
|
||
|
cache = FindModuleCache(search_paths, fscache, options)
|
||
|
for p in special_opts.packages:
|
||
|
if os.sep in p or os.altsep and os.altsep in p:
|
||
|
fail(f"Package name '{p}' cannot have a slash in it.", stderr, options)
|
||
|
p_targets = cache.find_modules_recursive(p)
|
||
|
if not p_targets:
|
||
|
fail(f"Can't find package '{p}'", stderr, options)
|
||
|
targets.extend(p_targets)
|
||
|
for m in special_opts.modules:
|
||
|
targets.append(BuildSource(None, m, None))
|
||
|
return targets, options
|
||
|
elif special_opts.command:
|
||
|
options.build_type = BuildType.PROGRAM_TEXT
|
||
|
targets = [BuildSource(None, None, "\n".join(special_opts.command))]
|
||
|
return targets, options
|
||
|
else:
|
||
|
try:
|
||
|
targets = create_source_list(special_opts.files, options, fscache)
|
||
|
# Variable named e2 instead of e to work around mypyc bug #620
|
||
|
# which causes issues when using the same variable to catch
|
||
|
# exceptions of different types.
|
||
|
except InvalidSourceList as e2:
|
||
|
fail(str(e2), stderr, options)
|
||
|
return targets, options
|
||
|
|
||
|
|
||
|
def process_package_roots(
|
||
|
fscache: FileSystemCache | None, parser: argparse.ArgumentParser, options: Options
|
||
|
) -> None:
|
||
|
"""Validate and normalize package_root."""
|
||
|
if fscache is None:
|
||
|
parser.error("--package-root does not work here (no fscache)")
|
||
|
assert fscache is not None # Since mypy doesn't know parser.error() raises.
|
||
|
# Do some stuff with drive letters to make Windows happy (esp. tests).
|
||
|
current_drive, _ = os.path.splitdrive(os.getcwd())
|
||
|
dot = os.curdir
|
||
|
dotslash = os.curdir + os.sep
|
||
|
dotdotslash = os.pardir + os.sep
|
||
|
trivial_paths = {dot, dotslash}
|
||
|
package_root = []
|
||
|
for root in options.package_root:
|
||
|
if os.path.isabs(root):
|
||
|
parser.error(f"Package root cannot be absolute: {root!r}")
|
||
|
drive, root = os.path.splitdrive(root)
|
||
|
if drive and drive != current_drive:
|
||
|
parser.error(f"Package root must be on current drive: {drive + root!r}")
|
||
|
# Empty package root is always okay.
|
||
|
if root:
|
||
|
root = os.path.relpath(root) # Normalize the heck out of it.
|
||
|
if not root.endswith(os.sep):
|
||
|
root = root + os.sep
|
||
|
if root.startswith(dotdotslash):
|
||
|
parser.error(f"Package root cannot be above current directory: {root!r}")
|
||
|
if root in trivial_paths:
|
||
|
root = ""
|
||
|
package_root.append(root)
|
||
|
options.package_root = package_root
|
||
|
# Pass the package root on the the filesystem cache.
|
||
|
fscache.set_package_root(package_root)
|
||
|
|
||
|
|
||
|
def process_cache_map(
|
||
|
parser: argparse.ArgumentParser, special_opts: argparse.Namespace, options: Options
|
||
|
) -> None:
|
||
|
"""Validate cache_map and copy into options.cache_map."""
|
||
|
n = len(special_opts.cache_map)
|
||
|
if n % 3 != 0:
|
||
|
parser.error("--cache-map requires one or more triples (see source)")
|
||
|
for i in range(0, n, 3):
|
||
|
source, meta_file, data_file = special_opts.cache_map[i : i + 3]
|
||
|
if source in options.cache_map:
|
||
|
parser.error(f"Duplicate --cache-map source {source})")
|
||
|
if not source.endswith(".py") and not source.endswith(".pyi"):
|
||
|
parser.error(f"Invalid --cache-map source {source} (triple[0] must be *.py[i])")
|
||
|
if not meta_file.endswith(".meta.json"):
|
||
|
parser.error(
|
||
|
"Invalid --cache-map meta_file %s (triple[1] must be *.meta.json)" % meta_file
|
||
|
)
|
||
|
if not data_file.endswith(".data.json"):
|
||
|
parser.error(
|
||
|
"Invalid --cache-map data_file %s (triple[2] must be *.data.json)" % data_file
|
||
|
)
|
||
|
options.cache_map[source] = (meta_file, data_file)
|
||
|
|
||
|
|
||
|
def maybe_write_junit_xml(td: float, serious: bool, messages: list[str], options: Options) -> None:
|
||
|
if options.junit_xml:
|
||
|
py_version = f"{options.python_version[0]}_{options.python_version[1]}"
|
||
|
util.write_junit_xml(
|
||
|
td, serious, messages, options.junit_xml, py_version, options.platform
|
||
|
)
|
||
|
|
||
|
|
||
|
def fail(msg: str, stderr: TextIO, options: Options) -> NoReturn:
|
||
|
"""Fail with a serious error."""
|
||
|
stderr.write(f"{msg}\n")
|
||
|
maybe_write_junit_xml(0.0, serious=True, messages=[msg], options=options)
|
||
|
sys.exit(2)
|
||
|
|
||
|
|
||
|
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 "
|
||
|
+ "(and no cache from previous mypy run)\n"
|
||
|
)
|
||
|
else:
|
||
|
sys.stderr.write("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):
|
||
|
# No missing stubs.
|
||
|
return []
|
||
|
with open(fnam) as f:
|
||
|
return [line.strip() for line in f]
|
||
|
|
||
|
|
||
|
def install_types(
|
||
|
formatter: util.FancyFormatter,
|
||
|
options: Options,
|
||
|
*,
|
||
|
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(options.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:")
|
||
|
assert options.python_executable, "Python executable required to install types"
|
||
|
cmd = [options.python_executable, "-m", "pip", "install"] + packages
|
||
|
print(formatter.style(" ".join(cmd), "none", bold=True))
|
||
|
print()
|
||
|
if not non_interactive:
|
||
|
x = input("Install? [yN] ")
|
||
|
if not x.strip() or not x.lower().startswith("y"):
|
||
|
print(formatter.style("mypy: Skipping installation", "red", bold=True))
|
||
|
sys.exit(2)
|
||
|
print()
|
||
|
subprocess.run(cmd)
|
||
|
return True
|