]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-117225: Move colorize functionality to own internal module (#118283)
authorHugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Wed, 1 May 2024 18:27:06 +0000 (21:27 +0300)
committerGitHub <noreply@github.com>
Wed, 1 May 2024 18:27:06 +0000 (12:27 -0600)
Lib/_colorize.py [new file with mode: 0644]
Lib/doctest.py
Lib/test/support/__init__.py
Lib/test/test__colorize.py [new file with mode: 0644]
Lib/test/test_doctest/test_doctest.py
Lib/test/test_traceback.py
Lib/traceback.py
Python/stdlib_module_names.h

diff --git a/Lib/_colorize.py b/Lib/_colorize.py
new file mode 100644 (file)
index 0000000..845fb57
--- /dev/null
@@ -0,0 +1,64 @@
+import io
+import os
+import sys
+
+COLORIZE = True
+
+
+class ANSIColors:
+    BOLD_GREEN = "\x1b[1;32m"
+    BOLD_MAGENTA = "\x1b[1;35m"
+    BOLD_RED = "\x1b[1;31m"
+    GREEN = "\x1b[32m"
+    GREY = "\x1b[90m"
+    MAGENTA = "\x1b[35m"
+    RED = "\x1b[31m"
+    RESET = "\x1b[0m"
+    YELLOW = "\x1b[33m"
+
+
+NoColors = ANSIColors()
+
+for attr in dir(NoColors):
+    if not attr.startswith("__"):
+        setattr(NoColors, attr, "")
+
+
+def get_colors(colorize: bool = False) -> ANSIColors:
+    if colorize or can_colorize():
+        return ANSIColors()
+    else:
+        return NoColors
+
+
+def can_colorize() -> bool:
+    if sys.platform == "win32":
+        try:
+            import nt
+
+            if not nt._supports_virtual_terminal():
+                return False
+        except (ImportError, AttributeError):
+            return False
+    if not sys.flags.ignore_environment:
+        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 not sys.flags.ignore_environment:
+        if "FORCE_COLOR" in os.environ:
+            return True
+        if os.environ.get("TERM") == "dumb":
+            return False
+
+    if not hasattr(sys.stderr, "fileno"):
+        return False
+
+    try:
+        return os.isatty(sys.stderr.fileno())
+    except io.UnsupportedOperation:
+        return sys.stderr.isatty()
index d8c632e47e7b7a3fae54602bd76daad7c85c255a..c531e3ca6a3d5e3612a4f5f86b6b41ad5e9fc653 100644 (file)
@@ -104,7 +104,8 @@ import traceback
 import unittest
 from io import StringIO, IncrementalNewlineDecoder
 from collections import namedtuple
-from traceback import _ANSIColors, _can_colorize
+import _colorize  # Used in doctests
+from _colorize import ANSIColors, can_colorize
 
 
 class TestResults(namedtuple('TestResults', 'failed attempted')):
@@ -1180,8 +1181,8 @@ class DocTestRunner:
     The `run` method is used to process a single DocTest case.  It
     returns a TestResults instance.
 
-        >>> save_colorize = traceback._COLORIZE
-        >>> traceback._COLORIZE = False
+        >>> save_colorize = _colorize.COLORIZE
+        >>> _colorize.COLORIZE = False
 
         >>> tests = DocTestFinder().find(_TestClass)
         >>> runner = DocTestRunner(verbose=False)
@@ -1234,7 +1235,7 @@ class DocTestRunner:
     overriding the methods `report_start`, `report_success`,
     `report_unexpected_exception`, and `report_failure`.
 
-        >>> traceback._COLORIZE = save_colorize
+        >>> _colorize.COLORIZE = save_colorize
     """
     # This divider string is used to separate failure messages, and to
     # separate sections of the summary.
@@ -1314,7 +1315,7 @@ class DocTestRunner:
 
     def _failure_header(self, test, example):
         red, reset = (
-            (_ANSIColors.RED, _ANSIColors.RESET) if _can_colorize() else ("", "")
+            (ANSIColors.RED, ANSIColors.RESET) if can_colorize() else ("", "")
         )
         out = [f"{red}{self.DIVIDER}{reset}"]
         if test.filename:
@@ -1556,8 +1557,8 @@ class DocTestRunner:
         # Make sure sys.displayhook just prints the value to stdout
         save_displayhook = sys.displayhook
         sys.displayhook = sys.__displayhook__
-        saved_can_colorize = traceback._can_colorize
-        traceback._can_colorize = lambda: False
+        saved_can_colorize = _colorize.can_colorize
+        _colorize.can_colorize = lambda: False
         color_variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
         for key in color_variables:
             color_variables[key] = os.environ.pop(key, None)
@@ -1569,7 +1570,7 @@ class DocTestRunner:
             sys.settrace(save_trace)
             linecache.getlines = self.save_linecache_getlines
             sys.displayhook = save_displayhook
-            traceback._can_colorize = saved_can_colorize
+            _colorize.can_colorize = saved_can_colorize
             for key, value in color_variables.items():
                 if value is not None:
                     os.environ[key] = value
@@ -1609,20 +1610,13 @@ class DocTestRunner:
             else:
                 failed.append((name, (failures, tries, skips)))
 
-        if _can_colorize():
-            bold_green = _ANSIColors.BOLD_GREEN
-            bold_red = _ANSIColors.BOLD_RED
-            green = _ANSIColors.GREEN
-            red = _ANSIColors.RED
-            reset = _ANSIColors.RESET
-            yellow = _ANSIColors.YELLOW
-        else:
-            bold_green = ""
-            bold_red = ""
-            green = ""
-            red = ""
-            reset = ""
-            yellow = ""
+        ansi = _colorize.get_colors()
+        bold_green = ansi.BOLD_GREEN
+        bold_red = ansi.BOLD_RED
+        green = ansi.GREEN
+        red = ansi.RED
+        reset = ansi.RESET
+        yellow = ansi.YELLOW
 
         if verbose:
             if notests:
index 52573e665a1273f2d1c731bfc54ae082ef116cd8..999fffb03ed59a182ffb1f93a3bd0fd8a4fb5a24 100644 (file)
@@ -2579,20 +2579,21 @@ def copy_python_src_ignore(path, names):
         }
     return ignored
 
+
 def force_not_colorized(func):
     """Force the terminal not to be colorized."""
     @functools.wraps(func)
     def wrapper(*args, **kwargs):
-        import traceback
-        original_fn = traceback._can_colorize
+        import _colorize
+        original_fn = _colorize.can_colorize
         variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
         try:
             for key in variables:
                 variables[key] = os.environ.pop(key, None)
-            traceback._can_colorize = lambda: False
+            _colorize.can_colorize = lambda: False
             return func(*args, **kwargs)
         finally:
-            traceback._can_colorize = original_fn
+            _colorize.can_colorize = original_fn
             for key, value in variables.items():
                 if value is not None:
                     os.environ[key] = value
diff --git a/Lib/test/test__colorize.py b/Lib/test/test__colorize.py
new file mode 100644 (file)
index 0000000..d55b97a
--- /dev/null
@@ -0,0 +1,59 @@
+import contextlib
+import sys
+import unittest
+import unittest.mock
+import _colorize
+from test.support import force_not_colorized
+
+ORIGINAL_CAN_COLORIZE = _colorize.can_colorize
+
+
+def setUpModule():
+    _colorize.can_colorize = lambda: False
+
+
+def tearDownModule():
+    _colorize.can_colorize = ORIGINAL_CAN_COLORIZE
+
+
+class TestColorizeFunction(unittest.TestCase):
+    @force_not_colorized
+    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:
+
+            flags = unittest.mock.MagicMock(ignore_environment=False)
+            with (unittest.mock.patch("os.isatty") as isatty_mock,
+                  unittest.mock.patch("sys.flags", flags),
+                  unittest.mock.patch("_colorize.can_colorize", ORIGINAL_CAN_COLORIZE)):
+                isatty_mock.return_value = True
+                with unittest.mock.patch("os.environ", {'TERM': 'dumb'}):
+                    self.assertEqual(_colorize.can_colorize(), False)
+                with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '1'}):
+                    self.assertEqual(_colorize.can_colorize(), True)
+                with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '0'}):
+                    self.assertEqual(_colorize.can_colorize(), False)
+                with unittest.mock.patch("os.environ", {'NO_COLOR': '1'}):
+                    self.assertEqual(_colorize.can_colorize(), False)
+                with unittest.mock.patch("os.environ",
+                                         {'NO_COLOR': '1', "PYTHON_COLORS": '1'}):
+                    self.assertEqual(_colorize.can_colorize(), True)
+                with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1'}):
+                    self.assertEqual(_colorize.can_colorize(), True)
+                with unittest.mock.patch("os.environ",
+                                         {'FORCE_COLOR': '1', 'NO_COLOR': '1'}):
+                    self.assertEqual(_colorize.can_colorize(), False)
+                with unittest.mock.patch("os.environ",
+                                         {'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}):
+                    self.assertEqual(_colorize.can_colorize(), False)
+                isatty_mock.return_value = False
+                with unittest.mock.patch("os.environ", {}):
+                    self.assertEqual(_colorize.can_colorize(), False)
+
+
+if __name__ == "__main__":
+    unittest.main()
index 0f1e584e22a888fb799a085240e8dcadc63ceec1..3a173e823dd9a3fee22f1a8075189dbe8b90600e 100644 (file)
@@ -16,7 +16,7 @@ import unittest
 import tempfile
 import types
 import contextlib
-import traceback
+import _colorize
 
 
 def doctest_skip_if(condition):
@@ -893,8 +893,8 @@ Unit tests for the `DocTestRunner` class.
 DocTestRunner is used to run DocTest test cases, and to accumulate
 statistics.  Here's a simple DocTest case we can use:
 
-    >>> save_colorize = traceback._COLORIZE
-    >>> traceback._COLORIZE = False
+    >>> save_colorize = _colorize.COLORIZE
+    >>> _colorize.COLORIZE = False
 
     >>> def f(x):
     ...     '''
@@ -951,7 +951,7 @@ the failure and proceeds to the next example:
     ok
     TestResults(failed=1, attempted=3)
 
-    >>> traceback._COLORIZE = save_colorize
+    >>> _colorize.COLORIZE = save_colorize
 """
     def verbose_flag(): r"""
 The `verbose` flag makes the test runner generate more detailed
@@ -1027,8 +1027,8 @@ An expected exception is specified with a traceback message.  The
 lines between the first line and the type/value may be omitted or
 replaced with any other string:
 
-    >>> save_colorize = traceback._COLORIZE
-    >>> traceback._COLORIZE = False
+    >>> save_colorize = _colorize.COLORIZE
+    >>> _colorize.COLORIZE = False
 
     >>> def f(x):
     ...     '''
@@ -1261,7 +1261,7 @@ unexpected exception:
         ZeroDivisionError: integer division or modulo by zero
     TestResults(failed=1, attempted=1)
 
-    >>> traceback._COLORIZE = save_colorize
+    >>> _colorize.COLORIZE = save_colorize
 """
     def displayhook(): r"""
 Test that changing sys.displayhook doesn't matter for doctest.
@@ -1303,8 +1303,8 @@ together).
 The DONT_ACCEPT_TRUE_FOR_1 flag disables matches between True/False
 and 1/0:
 
-    >>> save_colorize = traceback._COLORIZE
-    >>> traceback._COLORIZE = False
+    >>> save_colorize = _colorize.COLORIZE
+    >>> _colorize.COLORIZE = False
 
     >>> def f(x):
     ...     '>>> True\n1\n'
@@ -1725,7 +1725,7 @@ more than one flag value.  Here we verify that's fixed:
 
 Clean up.
     >>> del doctest.OPTIONFLAGS_BY_NAME[unlikely]
-    >>> traceback._COLORIZE = save_colorize
+    >>> _colorize.COLORIZE = save_colorize
 
     """
 
@@ -1736,8 +1736,8 @@ Option directives can be used to turn option flags on or off for a
 single example.  To turn an option on for an example, follow that
 example with a comment of the form ``# doctest: +OPTION``:
 
-    >>> save_colorize = traceback._COLORIZE
-    >>> traceback._COLORIZE = False
+    >>> save_colorize = _colorize.COLORIZE
+    >>> _colorize.COLORIZE = False
 
     >>> def f(x): r'''
     ...     >>> print(list(range(10)))      # should fail: no ellipsis
@@ -1947,7 +1947,7 @@ source:
     Traceback (most recent call last):
     ValueError: line 0 of the doctest for s has an option directive on a line with no example: '# doctest: +ELLIPSIS'
 
-    >>> traceback._COLORIZE = save_colorize
+    >>> _colorize.COLORIZE = save_colorize
 """
 
 def test_testsource(): r"""
@@ -2031,8 +2031,8 @@ if not hasattr(sys, 'gettrace') or not sys.gettrace():
         with a version that restores stdout.  This is necessary for you to
         see debugger output.
 
-          >>> save_colorize = traceback._COLORIZE
-          >>> traceback._COLORIZE = False
+          >>> save_colorize = _colorize.COLORIZE
+          >>> _colorize.COLORIZE = False
 
           >>> doc = '''
           ... >>> x = 42
@@ -2157,7 +2157,7 @@ if not hasattr(sys, 'gettrace') or not sys.gettrace():
               9
           TestResults(failed=1, attempted=3)
 
-          >>> traceback._COLORIZE = save_colorize
+          >>> _colorize.COLORIZE = save_colorize
           """
 
     def test_pdb_set_trace_nested():
@@ -2694,8 +2694,8 @@ calling module.  The return value is (#failures, #tests).
 
 We don't want color or `-v` in sys.argv for these tests.
 
-    >>> save_colorize = traceback._COLORIZE
-    >>> traceback._COLORIZE = False
+    >>> save_colorize = _colorize.COLORIZE
+    >>> _colorize.COLORIZE = False
 
     >>> save_argv = sys.argv
     >>> if '-v' in sys.argv:
@@ -2863,7 +2863,7 @@ Test the verbose output:
     TestResults(failed=0, attempted=2)
     >>> doctest.master = None  # Reset master.
     >>> sys.argv = save_argv
-    >>> traceback._COLORIZE = save_colorize
+    >>> _colorize.COLORIZE = save_colorize
 """
 
 class TestImporter(importlib.abc.MetaPathFinder, importlib.abc.ResourceLoader):
@@ -3001,8 +3001,8 @@ if supports_unicode:
     def test_unicode(): """
 Check doctest with a non-ascii filename:
 
-    >>> save_colorize = traceback._COLORIZE
-    >>> traceback._COLORIZE = False
+    >>> save_colorize = _colorize.COLORIZE
+    >>> _colorize.COLORIZE = False
 
     >>> doc = '''
     ... >>> raise Exception('clĂ©')
@@ -3030,7 +3030,7 @@ Check doctest with a non-ascii filename:
         Exception: clĂ©
     TestResults(failed=1, attempted=1)
 
-    >>> traceback._COLORIZE = save_colorize
+    >>> _colorize.COLORIZE = save_colorize
     """
 
 
@@ -3325,8 +3325,8 @@ def test_run_doctestsuite_multiple_times():
 
 def test_exception_with_note(note):
     """
-    >>> save_colorize = traceback._COLORIZE
-    >>> traceback._COLORIZE = False
+    >>> save_colorize = _colorize.COLORIZE
+    >>> _colorize.COLORIZE = False
 
     >>> test_exception_with_note('Note')
     Traceback (most recent call last):
@@ -3378,7 +3378,7 @@ def test_exception_with_note(note):
         note
     TestResults(failed=1, attempted=...)
 
-    >>> traceback._COLORIZE = save_colorize
+    >>> _colorize.COLORIZE = save_colorize
     """
     exc = ValueError('Text')
     exc.add_note(note)
@@ -3459,8 +3459,8 @@ def test_syntax_error_subclass_from_stdlib():
 
 def test_syntax_error_with_incorrect_expected_note():
     """
-    >>> save_colorize = traceback._COLORIZE
-    >>> traceback._COLORIZE = False
+    >>> save_colorize = _colorize.COLORIZE
+    >>> _colorize.COLORIZE = False
 
     >>> def f(x):
     ...     r'''
@@ -3491,7 +3491,7 @@ def test_syntax_error_with_incorrect_expected_note():
         note2
     TestResults(failed=1, attempted=...)
 
-    >>> traceback._COLORIZE = save_colorize
+    >>> _colorize.COLORIZE = save_colorize
     """
 
 
index 41f82512480d4f62b568e301fc7e2665c64103db..8969e0174c98c1e00bc48edcd89f19caaa48a96f 100644 (file)
@@ -26,9 +26,9 @@ from test.support import force_not_colorized
 import json
 import textwrap
 import traceback
-import contextlib
 from functools import partial
 from pathlib import Path
+import _colorize
 
 MODULE_PREFIX = f'{__name__}.' if __name__ == '__main__' else ''
 
@@ -40,25 +40,18 @@ test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next', 'tb_lasti'])
 
 LEVENSHTEIN_DATA_FILE = Path(__file__).parent / 'levenshtein_examples.json'
 
-ORIGINAL_CAN_COLORIZE = traceback._can_colorize
-
-def setUpModule():
-    traceback._can_colorize = lambda: False
-
-def tearDownModule():
-    traceback._can_colorize = ORIGINAL_CAN_COLORIZE
 
 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
+        self.colorize = _colorize.COLORIZE
+        _colorize.COLORIZE = False
 
     def tearDown(self):
         super().tearDown()
-        traceback._COLORIZE = self.colorize
+        _colorize.COLORIZE = self.colorize
 
     def get_exception_format(self, func, exc):
         try:
@@ -4478,9 +4471,9 @@ class TestColorizedTraceback(unittest.TestCase):
                 e, capture_locals=True
             )
         lines = "".join(exc.format(colorize=True))
-        red = traceback._ANSIColors.RED
-        boldr = traceback._ANSIColors.BOLD_RED
-        reset = traceback._ANSIColors.RESET
+        red = _colorize.ANSIColors.RED
+        boldr = _colorize.ANSIColors.BOLD_RED
+        reset = _colorize.ANSIColors.RESET
         self.assertIn("y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, lines)
         self.assertIn("return " + red + "(lambda *args: foo(*args))" + reset + boldr + "(1,2,3,4)" + reset, lines)
         self.assertIn("return (lambda *args: " + red + "foo" + reset + boldr + "(*args)" + reset + ")(1,2,3,4)", lines)
@@ -4496,11 +4489,11 @@ class TestColorizedTraceback(unittest.TestCase):
                 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
+        red = _colorize.ANSIColors.RED
+        magenta = _colorize.ANSIColors.MAGENTA
+        boldm = _colorize.ANSIColors.BOLD_MAGENTA
+        boldr = _colorize.ANSIColors.BOLD_RED
+        reset = _colorize.ANSIColors.RESET
         expected = "".join([
         f'  File {magenta}"<string>"{reset}, line {magenta}1{reset}\n',
         f'    a {boldr}${reset} b\n',
@@ -4519,15 +4512,15 @@ class TestColorizedTraceback(unittest.TestCase):
             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):
+                with unittest.mock.patch('_colorize.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
+        red = _colorize.ANSIColors.RED
+        boldr = _colorize.ANSIColors.BOLD_RED
+        magenta = _colorize.ANSIColors.MAGENTA
+        boldm = _colorize.ANSIColors.BOLD_MAGENTA
+        reset = _colorize.ANSIColors.RESET
         lno_foo = foo.__code__.co_firstlineno
         expected = ['Traceback (most recent call last):',
             f'  File {magenta}"{__file__}"{reset}, '
@@ -4541,38 +4534,6 @@ class TestColorizedTraceback(unittest.TestCase):
             f'{boldm}ZeroDivisionError{reset}: {magenta}division by zero{reset}']
         self.assertEqual(actual, expected)
 
-    @force_not_colorized
-    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:
-
-            flags = unittest.mock.MagicMock(ignore_environment=False)
-            with (unittest.mock.patch("os.isatty") as isatty_mock,
-                  unittest.mock.patch("sys.flags", flags),
-                  unittest.mock.patch("traceback._can_colorize", ORIGINAL_CAN_COLORIZE)):
-                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
-                with unittest.mock.patch("os.environ", {}):
-                    self.assertEqual(traceback._can_colorize(), False)
 
 if __name__ == "__main__":
     unittest.main()
index 6ce745f32d132422469fa72c09fd7a9f97e26353..8403173ade7b6c9bdbb31248177af2e725dc92ad 100644 (file)
@@ -1,7 +1,5 @@
 """Extract, format and print information about Python stack traces."""
 
-import os
-import io
 import collections.abc
 import itertools
 import linecache
@@ -9,6 +7,8 @@ import sys
 import textwrap
 import warnings
 from contextlib import suppress
+import _colorize
+from _colorize import ANSIColors
 
 __all__ = ['extract_stack', 'extract_tb', 'format_exception',
            'format_exception_only', 'format_list', 'format_stack',
@@ -21,7 +21,6 @@ __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
@@ -133,41 +132,10 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
 
 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 not sys.flags.ignore_environment:
-        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 not sys.flags.ignore_environment:
-        if "FORCE_COLOR" in os.environ:
-            return True
-        if os.environ.get("TERM") == "dumb":
-            return False
-
-    if not hasattr(sys.stderr, "fileno"):
-        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__
-    colorize = _can_colorize()
+    colorize = _colorize.can_colorize()
     return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)
 
 
@@ -214,9 +182,9 @@ def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=
     end_char = "\n" if insert_final_newline else ""
     if colorize:
         if value is None or not valuestr:
-            line = f"{_ANSIColors.BOLD_MAGENTA}{etype}{_ANSIColors.RESET}{end_char}"
+            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}"
+            line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}: {ANSIColors.MAGENTA}{valuestr}{ANSIColors.RESET}{end_char}"
     else:
         if value is None or not valuestr:
             line = f"{etype}{end_char}"
@@ -224,6 +192,7 @@ def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=
             line = f"{etype}: {valuestr}{end_char}"
     return line
 
+
 def _safe_string(value, what, func=str):
     try:
         return func(value)
@@ -449,17 +418,6 @@ 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'
-    GREEN = "\x1b[32m"
-    BOLD_GREEN = "\x1b[1;32m"
-    GREY = '\x1b[90m'
-    RESET = '\x1b[0m'
-    YELLOW = "\x1b[33m"
-
 
 class StackSummary(list):
     """A list of FrameSummary objects, representing a stack of frames."""
@@ -564,15 +522,15 @@ class StackSummary(list):
             filename = "<stdin>"
         if colorize:
             row.append('  File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format(
-                    _ANSIColors.MAGENTA,
+                    ANSIColors.MAGENTA,
                     filename,
-                    _ANSIColors.RESET,
-                    _ANSIColors.MAGENTA,
+                    ANSIColors.RESET,
+                    ANSIColors.MAGENTA,
                     frame_summary.lineno,
-                    _ANSIColors.RESET,
-                    _ANSIColors.MAGENTA,
+                    ANSIColors.RESET,
+                    ANSIColors.MAGENTA,
                     frame_summary.name,
-                    _ANSIColors.RESET,
+                    ANSIColors.RESET,
                     )
             )
         else:
@@ -696,11 +654,11 @@ class StackSummary(list):
                         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)
+                                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)
+                                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))
@@ -1307,12 +1265,12 @@ class TracebackException:
         if self.lineno is not None:
             if colorize:
                 yield '  File {}"{}"{}, line {}{}{}\n'.format(
-                    _ANSIColors.MAGENTA,
+                    ANSIColors.MAGENTA,
                     self.filename or "<string>",
-                    _ANSIColors.RESET,
-                    _ANSIColors.MAGENTA,
+                    ANSIColors.RESET,
+                    ANSIColors.MAGENTA,
                     self.lineno,
-                    _ANSIColors.RESET,
+                    ANSIColors.RESET,
                     )
             else:
                 yield '  File "{}", line {}\n'.format(
@@ -1352,11 +1310,11 @@ class TracebackException:
                         # colorize from colno to end_colno
                         ltext = (
                             ltext[:colno] +
-                            _ANSIColors.BOLD_RED + ltext[colno:end_colno] + _ANSIColors.RESET +
+                            ANSIColors.BOLD_RED + ltext[colno:end_colno] + ANSIColors.RESET +
                             ltext[end_colno:]
                         )
-                        start_color = _ANSIColors.BOLD_RED
-                        end_color = _ANSIColors.RESET
+                        start_color = ANSIColors.BOLD_RED
+                        end_color = ANSIColors.RESET
                     yield '    {}\n'.format(ltext)
                     yield '    {}{}{}{}\n'.format(
                         "".join(caretspace),
@@ -1369,12 +1327,12 @@ class TracebackException:
         msg = self.msg or "<no detail available>"
         if colorize:
             yield "{}{}{}: {}{}{}{}\n".format(
-                _ANSIColors.BOLD_MAGENTA,
+                ANSIColors.BOLD_MAGENTA,
                 stype,
-                _ANSIColors.RESET,
-                _ANSIColors.MAGENTA,
+                ANSIColors.RESET,
+                ANSIColors.MAGENTA,
                 msg,
-                _ANSIColors.RESET,
+                ANSIColors.RESET,
                 filename_suffix)
         else:
             yield "{}: {}{}\n".format(stype, msg, filename_suffix)
index f44abf1f9c3121d89dbf76f1f56ce86ab5a7e5c7..ba320842173b48449ded58537b25a4edbdb456d8 100644 (file)
@@ -20,6 +20,7 @@ static const char* _Py_stdlib_module_names[] = {
 "_codecs_tw",
 "_collections",
 "_collections_abc",
+"_colorize",
 "_compat_pickle",
 "_compression",
 "_contextvars",