Skip to content

Commit 345e1e0

Browse files
authored
gh-112730: Make the test suite resilient to color-activation environment variables (#117672)
1 parent 59a4d52 commit 345e1e0

File tree

13 files changed

+89
-16
lines changed

13 files changed

+89
-16
lines changed

.github/workflows/reusable-ubuntu.yml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jobs:
1414
timeout-minutes: 60
1515
runs-on: ubuntu-20.04
1616
env:
17+
FORCE_COLOR: 1
1718
OPENSSL_VER: 3.0.13
1819
PYTHONSTRICTEXTENSIONBUILD: 1
1920
steps:

Lib/doctest.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1556,7 +1556,11 @@ def out(s):
15561556
# Make sure sys.displayhook just prints the value to stdout
15571557
save_displayhook = sys.displayhook
15581558
sys.displayhook = sys.__displayhook__
1559-
1559+
saved_can_colorize = traceback._can_colorize
1560+
traceback._can_colorize = lambda: False
1561+
color_variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
1562+
for key in color_variables:
1563+
color_variables[key] = os.environ.pop(key, None)
15601564
try:
15611565
return self.__run(test, compileflags, out)
15621566
finally:
@@ -1565,6 +1569,10 @@ def out(s):
15651569
sys.settrace(save_trace)
15661570
linecache.getlines = self.save_linecache_getlines
15671571
sys.displayhook = save_displayhook
1572+
traceback._can_colorize = saved_can_colorize
1573+
for key, value in color_variables.items():
1574+
if value is not None:
1575+
os.environ[key] = value
15681576
if clear_globs:
15691577
test.globs.clear()
15701578
import builtins

Lib/idlelib/idle_test/test_run.py

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from unittest import mock
99
import idlelib
1010
from idlelib.idle_test.mock_idle import Func
11+
from test.support import force_not_colorized
1112

1213
idlelib.testing = True # Use {} for executing test user code.
1314

@@ -46,6 +47,7 @@ def __eq__(self, other):
4647
"Did you mean: 'real'?\n"),
4748
)
4849

50+
@force_not_colorized
4951
def test_get_message(self):
5052
for code, exc, msg in self.data:
5153
with self.subTest(code=code):
@@ -57,6 +59,7 @@ def test_get_message(self):
5759
expect = f'{exc.__name__}: {msg}'
5860
self.assertEqual(actual, expect)
5961

62+
@force_not_colorized
6063
@mock.patch.object(run, 'cleanup_traceback',
6164
new_callable=lambda: (lambda t, e: None))
6265
def test_get_multiple_message(self, mock):

Lib/test/support/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"Py_DEBUG", "exceeds_recursion_limit", "get_c_recursion_limit",
6060
"skip_on_s390x",
6161
"without_optimizer",
62+
"force_not_colorized"
6263
]
6364

6465

@@ -2557,3 +2558,22 @@ def copy_python_src_ignore(path, names):
25572558
'build',
25582559
}
25592560
return ignored
2561+
2562+
def force_not_colorized(func):
2563+
"""Force the terminal not to be colorized."""
2564+
@functools.wraps(func)
2565+
def wrapper(*args, **kwargs):
2566+
import traceback
2567+
original_fn = traceback._can_colorize
2568+
variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
2569+
try:
2570+
for key in variables:
2571+
variables[key] = os.environ.pop(key, None)
2572+
traceback._can_colorize = lambda: False
2573+
return func(*args, **kwargs)
2574+
finally:
2575+
traceback._can_colorize = original_fn
2576+
for key, value in variables.items():
2577+
if value is not None:
2578+
os.environ[key] = value
2579+
return wrapper

Lib/test/test_cmd_line.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import unittest
1111
from test import support
1212
from test.support import os_helper
13+
from test.support import force_not_colorized
1314
from test.support.script_helper import (
1415
spawn_python, kill_python, assert_python_ok, assert_python_failure,
1516
interpreter_requires_environment
@@ -1027,6 +1028,7 @@ def test_sys_flags_not_set(self):
10271028

10281029

10291030
class SyntaxErrorTests(unittest.TestCase):
1031+
@force_not_colorized
10301032
def check_string(self, code):
10311033
proc = subprocess.run([sys.executable, "-"], input=code,
10321034
stdout=subprocess.PIPE, stderr=subprocess.PIPE)

Lib/test/test_exceptions.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from test.support import (captured_stderr, check_impl_detail,
1313
cpython_only, gc_collect,
1414
no_tracing, script_helper,
15-
SuppressCrashReport)
15+
SuppressCrashReport,
16+
force_not_colorized)
1617
from test.support.import_helper import import_module
1718
from test.support.os_helper import TESTFN, unlink
1819
from test.support.warnings_helper import check_warnings
@@ -41,6 +42,7 @@ def __str__(self):
4142

4243
# XXX This is not really enough, each *operation* should be tested!
4344

45+
4446
class ExceptionTests(unittest.TestCase):
4547

4648
def raise_catch(self, exc, excname):
@@ -1994,6 +1996,7 @@ def write_source(self, source):
19941996
_rc, _out, err = script_helper.assert_python_failure('-Wd', '-X', 'utf8', TESTFN)
19951997
return err.decode('utf-8').splitlines()
19961998

1999+
@force_not_colorized
19972000
def test_assertion_error_location(self):
19982001
cases = [
19992002
('assert None',
@@ -2070,6 +2073,7 @@ def test_assertion_error_location(self):
20702073
result = self.write_source(source)
20712074
self.assertEqual(result[-3:], expected)
20722075

2076+
@force_not_colorized
20732077
def test_multiline_not_highlighted(self):
20742078
cases = [
20752079
("""
@@ -2102,6 +2106,7 @@ def test_multiline_not_highlighted(self):
21022106

21032107

21042108
class SyntaxErrorTests(unittest.TestCase):
2109+
@force_not_colorized
21052110
def test_range_of_offsets(self):
21062111
cases = [
21072112
# Basic range from 2->7

Lib/test/test_interpreters/test_api.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
_interpreters = import_helper.import_module('_interpreters')
1313
from test.support import Py_GIL_DISABLED
1414
from test.support import interpreters
15+
from test.support import force_not_colorized
1516
from test.support.interpreters import (
1617
InterpreterError, InterpreterNotFoundError, ExecutionFailed,
1718
)
@@ -735,6 +736,7 @@ def test_failure(self):
735736
with self.assertRaises(ExecutionFailed):
736737
interp.exec('raise Exception')
737738

739+
@force_not_colorized
738740
def test_display_preserved_exception(self):
739741
tempdir = self.temp_dir()
740742
modfile = self.make_module('spam', tempdir, text="""

Lib/test/test_sys.py

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from test.support.script_helper import assert_python_ok, assert_python_failure
1717
from test.support import threading_helper
1818
from test.support import import_helper
19+
from test.support import force_not_colorized
1920
try:
2021
from test.support import interpreters
2122
except ImportError:
@@ -145,6 +146,7 @@ def f():
145146

146147
class ExceptHookTest(unittest.TestCase):
147148

149+
@force_not_colorized
148150
def test_original_excepthook(self):
149151
try:
150152
raise ValueError(42)
@@ -156,6 +158,7 @@ def test_original_excepthook(self):
156158

157159
self.assertRaises(TypeError, sys.__excepthook__)
158160

161+
@force_not_colorized
159162
def test_excepthook_bytes_filename(self):
160163
# bpo-37467: sys.excepthook() must not crash if a filename
161164
# is a bytes string
@@ -793,6 +796,7 @@ def test_sys_getwindowsversion_no_instantiation(self):
793796
def test_clear_type_cache(self):
794797
sys._clear_type_cache()
795798

799+
@force_not_colorized
796800
@support.requires_subprocess()
797801
def test_ioencoding(self):
798802
env = dict(os.environ)
@@ -1108,6 +1112,7 @@ def test_getandroidapilevel(self):
11081112
self.assertIsInstance(level, int)
11091113
self.assertGreater(level, 0)
11101114

1115+
@force_not_colorized
11111116
@support.requires_subprocess()
11121117
def test_sys_tracebacklimit(self):
11131118
code = """if 1:

Lib/test/test_threading.py

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from test.support import verbose, cpython_only, os_helper
88
from test.support.import_helper import import_module
99
from test.support.script_helper import assert_python_ok, assert_python_failure
10+
from test.support import force_not_colorized
1011

1112
import random
1213
import sys
@@ -1793,6 +1794,7 @@ def setUp(self):
17931794
restore_default_excepthook(self)
17941795
super().setUp()
17951796

1797+
@force_not_colorized
17961798
def test_excepthook(self):
17971799
with support.captured_output("stderr") as stderr:
17981800
thread = ThreadRunFail(name="excepthook thread")
@@ -1806,6 +1808,7 @@ def test_excepthook(self):
18061808
self.assertIn('ValueError: run failed', stderr)
18071809

18081810
@support.cpython_only
1811+
@force_not_colorized
18091812
def test_excepthook_thread_None(self):
18101813
# threading.excepthook called with thread=None: log the thread
18111814
# identifier in this case.

Lib/test/test_traceback.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from test.support.os_helper import TESTFN, unlink
2222
from test.support.script_helper import assert_python_ok, assert_python_failure
2323
from test.support.import_helper import forget
24+
from test.support import force_not_colorized
2425

2526
import json
2627
import textwrap
@@ -39,6 +40,13 @@
3940

4041
LEVENSHTEIN_DATA_FILE = Path(__file__).parent / 'levenshtein_examples.json'
4142

43+
ORIGINAL_CAN_COLORIZE = traceback._can_colorize
44+
45+
def setUpModule():
46+
traceback._can_colorize = lambda: False
47+
48+
def tearDownModule():
49+
traceback._can_colorize = ORIGINAL_CAN_COLORIZE
4250

4351
class TracebackCases(unittest.TestCase):
4452
# For now, a very minimal set of tests. I want to be sure that
@@ -124,6 +132,7 @@ def test_nocaret(self):
124132
self.assertEqual(len(err), 3)
125133
self.assertEqual(err[1].strip(), "bad syntax")
126134

135+
@force_not_colorized
127136
def test_no_caret_with_no_debug_ranges_flag(self):
128137
# Make sure that if `-X no_debug_ranges` is used, there are no carets
129138
# in the traceback.
@@ -401,7 +410,7 @@ def do_test(firstlines, message, charset, lineno):
401410
""".format(firstlines, message))
402411

403412
process = subprocess.Popen([sys.executable, TESTFN],
404-
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
413+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env={})
405414
stdout, stderr = process.communicate()
406415
stdout = stdout.decode(output_encoding).splitlines()
407416
finally:
@@ -4354,13 +4363,18 @@ def foo():
43544363
f'{boldm}ZeroDivisionError{reset}: {magenta}division by zero{reset}']
43554364
self.assertEqual(actual, expected)
43564365

4366+
@force_not_colorized
43574367
def test_colorized_detection_checks_for_environment_variables(self):
43584368
if sys.platform == "win32":
43594369
virtual_patching = unittest.mock.patch("nt._supports_virtual_terminal", return_value=True)
43604370
else:
43614371
virtual_patching = contextlib.nullcontext()
43624372
with virtual_patching:
4363-
with unittest.mock.patch("os.isatty") as isatty_mock:
4373+
4374+
flags = unittest.mock.MagicMock(ignore_environment=False)
4375+
with (unittest.mock.patch("os.isatty") as isatty_mock,
4376+
unittest.mock.patch("sys.flags", flags),
4377+
unittest.mock.patch("traceback._can_colorize", ORIGINAL_CAN_COLORIZE)):
43644378
isatty_mock.return_value = True
43654379
with unittest.mock.patch("os.environ", {'TERM': 'dumb'}):
43664380
self.assertEqual(traceback._can_colorize(), False)
@@ -4379,7 +4393,8 @@ def test_colorized_detection_checks_for_environment_variables(self):
43794393
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}):
43804394
self.assertEqual(traceback._can_colorize(), False)
43814395
isatty_mock.return_value = False
4382-
self.assertEqual(traceback._can_colorize(), False)
4396+
with unittest.mock.patch("os.environ", {}):
4397+
self.assertEqual(traceback._can_colorize(), False)
43834398

43844399
if __name__ == "__main__":
43854400
unittest.main()

Lib/test/test_tracemalloc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,7 @@ def check_env_var_invalid(self, nframe):
942942
with support.SuppressCrashReport():
943943
ok, stdout, stderr = assert_python_failure(
944944
'-c', 'pass',
945-
PYTHONTRACEMALLOC=str(nframe))
945+
PYTHONTRACEMALLOC=str(nframe), __cleanenv=True)
946946

947947
if b'ValueError: the number of frames must be in range' in stderr:
948948
return

Lib/test/test_warnings/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from test.support import import_helper
1313
from test.support import os_helper
1414
from test.support import warnings_helper
15+
from test.support import force_not_colorized
1516
from test.support.script_helper import assert_python_ok, assert_python_failure
1617

1718
from test.test_warnings.data import package_helper
@@ -1239,6 +1240,7 @@ def test_comma_separated_warnings(self):
12391240
self.assertEqual(stdout,
12401241
b"['ignore::DeprecationWarning', 'ignore::UnicodeWarning']")
12411242

1243+
@force_not_colorized
12421244
def test_envvar_and_command_line(self):
12431245
rc, stdout, stderr = assert_python_ok("-Wignore::UnicodeWarning", "-c",
12441246
"import sys; sys.stdout.write(str(sys.warnoptions))",
@@ -1247,6 +1249,7 @@ def test_envvar_and_command_line(self):
12471249
self.assertEqual(stdout,
12481250
b"['ignore::DeprecationWarning', 'ignore::UnicodeWarning']")
12491251

1252+
@force_not_colorized
12501253
def test_conflicting_envvar_and_command_line(self):
12511254
rc, stdout, stderr = assert_python_failure("-Werror::DeprecationWarning", "-c",
12521255
"import sys, warnings; sys.stdout.write(str(sys.warnoptions)); "

Lib/traceback.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -141,24 +141,30 @@ def _can_colorize():
141141
return False
142142
except (ImportError, AttributeError):
143143
return False
144-
145-
if os.environ.get("PYTHON_COLORS") == "0":
146-
return False
147-
if os.environ.get("PYTHON_COLORS") == "1":
148-
return True
149-
if "NO_COLOR" in os.environ:
150-
return False
144+
if not sys.flags.ignore_environment:
145+
if os.environ.get("PYTHON_COLORS") == "0":
146+
return False
147+
if os.environ.get("PYTHON_COLORS") == "1":
148+
return True
149+
if "NO_COLOR" in os.environ:
150+
return False
151151
if not _COLORIZE:
152152
return False
153-
if "FORCE_COLOR" in os.environ:
154-
return True
155-
if os.environ.get("TERM") == "dumb":
153+
if not sys.flags.ignore_environment:
154+
if "FORCE_COLOR" in os.environ:
155+
return True
156+
if os.environ.get("TERM") == "dumb":
157+
return False
158+
159+
if not hasattr(sys.stderr, "fileno"):
156160
return False
161+
157162
try:
158163
return os.isatty(sys.stderr.fileno())
159164
except io.UnsupportedOperation:
160165
return sys.stderr.isatty()
161166

167+
162168
def _print_exception_bltin(exc, /):
163169
file = sys.stderr if sys.stderr is not None else sys.__stderr__
164170
colorize = _can_colorize()

0 commit comments

Comments
 (0)