Skip to content

[refactor] increase consistency in stdout and stderr optional arguments #19638

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
29 changes: 13 additions & 16 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 2 additions & 4 deletions mypy/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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.

Expand Down
44 changes: 27 additions & 17 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
)
Expand Down
2 changes: 1 addition & 1 deletion mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this one directly use sys.stderr? I don't know, probably some reason. It was like that when I got here.


def error_callback(msg: str) -> typing.NoReturn:
print(_style("error:", color="red", bold=True), msg)
Expand Down
12 changes: 10 additions & 2 deletions mypy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Copy link
Contributor Author

@wyattscarpenter wyattscarpenter Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether it matters whether or not it's a dummy term if stdout or stderr are None, but since None means no printing I figured that's a pretty dummy term... (In case it's not clear: the only reason I included the check for None is because python has no safe navigation operator that would let us go f_out.isatty())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check could also be distributed like

if not should_force_color() and (not (f_out and f_out.isatty()) or not (f_err and f_err.isatty()))

I have no strong feelings on that.

return
if sys.platform == "win32":
Expand Down
Loading