https://github.com/python/cpython/commit/6f167d71347de6717d9f6b64026e21f23d41ef0b
commit: 6f167d71347de6717d9f6b64026e21f23d41ef0b
branch: main
author: Hugo van Kemenade <[email protected]>
committer: hugovk <[email protected]>
date: 2025-01-20T12:52:42+02:00
summary:

gh-128595: Default to stdout isatty for colour detection instead of stderr 
(#128498)

Co-authored-by: Serhiy Storchaka <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-01-07-21-48-32.gh-issue-128498.n6jtlW.rst
M Lib/_colorize.py
M Lib/doctest.py
M Lib/test/libregrtest/single.py
M Lib/test/support/__init__.py
M Lib/traceback.py
M Lib/unittest/result.py
M Lib/unittest/runner.py

diff --git a/Lib/_colorize.py b/Lib/_colorize.py
index f609901887a26b..bab2e599b2c810 100644
--- a/Lib/_colorize.py
+++ b/Lib/_colorize.py
@@ -26,14 +26,17 @@ 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 colorize or can_colorize(file=file):
         return ANSIColors()
     else:
         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.stderr, "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.stderr.fileno())
+        return os.isatty(file.fileno())
     except io.UnsupportedOperation:
-        return sys.stderr.isatty()
+        return file.isatty()
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/test/libregrtest/single.py b/Lib/test/libregrtest/single.py
index 0e174f82abed28..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()
-    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,18 +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}", flush=True)
+            print(
+                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
@@ -203,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
@@ -220,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
 
@@ -303,7 +312,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/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)
 
 
diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py
index 97262735aa8311..b8ea396db6772e 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()))
+        colorize = hasattr(self, "stream") and can_colorize(file=self.stream)
+        msgLines = list(tb_e.format(colorize=colorize))
 
         if self.buffer:
             output = sys.stdout.getvalue()
diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py
index d60c295a1eddf7..eb0234a2617680 100644
--- a/Lib/unittest/runner.py
+++ b/Lib/unittest/runner.py
@@ -45,7 +45,7 @@ def __init__(self, stream, descriptions, verbosity, *, 
durations=None):
         self.showAll = verbosity > 1
         self.dots = verbosity == 1
         self.descriptions = descriptions
-        self._ansi = get_colors()
+        self._ansi = get_colors(file=stream)
         self._newline = True
         self.durations = durations
 
@@ -286,7 +286,7 @@ def run(self, test):
             expected_fails, unexpected_successes, skipped = results
 
         infos = []
-        ansi = get_colors()
+        ansi = get_colors(file=self.stream)
         bold_red = ansi.BOLD_RED
         green = ansi.GREEN
         red = ansi.RED
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..9a241e37c20a44
--- /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 color detection instead of stderr. Patch by
+Hugo van Kemenade.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to