diff --git a/mypy/build.py b/mypy/build.py index 71575de9d877..f4f718dbc89c 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -210,8 +210,8 @@ def _build( alt_lib_path: str | None, flush_errors: Callable[[str | None, list[str], bool], None], fscache: FileSystemCache | None, - stdout: TextIO, - stderr: TextIO, + stdout: TextIO | None, + stderr: TextIO | None, extra_plugins: Sequence[Plugin], ) -> BuildResult: if platform.python_implementation() == "CPython": @@ -398,7 +398,7 @@ def import_priority(imp: ImportBase, toplevel_priority: int) -> int: def load_plugins_from_config( - options: Options, errors: Errors, stdout: TextIO + options: Options, errors: Errors, stdout: TextIO | None ) -> tuple[list[Plugin], dict[str, str]]: """Load all configured plugins. @@ -490,7 +490,7 @@ def plugin_error(message: str) -> NoReturn: def load_plugins( - options: Options, errors: Errors, stdout: TextIO, extra_plugins: Sequence[Plugin] + options: Options, errors: Errors, stdout: TextIO | None, extra_plugins: Sequence[Plugin] ) -> tuple[Plugin, dict[str, str]]: """Load all configured plugins. @@ -606,8 +606,8 @@ def __init__( errors: Errors, flush_errors: Callable[[str | None, list[str], bool], None], fscache: FileSystemCache, - stdout: TextIO, - stderr: TextIO, + stdout: TextIO | None, + stderr: TextIO | None, error_formatter: ErrorFormatter | None = None, ) -> None: self.stats: dict[str, Any] = {} # Values are ints or floats @@ -876,10 +876,9 @@ def verbosity(self) -> int: def log(self, *message: str) -> None: if self.verbosity() >= 1: if message: - print("LOG: ", *message, file=self.stderr) + print("LOG: ", *message, file=self.stderr, flush=True) else: - print(file=self.stderr) - self.stderr.flush() + print(file=self.stderr, flush=True) def log_fine_grained(self, *message: str) -> None: import mypy.build @@ -889,15 +888,13 @@ def log_fine_grained(self, *message: str) -> None: elif mypy.build.DEBUG_FINE_GRAINED: # Output log in a simplified format that is quick to browse. if message: - print(*message, file=self.stderr) + print(*message, file=self.stderr, flush=True) else: - print(file=self.stderr) - self.stderr.flush() + print(file=self.stderr, flush=True) def trace(self, *message: str) -> None: if self.verbosity() >= 2: - print("TRACE:", *message, file=self.stderr) - self.stderr.flush() + print("TRACE:", *message, file=self.stderr, flush=True) def add_stats(self, **kwds: Any) -> None: for key, value in kwds.items(): @@ -1075,7 +1072,7 @@ def read_plugins_snapshot(manager: BuildManager) -> dict[str, str] | None: def read_quickstart_file( - options: Options, stdout: TextIO + options: Options, stdout: TextIO | None ) -> dict[str, tuple[float, int, str]] | None: quickstart: dict[str, tuple[float, int, str]] | None = None if options.quickstart_file: @@ -2879,7 +2876,7 @@ def log_configuration(manager: BuildManager, sources: list[BuildSource]) -> None # The driver -def dispatch(sources: list[BuildSource], manager: BuildManager, stdout: TextIO) -> Graph: +def dispatch(sources: list[BuildSource], manager: BuildManager, stdout: TextIO | None) -> Graph: log_configuration(manager, sources) t0 = time.time() diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 5f08f342241e..2bc614cd2028 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -314,7 +314,6 @@ def parse_config_file( options: Options, set_strict_flags: Callable[[], None], filename: str | None, - stdout: TextIO | None = None, stderr: TextIO | None = None, ) -> None: """Parse a config file into an Options object. @@ -323,8 +322,7 @@ def parse_config_file( If filename is None, fall back to default config files. """ - stdout = stdout or sys.stdout - stderr = stderr or sys.stderr + stderr = stderr if stderr is not None else sys.stderr ret = ( _parse_individual_file(filename, stderr) @@ -497,7 +495,7 @@ def parse_section( set_strict_flags: Callable[[], None], section: Mapping[str, Any], config_types: dict[str, Any], - stderr: TextIO = sys.stderr, + stderr: TextIO | None, ) -> tuple[dict[str, object], dict[str, str]]: """Parse one section of a config file. diff --git a/mypy/main.py b/mypy/main.py index fd50c7677a11..26d2a12f03d0 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -62,8 +62,8 @@ def stat_proxy(path: str) -> os.stat_result: def main( *, args: list[str] | None = None, - stdout: TextIO = sys.stdout, - stderr: TextIO = sys.stderr, + stdout: TextIO | None = None, + stderr: TextIO | None = None, clean_exit: bool = False, ) -> None: """Main entry point to the type checker. @@ -74,6 +74,15 @@ def main( clean_exit: Don't hard kill the process on exit. This allows catching SystemExit. """ + # As a common pattern around the codebase, we tend to do this instead of + # using default arguments that are mutable objects (due to Python's + # famously counterintuitive behavior about those): use a sentinel, then + # set. If there is no `= None` after the type, we don't manipulate it thus. + stdout = stdout if stdout is not None else sys.stdout + stderr = stderr if stderr is not None else sys.stderr + # sys.stdout and sys.stderr might technically be None, but this fact isn't + # currently enforced by the stubs (they are marked as MaybeNone (=Any)). + util.check_python_version("mypy") t0 = time.time() # To log stat() calls: os.stat = stat_proxy @@ -150,11 +159,14 @@ def main( summary = formatter.format_error( n_errors, n_files, len(sources), blockers=blockers, use_color=options.color_output ) - stdout.write(summary + "\n") + print(summary, file=stdout, flush=True) # 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() + print( + formatter.format_success(len(sources), options.color_output), + file=stdout, + flush=True, + ) if options.install_types and not options.non_interactive: result = install_types(formatter, options, after_run=True, non_interactive=False) @@ -180,13 +192,12 @@ def run_build( options: Options, fscache: FileSystemCache, t0: float, - stdout: TextIO, - stderr: TextIO, + stdout: TextIO | None, + stderr: TextIO | None, ) -> tuple[build.BuildResult | None, list[str], bool]: formatter = util.FancyFormatter( stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) ) - messages = [] messages_by_file = defaultdict(list) @@ -238,13 +249,12 @@ def flush_errors(filename: str | None, new_messages: list[str], serious: bool) - def show_messages( - messages: list[str], f: TextIO, formatter: util.FancyFormatter, options: Options + messages: list[str], f: TextIO | None, formatter: util.FancyFormatter, options: Options ) -> None: for msg in messages: if options.color_output: msg = formatter.colorize(msg) - f.write(msg + "\n") - f.flush() + print(msg, file=f, flush=True) # Make the help output a little less jarring. @@ -399,7 +409,7 @@ def _print_message(self, message: str, file: SupportsWrite[str] | None = None) - if message: if file is None: file = self.stderr - file.write(message) + print(message, file=file, end="") # =============== # Exiting methods @@ -465,8 +475,8 @@ def __call__( def define_options( program: str = "mypy", header: str = HEADER, - stdout: TextIO = sys.stdout, - stderr: TextIO = sys.stderr, + stdout: TextIO | None = None, + stderr: TextIO | None = None, server_options: bool = False, ) -> tuple[CapturableArgumentParser, list[str], list[tuple[str, bool]]]: """Define the options in the parser (by calling a bunch of methods that express/build our desired command-line flags). @@ -1379,7 +1389,7 @@ def set_strict_flags() -> None: setattr(options, dest, value) # Parse config file first, so command line can override. - parse_config_file(options, set_strict_flags, config_file, stdout, stderr) + parse_config_file(options, set_strict_flags, config_file, stderr) # Set strict flags before parsing (if strict mode enabled), so other command # line options can override. @@ -1634,9 +1644,9 @@ def maybe_write_junit_xml( ) -def fail(msg: str, stderr: TextIO, options: Options) -> NoReturn: +def fail(msg: str, stderr: TextIO | None, options: Options) -> NoReturn: """Fail with a serious error.""" - stderr.write(f"{msg}\n") + print(msg, file=stderr) maybe_write_junit_xml( 0.0, serious=True, all_messages=[msg], messages_by_file={None: [msg]}, options=options ) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ef8c8dc318e1..c5633565ad5b 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -2016,7 +2016,7 @@ def test_stubs(args: _Arguments, use_builtins_fixtures: bool = False) -> int: def set_strict_flags() -> None: # not needed yet return - parse_config_file(options, set_strict_flags, options.config_file, sys.stdout, sys.stderr) + parse_config_file(options, set_strict_flags, options.config_file, sys.stderr) def error_callback(msg: str) -> typing.NoReturn: print(_style("error:", color="red", bold=True), msg) diff --git a/mypy/util.py b/mypy/util.py index d7ff2a367fa2..9ddb7f137ee1 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -592,7 +592,11 @@ class FancyFormatter: """ def __init__( - self, f_out: IO[str], f_err: IO[str], hide_error_codes: bool, hide_success: bool = False + self, + f_out: IO[str] | None, + f_err: IO[str] | None, + hide_error_codes: bool, + hide_success: bool = False, ) -> None: self.hide_error_codes = hide_error_codes self.hide_success = hide_success @@ -601,7 +605,11 @@ def __init__( if sys.platform not in ("linux", "darwin", "win32", "emscripten"): self.dummy_term = True return - if not should_force_color() and (not f_out.isatty() or not f_err.isatty()): + if ( + (f_out is None or f_err is None) + or not should_force_color() + and (not f_out.isatty() or not f_err.isatty()) + ): self.dummy_term = True return if sys.platform == "win32":