Skip to content

Commit 1e0a640

Browse files
hugovkSonicField
authored andcommitted
pythongh-117225: Move colorize functionality to own internal module (python#118283)
1 parent 7e10b3f commit 1e0a640

File tree

8 files changed

+218
-180
lines changed

8 files changed

+218
-180
lines changed

Lib/_colorize.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import io
2+
import os
3+
import sys
4+
5+
COLORIZE = True
6+
7+
8+
class ANSIColors:
9+
BOLD_GREEN = "\x1b[1;32m"
10+
BOLD_MAGENTA = "\x1b[1;35m"
11+
BOLD_RED = "\x1b[1;31m"
12+
GREEN = "\x1b[32m"
13+
GREY = "\x1b[90m"
14+
MAGENTA = "\x1b[35m"
15+
RED = "\x1b[31m"
16+
RESET = "\x1b[0m"
17+
YELLOW = "\x1b[33m"
18+
19+
20+
NoColors = ANSIColors()
21+
22+
for attr in dir(NoColors):
23+
if not attr.startswith("__"):
24+
setattr(NoColors, attr, "")
25+
26+
27+
def get_colors(colorize: bool = False) -> ANSIColors:
28+
if colorize or can_colorize():
29+
return ANSIColors()
30+
else:
31+
return NoColors
32+
33+
34+
def can_colorize() -> bool:
35+
if sys.platform == "win32":
36+
try:
37+
import nt
38+
39+
if not nt._supports_virtual_terminal():
40+
return False
41+
except (ImportError, AttributeError):
42+
return False
43+
if not sys.flags.ignore_environment:
44+
if os.environ.get("PYTHON_COLORS") == "0":
45+
return False
46+
if os.environ.get("PYTHON_COLORS") == "1":
47+
return True
48+
if "NO_COLOR" in os.environ:
49+
return False
50+
if not COLORIZE:
51+
return False
52+
if not sys.flags.ignore_environment:
53+
if "FORCE_COLOR" in os.environ:
54+
return True
55+
if os.environ.get("TERM") == "dumb":
56+
return False
57+
58+
if not hasattr(sys.stderr, "fileno"):
59+
return False
60+
61+
try:
62+
return os.isatty(sys.stderr.fileno())
63+
except io.UnsupportedOperation:
64+
return sys.stderr.isatty()

Lib/doctest.py

+16-22
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def _test():
104104
import unittest
105105
from io import StringIO, IncrementalNewlineDecoder
106106
from collections import namedtuple
107-
from traceback import _ANSIColors, _can_colorize
107+
import _colorize # Used in doctests
108+
from _colorize import ANSIColors, can_colorize
108109

109110

110111
class TestResults(namedtuple('TestResults', 'failed attempted')):
@@ -1180,8 +1181,8 @@ class DocTestRunner:
11801181
The `run` method is used to process a single DocTest case. It
11811182
returns a TestResults instance.
11821183
1183-
>>> save_colorize = traceback._COLORIZE
1184-
>>> traceback._COLORIZE = False
1184+
>>> save_colorize = _colorize.COLORIZE
1185+
>>> _colorize.COLORIZE = False
11851186
11861187
>>> tests = DocTestFinder().find(_TestClass)
11871188
>>> runner = DocTestRunner(verbose=False)
@@ -1234,7 +1235,7 @@ class DocTestRunner:
12341235
overriding the methods `report_start`, `report_success`,
12351236
`report_unexpected_exception`, and `report_failure`.
12361237
1237-
>>> traceback._COLORIZE = save_colorize
1238+
>>> _colorize.COLORIZE = save_colorize
12381239
"""
12391240
# This divider string is used to separate failure messages, and to
12401241
# separate sections of the summary.
@@ -1314,7 +1315,7 @@ def report_unexpected_exception(self, out, test, example, exc_info):
13141315

13151316
def _failure_header(self, test, example):
13161317
red, reset = (
1317-
(_ANSIColors.RED, _ANSIColors.RESET) if _can_colorize() else ("", "")
1318+
(ANSIColors.RED, ANSIColors.RESET) if can_colorize() else ("", "")
13181319
)
13191320
out = [f"{red}{self.DIVIDER}{reset}"]
13201321
if test.filename:
@@ -1556,8 +1557,8 @@ def out(s):
15561557
# Make sure sys.displayhook just prints the value to stdout
15571558
save_displayhook = sys.displayhook
15581559
sys.displayhook = sys.__displayhook__
1559-
saved_can_colorize = traceback._can_colorize
1560-
traceback._can_colorize = lambda: False
1560+
saved_can_colorize = _colorize.can_colorize
1561+
_colorize.can_colorize = lambda: False
15611562
color_variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
15621563
for key in color_variables:
15631564
color_variables[key] = os.environ.pop(key, None)
@@ -1569,7 +1570,7 @@ def out(s):
15691570
sys.settrace(save_trace)
15701571
linecache.getlines = self.save_linecache_getlines
15711572
sys.displayhook = save_displayhook
1572-
traceback._can_colorize = saved_can_colorize
1573+
_colorize.can_colorize = saved_can_colorize
15731574
for key, value in color_variables.items():
15741575
if value is not None:
15751576
os.environ[key] = value
@@ -1609,20 +1610,13 @@ def summarize(self, verbose=None):
16091610
else:
16101611
failed.append((name, (failures, tries, skips)))
16111612

1612-
if _can_colorize():
1613-
bold_green = _ANSIColors.BOLD_GREEN
1614-
bold_red = _ANSIColors.BOLD_RED
1615-
green = _ANSIColors.GREEN
1616-
red = _ANSIColors.RED
1617-
reset = _ANSIColors.RESET
1618-
yellow = _ANSIColors.YELLOW
1619-
else:
1620-
bold_green = ""
1621-
bold_red = ""
1622-
green = ""
1623-
red = ""
1624-
reset = ""
1625-
yellow = ""
1613+
ansi = _colorize.get_colors()
1614+
bold_green = ansi.BOLD_GREEN
1615+
bold_red = ansi.BOLD_RED
1616+
green = ansi.GREEN
1617+
red = ansi.RED
1618+
reset = ansi.RESET
1619+
yellow = ansi.YELLOW
16261620

16271621
if verbose:
16281622
if notests:

Lib/test/support/__init__.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -2579,20 +2579,21 @@ def copy_python_src_ignore(path, names):
25792579
}
25802580
return ignored
25812581

2582+
25822583
def force_not_colorized(func):
25832584
"""Force the terminal not to be colorized."""
25842585
@functools.wraps(func)
25852586
def wrapper(*args, **kwargs):
2586-
import traceback
2587-
original_fn = traceback._can_colorize
2587+
import _colorize
2588+
original_fn = _colorize.can_colorize
25882589
variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
25892590
try:
25902591
for key in variables:
25912592
variables[key] = os.environ.pop(key, None)
2592-
traceback._can_colorize = lambda: False
2593+
_colorize.can_colorize = lambda: False
25932594
return func(*args, **kwargs)
25942595
finally:
2595-
traceback._can_colorize = original_fn
2596+
_colorize.can_colorize = original_fn
25962597
for key, value in variables.items():
25972598
if value is not None:
25982599
os.environ[key] = value

Lib/test/test__colorize.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import contextlib
2+
import sys
3+
import unittest
4+
import unittest.mock
5+
import _colorize
6+
from test.support import force_not_colorized
7+
8+
ORIGINAL_CAN_COLORIZE = _colorize.can_colorize
9+
10+
11+
def setUpModule():
12+
_colorize.can_colorize = lambda: False
13+
14+
15+
def tearDownModule():
16+
_colorize.can_colorize = ORIGINAL_CAN_COLORIZE
17+
18+
19+
class TestColorizeFunction(unittest.TestCase):
20+
@force_not_colorized
21+
def test_colorized_detection_checks_for_environment_variables(self):
22+
if sys.platform == "win32":
23+
virtual_patching = unittest.mock.patch("nt._supports_virtual_terminal",
24+
return_value=True)
25+
else:
26+
virtual_patching = contextlib.nullcontext()
27+
with virtual_patching:
28+
29+
flags = unittest.mock.MagicMock(ignore_environment=False)
30+
with (unittest.mock.patch("os.isatty") as isatty_mock,
31+
unittest.mock.patch("sys.flags", flags),
32+
unittest.mock.patch("_colorize.can_colorize", ORIGINAL_CAN_COLORIZE)):
33+
isatty_mock.return_value = True
34+
with unittest.mock.patch("os.environ", {'TERM': 'dumb'}):
35+
self.assertEqual(_colorize.can_colorize(), False)
36+
with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '1'}):
37+
self.assertEqual(_colorize.can_colorize(), True)
38+
with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '0'}):
39+
self.assertEqual(_colorize.can_colorize(), False)
40+
with unittest.mock.patch("os.environ", {'NO_COLOR': '1'}):
41+
self.assertEqual(_colorize.can_colorize(), False)
42+
with unittest.mock.patch("os.environ",
43+
{'NO_COLOR': '1', "PYTHON_COLORS": '1'}):
44+
self.assertEqual(_colorize.can_colorize(), True)
45+
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1'}):
46+
self.assertEqual(_colorize.can_colorize(), True)
47+
with unittest.mock.patch("os.environ",
48+
{'FORCE_COLOR': '1', 'NO_COLOR': '1'}):
49+
self.assertEqual(_colorize.can_colorize(), False)
50+
with unittest.mock.patch("os.environ",
51+
{'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}):
52+
self.assertEqual(_colorize.can_colorize(), False)
53+
isatty_mock.return_value = False
54+
with unittest.mock.patch("os.environ", {}):
55+
self.assertEqual(_colorize.can_colorize(), False)
56+
57+
58+
if __name__ == "__main__":
59+
unittest.main()

0 commit comments

Comments
 (0)