From be7ba3e36f3abca7a7b7515b946ad4088d020f5d 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/10] 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 08a260a1f01ee5ca0fa40e43dfb212f50a7f84f7 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/10] 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 591c2f57582e2fcd07f5fee0142829968061f4b1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 9 Jan 2025 20:41:10 +0200 Subject: [PATCH 03/10] Disable colour for some tests --- Lib/test/test_code_module.py | 4 ++-- Lib/test/test_traceback.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) 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 d1b6a9ae01a1d5318338c41c8c5caea6a5eb2bff Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:17:14 +0200 Subject: [PATCH 04/10] Detype --- Lib/test/support/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index db29f0428a19ba..a277d95decaa1e 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2834,11 +2834,11 @@ def is_slot_wrapper(name, value): yield name, True -def _disable_terminal_color() -> Callable[[], bool]: +def _disable_terminal_color(): import _colorize original_fn = _colorize.can_colorize - variables: dict[str, str | None] = { + variables = { "PYTHON_COLORS": None, "FORCE_COLOR": None, "NO_COLOR": None, @@ -2850,9 +2850,7 @@ def _disable_terminal_color() -> Callable[[], bool]: return original_fn, variables -def _re_enable_terminal_color( - original_fn: Callable[[], bool], variables: dict[str, str | None] -): +def _re_enable_terminal_color(original_fn, variables): import _colorize _colorize.can_colorize = original_fn From 8eec75a56ec76ea2fe956571e5a24b14e779a4e1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:44:27 +0200 Subject: [PATCH 05/10] Separate submodule import from other imports --- Lib/test/test_code_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py index 11dce808c9415e..20b960ce8d1e02 100644 --- a/Lib/test/test_code_module.py +++ b/Lib/test/test_code_module.py @@ -5,7 +5,8 @@ from textwrap import dedent from contextlib import ExitStack from unittest import mock -from test.support import force_not_colorized_test_class, import_helper +from test.support import force_not_colorized_test_class +from test.support import import_helper code = import_helper.import_module('code') From 966479ad1d13da624af577f07b4be26ebe936158 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:14:31 +0200 Subject: [PATCH 06/10] Detype Co-authored-by: Erlend E. Aasland --- 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 a277d95decaa1e..ba0b8450e7bc02 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 e9aa442e13aa5fc13219ce963380c3866911ae06 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:45:44 +0200 Subject: [PATCH 07/10] Use setUpClass/tearDownClass instead of setUp/tearDown --- Lib/test/support/__init__.py | 40 +++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index ba0b8450e7bc02..6092d8880e110e 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2870,24 +2870,30 @@ def wrapper(*args, **kwargs): _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 - 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 +def force_not_colorized_test_class(cls): + """Force the terminal not to be colorized for the entire test class.""" + original_setUpClass = cls.setUpClass + original_tearDownClass = cls.tearDownClass + + @classmethod + @functools.wraps(cls.setUpClass) + def new_setUpClass(cls): + original_fn, variables = _disable_terminal_color() + cls._original_fn = original_fn + cls._variables = variables + if original_setUpClass: + original_setUpClass() + + @classmethod + @functools.wraps(cls.tearDownClass) + def new_tearDownClass(cls): + if original_tearDownClass: + original_tearDownClass() + _re_enable_terminal_color(cls._original_fn, cls._variables) + + cls.setUpClass = new_setUpClass + cls.tearDownClass = new_tearDownClass return cls From 72260f85b52a5e6c6e6ed573771dace2640785fa Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:42:35 +0200 Subject: [PATCH 08/10] Use EnvironmentVarGuard and swap_attrs in force_not_colorized and force_not_colorized_test_class --- Lib/test/support/__init__.py | 65 +++++++++++++----------------------- 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 6092d8880e110e..4c917a8784176d 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2833,67 +2833,48 @@ def is_slot_wrapper(name, value): yield name, True -def _disable_terminal_color(): - import _colorize - - original_fn = _colorize.can_colorize - variables = { - "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, variables): - 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): - try: - original_fn, variables = _disable_terminal_color() + import _colorize + from .os_helper import EnvironmentVarGuard + + with ( + swap_attr(_colorize, "can_colorize", lambda: False), + EnvironmentVarGuard() as env, + ): + for var in {"FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS"}: + env.unset(var) + env.set("NO_COLOR", "1") + return func(*args, **kwargs) - finally: - _re_enable_terminal_color(original_fn, variables) + return wrapper def force_not_colorized_test_class(cls): """Force the terminal not to be colorized for the entire test class.""" original_setUpClass = cls.setUpClass - original_tearDownClass = cls.tearDownClass @classmethod @functools.wraps(cls.setUpClass) def new_setUpClass(cls): - original_fn, variables = _disable_terminal_color() - cls._original_fn = original_fn - cls._variables = variables + import _colorize + from .os_helper import EnvironmentVarGuard + + cls.enterClassContext( + swap_attr(_colorize, "can_colorize", lambda: False) + ) + env = cls.enterClassContext(EnvironmentVarGuard()) + for var in {"FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS"}: + env.unset(var) + env.set("NO_COLOR", "1") + if original_setUpClass: original_setUpClass() - @classmethod - @functools.wraps(cls.tearDownClass) - def new_tearDownClass(cls): - if original_tearDownClass: - original_tearDownClass() - _re_enable_terminal_color(cls._original_fn, cls._variables) - cls.setUpClass = new_setUpClass - cls.tearDownClass = new_tearDownClass return cls From 6daa3c33bb4171693d522e4880c305b87a400b23 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:53:42 +0200 Subject: [PATCH 09/10] Fix tests when running with FORCE_COLOR=1 --- Lib/test/test_exceptions.py | 1 + Lib/test/test_unittest/test_result.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 6ccfa9575f8569..206e22e791e02a 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2274,6 +2274,7 @@ def test_range_of_offsets(self): self.assertIn(expected, err.getvalue()) the_exception = exc + @force_not_colorized def test_subclass(self): class MySyntaxError(SyntaxError): pass diff --git a/Lib/test/test_unittest/test_result.py b/Lib/test/test_unittest/test_result.py index 746b9fa2677717..ad6f52d7e0260e 100644 --- a/Lib/test/test_unittest/test_result.py +++ b/Lib/test/test_unittest/test_result.py @@ -1,13 +1,15 @@ import io import sys import textwrap - -from test.support import warnings_helper, captured_stdout - import traceback import unittest from unittest.util import strclass -from test.support import force_not_colorized +from test.support import warnings_helper +from test.support import ( + captured_stdout, + force_not_colorized, + force_not_colorized_test_class, +) from test.test_unittest.support import BufferedWriter @@ -772,6 +774,7 @@ def testFoo(self): runner.run(Test('testFoo')) +@force_not_colorized_test_class class TestOutputBuffering(unittest.TestCase): def setUp(self): From 0554e911e3078dbb389f20f52059bf74e79163fd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:46:00 +0200 Subject: [PATCH 10/10] Refactor --- Lib/test/support/__init__.py | 43 +++++++++++++++--------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 4c917a8784176d..ee9520a8838625 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2833,23 +2833,27 @@ def is_slot_wrapper(name, value): yield name, True +@contextlib.contextmanager +def no_color(): + import _colorize + from .os_helper import EnvironmentVarGuard + + with ( + swap_attr(_colorize, "can_colorize", lambda: False), + EnvironmentVarGuard() as env, + ): + for var in {"FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS"}: + env.unset(var) + env.set("NO_COLOR", "1") + yield + + def force_not_colorized(func): """Force the terminal not to be colorized.""" @functools.wraps(func) def wrapper(*args, **kwargs): - import _colorize - from .os_helper import EnvironmentVarGuard - - with ( - swap_attr(_colorize, "can_colorize", lambda: False), - EnvironmentVarGuard() as env, - ): - for var in {"FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS"}: - env.unset(var) - env.set("NO_COLOR", "1") - + with no_color(): return func(*args, **kwargs) - return wrapper @@ -2860,19 +2864,8 @@ def force_not_colorized_test_class(cls): @classmethod @functools.wraps(cls.setUpClass) def new_setUpClass(cls): - import _colorize - from .os_helper import EnvironmentVarGuard - - cls.enterClassContext( - swap_attr(_colorize, "can_colorize", lambda: False) - ) - env = cls.enterClassContext(EnvironmentVarGuard()) - for var in {"FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS"}: - env.unset(var) - env.set("NO_COLOR", "1") - - if original_setUpClass: - original_setUpClass() + cls.enterClassContext(no_color()) + original_setUpClass() cls.setUpClass = new_setUpClass return cls