From 4ad090d40872507689b4292c33a75d1468382b92 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:01:46 +0200 Subject: [PATCH 01/17] Add test class helper to force no terminal colour --- Lib/test/support/__init__.py | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 42e7b876594fa7..173b270f06a170 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -60,6 +60,7 @@ "skip_on_s390x", "without_optimizer", "force_not_colorized", + "force_not_colorized_test_class", "BrokenIter", "in_systemd_nspawn_sync_suppressed", "run_no_yield_async_fn", "run_yielding_async_fn", "async_yield", @@ -2856,6 +2857,44 @@ def wrapper(*args, **kwargs): return wrapper + +def force_not_colorized_test_class(cls): + """Force the terminal not to be colorized.""" + original_setup = cls.setUp + original_teardown = cls.tearDown + + @functools.wraps(cls.setUp) + def setUp_wrapper(self, *args, **kwargs): + import _colorize + + self._original_fn = _colorize.can_colorize + self._variables: dict[str, str | None] = { + "PYTHON_COLORS": None, + "FORCE_COLOR": None, + "NO_COLOR": None, + } + for key in self._variables: + self._variables[key] = os.environ.pop(key, None) + os.environ["NO_COLOR"] = "1" + _colorize.can_colorize = lambda: False + return original_setup(self, *args, **kwargs) + + @functools.wraps(cls.tearDown) + def tearDown_wrapper(self, *args, **kwargs): + import _colorize + + _colorize.can_colorize = self._original_fn + del os.environ["NO_COLOR"] + for key, value in self._variables.items(): + if value is not None: + os.environ[key] = value + return original_teardown(self, *args, **kwargs) + + cls.setUp = setUp_wrapper + cls.tearDown = tearDown_wrapper + return cls + + def initialized_with_pyrepl(): """Detect whether PyREPL was used during Python initialization.""" # If the main module has a __file__ attribute it's a Python module, which means PyREPL. From ab76e1b7aa2f1e92a65d3124c4e5d43fd9e0f106 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:33:01 +0200 Subject: [PATCH 02/17] Refactor --- Lib/test/support/__init__.py | 67 ++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 173b270f06a170..db29f0428a19ba 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -17,6 +17,7 @@ import types import unittest import warnings +from collections.abc import Callable __all__ = [ @@ -2833,31 +2834,45 @@ def is_slot_wrapper(name, value): yield name, True +def _disable_terminal_color() -> Callable[[], bool]: + import _colorize + + original_fn = _colorize.can_colorize + variables: dict[str, str | None] = { + "PYTHON_COLORS": None, + "FORCE_COLOR": None, + "NO_COLOR": None, + } + for key in variables: + variables[key] = os.environ.pop(key, None) + os.environ["NO_COLOR"] = "1" + _colorize.can_colorize = lambda: False + return original_fn, variables + + +def _re_enable_terminal_color( + original_fn: Callable[[], bool], variables: dict[str, str | None] +): + import _colorize + + _colorize.can_colorize = original_fn + del os.environ["NO_COLOR"] + for key, value in variables.items(): + if value is not None: + os.environ[key] = value + + def force_not_colorized(func): """Force the terminal not to be colorized.""" @functools.wraps(func) def wrapper(*args, **kwargs): - import _colorize - original_fn = _colorize.can_colorize - variables: dict[str, str | None] = { - "PYTHON_COLORS": None, "FORCE_COLOR": None, "NO_COLOR": None - } try: - for key in variables: - variables[key] = os.environ.pop(key, None) - os.environ["NO_COLOR"] = "1" - _colorize.can_colorize = lambda: False + original_fn, variables = _disable_terminal_color() return func(*args, **kwargs) finally: - _colorize.can_colorize = original_fn - del os.environ["NO_COLOR"] - for key, value in variables.items(): - if value is not None: - os.environ[key] = value + _re_enable_terminal_color(original_fn, variables) return wrapper - - def force_not_colorized_test_class(cls): """Force the terminal not to be colorized.""" original_setup = cls.setUp @@ -2865,29 +2880,13 @@ def force_not_colorized_test_class(cls): @functools.wraps(cls.setUp) def setUp_wrapper(self, *args, **kwargs): - import _colorize + self._original_fn, self._variables = _disable_terminal_color() - self._original_fn = _colorize.can_colorize - self._variables: dict[str, str | None] = { - "PYTHON_COLORS": None, - "FORCE_COLOR": None, - "NO_COLOR": None, - } - for key in self._variables: - self._variables[key] = os.environ.pop(key, None) - os.environ["NO_COLOR"] = "1" - _colorize.can_colorize = lambda: False return original_setup(self, *args, **kwargs) @functools.wraps(cls.tearDown) def tearDown_wrapper(self, *args, **kwargs): - import _colorize - - _colorize.can_colorize = self._original_fn - del os.environ["NO_COLOR"] - for key, value in self._variables.items(): - if value is not None: - os.environ[key] = value + _re_enable_terminal_color(self._original_fn, self._variables) return original_teardown(self, *args, **kwargs) cls.setUp = setUp_wrapper From bb90f8924934ccdfecf5573fae0251d22f26a2e7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 4 Jan 2025 16:54:07 +0200 Subject: [PATCH 03/17] Default to stdout isatty for colour detection instead of stderr --- Lib/_colorize.py | 6 +++--- Lib/test/test_code_module.py | 4 ++-- Lib/test/test_traceback.py | 9 ++++++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index f609901887a26b..be609c6d90a853 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -49,7 +49,7 @@ def can_colorize() -> bool: if os.environ.get("TERM") == "dumb": return False - if not hasattr(sys.stderr, "fileno"): + if not hasattr(sys.stdout, "fileno"): return False if sys.platform == "win32": @@ -62,6 +62,6 @@ def can_colorize() -> bool: return False try: - return os.isatty(sys.stderr.fileno()) + return os.isatty(sys.stdout.fileno()) except io.UnsupportedOperation: - return sys.stderr.isatty() + return sys.stdout.isatty() diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py index 37c7bc772ed8c7..11dce808c9415e 100644 --- a/Lib/test/test_code_module.py +++ b/Lib/test/test_code_module.py @@ -5,8 +5,7 @@ from textwrap import dedent from contextlib import ExitStack from unittest import mock -from test.support import import_helper - +from test.support import force_not_colorized_test_class, import_helper code = import_helper.import_module('code') @@ -30,6 +29,7 @@ def mock_sys(self): del self.sysmod.ps2 +@force_not_colorized_test_class class TestInteractiveConsole(unittest.TestCase, MockSys): maxDiff = None diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 31f0a61d6a9d59..abdfc4638f2e9c 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -21,7 +21,7 @@ from test.support.os_helper import TESTFN, unlink from test.support.script_helper import assert_python_ok, assert_python_failure from test.support.import_helper import forget -from test.support import force_not_colorized +from test.support import force_not_colorized, force_not_colorized_test_class import json import textwrap @@ -1712,6 +1712,7 @@ def f(): @requires_debug_ranges() +@force_not_colorized_test_class class PurePythonTracebackErrorCaretTests( PurePythonExceptionFormattingMixin, TracebackErrorLocationCaretTestBase, @@ -1725,6 +1726,7 @@ class PurePythonTracebackErrorCaretTests( @cpython_only @requires_debug_ranges() +@force_not_colorized_test_class class CPythonTracebackErrorCaretTests( CAPIExceptionFormattingMixin, TracebackErrorLocationCaretTestBase, @@ -1736,6 +1738,7 @@ class CPythonTracebackErrorCaretTests( @cpython_only @requires_debug_ranges() +@force_not_colorized_test_class class CPythonTracebackLegacyErrorCaretTests( CAPIExceptionFormattingLegacyMixin, TracebackErrorLocationCaretTestBase, @@ -2149,10 +2152,12 @@ def test_print_exception_bad_type_python(self): boundaries = re.compile( '(%s|%s)' % (re.escape(cause_message), re.escape(context_message))) +@force_not_colorized_test_class class TestTracebackFormat(unittest.TestCase, TracebackFormatMixin): pass @cpython_only +@force_not_colorized_test_class class TestFallbackTracebackFormat(unittest.TestCase, TracebackFormatMixin): DEBUG_RANGES = False def setUp(self) -> None: @@ -2940,6 +2945,7 @@ def f(): self.assertEqual(report, expected) +@force_not_colorized_test_class class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): # # This checks reporting through the 'traceback' module, with both @@ -2956,6 +2962,7 @@ def get_report(self, e): return s +@force_not_colorized_test_class class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): # # This checks built-in reporting by the interpreter. From 172f8c389934bbdfcc0dbf7b9b2ad2839e1c88e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 7 Jan 2025 21:49:07 +0200 Subject: [PATCH 04/17] Add NEWS --- .../next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst diff --git a/Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst b/Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst new file mode 100644 index 00000000000000..812f3df2a4ef39 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst @@ -0,0 +1,2 @@ +Default to stdout isatty for colour detection instead of stderr. Patch by +Hugo van Kemenade. From b7855ade9a85b26bcf4c4b9c10d9a495808a7653 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:38:51 +0200 Subject: [PATCH 05/17] Fix merge conflict --- Lib/test/support/__init__.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 137679d7aafd5e..fb98b3e243c3be 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2857,26 +2857,6 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper -def force_not_colorized_test_class(cls): - """Force the terminal not to be colorized.""" - original_setup = cls.setUp - original_teardown = cls.tearDown - - @functools.wraps(cls.setUp) - def setUp_wrapper(self, *args, **kwargs): - self._original_fn, self._variables = _disable_terminal_color() - - return original_setup(self, *args, **kwargs) - - @functools.wraps(cls.tearDown) - def tearDown_wrapper(self, *args, **kwargs): - _re_enable_terminal_color(self._original_fn, self._variables) - return original_teardown(self, *args, **kwargs) - - cls.setUp = setUp_wrapper - cls.tearDown = tearDown_wrapper - return cls - def force_not_colorized_test_class(cls): """Force the terminal not to be colorized for the entire test class.""" From 8e24cc89d243dd2814b77361a8b87a3f6ad79cc2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:39:46 +0200 Subject: [PATCH 06/17] Remove unused import --- Lib/test/support/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index fb98b3e243c3be..ee9520a8838625 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -17,7 +17,6 @@ import types import unittest import warnings -from collections.abc import Callable __all__ = [ From 2b88c85e729fdc7cbdf5390c95ed4373768d1c69 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:44:38 +0200 Subject: [PATCH 07/17] Check can_colorize using same output stream as traceback --- Lib/_colorize.py | 9 ++++++--- Lib/traceback.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index be609c6d90a853..f0fbf2880f893c 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -33,7 +33,10 @@ def get_colors(colorize: bool = False) -> ANSIColors: return NoColors -def can_colorize() -> bool: +def can_colorize(*, file=None) -> bool: + if file is None: + file = sys.stdout + if not sys.flags.ignore_environment: if os.environ.get("PYTHON_COLORS") == "0": return False @@ -49,7 +52,7 @@ def can_colorize() -> bool: if os.environ.get("TERM") == "dumb": return False - if not hasattr(sys.stdout, "fileno"): + if not hasattr(file, "fileno"): return False if sys.platform == "win32": @@ -62,6 +65,6 @@ def can_colorize() -> bool: return False try: - return os.isatty(sys.stdout.fileno()) + return os.isatty(file.fileno()) except io.UnsupportedOperation: return sys.stdout.isatty() diff --git a/Lib/traceback.py b/Lib/traceback.py index 6367c00e4d4b86..3ed06af15a0a89 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -135,7 +135,7 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ def _print_exception_bltin(exc, /): file = sys.stderr if sys.stderr is not None else sys.__stderr__ - colorize = _colorize.can_colorize() + colorize = _colorize.can_colorize(file=file) return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize) From 4aa46c5f94f8302be294a94d850e063f414a7117 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:44:55 +0200 Subject: [PATCH 08/17] Check can_colorize using same output stream as unittest --- Lib/doctest.py | 2 +- Lib/unittest/result.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index bb281fc483c41c..e02e73ed722f7e 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1558,7 +1558,7 @@ def out(s): save_displayhook = sys.displayhook sys.displayhook = sys.__displayhook__ saved_can_colorize = _colorize.can_colorize - _colorize.can_colorize = lambda: False + _colorize.can_colorize = lambda *args, **kwargs: False color_variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None} for key in color_variables: color_variables[key] = os.environ.pop(key, None) diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index 97262735aa8311..e2f0353fead032 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -191,7 +191,8 @@ def _exc_info_to_string(self, err, test): capture_locals=self.tb_locals, compact=True) from _colorize import can_colorize - msgLines = list(tb_e.format(colorize=can_colorize())) + file = self.stream if hasattr(self, "stream") else None + msgLines = list(tb_e.format(colorize=can_colorize(file=file))) if self.buffer: output = sys.stdout.getvalue() From d0ce62201d6dd1670ae0de977c5fa141aa622bb5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:31:47 +0200 Subject: [PATCH 09/17] Check get_colors using the same output stream --- Lib/_colorize.py | 7 +++++-- Lib/test/libregrtest/single.py | 4 ++-- Lib/test/support/__init__.py | 2 +- Lib/unittest/runner.py | 6 ++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index f0fbf2880f893c..0dd8db4aa6e1ef 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -26,8 +26,11 @@ class ANSIColors: setattr(NoColors, attr, "") -def get_colors(colorize: bool = False) -> ANSIColors: - if colorize or can_colorize(): +def get_colors(colorize: bool = False, *, file=None) -> ANSIColors: + if file is None: + file = sys.stdout + + if colorize or can_colorize(file=file): return ANSIColors() else: return NoColors diff --git a/Lib/test/libregrtest/single.py b/Lib/test/libregrtest/single.py index 0e174f82abed28..125a88c0f98f09 100644 --- a/Lib/test/libregrtest/single.py +++ b/Lib/test/libregrtest/single.py @@ -162,7 +162,7 @@ def test_func(): def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, display_failure: bool = True) -> None: # Handle exceptions, detect environment changes. - ansi = get_colors() + ansi = get_colors(file=sys.stderr) red, reset, yellow = ansi.RED, ansi.RESET, ansi.YELLOW # Reset the environment_altered flag to detect if a test altered @@ -303,7 +303,7 @@ def run_single_test(test_name: TestName, runtests: RunTests) -> TestResult: If runtests.use_junit, xml_data is a list containing each generated testsuite element. """ - ansi = get_colors() + ansi = get_colors(file=sys.stderr) red, reset, yellow = ansi.BOLD_RED, ansi.RESET, ansi.YELLOW start_time = time.perf_counter() diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index ee9520a8838625..e05e91babc2499 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2839,7 +2839,7 @@ def no_color(): from .os_helper import EnvironmentVarGuard with ( - swap_attr(_colorize, "can_colorize", lambda: False), + swap_attr(_colorize, "can_colorize", lambda file=None: False), EnvironmentVarGuard() as env, ): for var in {"FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS"}: diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py index d60c295a1eddf7..5784ca48b54c9d 100644 --- a/Lib/unittest/runner.py +++ b/Lib/unittest/runner.py @@ -45,7 +45,8 @@ def __init__(self, stream, descriptions, verbosity, *, durations=None): self.showAll = verbosity > 1 self.dots = verbosity == 1 self.descriptions = descriptions - self._ansi = get_colors() + file = sys.stderr if stream == "" else None + self._ansi = get_colors(file=file) self._newline = True self.durations = durations @@ -286,7 +287,8 @@ def run(self, test): expected_fails, unexpected_successes, skipped = results infos = [] - ansi = get_colors() + file = sys.stderr if self.stream == "" else None + ansi = get_colors(file=file) bold_red = ansi.BOLD_RED green = ansi.GREEN red = ansi.RED From 7debe697b40b26d6b7d4f410b6a287779c78c7dd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:00:15 +0200 Subject: [PATCH 10/17] Apply suggestions from code review Co-authored-by: Serhiy Storchaka Co-authored-by: Victor Stinner --- Lib/_colorize.py | 3 --- Lib/unittest/result.py | 4 ++-- .../Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 0dd8db4aa6e1ef..8517e14d99a8d6 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -27,9 +27,6 @@ class ANSIColors: def get_colors(colorize: bool = False, *, file=None) -> ANSIColors: - if file is None: - file = sys.stdout - if colorize or can_colorize(file=file): return ANSIColors() else: diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index e2f0353fead032..23b550991bc9c9 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -191,8 +191,8 @@ def _exc_info_to_string(self, err, test): capture_locals=self.tb_locals, compact=True) from _colorize import can_colorize - file = self.stream if hasattr(self, "stream") else None - msgLines = list(tb_e.format(colorize=can_colorize(file=file))) + colorize = hasattr(self, "stream") and can_colorize(self.stream) + msgLines = list(tb_e.format(colorize=colorize) if self.buffer: output = sys.stdout.getvalue() diff --git a/Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst b/Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst index 812f3df2a4ef39..9a241e37c20a44 100644 --- a/Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst +++ b/Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst @@ -1,2 +1,2 @@ -Default to stdout isatty for colour detection instead of stderr. Patch by +Default to stdout isatty for color detection instead of stderr. Patch by Hugo van Kemenade. From 6faf53c79b7067a532ba31e0397e0f7fc710d878 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:02:07 +0200 Subject: [PATCH 11/17] Add missing parenthesis --- Lib/unittest/result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index 23b550991bc9c9..0fa5ec9eb483ce 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -192,7 +192,7 @@ def _exc_info_to_string(self, err, test): from _colorize import can_colorize colorize = hasattr(self, "stream") and can_colorize(self.stream) - msgLines = list(tb_e.format(colorize=colorize) + msgLines = list(tb_e.format(colorize=colorize)) if self.buffer: output = sys.stdout.getvalue() From e1547d04407f45e67f94fb8a8c488de108fb85dd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:05:23 +0200 Subject: [PATCH 12/17] Name the argument --- Lib/unittest/result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index 0fa5ec9eb483ce..b8ea396db6772e 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -191,7 +191,7 @@ def _exc_info_to_string(self, err, test): capture_locals=self.tb_locals, compact=True) from _colorize import can_colorize - colorize = hasattr(self, "stream") and can_colorize(self.stream) + colorize = hasattr(self, "stream") and can_colorize(file=self.stream) msgLines = list(tb_e.format(colorize=colorize)) if self.buffer: From 7612bff993ca3087351fdfdb8c9a78238b6fa7a8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:06:53 +0200 Subject: [PATCH 13/17] Also print skips to stderr --- Lib/test/libregrtest/single.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/test/libregrtest/single.py b/Lib/test/libregrtest/single.py index 125a88c0f98f09..e9c9adabacd202 100644 --- a/Lib/test/libregrtest/single.py +++ b/Lib/test/libregrtest/single.py @@ -184,12 +184,20 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, _load_run_test(result, runtests) except support.ResourceDenied as exc: if not quiet and not pgo: - print(f"{yellow}{test_name} skipped -- {exc}{reset}", flush=True) + print( + f"{yellow}{test_name} skipped -- {exc}{reset}", + file=sys.stderr, + flush=True, + ) result.state = State.RESOURCE_DENIED return except unittest.SkipTest as exc: if not quiet and not pgo: - print(f"{yellow}{test_name} skipped -- {exc}{reset}", flush=True) + print( + f"{yellow}{test_name} skipped -- {exc}{reset}", + file=sys.stderr, + flush=True, + ) result.state = State.SKIPPED return except support.TestFailedWithDetails as exc: From 44828f0f9a883d55b97d857a853f7aa11c940b4e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:19:21 +0200 Subject: [PATCH 14/17] Check file not sys.stdout Co-authored-by: Victor Stinner --- Lib/_colorize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 8517e14d99a8d6..bab2e599b2c810 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -67,4 +67,4 @@ def can_colorize(*, file=None) -> bool: try: return os.isatty(file.fileno()) except io.UnsupportedOperation: - return sys.stdout.isatty() + return file.isatty() From 36eb18e1bcfdf4b6470ffb1013b883c235803d0b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:18:19 +0200 Subject: [PATCH 15/17] file=stream --- Lib/unittest/runner.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py index 5784ca48b54c9d..eb0234a2617680 100644 --- a/Lib/unittest/runner.py +++ b/Lib/unittest/runner.py @@ -45,8 +45,7 @@ def __init__(self, stream, descriptions, verbosity, *, durations=None): self.showAll = verbosity > 1 self.dots = verbosity == 1 self.descriptions = descriptions - file = sys.stderr if stream == "" else None - self._ansi = get_colors(file=file) + self._ansi = get_colors(file=stream) self._newline = True self.durations = durations @@ -287,8 +286,7 @@ def run(self, test): expected_fails, unexpected_successes, skipped = results infos = [] - file = sys.stderr if self.stream == "" else None - ansi = get_colors(file=file) + ansi = get_colors(file=self.stream) bold_red = ansi.BOLD_RED green = ansi.GREEN red = ansi.RED From f5857b5229930465589eefdfbbd8a5baf0e34c8f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:18:49 +0200 Subject: [PATCH 16/17] Print to stdout --- Lib/test/libregrtest/single.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib/test/libregrtest/single.py b/Lib/test/libregrtest/single.py index e9c9adabacd202..274c493b7cf3e9 100644 --- a/Lib/test/libregrtest/single.py +++ b/Lib/test/libregrtest/single.py @@ -184,11 +184,7 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, _load_run_test(result, runtests) except support.ResourceDenied as exc: if not quiet and not pgo: - print( - f"{yellow}{test_name} skipped -- {exc}{reset}", - file=sys.stderr, - flush=True, - ) + print(f"{yellow}{test_name} skipped -- {exc}{reset}", flush=True) result.state = State.RESOURCE_DENIED return except unittest.SkipTest as exc: From 3047d2af5f6e98392f655f1e2588ee8038e5a00b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:26:20 +0200 Subject: [PATCH 17/17] Log skipped to stdout, log failures to stderr --- Lib/test/libregrtest/single.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Lib/test/libregrtest/single.py b/Lib/test/libregrtest/single.py index 274c493b7cf3e9..54df688bbc470e 100644 --- a/Lib/test/libregrtest/single.py +++ b/Lib/test/libregrtest/single.py @@ -162,8 +162,8 @@ def test_func(): def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, display_failure: bool = True) -> None: # Handle exceptions, detect environment changes. - ansi = get_colors(file=sys.stderr) - red, reset, yellow = ansi.RED, ansi.RESET, ansi.YELLOW + stdout = get_colors(file=sys.stdout) + stderr = get_colors(file=sys.stderr) # Reset the environment_altered flag to detect if a test altered # the environment @@ -184,22 +184,24 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, _load_run_test(result, runtests) except support.ResourceDenied as exc: if not quiet and not pgo: - print(f"{yellow}{test_name} skipped -- {exc}{reset}", flush=True) + print( + f"{stdout.YELLOW}{test_name} skipped -- {exc}{stdout.RESET}", + flush=True, + ) result.state = State.RESOURCE_DENIED return except unittest.SkipTest as exc: if not quiet and not pgo: print( - f"{yellow}{test_name} skipped -- {exc}{reset}", - file=sys.stderr, + f"{stdout.YELLOW}{test_name} skipped -- {exc}{stdout.RESET}", flush=True, ) result.state = State.SKIPPED return except support.TestFailedWithDetails as exc: - msg = f"{red}test {test_name} failed{reset}" + msg = f"{stderr.RED}test {test_name} failed{stderr.RESET}" if display_failure: - msg = f"{red}{msg} -- {exc}{reset}" + msg = f"{stderr.RED}{msg} -- {exc}{stderr.RESET}" print(msg, file=sys.stderr, flush=True) result.state = State.FAILED result.errors = exc.errors @@ -207,9 +209,9 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, result.stats = exc.stats return except support.TestFailed as exc: - msg = f"{red}test {test_name} failed{reset}" + msg = f"{stderr.RED}test {test_name} failed{stderr.RESET}" if display_failure: - msg = f"{red}{msg} -- {exc}{reset}" + msg = f"{stderr.RED}{msg} -- {exc}{stderr.RESET}" print(msg, file=sys.stderr, flush=True) result.state = State.FAILED result.stats = exc.stats @@ -224,8 +226,11 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, except: if not pgo: msg = traceback.format_exc() - print(f"{red}test {test_name} crashed -- {msg}{reset}", - file=sys.stderr, flush=True) + print( + f"{stderr.RED}test {test_name} crashed -- {msg}{stderr.RESET}", + file=sys.stderr, + flush=True, + ) result.state = State.UNCAUGHT_EXC return