Skip to content

Commit

Permalink
Adds ability to configure stderr output color
Browse files Browse the repository at this point in the history
  • Loading branch information
ssbarnea committed Dec 10, 2024
1 parent c7f2caf commit d4a789c
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 11 deletions.
2 changes: 2 additions & 0 deletions docs/changelog/3426.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Adds ability to configure the stderr color for output received from external
commands.
8 changes: 8 additions & 0 deletions src/tox/config/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Type, TypeVar, cast

from colorama import Fore

from tox.config.loader.str_convert import StrConvert
from tox.plugin import NAME
from tox.util.ci import is_ci
Expand Down Expand Up @@ -366,6 +368,12 @@ def add_color_flags(parser: ArgumentParser) -> None:
choices=["yes", "no"],
help="should output be enriched with colors, default is yes unless TERM=dumb or NO_COLOR is defined.",
)
parser.add_argument(
"--stderr-color",
default="RED",
choices=[*Fore.__dict__.keys()],
help="color for stderr output, use RESET for terminal defaults.",
)


def add_exit_and_dump_after(parser: ArgumentParser) -> None:
Expand Down
19 changes: 10 additions & 9 deletions src/tox/execute/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,19 @@ def call(
env: ToxEnv,
) -> Iterator[ExecuteStatus]:
start = time.monotonic()
stderr_color = None
if self._colored:
try:
cfg_color = env.conf._conf.options.stderr_color # noqa: SLF001
stderr_color = getattr(Fore, cfg_color)
except (AttributeError, KeyError, TypeError): # many tests have a mocked 'env'
stderr_color = Fore.RED
try:
# collector is what forwards the content from the file streams to the standard streams
out, err = out_err[0].buffer, out_err[1].buffer
out_sync = SyncWrite(
out.name,
out if show else None, # type: ignore[arg-type]
)
err_sync = SyncWrite(
err.name,
err if show else None, # type: ignore[arg-type]
Fore.RED if self._colored else None,
)
out_sync = SyncWrite(out.name, out if show else None) # type: ignore[arg-type]
err_sync = SyncWrite(err.name, err if show else None, stderr_color) # type: ignore[arg-type]

with out_sync, err_sync:
instance = self.build_instance(request, self._option_class(env), out_sync, err_sync)
with instance as status:
Expand Down
2 changes: 2 additions & 0 deletions tests/config/cli/test_cli_env_var.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def test_verbose_no_test() -> None:
"verbose": 4,
"quiet": 0,
"colored": "no",
"stderr_color": "RED",
"work_dir": None,
"root_dir": None,
"config_file": None,
Expand Down Expand Up @@ -90,6 +91,7 @@ def test_env_var_exhaustive_parallel_values(
assert vars(options.parsed) == {
"always_copy": False,
"colored": "no",
"stderr_color": "RED",
"command": "legacy",
"default_runner": "virtualenv",
"develop": False,
Expand Down
2 changes: 2 additions & 0 deletions tests/config/cli/test_cli_ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
def default_options() -> dict[str, Any]:
return {
"colored": "no",
"stderr_color": "RED",
"command": "r",
"default_runner": "virtualenv",
"develop": False,
Expand Down Expand Up @@ -200,6 +201,7 @@ def test_ini_exhaustive_parallel_values(core_handlers: dict[str, Callable[[State
options = get_options("p")
assert vars(options.parsed) == {
"colored": "yes",
"stderr_color": "RED",
"command": "p",
"default_runner": "virtualenv",
"develop": False,
Expand Down
14 changes: 12 additions & 2 deletions tests/execute/local_subprocess/test_local_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,30 @@ def read_out_err(self) -> tuple[str, str]:
@pytest.mark.parametrize("color", [True, False], ids=["color", "no_color"])
@pytest.mark.parametrize(("out", "err"), [("out", "err"), ("", "")], ids=["simple", "nothing"])
@pytest.mark.parametrize("show", [True, False], ids=["show", "no_show"])
@pytest.mark.parametrize(
"stderr_color",
["RED", "YELLOW", "RESET"],
ids=["stderr_color_default", "stderr_color_yellow", "stderr_color_reset"],
)
def test_local_execute_basic_pass( # noqa: PLR0913
caplog: LogCaptureFixture,
os_env: dict[str, str],
out: str,
err: str,
show: bool,
color: bool,
stderr_color: str,
) -> None:
caplog.set_level(logging.NOTSET)
executor = LocalSubProcessExecutor(colored=color)

tox_env = MagicMock()
tox_env.conf._conf.options.stderr_color = stderr_color # noqa: SLF001
code = f"import sys; print({out!r}, end=''); print({err!r}, end='', file=sys.stderr)"
request = ExecuteRequest(cmd=[sys.executable, "-c", code], cwd=Path(), env=os_env, stdin=StdinSource.OFF, run_id="")
out_err = FakeOutErr()
with executor.call(request, show=show, out_err=out_err.out_err, env=MagicMock()) as status:

with executor.call(request, show=show, out_err=out_err.out_err, env=tox_env) as status:
while status.exit_code is None: # pragma: no branch
status.wait()
assert status.out == out.encode()
Expand All @@ -77,7 +87,7 @@ def test_local_execute_basic_pass( # noqa: PLR0913
out_got, err_got = out_err.read_out_err()
if show:
assert out_got == out
expected = (f"{Fore.RED}{err}{Fore.RESET}" if color else err) if err else ""
expected = f"{Fore.__dict__[stderr_color]}{err}{Fore.RESET}" if color and err else err
assert err_got == expected
else:
assert not out_got
Expand Down

0 comments on commit d4a789c

Please sign in to comment.