Skip to content

Commit b34512c

Browse files
committed
Add force option
1 parent 7029b4c commit b34512c

File tree

8 files changed

+136
-92
lines changed

8 files changed

+136
-92
lines changed

docs/source/command_line.rst

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -923,25 +923,28 @@ in error messages.
923923
Use visually nicer output in error messages: use soft word wrap,
924924
show source code snippets, and show error location markers.
925925

926-
.. option:: --color-output[=auto]
926+
.. option:: --color-output[=force]
927927

928-
Enables colored output in error messages.
928+
``--color-output`` enables colored output if the output is going to a terminal.
929929

930-
When ``--color-output=auto`` is given, uses colored output if the output
931-
(both stdout and stderr) is going to a tty. This is also the default.
930+
``--color-output=force`` enables colored output unconditionally.
931+
932+
Output will still be uncolored if mypy fails to detect a color code scheme.
932933

933934
.. note::
934935
When the environment variable ``MYPY_FORCE_COLOR`` is set to a
935-
non-``0`` non-empty string, mypy always enables colored output
936-
(even if ``--no-color-output`` is given).
936+
non-``0`` non-empty string, mypy ignores ``--color-output[=force]``
937+
and ``--no-color-output``, and behaves as if ``--color-output=force``
938+
is given.
939+
940+
If ``MYPY_FORCE_COLOR`` is ``0``, it has no effect.
937941

938-
.. Note: Here I decide not to document ``FORCE_COLOR`` as its
939-
logic seems counter-intuitive from earlier conventions
940-
(PR13853)
942+
If ``MYPY_FORCE_COLOR`` is not defined, but ``FORCE_COLOR`` is defined,
943+
it is treated the same way (like a fallback).
941944

942945
.. option:: --no-color-output
943946

944-
Disables colored output in error messages.
947+
Disables colored output.
945948

946949
See also note above.
947950

mypy/dmypy_server.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -200,17 +200,12 @@ def __init__(self, options: Options, status_file: str, timeout: int | None = Non
200200
options.local_partial_types = True
201201
self.status_file = status_file
202202

203-
# Type annotation needed for mypy (Pyright understands this)
204-
use_color: bool | Literal["auto"] = (
205-
True
206-
if util.should_force_color()
207-
else ("auto" if options.color_output == "auto" else cast(bool, options.color_output))
208-
)
209-
210203
# Since the object is created in the parent process we can check
211204
# the output terminal options here.
212205
self.formatter = FancyFormatter(
213-
sys.stdout, sys.stderr, options.hide_error_codes, color_request=use_color
206+
sys.stdout, sys.stderr, options.hide_error_codes,
207+
color_request=util.should_force_color() or options.color_output is not False,
208+
warn_color_fail=options.warn_color_fail
214209
)
215210

216211
def _response_metadata(self) -> dict[str, str]:
@@ -853,10 +848,8 @@ def pretty_messages(
853848
) -> list[str]:
854849
use_color = (
855850
True
856-
if util.should_force_color()
857-
else (
858-
is_tty if self.options.color_output == "auto" else bool(self.options.color_output)
859-
)
851+
if util.should_force_color() or self.options.color_output == "force"
852+
else (is_tty if self.options.color_output is True else False)
860853
)
861854
fit_width = self.options.pretty and is_tty
862855
if fit_width:

mypy/main.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,20 @@ def main(
9090
if clean_exit:
9191
options.fast_exit = False
9292

93-
# Type annotation needed for mypy (Pyright understands this)
94-
use_color: bool | Literal["auto"] = (
95-
True
96-
if util.should_force_color()
97-
else ("auto" if options.color_output == "auto" else cast(bool, options.color_output))
98-
)
99-
10093
formatter = util.FancyFormatter(
10194
stdout,
10295
stderr,
10396
options.hide_error_codes,
10497
hide_success=bool(options.output),
105-
color_request=use_color,
98+
color_request=util.should_force_color() or options.color_output is not False,
99+
warn_color_fail=options.warn_color_fail,
100+
)
101+
102+
# Type annotation needed for mypy (Pyright understands this)
103+
use_color: bool = (
104+
True if util.should_force_color() or options.color_output == "force"
105+
else formatter.default_colored if options.color_output is True
106+
else False
106107
)
107108

108109
if options.allow_redefinition_new and not options.local_partial_types:
@@ -195,14 +196,15 @@ def run_build(
195196
t0: float,
196197
stdout: TextIO,
197198
stderr: TextIO,
198-
use_color: bool | Literal["auto"],
199+
use_color: bool,
199200
) -> tuple[build.BuildResult | None, list[str], bool]:
200201
formatter = util.FancyFormatter(
201202
stdout,
202203
stderr,
203204
options.hide_error_codes,
204205
hide_success=bool(options.output),
205206
color_request=use_color,
207+
warn_color_fail=options.warn_color_fail,
206208
)
207209

208210
messages = []
@@ -260,10 +262,8 @@ def show_messages(
260262
f: TextIO,
261263
formatter: util.FancyFormatter,
262264
options: Options,
263-
use_color: bool | Literal["auto"],
265+
use_color: bool,
264266
) -> None:
265-
if use_color == "auto":
266-
use_color = formatter.default_colored
267267
for msg in messages:
268268
if use_color:
269269
msg = formatter.colorize(msg)
@@ -495,8 +495,8 @@ def __call__(
495495
values: str | Sequence[Any] | None,
496496
option_string: str | None = None,
497497
) -> None:
498-
assert values in ("auto", None)
499-
setattr(namespace, self.dest, True if values is None else "auto")
498+
assert values in ("force", None)
499+
setattr(namespace, self.dest, True if values is None else "force")
500500

501501

502502
def define_options(
@@ -1037,15 +1037,21 @@ def add_invertible_flag(
10371037
dest="color_output",
10381038
action=ColorOutputAction,
10391039
nargs="?",
1040-
choices=["auto"],
1041-
help="Colorize error messages (inverse: --no-color-output). "
1042-
"Detects if to use color when option is omitted and --no-color-output "
1043-
"is not given, or when --color-output=auto",
1040+
choices=["force"],
1041+
help="Colorize error messages if output is going to a terminal (inverse: --no-color-output). "
1042+
"When --color-output=auto, colorizes output unconditionally",
10441043
)
10451044
error_group.add_argument(
10461045
"--no-color-output", dest="color_output", action="store_false", help=argparse.SUPPRESS
10471046
)
1048-
# error_group.set_defaults(color_output="auto")
1047+
add_invertible_flag(
1048+
"--warn-color-fail",
1049+
dest="warn_color_fail",
1050+
default=False,
1051+
help="Print warning message when mypy cannot detect "
1052+
"a terminal color scheme and colored output is requested",
1053+
group=error_group,
1054+
)
10491055
add_invertible_flag(
10501056
"--no-error-summary",
10511057
dest="error_summary",

mypy/options.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import warnings
88
from collections.abc import Mapping
99
from re import Pattern
10-
from typing import Any, Callable, Final
10+
from typing import Any, Callable, Final, Literal
1111

1212
from mypy import defaults
1313
from mypy.errorcodes import ErrorCode, error_codes
@@ -206,8 +206,9 @@ def __init__(self) -> None:
206206
self.show_error_context = False
207207

208208
# Use nicer output (when possible).
209-
self.color_output = "auto"
209+
self.color_output: bool | Literal["force"] = True
210210
self.error_summary = True
211+
self.warn_color_fail: bool = False
211212

212213
# Assume arguments with default values of None are Optional
213214
self.implicit_optional = False

mypy/test/test_color_output.py

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,80 @@
11
from functools import partial
2-
from subprocess import run
3-
from typing import Any
4-
2+
import subprocess
3+
from typing import TYPE_CHECKING, Any
4+
import sys
55
import pytest
66

7-
# TODO Would like help with this test, how do I make it runnable?
7+
#XXX Would like help with this test, how do I make it runnable?
8+
9+
# Haven't run this test yet
10+
11+
PTY_SIZE = (80, 40)
812

13+
if sys.platform == "win32":
14+
if TYPE_CHECKING:
15+
from winpty.winpty import PTY
16+
else:
17+
from winpty import PTY
918

10-
def test(expect_color: bool, *args: Any, **kwargs: Any) -> None:
11-
res = run(*args, capture_output=True, **kwargs)
12-
if "Found" not in res.stdout: # ??
19+
def run_pty(cmd: str, env: dict[str, str] = {}) -> tuple[str, str]:
20+
pty = PTY(*PTY_SIZE)
21+
# For the purposes of this test, str.split() is enough
22+
appname, cmdline = cmd.split(maxsplit=1)
23+
pty.spawn(appname, cmdline, "\0".join(map(lambda kv: f"{kv[0]}={kv[1]}", env.items())))
24+
while pty.isalive():
25+
pass
26+
return pty.read(), pty.read_stderr()
27+
elif sys.platform == "unix":
28+
from pty import openpty
29+
30+
def run_pty(cmd: str, env: dict[str, str] = {}) -> tuple[str, str]:
31+
# TODO Would like help checking quality of this function,
32+
# it's partially written by Copilot because I'm not familiar with Unix openpty
33+
master_fd, slave_fd = openpty()
34+
try:
35+
p = subprocess.run(cmd, stdout=slave_fd, stderr=subprocess.PIPE, env=env, text=True)
36+
os.close(slave_fd)
37+
return os.read(slave_fd, 10000).decode(), p.stderr
38+
finally:
39+
os.close(master_fd)
40+
def test(expect_color: bool, pty: bool, cmd: str, env: dict[str, str] = {}) -> None:
41+
if pty:
42+
stdout, stderr = run_pty(cmd, env=env)
43+
else:
44+
proc = subprocess.run(cmd, capture_output=True, env=env, text=True)
45+
stdout = proc.stdout
46+
stderr = proc.stderr
47+
if "Found" not in stdout: # ??
1348
pytest.fail("Command failed to complete or did not detect type error")
1449
if expect_color: # Expect color control chars
15-
assert "<string>:1: error:" not in res.stdout
16-
assert "\nFound" not in res.stdout
50+
assert "<string>:1: error:" not in stdout
51+
assert "\nFound" not in stdout
1752
else: # Expect no color control chars
18-
assert "<string>:1: error:" in res.stdout
19-
assert "\nFound" in res.stdout
53+
assert "<string>:1: error:" in stdout
54+
assert "\nFound" in stdout
55+
2056

57+
def test_pty(expect_color: bool, cmd: str, env: dict[str, str] = {}) -> None:
58+
test(expect_color, True, cmd, env)
2159

22-
colored = partial(test, True)
23-
not_colored = partial(test, False)
60+
def test_not_pty(expect_color: bool, cmd: str, env: dict[str, str] = {}) -> None:
61+
test(expect_color, False, cmd, env)
2462

2563

2664
@pytest.mark.parametrize("command", ["mypy", "dmypy run --"])
27-
def test_color_output(command: str) -> None:
65+
def test_it(command: str) -> None:
2866
# Note: Though we don't check stderr, capturing it is useful
2967
# because it provides traceback if mypy crashes due to exception
3068
# and pytest reveals it upon failure (?)
31-
not_colored(f"{command} -c \"1+'a'\"")
32-
colored(f"{command} -c \"1+'a'\"", env={"MYPY_FORCE_COLOR": "1"})
33-
colored(f"{command} -c \"1+'a'\" --color-output")
34-
not_colored(f"{command} -c \"1+'a'\" --no-color-output")
35-
colored(f"{command} -c \"1+'a'\" --no-color-output", env={"MYPY_FORCE_COLOR": "1"}) # TODO
69+
test_pty(True, "mypy -c \"1+'a'\" --color-output=force")
70+
test_pty(False, "mypy -c \"1+'a'\" --no-color-output")
71+
test_not_pty(False, "mypy -c \"1+'a'\" --color-output")
72+
test_not_pty(True, "mypy -c \"1+'a'\" --color-output=force")
73+
test_not_pty(False, "mypy -c \"1+'a'\" --color-output", {"MYPY_FORCE_COLOR": "1"})
74+
test_not_pty(True, "mypy -c \"1+'a'\" --color-output=force", {"MYPY_FORCE_COLOR": "1"})
75+
test_not_pty(False, "mypy -c \"1+'a'\" --no-color-output", {"MYPY_FORCE_COLOR": "1"})
76+
test_not_pty(False, "mypy -c \"1+'a'\" --no-color-output", {"FORCE_COLOR": "1"})
77+
test_not_pty(False, "mypy -c \"1+'a'\" --color-output", {"MYPY_FORCE_COLOR": "0"})
3678

3779

3880
# TODO: Tests in the terminal (require manual testing?)

0 commit comments

Comments
 (0)