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

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
This commit is contained in:
Hugo van Kemenade 2025-01-20 12:52:42 +02:00 committed by GitHub
parent a429159797
commit 6f167d7134
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 38 additions and 23 deletions

View file

@ -26,14 +26,17 @@ for attr in dir(NoColors):
setattr(NoColors, attr, "") setattr(NoColors, attr, "")
def get_colors(colorize: bool = False) -> ANSIColors: def get_colors(colorize: bool = False, *, file=None) -> ANSIColors:
if colorize or can_colorize(): if colorize or can_colorize(file=file):
return ANSIColors() return ANSIColors()
else: else:
return NoColors 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 not sys.flags.ignore_environment:
if os.environ.get("PYTHON_COLORS") == "0": if os.environ.get("PYTHON_COLORS") == "0":
return False return False
@ -49,7 +52,7 @@ def can_colorize() -> bool:
if os.environ.get("TERM") == "dumb": if os.environ.get("TERM") == "dumb":
return False return False
if not hasattr(sys.stderr, "fileno"): if not hasattr(file, "fileno"):
return False return False
if sys.platform == "win32": if sys.platform == "win32":
@ -62,6 +65,6 @@ def can_colorize() -> bool:
return False return False
try: try:
return os.isatty(sys.stderr.fileno()) return os.isatty(file.fileno())
except io.UnsupportedOperation: except io.UnsupportedOperation:
return sys.stderr.isatty() return file.isatty()

View file

@ -1558,7 +1558,7 @@ class DocTestRunner:
save_displayhook = sys.displayhook save_displayhook = sys.displayhook
sys.displayhook = sys.__displayhook__ sys.displayhook = sys.__displayhook__
saved_can_colorize = _colorize.can_colorize 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} color_variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
for key in color_variables: for key in color_variables:
color_variables[key] = os.environ.pop(key, None) color_variables[key] = os.environ.pop(key, None)

View file

@ -162,8 +162,8 @@ def _load_run_test(result: TestResult, runtests: RunTests) -> None:
def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
display_failure: bool = True) -> None: display_failure: bool = True) -> None:
# Handle exceptions, detect environment changes. # Handle exceptions, detect environment changes.
ansi = get_colors() stdout = get_colors(file=sys.stdout)
red, reset, yellow = ansi.RED, ansi.RESET, ansi.YELLOW stderr = get_colors(file=sys.stderr)
# Reset the environment_altered flag to detect if a test altered # Reset the environment_altered flag to detect if a test altered
# the environment # the environment
@ -184,18 +184,24 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
_load_run_test(result, runtests) _load_run_test(result, runtests)
except support.ResourceDenied as exc: except support.ResourceDenied as exc:
if not quiet and not pgo: 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 result.state = State.RESOURCE_DENIED
return return
except unittest.SkipTest as exc: except unittest.SkipTest as exc:
if not quiet and not pgo: 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 result.state = State.SKIPPED
return return
except support.TestFailedWithDetails as exc: 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: if display_failure:
msg = f"{red}{msg} -- {exc}{reset}" msg = f"{stderr.RED}{msg} -- {exc}{stderr.RESET}"
print(msg, file=sys.stderr, flush=True) print(msg, file=sys.stderr, flush=True)
result.state = State.FAILED result.state = State.FAILED
result.errors = exc.errors result.errors = exc.errors
@ -203,9 +209,9 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
result.stats = exc.stats result.stats = exc.stats
return return
except support.TestFailed as exc: 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: if display_failure:
msg = f"{red}{msg} -- {exc}{reset}" msg = f"{stderr.RED}{msg} -- {exc}{stderr.RESET}"
print(msg, file=sys.stderr, flush=True) print(msg, file=sys.stderr, flush=True)
result.state = State.FAILED result.state = State.FAILED
result.stats = exc.stats result.stats = exc.stats
@ -220,8 +226,11 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
except: except:
if not pgo: if not pgo:
msg = traceback.format_exc() msg = traceback.format_exc()
print(f"{red}test {test_name} crashed -- {msg}{reset}", print(
file=sys.stderr, flush=True) f"{stderr.RED}test {test_name} crashed -- {msg}{stderr.RESET}",
file=sys.stderr,
flush=True,
)
result.state = State.UNCAUGHT_EXC result.state = State.UNCAUGHT_EXC
return 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 If runtests.use_junit, xml_data is a list containing each generated
testsuite element. testsuite element.
""" """
ansi = get_colors() ansi = get_colors(file=sys.stderr)
red, reset, yellow = ansi.BOLD_RED, ansi.RESET, ansi.YELLOW red, reset, yellow = ansi.BOLD_RED, ansi.RESET, ansi.YELLOW
start_time = time.perf_counter() start_time = time.perf_counter()

View file

@ -2839,7 +2839,7 @@ def no_color():
from .os_helper import EnvironmentVarGuard from .os_helper import EnvironmentVarGuard
with ( with (
swap_attr(_colorize, "can_colorize", lambda: False), swap_attr(_colorize, "can_colorize", lambda file=None: False),
EnvironmentVarGuard() as env, EnvironmentVarGuard() as env,
): ):
for var in {"FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS"}: for var in {"FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS"}:

View file

@ -135,7 +135,7 @@ BUILTIN_EXCEPTION_LIMIT = object()
def _print_exception_bltin(exc, /): def _print_exception_bltin(exc, /):
file = sys.stderr if sys.stderr is not None else sys.__stderr__ 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) return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)

View file

@ -191,7 +191,8 @@ class TestResult(object):
capture_locals=self.tb_locals, compact=True) capture_locals=self.tb_locals, compact=True)
from _colorize import can_colorize 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: if self.buffer:
output = sys.stdout.getvalue() output = sys.stdout.getvalue()

View file

@ -45,7 +45,7 @@ class TextTestResult(result.TestResult):
self.showAll = verbosity > 1 self.showAll = verbosity > 1
self.dots = verbosity == 1 self.dots = verbosity == 1
self.descriptions = descriptions self.descriptions = descriptions
self._ansi = get_colors() self._ansi = get_colors(file=stream)
self._newline = True self._newline = True
self.durations = durations self.durations = durations
@ -286,7 +286,7 @@ class TextTestRunner(object):
expected_fails, unexpected_successes, skipped = results expected_fails, unexpected_successes, skipped = results
infos = [] infos = []
ansi = get_colors() ansi = get_colors(file=self.stream)
bold_red = ansi.BOLD_RED bold_red = ansi.BOLD_RED
green = ansi.GREEN green = ansi.GREEN
red = ansi.RED red = ansi.RED

View file

@ -0,0 +1,2 @@
Default to stdout isatty for color detection instead of stderr. Patch by
Hugo van Kemenade.