From f08e7308c6938cf0557d976257d8323430dd9eaa Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 10 Aug 2025 16:20:59 -0700 Subject: [PATCH 1/8] be a little more consistent with stdout and stderr optional arguments --- mypy/build.py | 16 ++++++++-------- mypy/config_parser.py | 2 +- mypy/main.py | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 71575de9d877..278e00e0d0f3 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 @@ -1075,7 +1075,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 +2879,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..68542614a768 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -497,7 +497,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..c65d40fbb9ce 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -465,8 +465,8 @@ def __call__( def define_options( program: str = "mypy", header: str = HEADER, - stdout: TextIO = sys.stdout, - stderr: TextIO = sys.stderr, + stdout: TextIO | None, + stderr: TextIO | 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). @@ -1634,9 +1634,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 ) From cf0de50e8f5af9cc2dbf9e60a2993897bf84ee8a Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 10 Aug 2025 16:27:15 -0700 Subject: [PATCH 2/8] simplify the function signature of parse_config_file, which did not even use stdout anyway --- mypy/config_parser.py | 2 -- mypy/main.py | 2 +- mypy/stubtest.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 68542614a768..2c2408a40958 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,7 +322,6 @@ def parse_config_file( If filename is None, fall back to default config files. """ - stdout = stdout or sys.stdout stderr = stderr or sys.stderr ret = ( diff --git a/mypy/main.py b/mypy/main.py index c65d40fbb9ce..49c5eace8d35 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -1379,7 +1379,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. 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) From f390f25ced74c5f52c1adc2e1599b084f338d0fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 Aug 2025 23:30:15 +0000 Subject: [PATCH 3/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 278e00e0d0f3..5fbdc2654c30 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -398,7 +398,7 @@ def import_priority(imp: ImportBase, toplevel_priority: int) -> int: def load_plugins_from_config( - options: Options, errors: Errors, stdout: TextIO|None + 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|None, extra_plugins: Sequence[Plugin] + options: Options, errors: Errors, stdout: TextIO | None, extra_plugins: Sequence[Plugin] ) -> tuple[Plugin, dict[str, str]]: """Load all configured plugins. From ba3641f03e1e62e1b740e258781e4dfdd8dd4c77 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 10 Aug 2025 22:27:23 -0700 Subject: [PATCH 4/8] propagate fixes, & simplify --- mypy/build.py | 13 +++++-------- mypy/config_parser.py | 2 +- mypy/main.py | 37 ++++++++++++++++++++++--------------- mypy/util.py | 4 ++-- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 5fbdc2654c30..f4f718dbc89c 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -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(): diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 2c2408a40958..2bc614cd2028 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -322,7 +322,7 @@ def parse_config_file( If filename is None, fall back to default config files. """ - stderr = stderr or sys.stderr + stderr = stderr if stderr is not None else sys.stderr ret = ( _parse_individual_file(filename, stderr) diff --git a/mypy/main.py b/mypy/main.py index 49c5eace8d35..002979477941 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. + 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,10 @@ 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 +188,14 @@ def run_build( options: Options, fscache: FileSystemCache, t0: float, - stdout: TextIO, - stderr: TextIO, + stdout: TextIO | None = None, + stderr: TextIO | None = None, ) -> tuple[build.BuildResult | None, list[str], bool]: formatter = util.FancyFormatter( stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) ) - + stdout = stdout if stdout is not None else sys.stdout + stderr = stderr if stderr is not None else sys.stderr messages = [] messages_by_file = defaultdict(list) @@ -238,14 +247,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. class AugmentedHelpFormatter(argparse.RawDescriptionHelpFormatter): @@ -399,7 +406,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) # =============== # Exiting methods @@ -465,8 +472,8 @@ def __call__( def define_options( program: str = "mypy", header: str = HEADER, - stdout: TextIO | None, - stderr: TextIO | None, + 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). diff --git a/mypy/util.py b/mypy/util.py index d7ff2a367fa2..c2f6ae609acf 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -592,7 +592,7 @@ 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 +601,7 @@ 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": From 5cfcbc46a9ee84cf15f1b1e43de624537a71f5a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 05:31:17 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/main.py | 7 ++++++- mypy/util.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 002979477941..1ea6998957ef 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -162,7 +162,11 @@ def main( print(summary, file=stdout, flush=True) # Only notes should also output success elif not messages or n_notes == len(messages): - print(formatter.format_success(len(sources), options.color_output), file=stdout, flush=True) + 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) @@ -254,6 +258,7 @@ def show_messages( msg = formatter.colorize(msg) print(msg, file=f, flush=True) + # Make the help output a little less jarring. class AugmentedHelpFormatter(argparse.RawDescriptionHelpFormatter): def __init__(self, prog: str, **kwargs: Any) -> None: diff --git a/mypy/util.py b/mypy/util.py index c2f6ae609acf..9ddb7f137ee1 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -592,7 +592,11 @@ class FancyFormatter: """ def __init__( - self, f_out: IO[str] | None, f_err: IO[str] | None, 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 (f_out is None or f_err is None) or 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": From b040095b8438b337e01325166e556cd7a98f8b6a Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Mon, 11 Aug 2025 01:44:10 -0400 Subject: [PATCH 6/8] Update mypy/main.py: the only write without a newline... --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index 1ea6998957ef..b75e652078cd 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -411,7 +411,7 @@ def _print_message(self, message: str, file: SupportsWrite[str] | None = None) - if message: if file is None: file = self.stderr - print(message, file=file) + print(message, file=file, end='') # =============== # Exiting methods From ad9b9c1d06e3685890787fb059e11c930deb5f06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 05:45:30 +0000 Subject: [PATCH 7/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/main.py b/mypy/main.py index b75e652078cd..617b4dd0ec35 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -411,7 +411,7 @@ def _print_message(self, message: str, file: SupportsWrite[str] | None = None) - if message: if file is None: file = self.stderr - print(message, file=file, end='') + print(message, file=file, end="") # =============== # Exiting methods From 7e16ac4f80468b723a74eb0ffed9e56b88d2f3dd Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sun, 10 Aug 2025 22:55:53 -0700 Subject: [PATCH 8/8] run_build doesn't need to have default arguments I guess reasonable minds could disagree on this --- mypy/main.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 617b4dd0ec35..26d2a12f03d0 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -77,7 +77,7 @@ def main( # 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. + # 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 @@ -192,14 +192,12 @@ def run_build( options: Options, fscache: FileSystemCache, t0: float, - stdout: TextIO | None = None, - stderr: TextIO | None = None, + 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) ) - stdout = stdout if stdout is not None else sys.stdout - stderr = stderr if stderr is not None else sys.stderr messages = [] messages_by_file = defaultdict(list)