Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
433159b
Some progress
studyingegret Aug 18, 2025
807f022
More changes
studyingegret Aug 18, 2025
2188acb
Merge remote-tracking branch 'refs/remotes/origin/master' into color-…
studyingegret Aug 18, 2025
30e8e43
Document the option
studyingegret Aug 19, 2025
ff79d3c
Fix type errors
studyingegret Aug 19, 2025
5a3d5ff
Merge branch 'master' into color-output-option
studyingegret Aug 19, 2025
b3c4e87
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 19, 2025
068f116
Fix Ruff lint UP022
studyingegret Aug 19, 2025
ecb8e9a
Merge branch 'color-output-option' of https://github.com/studyingegre…
studyingegret Aug 19, 2025
cc905b8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 19, 2025
2dfb813
lint: Delete unused "type: ignore" comment
studyingegret Aug 19, 2025
0a1351c
Merge branch 'color-output-option' of https://github.com/studyingegre…
studyingegret Aug 19, 2025
819b867
Some fixes
studyingegret Aug 19, 2025
5879267
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 19, 2025
bd4460d
Remove MYPY_FORCE_COLOR and FORCE_COLOR in workflows/test.yml (??)
studyingegret Aug 19, 2025
10ca3ab
Merge branch 'color-output-option' of https://github.com/studyingegre…
studyingegret Aug 19, 2025
91be93d
Delete some workflows for this fork only (?)
studyingegret Aug 19, 2025
a873c67
Change workflows for this fork only (?)
studyingegret Aug 19, 2025
2fe22d5
Merge branch 'color-output-option' of https://github.com/studyingegre…
studyingegret Aug 19, 2025
af56737
...
studyingegret Aug 19, 2025
0fd4b98
!!!
studyingegret Aug 19, 2025
3e101fe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 19, 2025
1bce878
Revert changed workflow files
studyingegret Aug 19, 2025
89c0c93
Merge branch 'color-output-option' of https://github.com/studyingegre…
studyingegret Aug 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ jobs:
env:
TOX_SKIP_MISSING_INTERPRETERS: False
# Rich (pip) -- Disable color for windows + pytest
FORCE_COLOR: ${{ !(startsWith(matrix.os, 'windows-') && startsWith(matrix.toxenv, 'py')) && 1 || 0 }}
#FORCE_COLOR: ${{ !(startsWith(matrix.os, 'windows-') && startsWith(matrix.toxenv, 'py')) && 1 || 0 }}
# Tox
PY_COLORS: 1
# Python -- Disable argparse help colors (3.14+)
PYTHON_COLORS: 0
# Mypy (see https://github.com/python/mypy/issues/7771)
TERM: xterm-color
MYPY_FORCE_COLOR: 1
#MYPY_FORCE_COLOR: 1
MYPY_FORCE_TERMINAL_WIDTH: 200
# Pytest
PYTEST_ADDOPTS: --color=yes
Expand Down
20 changes: 19 additions & 1 deletion docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -923,9 +923,27 @@ in error messages.
Use visually nicer output in error messages: use soft word wrap,
show source code snippets, and show error location markers.

.. option:: --color-output[=auto]

Enables colored output in error messages.

When ``--color-output=auto`` is given, uses colored output if the output
(both stdout and stderr) is going to a tty. This is also the default.

.. note::
When the environment variable ``MYPY_FORCE_COLOR`` is set to a
non-``0`` non-empty string, mypy always enables colored output
(even if ``--no-color-output`` is given).

.. Note: Here I decide not to document ``FORCE_COLOR`` as its
logic seems counter-intuitive from earlier conventions
(PR13853)

.. option:: --no-color-output

This flag will disable color output in error messages, enabled by default.
Disables colored output in error messages.

See also note above.

.. option:: --no-error-summary

Expand Down
22 changes: 19 additions & 3 deletions mypy/dmypy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
import traceback
from collections.abc import Sequence, Set as AbstractSet
from contextlib import redirect_stderr, redirect_stdout
from typing import Any, Callable, Final
from typing import Any, Callable, Final, Literal, cast
from typing_extensions import TypeAlias as _TypeAlias

import mypy.build
import mypy.errors
import mypy.main
from mypy import util
from mypy.dmypy_util import WriteToConn, receive, send
from mypy.find_sources import InvalidSourceList, create_source_list
from mypy.fscache import FileSystemCache
Expand Down Expand Up @@ -199,9 +200,18 @@ def __init__(self, options: Options, status_file: str, timeout: int | None = Non
options.local_partial_types = True
self.status_file = status_file

# Type annotation needed for mypy (Pyright understands this)
Copy link
Member

Choose a reason for hiding this comment

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

LOL, are you serious?

use_color: bool | Literal["auto"] = (
True
if util.should_force_color()
else ("auto" if options.color_output == "auto" else cast(bool, options.color_output))
)

# Since the object is created in the parent process we can check
# the output terminal options here.
self.formatter = FancyFormatter(sys.stdout, sys.stderr, options.hide_error_codes)
self.formatter = FancyFormatter(
sys.stdout, sys.stderr, options.hide_error_codes, color_request=use_color
)

def _response_metadata(self) -> dict[str, str]:
py_version = f"{self.options.python_version[0]}_{self.options.python_version[1]}"
Expand Down Expand Up @@ -841,7 +851,13 @@ def pretty_messages(
is_tty: bool = False,
terminal_width: int | None = None,
) -> list[str]:
use_color = self.options.color_output and is_tty
use_color = (
True
if util.should_force_color()
else (
is_tty if self.options.color_output == "auto" else bool(self.options.color_output)
)
)
fit_width = self.options.pretty and is_tty
if fit_width:
messages = self.formatter.fit_in_terminal(
Expand Down
81 changes: 64 additions & 17 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from collections.abc import Sequence
from gettext import gettext
from io import TextIOWrapper
from typing import IO, TYPE_CHECKING, Any, Final, NoReturn, TextIO
from typing import IO, TYPE_CHECKING, Any, Final, Literal, NoReturn, TextIO, cast

from mypy import build, defaults, state, util
from mypy.config_parser import (
Expand Down Expand Up @@ -90,8 +90,19 @@ def main(
if clean_exit:
options.fast_exit = False

# Type annotation needed for mypy (Pyright understands this)
use_color: bool | Literal["auto"] = (
True
if util.should_force_color()
else ("auto" if options.color_output == "auto" else cast(bool, options.color_output))
)

formatter = util.FancyFormatter(
stdout, stderr, options.hide_error_codes, hide_success=bool(options.output)
stdout,
stderr,
options.hide_error_codes,
hide_success=bool(options.output),
color_request=use_color,
)

if options.allow_redefinition_new and not options.local_partial_types:
Expand Down Expand Up @@ -124,7 +135,7 @@ def main(
install_types(formatter, options, non_interactive=options.non_interactive)
return

res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr, use_color)

if options.non_interactive:
missing_pkgs = read_types_packages_to_install(options.cache_dir, after_run=True)
Expand All @@ -133,8 +144,10 @@ def main(
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)
res, messages, blockers = run_build(
sources, options, fscache, t0, stdout, stderr, use_color
)
show_messages(messages, stderr, formatter, options, use_color)

if MEM_PROFILE:
from mypy.memprofile import print_memory_profile
Expand All @@ -148,12 +161,12 @@ def main(
if options.error_summary:
if n_errors:
summary = formatter.format_error(
n_errors, n_files, len(sources), blockers=blockers, use_color=options.color_output
n_errors, n_files, len(sources), blockers=blockers, use_color=use_color
)
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.write(formatter.format_success(len(sources), use_color) + "\n")
stdout.flush()

if options.install_types and not options.non_interactive:
Expand Down Expand Up @@ -182,9 +195,14 @@ def run_build(
t0: float,
stdout: TextIO,
stderr: TextIO,
use_color: bool | Literal["auto"],
) -> tuple[build.BuildResult | None, list[str], bool]:
formatter = util.FancyFormatter(
stdout, stderr, options.hide_error_codes, hide_success=bool(options.output)
stdout,
stderr,
options.hide_error_codes,
hide_success=bool(options.output),
color_request=use_color,
)

messages = []
Expand All @@ -200,7 +218,7 @@ def flush_errors(filename: str | None, new_messages: list[str], serious: bool) -
# Collect messages and possibly show them later.
return
f = stderr if serious else stdout
show_messages(new_messages, f, formatter, options)
show_messages(new_messages, f, formatter, options, use_color)

serious = False
blockers = False
Expand Down Expand Up @@ -238,10 +256,16 @@ 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,
formatter: util.FancyFormatter,
options: Options,
use_color: bool | Literal["auto"],
) -> None:
if use_color == "auto":
use_color = formatter.default_colored
for msg in messages:
if options.color_output:
if use_color:
msg = formatter.colorize(msg)
f.write(msg + "\n")
f.flush()
Expand Down Expand Up @@ -462,6 +486,19 @@ def __call__(
parser.exit()


# Coupled with the usage in define_options
class ColorOutputAction(argparse.Action):
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: str | Sequence[Any] | None,
option_string: str | None = None,
) -> None:
assert values in ("auto", None)
setattr(namespace, self.dest, True if values is None else "auto")


def define_options(
program: str = "mypy",
header: str = HEADER,
Expand Down Expand Up @@ -993,13 +1030,22 @@ def add_invertible_flag(
" and show error location markers",
group=error_group,
)
add_invertible_flag(
"--no-color-output",
# XXX Setting default doesn't seem to work unless I change
# the attribute in options.Options
error_group.add_argument(
"--color-output",
dest="color_output",
default=True,
help="Do not colorize error messages",
group=error_group,
action=ColorOutputAction,
nargs="?",
choices=["auto"],
help="Colorize error messages (inverse: --no-color-output). "
"Detects if to use color when option is omitted and --no-color-output "
"is not given, or when --color-output=auto",
)
error_group.add_argument(
"--no-color-output", dest="color_output", action="store_false", help=argparse.SUPPRESS
)
# error_group.set_defaults(color_output="auto")
add_invertible_flag(
"--no-error-summary",
dest="error_summary",
Expand Down Expand Up @@ -1527,7 +1573,8 @@ def set_strict_flags() -> None:
reason = cache.find_module(p)
if reason is ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS:
fail(
f"Package '{p}' cannot be type checked due to missing py.typed marker. See https://mypy.readthedocs.io/en/stable/installed_packages.html for more details",
f"Package '{p}' cannot be type checked due to missing py.typed marker. "
"See https://mypy.readthedocs.io/en/stable/installed_packages.html for more details",
stderr,
options,
)
Expand Down
2 changes: 1 addition & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def __init__(self) -> None:
self.show_error_context = False

# Use nicer output (when possible).
self.color_output = True
self.color_output = "auto"
self.error_summary = True

# Assume arguments with default values of None are Optional
Expand Down
57 changes: 57 additions & 0 deletions mypy/test/test_color_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from functools import partial
from subprocess import run
from typing import Any

import pytest

# TODO Would like help with this test, how do I make it runnable?


def test(expect_color: bool, *args: Any, **kwargs: Any) -> None:
res = run(*args, capture_output=True, **kwargs)
if "Found" not in res.stdout: # ??
pytest.fail("Command failed to complete or did not detect type error")
if expect_color: # Expect color control chars
assert "<string>:1: error:" not in res.stdout
assert "\nFound" not in res.stdout
else: # Expect no color control chars
assert "<string>:1: error:" in res.stdout
assert "\nFound" in res.stdout


colored = partial(test, True)
not_colored = partial(test, False)


@pytest.mark.parametrize("command", ["mypy", "dmypy run --"])
def test_color_output(command: str) -> None:
# Note: Though we don't check stderr, capturing it is useful
# because it provides traceback if mypy crashes due to exception
# and pytest reveals it upon failure (?)
not_colored(f"{command} -c \"1+'a'\"")
colored(f"{command} -c \"1+'a'\"", env={"MYPY_FORCE_COLOR": "1"})
colored(f"{command} -c \"1+'a'\" --color-output")
not_colored(f"{command} -c \"1+'a'\" --no-color-output")
colored(f"{command} -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) # TODO


# TODO: Tests in the terminal (require manual testing?)
"""
In the terminal:
colored: mypy -c "1+'a'"
colored: mypy -c "1+'a'" --color-output
not colored: mypy -c "1+'a'" --no-color-output
colored: mypy -c "1+'a'" --color-output (with MYPY_FORCE_COLOR=1)
colored: mypy -c "1+'a'" --no-color-output (with MYPY_FORCE_COLOR=1)

To test, save this as a .bat and run in a Windows terminal (I don't know the Unix equivalent):

set MYPY_FORCE_COLOR=
mypy -c "1+'a'"
mypy -c "1+'a'" --color-output
mypy -c "1+'a'" --no-color-output
set MYPY_FORCE_COLOR=1
mypy -c "1+'a'" --color-output
mypy -c "1+'a'" --no-color-output
set MYPY_FORCE_COLOR=
"""
Loading
Loading