gh-112730: Use color to highlight error locations (gh-112732)

Signed-off-by: Pablo Galindo <pablogsal@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
Pablo Galindo Salgado 2023-12-06 22:29:54 +00:00 committed by GitHub
parent 3870d19d15
commit 16448cab44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 369 additions and 40 deletions

View File

@ -612,6 +612,27 @@ Miscellaneous options
.. versionadded:: 3.13
The ``-X presite`` option.
Controlling Color
~~~~~~~~~~~~~~~~~
The Python interpreter is configured by default to use colors to highlight
output in certain situations such as when displaying tracebacks. This
behavior can be controlled by setting different environment variables.
Setting the environment variable ``TERM`` to ``dumb`` will disable color.
If the environment variable ``FORCE_COLOR`` is set, then color will be
enabled regardless of the value of TERM. This is useful on CI systems which
arent terminals but can none-the-less display ANSI escape sequences.
If the environment variable ``NO_COLOR`` is set, Python will disable all color
in the output. This takes precedence over ``FORCE_COLOR``.
All these environment variables are used also by other tools to control color
output. To control the color output only in the Python interpreter, the
:envvar:`PYTHON_COLORS` environment variable can be used. This variable takes
precedence over ``NO_COLOR``, which in turn takes precedence over
``FORCE_COLOR``.
Options you shouldn't use
~~~~~~~~~~~~~~~~~~~~~~~~~
@ -1110,6 +1131,12 @@ conflict.
.. versionadded:: 3.13
.. envvar:: PYTHON_COLORS
If this variable is set to ``1``, the interpreter will colorize various kinds
of output. Setting it to ``0`` deactivates this behavior.
.. versionadded:: 3.13
Debug-mode variables
~~~~~~~~~~~~~~~~~~~~

View File

@ -85,7 +85,13 @@ Important deprecations, removals or restrictions:
New Features
============
Improved Error Messages
-----------------------
* The interpreter now colorizes error messages when displaying tracebacks by default.
This feature can be controlled via the new :envvar:`PYTHON_COLORS` environment
variable as well as the canonical ``NO_COLOR`` and ``FORCE_COLOR`` environment
variables. (Contributed by Pablo Galindo Salgado in :gh:`112730`.)
Other Language Changes
======================

View File

@ -8,6 +8,7 @@ import types
import inspect
import builtins
import unittest
import unittest.mock
import re
import tempfile
import random
@ -24,6 +25,7 @@ from test.support.import_helper import forget
import json
import textwrap
import traceback
import contextlib
from functools import partial
from pathlib import Path
@ -41,6 +43,14 @@ LEVENSHTEIN_DATA_FILE = Path(__file__).parent / 'levenshtein_examples.json'
class TracebackCases(unittest.TestCase):
# For now, a very minimal set of tests. I want to be sure that
# formatting of SyntaxErrors works based on changes for 2.1.
def setUp(self):
super().setUp()
self.colorize = traceback._COLORIZE
traceback._COLORIZE = False
def tearDown(self):
super().tearDown()
traceback._COLORIZE = self.colorize
def get_exception_format(self, func, exc):
try:
@ -521,7 +531,7 @@ class TracebackCases(unittest.TestCase):
self.assertEqual(
str(inspect.signature(traceback.print_exception)),
('(exc, /, value=<implicit>, tb=<implicit>, '
'limit=None, file=None, chain=True)'))
'limit=None, file=None, chain=True, **kwargs)'))
self.assertEqual(
str(inspect.signature(traceback.format_exception)),
@ -3031,7 +3041,7 @@ class TestStack(unittest.TestCase):
def test_custom_format_frame(self):
class CustomStackSummary(traceback.StackSummary):
def format_frame_summary(self, frame_summary):
def format_frame_summary(self, frame_summary, colorize=False):
return f'{frame_summary.filename}:{frame_summary.lineno}'
def some_inner():
@ -3056,7 +3066,7 @@ class TestStack(unittest.TestCase):
tb = g()
class Skip_G(traceback.StackSummary):
def format_frame_summary(self, frame_summary):
def format_frame_summary(self, frame_summary, colorize=False):
if frame_summary.name == 'g':
return None
return super().format_frame_summary(frame_summary)
@ -3076,7 +3086,6 @@ class Unrepresentable:
raise Exception("Unrepresentable")
class TestTracebackException(unittest.TestCase):
def do_test_smoke(self, exc, expected_type_str):
try:
raise exc
@ -4245,6 +4254,115 @@ class MiscTest(unittest.TestCase):
res3 = traceback._levenshtein_distance(a, b, threshold)
self.assertGreater(res3, threshold, msg=(a, b, threshold))
class TestColorizedTraceback(unittest.TestCase):
def test_colorized_traceback(self):
def foo(*args):
x = {'a':{'b': None}}
y = x['a']['b']['c']
def baz(*args):
return foo(1,2,3,4)
def bar():
return baz(1,
2,3
,4)
try:
bar()
except Exception as e:
exc = traceback.TracebackException.from_exception(
e, capture_locals=True
)
lines = "".join(exc.format(colorize=True))
red = traceback._ANSIColors.RED
boldr = traceback._ANSIColors.BOLD_RED
reset = traceback._ANSIColors.RESET
self.assertIn("y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, lines)
self.assertIn("return " + red + "foo" + reset + boldr + "(1,2,3,4)" + reset, lines)
self.assertIn("return " + red + "baz" + reset + boldr + "(1," + reset, lines)
self.assertIn(boldr + "2,3" + reset, lines)
self.assertIn(boldr + ",4)" + reset, lines)
self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines)
def test_colorized_syntax_error(self):
try:
compile("a $ b", "<string>", "exec")
except SyntaxError as e:
exc = traceback.TracebackException.from_exception(
e, capture_locals=True
)
actual = "".join(exc.format(colorize=True))
red = traceback._ANSIColors.RED
magenta = traceback._ANSIColors.MAGENTA
boldm = traceback._ANSIColors.BOLD_MAGENTA
boldr = traceback._ANSIColors.BOLD_RED
reset = traceback._ANSIColors.RESET
expected = "".join([
f' File {magenta}"<string>"{reset}, line {magenta}1{reset}\n',
f' a {boldr}${reset} b\n',
f' {boldr}^{reset}\n',
f'{boldm}SyntaxError{reset}: {magenta}invalid syntax{reset}\n']
)
self.assertIn(expected, actual)
def test_colorized_traceback_is_the_default(self):
def foo():
1/0
from _testcapi import exception_print
try:
foo()
self.fail("No exception thrown.")
except Exception as e:
with captured_output("stderr") as tbstderr:
with unittest.mock.patch('traceback._can_colorize', return_value=True):
exception_print(e)
actual = tbstderr.getvalue().splitlines()
red = traceback._ANSIColors.RED
boldr = traceback._ANSIColors.BOLD_RED
magenta = traceback._ANSIColors.MAGENTA
boldm = traceback._ANSIColors.BOLD_MAGENTA
reset = traceback._ANSIColors.RESET
lno_foo = foo.__code__.co_firstlineno
expected = ['Traceback (most recent call last):',
f' File {magenta}"{__file__}"{reset}, '
f'line {magenta}{lno_foo+5}{reset}, in {magenta}test_colorized_traceback_is_the_default{reset}',
f' {red}foo{reset+boldr}(){reset}',
f' {red}~~~{reset+boldr}^^{reset}',
f' File {magenta}"{__file__}"{reset}, '
f'line {magenta}{lno_foo+1}{reset}, in {magenta}foo{reset}',
f' {red}1{reset+boldr}/{reset+red}0{reset}',
f' {red}~{reset+boldr}^{reset+red}~{reset}',
f'{boldm}ZeroDivisionError{reset}: {magenta}division by zero{reset}']
self.assertEqual(actual, expected)
def test_colorized_detection_checks_for_environment_variables(self):
if sys.platform == "win32":
virtual_patching = unittest.mock.patch("nt._supports_virtual_terminal", return_value=True)
else:
virtual_patching = contextlib.nullcontext()
with virtual_patching:
with unittest.mock.patch("os.isatty") as isatty_mock:
isatty_mock.return_value = True
with unittest.mock.patch("os.environ", {'TERM': 'dumb'}):
self.assertEqual(traceback._can_colorize(), False)
with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '1'}):
self.assertEqual(traceback._can_colorize(), True)
with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '0'}):
self.assertEqual(traceback._can_colorize(), False)
with unittest.mock.patch("os.environ", {'NO_COLOR': '1'}):
self.assertEqual(traceback._can_colorize(), False)
with unittest.mock.patch("os.environ", {'NO_COLOR': '1', "PYTHON_COLORS": '1'}):
self.assertEqual(traceback._can_colorize(), True)
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1'}):
self.assertEqual(traceback._can_colorize(), True)
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', 'NO_COLOR': '1'}):
self.assertEqual(traceback._can_colorize(), False)
with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}):
self.assertEqual(traceback._can_colorize(), False)
isatty_mock.return_value = False
self.assertEqual(traceback._can_colorize(), False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,5 +1,7 @@
"""Extract, format and print information about Python stack traces."""
import os
import io
import collections.abc
import itertools
import linecache
@ -19,6 +21,8 @@ __all__ = ['extract_stack', 'extract_tb', 'format_exception',
# Formatting and printing lists of traceback lines.
#
_COLORIZE = True
def print_list(extracted_list, file=None):
"""Print the list of tuples as returned by extract_tb() or
extract_stack() as a formatted stack trace to the given file."""
@ -110,7 +114,7 @@ def _parse_value_tb(exc, value, tb):
def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
file=None, chain=True):
file=None, chain=True, **kwargs):
"""Print exception up to 'limit' stack trace entries from 'tb' to 'file'.
This differs from print_tb() in the following ways: (1) if
@ -121,17 +125,44 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
occurred with a caret on the next line indicating the approximate
position of the error.
"""
colorize = kwargs.get("colorize", False)
value, tb = _parse_value_tb(exc, value, tb)
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
te.print(file=file, chain=chain)
te.print(file=file, chain=chain, colorize=colorize)
BUILTIN_EXCEPTION_LIMIT = object()
def _can_colorize():
if sys.platform == "win32":
try:
import nt
if not nt._supports_virtual_terminal():
return False
except (ImportError, AttributeError):
return False
if os.environ.get("PYTHON_COLORS") == "0":
return False
if os.environ.get("PYTHON_COLORS") == "1":
return True
if "NO_COLOR" in os.environ:
return False
if not _COLORIZE:
return False
if "FORCE_COLOR" in os.environ:
return True
if os.environ.get("TERM") == "dumb":
return False
try:
return os.isatty(sys.stderr.fileno())
except io.UnsupportedOperation:
return sys.stderr.isatty()
def _print_exception_bltin(exc, /):
file = sys.stderr if sys.stderr is not None else sys.__stderr__
return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file)
colorize = _can_colorize()
return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)
def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
@ -172,13 +203,19 @@ def format_exception_only(exc, /, value=_sentinel, *, show_group=False):
# -- not official API but folk probably use these two functions.
def _format_final_exc_line(etype, value, *, insert_final_newline=True):
def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=False):
valuestr = _safe_string(value, 'exception')
end_char = "\n" if insert_final_newline else ""
if value is None or not valuestr:
line = f"{etype}{end_char}"
if colorize:
if value is None or not valuestr:
line = f"{_ANSIColors.BOLD_MAGENTA}{etype}{_ANSIColors.RESET}{end_char}"
else:
line = f"{_ANSIColors.BOLD_MAGENTA}{etype}{_ANSIColors.RESET}: {_ANSIColors.MAGENTA}{valuestr}{_ANSIColors.RESET}{end_char}"
else:
line = f"{etype}: {valuestr}{end_char}"
if value is None or not valuestr:
line = f"{etype}{end_char}"
else:
line = f"{etype}: {valuestr}{end_char}"
return line
def _safe_string(value, what, func=str):
@ -406,6 +443,14 @@ def _get_code_position(code, instruction_index):
_RECURSIVE_CUTOFF = 3 # Also hardcoded in traceback.c.
class _ANSIColors:
RED = '\x1b[31m'
BOLD_RED = '\x1b[1;31m'
MAGENTA = '\x1b[35m'
BOLD_MAGENTA = '\x1b[1;35m'
GREY = '\x1b[90m'
RESET = '\x1b[0m'
class StackSummary(list):
"""A list of FrameSummary objects, representing a stack of frames."""
@ -496,18 +541,33 @@ class StackSummary(list):
result.append(FrameSummary(filename, lineno, name, line=line))
return result
def format_frame_summary(self, frame_summary):
def format_frame_summary(self, frame_summary, **kwargs):
"""Format the lines for a single FrameSummary.
Returns a string representing one frame involved in the stack. This
gets called for every frame to be printed in the stack summary.
"""
colorize = kwargs.get("colorize", False)
row = []
filename = frame_summary.filename
if frame_summary.filename.startswith("<stdin>-"):
filename = "<stdin>"
row.append(' File "{}", line {}, in {}\n'.format(
filename, frame_summary.lineno, frame_summary.name))
if colorize:
row.append(' File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format(
_ANSIColors.MAGENTA,
filename,
_ANSIColors.RESET,
_ANSIColors.MAGENTA,
frame_summary.lineno,
_ANSIColors.RESET,
_ANSIColors.MAGENTA,
frame_summary.name,
_ANSIColors.RESET,
)
)
else:
row.append(' File "{}", line {}, in {}\n'.format(
filename, frame_summary.lineno, frame_summary.name))
if frame_summary._dedented_lines and frame_summary._dedented_lines.strip():
if (
frame_summary.colno is None or
@ -619,7 +679,31 @@ class StackSummary(list):
carets.append(secondary_char)
else:
carets.append(primary_char)
result.append("".join(carets) + "\n")
if colorize:
# Replace the previous line with a red version of it only in the parts covered
# by the carets.
line = result[-1]
colorized_line_parts = []
colorized_carets_parts = []
for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):
caret_group = list(group)
if color == "^":
colorized_line_parts.append(_ANSIColors.BOLD_RED + "".join(char for char, _ in caret_group) + _ANSIColors.RESET)
colorized_carets_parts.append(_ANSIColors.BOLD_RED + "".join(caret for _, caret in caret_group) + _ANSIColors.RESET)
elif color == "~":
colorized_line_parts.append(_ANSIColors.RED + "".join(char for char, _ in caret_group) + _ANSIColors.RESET)
colorized_carets_parts.append(_ANSIColors.RED + "".join(caret for _, caret in caret_group) + _ANSIColors.RESET)
else:
colorized_line_parts.append("".join(char for char, _ in caret_group))
colorized_carets_parts.append("".join(caret for _, caret in caret_group))
colorized_line = "".join(colorized_line_parts)
colorized_carets = "".join(colorized_carets_parts)
result[-1] = colorized_line
result.append(colorized_carets + "\n")
else:
result.append("".join(carets) + "\n")
# display significant lines
sig_lines_list = sorted(significant_lines)
@ -643,7 +727,7 @@ class StackSummary(list):
return ''.join(row)
def format(self):
def format(self, **kwargs):
"""Format the stack ready for printing.
Returns a list of strings ready for printing. Each string in the
@ -655,13 +739,14 @@ class StackSummary(list):
repetitions are shown, followed by a summary line stating the exact
number of further repetitions.
"""
colorize = kwargs.get("colorize", False)
result = []
last_file = None
last_line = None
last_name = None
count = 0
for frame_summary in self:
formatted_frame = self.format_frame_summary(frame_summary)
formatted_frame = self.format_frame_summary(frame_summary, colorize=colorize)
if formatted_frame is None:
continue
if (last_file is None or last_file != frame_summary.filename or
@ -1118,7 +1203,7 @@ class TracebackException:
def __str__(self):
return self._str
def format_exception_only(self, *, show_group=False, _depth=0):
def format_exception_only(self, *, show_group=False, _depth=0, **kwargs):
"""Format the exception part of the traceback.
The return value is a generator of strings, each ending in a newline.
@ -1135,10 +1220,11 @@ class TracebackException:
:exc:`BaseExceptionGroup`, the nested exceptions are included as
well, recursively, with indentation relative to their nesting depth.
"""
colorize = kwargs.get("colorize", False)
indent = 3 * _depth * ' '
if not self._have_exc_type:
yield indent + _format_final_exc_line(None, self._str)
yield indent + _format_final_exc_line(None, self._str, colorize=colorize)
return
stype = self.exc_type_str
@ -1146,16 +1232,16 @@ class TracebackException:
if _depth > 0:
# Nested exceptions needs correct handling of multiline messages.
formatted = _format_final_exc_line(
stype, self._str, insert_final_newline=False,
stype, self._str, insert_final_newline=False, colorize=colorize
).split('\n')
yield from [
indent + l + '\n'
for l in formatted
]
else:
yield _format_final_exc_line(stype, self._str)
yield _format_final_exc_line(stype, self._str, colorize=colorize)
else:
yield from [indent + l for l in self._format_syntax_error(stype)]
yield from [indent + l for l in self._format_syntax_error(stype, colorize=colorize)]
if (
isinstance(self.__notes__, collections.abc.Sequence)
@ -1169,15 +1255,26 @@ class TracebackException:
if self.exceptions and show_group:
for ex in self.exceptions:
yield from ex.format_exception_only(show_group=show_group, _depth=_depth+1)
yield from ex.format_exception_only(show_group=show_group, _depth=_depth+1, colorize=colorize)
def _format_syntax_error(self, stype):
def _format_syntax_error(self, stype, **kwargs):
"""Format SyntaxError exceptions (internal helper)."""
# Show exactly where the problem was found.
colorize = kwargs.get("colorize", False)
filename_suffix = ''
if self.lineno is not None:
yield ' File "{}", line {}\n'.format(
self.filename or "<string>", self.lineno)
if colorize:
yield ' File {}"{}"{}, line {}{}{}\n'.format(
_ANSIColors.MAGENTA,
self.filename or "<string>",
_ANSIColors.RESET,
_ANSIColors.MAGENTA,
self.lineno,
_ANSIColors.RESET,
)
else:
yield ' File "{}", line {}\n'.format(
self.filename or "<string>", self.lineno)
elif self.filename is not None:
filename_suffix = ' ({})'.format(self.filename)
@ -1189,9 +1286,9 @@ class TracebackException:
rtext = text.rstrip('\n')
ltext = rtext.lstrip(' \n\f')
spaces = len(rtext) - len(ltext)
yield ' {}\n'.format(ltext)
if self.offset is not None:
if self.offset is None:
yield ' {}\n'.format(ltext)
else:
offset = self.offset
end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
if self.text and offset > len(self.text):
@ -1204,14 +1301,43 @@ class TracebackException:
# Convert 1-based column offset to 0-based index into stripped text
colno = offset - 1 - spaces
end_colno = end_offset - 1 - spaces
caretspace = ' '
if colno >= 0:
# non-space whitespace (likes tabs) must be kept for alignment
caretspace = ((c if c.isspace() else ' ') for c in ltext[:colno])
yield ' {}{}'.format("".join(caretspace), ('^' * (end_colno - colno) + "\n"))
start_color = end_color = ""
if colorize:
# colorize from colno to end_colno
ltext = (
ltext[:colno] +
_ANSIColors.BOLD_RED + ltext[colno:end_colno] + _ANSIColors.RESET +
ltext[end_colno:]
)
start_color = _ANSIColors.BOLD_RED
end_color = _ANSIColors.RESET
yield ' {}\n'.format(ltext)
yield ' {}{}{}{}\n'.format(
"".join(caretspace),
start_color,
('^' * (end_colno - colno)),
end_color,
)
else:
yield ' {}\n'.format(ltext)
msg = self.msg or "<no detail available>"
yield "{}: {}{}\n".format(stype, msg, filename_suffix)
if colorize:
yield "{}{}{}: {}{}{}{}\n".format(
_ANSIColors.BOLD_MAGENTA,
stype,
_ANSIColors.RESET,
_ANSIColors.MAGENTA,
msg,
_ANSIColors.RESET,
filename_suffix)
else:
yield "{}: {}{}\n".format(stype, msg, filename_suffix)
def format(self, *, chain=True, _ctx=None):
def format(self, *, chain=True, _ctx=None, **kwargs):
"""Format the exception.
If chain is not *True*, *__cause__* and *__context__* will not be formatted.
@ -1223,7 +1349,7 @@ class TracebackException:
The message indicating which exception occurred is always the last
string in the output.
"""
colorize = kwargs.get("colorize", False)
if _ctx is None:
_ctx = _ExceptionPrintContext()
@ -1253,8 +1379,8 @@ class TracebackException:
if exc.exceptions is None:
if exc.stack:
yield from _ctx.emit('Traceback (most recent call last):\n')
yield from _ctx.emit(exc.stack.format())
yield from _ctx.emit(exc.format_exception_only())
yield from _ctx.emit(exc.stack.format(colorize=colorize))
yield from _ctx.emit(exc.format_exception_only(colorize=colorize))
elif _ctx.exception_group_depth > self.max_group_depth:
# exception group, but depth exceeds limit
yield from _ctx.emit(
@ -1269,9 +1395,9 @@ class TracebackException:
yield from _ctx.emit(
'Exception Group Traceback (most recent call last):\n',
margin_char = '+' if is_toplevel else None)
yield from _ctx.emit(exc.stack.format())
yield from _ctx.emit(exc.stack.format(colorize=colorize))
yield from _ctx.emit(exc.format_exception_only())
yield from _ctx.emit(exc.format_exception_only(colorize=colorize))
num_excs = len(exc.exceptions)
if num_excs <= self.max_group_width:
n = num_excs
@ -1312,11 +1438,12 @@ class TracebackException:
_ctx.exception_group_depth = 0
def print(self, *, file=None, chain=True):
def print(self, *, file=None, chain=True, **kwargs):
"""Print the result of self.format(chain=chain) to 'file'."""
colorize = kwargs.get("colorize", False)
if file is None:
file = sys.stderr
for line in self.format(chain=chain):
for line in self.format(chain=chain, colorize=colorize):
print(line, file=file, end="")

View File

@ -0,0 +1 @@
Use color to highlight error locations in tracebacks. Patch by Pablo Galindo

View File

@ -11756,6 +11756,28 @@ exit:
#endif /* (defined(WIFEXITED) || defined(MS_WINDOWS)) */
#if defined(MS_WINDOWS)
PyDoc_STRVAR(os__supports_virtual_terminal__doc__,
"_supports_virtual_terminal($module, /)\n"
"--\n"
"\n"
"Checks if virtual terminal is supported in windows");
#define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF \
{"_supports_virtual_terminal", (PyCFunction)os__supports_virtual_terminal, METH_NOARGS, os__supports_virtual_terminal__doc__},
static PyObject *
os__supports_virtual_terminal_impl(PyObject *module);
static PyObject *
os__supports_virtual_terminal(PyObject *module, PyObject *Py_UNUSED(ignored))
{
return os__supports_virtual_terminal_impl(module);
}
#endif /* defined(MS_WINDOWS) */
#ifndef OS_TTYNAME_METHODDEF
#define OS_TTYNAME_METHODDEF
#endif /* !defined(OS_TTYNAME_METHODDEF) */
@ -12395,4 +12417,8 @@ exit:
#ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF
#define OS_WAITSTATUS_TO_EXITCODE_METHODDEF
#endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */
/*[clinic end generated code: output=2900675ac5219924 input=a9049054013a1b77]*/
#ifndef OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF
#define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF
#endif /* !defined(OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF) */
/*[clinic end generated code: output=ff0ec3371de19904 input=a9049054013a1b77]*/

View File

@ -16073,6 +16073,26 @@ os_waitstatus_to_exitcode_impl(PyObject *module, PyObject *status_obj)
}
#endif
#if defined(MS_WINDOWS)
/*[clinic input]
os._supports_virtual_terminal
Checks if virtual terminal is supported in windows
[clinic start generated code]*/
static PyObject *
os__supports_virtual_terminal_impl(PyObject *module)
/*[clinic end generated code: output=bd0556a6d9d99fe6 input=0752c98e5d321542]*/
{
DWORD mode = 0;
HANDLE handle = GetStdHandle(STD_ERROR_HANDLE);
if (!GetConsoleMode(handle, &mode)) {
Py_RETURN_FALSE;
}
return PyBool_FromLong(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
#endif
static PyMethodDef posix_methods[] = {
@ -16277,6 +16297,8 @@ static PyMethodDef posix_methods[] = {
OS__PATH_ISFILE_METHODDEF
OS__PATH_ISLINK_METHODDEF
OS__PATH_EXISTS_METHODDEF
OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF
{NULL, NULL} /* Sentinel */
};

View File

@ -293,6 +293,8 @@ static const char usage_envvars[] =
"PYTHON_FROZEN_MODULES : if this variable is set, it determines whether or not \n"
" frozen modules should be used. The default is \"on\" (or \"off\" if you are \n"
" running a local build).\n"
"PYTHON_COLORS : If this variable is set to 1, the interpreter will"
" colorize various kinds of output. Setting it to 0 deactivates this behavior.\n"
"These variables have equivalent command-line parameters (see --help for details):\n"
"PYTHONDEBUG : enable parser debug mode (-d)\n"
"PYTHONDONTWRITEBYTECODE : don't write .pyc files (-B)\n"