Skip to content
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