From 2c1108b44aca4493f2e364912b56f66d5a959d13 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:37:29 +0300 Subject: [PATCH 01/26] Add colour to doctest output and create _colorize module --- Lib/_colorize.py | 45 +++++++++++++++++++ Lib/doctest.py | 61 +++++++++++++++++++------- Lib/test/test_doctest/test_doctest.py | 9 +++- Lib/test/test_doctest/test_doctest2.py | 12 +++++ Python/stdlib_module_names.h | 1 + 5 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 Lib/_colorize.py diff --git a/Lib/_colorize.py b/Lib/_colorize.py new file mode 100644 index 00000000000000..412ead5d35f4e4 --- /dev/null +++ b/Lib/_colorize.py @@ -0,0 +1,45 @@ +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" + + +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() diff --git a/Lib/doctest.py b/Lib/doctest.py index fc0da590018b40..ce899d66988c3b 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -105,6 +105,8 @@ def _test(): from io import StringIO, IncrementalNewlineDecoder from collections import namedtuple +import _colorize # Used in doctests + class TestResults(namedtuple('TestResults', 'failed attempted')): def __new__(cls, failed, attempted, *, skipped=0): @@ -1172,6 +1174,9 @@ class DocTestRunner: The `run` method is used to process a single DocTest case. It returns a TestResults instance. + >>> save_colorize = _colorize._COLORIZE + >>> _colorize._COLORIZE = False + >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) >>> tests.sort(key = lambda test: test.name) @@ -1222,6 +1227,8 @@ class DocTestRunner: can be also customized by subclassing DocTestRunner, and overriding the methods `report_start`, `report_success`, `report_unexpected_exception`, and `report_failure`. + + >>> _colorize._COLORIZE = save_colorize """ # This divider string is used to separate failure messages, and to # separate sections of the summary. @@ -1566,10 +1573,12 @@ def summarize(self, verbose=None): summary is. If the verbosity is not specified, then the DocTestRunner's verbosity is used. """ + from _colorize import _ANSIColors, _can_colorize + if verbose is None: verbose = self._verbose - notests, passed, failed = [], [], [] + no_tests, passed, failed = [], [], [] total_tries = total_failures = total_skips = 0 for name, (failures, tries, skips) in self._stats.items(): @@ -1579,47 +1588,65 @@ def summarize(self, verbose=None): total_skips += skips if tries == 0: - notests.append(name) + no_tests.append(name) elif failures == 0: passed.append((name, tries)) 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 = "" + if verbose: - if notests: - print(f"{_n_items(notests)} had no tests:") - notests.sort() - for name in notests: + if no_tests: + print(f"{_n_items(no_tests)} had no tests:") + no_tests.sort() + for name in no_tests: print(f" {name}") if passed: - print(f"{_n_items(passed)} passed all tests:") + print(f"{green}{_n_items(passed)} passed all tests:{reset}") for name, count in sorted(passed): s = "" if count == 1 else "s" - print(f" {count:3d} test{s} in {name}") + print(f" {green}{count:3d} test{s} in {name}{reset}") if failed: print(self.DIVIDER) - print(f"{_n_items(failed)} had failures:") + print(f"{red}{_n_items(failed)} had failures:{reset}") for name, (failures, tries, skips) in sorted(failed): - print(f" {failures:3d} of {tries:3d} in {name}") + print(f"{red} {failures:3d} of {tries:3d} in {name}{reset}") if verbose: s = "" if total_tries == 1 else "s" print(f"{total_tries} test{s} in {_n_items(self._stats)}.") - and_f = f" and {total_failures} failed" if total_failures else "" - print(f"{total_tries - total_failures} passed{and_f}.") + and_f = ( + f" and {red}{total_failures} failed{reset}" + if total_failures else "" + ) + print(f"{green}{total_tries - total_failures} passed{reset}{and_f}.") if total_failures: s = "" if total_failures == 1 else "s" - msg = f"***Test Failed*** {total_failures} failure{s}" + msg = f"{bold_red}***Test Failed*** {total_failures} failure{s}{reset}" if total_skips: s = "" if total_skips == 1 else "s" - msg = f"{msg} and {total_skips} skipped test{s}" + msg = f"{msg} and {yellow}{total_skips} skipped test{s}{reset}" print(f"{msg}.") elif verbose: - print("Test passed.") + print(f"{bold_green}Test passed.{reset}") return TestResults(total_failures, total_tries, skipped=total_skips) @@ -1637,7 +1664,7 @@ def merge(self, other): d[name] = (failures, tries, skips) -def _n_items(items: list) -> str: +def _n_items(items: list | dict) -> str: """ Helper to pluralise the number of items in a list. """ @@ -1648,7 +1675,7 @@ def _n_items(items: list) -> str: class OutputChecker: """ - A class used to check the whether the actual output from a doctest + A class used to check whether the actual output from a doctest example matches the expected output. `OutputChecker` defines two methods: `check_output`, which compares a given pair of outputs, and returns true if they match; and `output_difference`, which diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index dd8cc9be3a4a8a..13ab3c9e6ca200 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -17,6 +17,8 @@ import types import contextlib +import _colorize # used in doctests + if not support.has_subprocess_support: raise unittest.SkipTest("test_CLI requires subprocess support.") @@ -466,7 +468,7 @@ def basics(): r""" >>> tests = finder.find(sample_func) >>> print(tests) # doctest: +ELLIPSIS - [] + [] The exact name depends on how test_doctest was invoked, so allow for leading path components. @@ -2634,8 +2636,10 @@ def test_testfile(): r""" called with the name of a file, which is taken to be relative to the calling module. The return value is (#failures, #tests). -We don't want `-v` in sys.argv for these tests. +We don't want colour or `-v` in sys.argv for these tests. + >>> save_colorize = _colorize._COLORIZE + >>> _colorize._COLORIZE = False >>> save_argv = sys.argv >>> if '-v' in sys.argv: ... sys.argv = [arg for arg in save_argv if arg != '-v'] @@ -2802,6 +2806,7 @@ def test_testfile(): r""" TestResults(failed=0, attempted=2) >>> doctest.master = None # Reset master. >>> sys.argv = save_argv + >>> _colorize._COLORIZE = save_colorize """ class TestImporter(importlib.abc.MetaPathFinder, importlib.abc.ResourceLoader): diff --git a/Lib/test/test_doctest/test_doctest2.py b/Lib/test/test_doctest/test_doctest2.py index ab8a0696736e23..ea9b430fad80a4 100644 --- a/Lib/test/test_doctest/test_doctest2.py +++ b/Lib/test/test_doctest/test_doctest2.py @@ -13,6 +13,9 @@ import sys import unittest + +import _colorize + if sys.flags.optimize >= 2: raise unittest.SkipTest("Cannot test docstrings with -O2") @@ -108,6 +111,15 @@ def clsm(cls, val): class Test(unittest.TestCase): + def setUp(self): + super().setUp() + self.colorize = _colorize._COLORIZE + _colorize._COLORIZE = False + + def tearDown(self): + super().tearDown() + _colorize._COLORIZE = self.colorize + def test_testmod(self): import doctest, sys EXPECTED = 19 diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index ac9d91b5e12885..b8fbb4f43434e7 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -19,6 +19,7 @@ static const char* _Py_stdlib_module_names[] = { "_codecs_tw", "_collections", "_collections_abc", +"_colorize", "_compat_pickle", "_compression", "_contextvars", From d27c0a853d44d768ef7e0f7fdcaacf0ab04035ae Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:38:42 +0300 Subject: [PATCH 02/26] Use _colorize in traceback module --- Lib/test/test_traceback.py | 55 ++++++++++++++++++++------------------ Lib/traceback.py | 41 ++++------------------------ 2 files changed, 34 insertions(+), 62 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index dd9b1850adf086..2f55d2392c484b 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -29,6 +29,8 @@ from functools import partial from pathlib import Path +import _colorize + MODULE_PREFIX = f'{__name__}.' if __name__ == '__main__' else '' test_code = namedtuple('code', ['co_filename', 'co_name']) @@ -45,12 +47,12 @@ class TracebackCases(unittest.TestCase): # 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: @@ -4291,9 +4293,9 @@ def bar(): 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 + "foo" + reset + boldr + "(1,2,3,4)" + reset, lines) self.assertIn("return " + red + "baz" + reset + boldr + "(1," + reset, lines) @@ -4309,11 +4311,11 @@ def test_colorized_syntax_error(self): 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}""{reset}, line {magenta}1{reset}\n', f' a {boldr}${reset} b\n', @@ -4332,15 +4334,15 @@ def 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): + 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}, ' @@ -4363,23 +4365,24 @@ def test_colorized_detection_checks_for_environment_variables(self): 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) + self.assertEqual(_colorize._can_colorize(), False) with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '1'}): - self.assertEqual(traceback._can_colorize(), True) + self.assertEqual(_colorize._can_colorize(), True) with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '0'}): - self.assertEqual(traceback._can_colorize(), False) + self.assertEqual(_colorize._can_colorize(), False) with unittest.mock.patch("os.environ", {'NO_COLOR': '1'}): - self.assertEqual(traceback._can_colorize(), False) + self.assertEqual(_colorize._can_colorize(), False) with unittest.mock.patch("os.environ", {'NO_COLOR': '1', "PYTHON_COLORS": '1'}): - self.assertEqual(traceback._can_colorize(), True) + self.assertEqual(_colorize._can_colorize(), True) with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1'}): - self.assertEqual(traceback._can_colorize(), True) + self.assertEqual(_colorize._can_colorize(), True) with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', 'NO_COLOR': '1'}): - self.assertEqual(traceback._can_colorize(), False) + self.assertEqual(_colorize._can_colorize(), False) with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}): - self.assertEqual(traceback._can_colorize(), False) + self.assertEqual(_colorize._can_colorize(), False) isatty_mock.return_value = False - self.assertEqual(traceback._can_colorize(), False) + self.assertEqual(_colorize._can_colorize(), False) + if __name__ == "__main__": unittest.main() diff --git a/Lib/traceback.py b/Lib/traceback.py index d27c7a726d2bb6..a294fe21574c5a 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1,7 +1,5 @@ """Extract, format and print information about Python stack traces.""" -import os -import io import collections.abc import itertools import linecache @@ -10,6 +8,8 @@ import warnings from contextlib import suppress +from _colorize import _ANSIColors + __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', 'format_tb', 'print_exc', 'format_exc', 'print_exception', @@ -17,12 +17,11 @@ 'FrameSummary', 'StackSummary', 'TracebackException', 'walk_stack', 'walk_tb'] + # # 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.""" @@ -133,33 +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 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, /): + from _colorize import _can_colorize + file = sys.stderr if sys.stderr is not None else sys.__stderr__ colorize = _can_colorize() return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize) @@ -443,13 +419,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' - GREY = '\x1b[90m' - RESET = '\x1b[0m' class StackSummary(list): """A list of FrameSummary objects, representing a stack of frames.""" From bb591b62dd8f6b5c573403b28f03a3dc8f4a4f38 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 17:19:02 +0300 Subject: [PATCH 03/26] Fix whitespace --- Lib/doctest.py | 38 ++++++++++++++++++++++++++ Lib/test/test_doctest/test_doctest.py | 25 +++++++++++++++++ Lib/test/test_doctest/test_doctest2.py | 1 + 3 files changed, 64 insertions(+) diff --git a/Lib/doctest.py b/Lib/doctest.py index ce899d66988c3b..749a187bc3742f 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -147,10 +147,13 @@ def __repr__(self): # Option constants. OPTIONFLAGS_BY_NAME = {} + + def register_optionflag(name): # Create a new flag unless `name` is already known. return OPTIONFLAGS_BY_NAME.setdefault(name, 1 << len(OPTIONFLAGS_BY_NAME)) + DONT_ACCEPT_TRUE_FOR_1 = register_optionflag('DONT_ACCEPT_TRUE_FOR_1') DONT_ACCEPT_BLANKLINE = register_optionflag('DONT_ACCEPT_BLANKLINE') NORMALIZE_WHITESPACE = register_optionflag('NORMALIZE_WHITESPACE') @@ -194,6 +197,7 @@ def register_optionflag(name): # 8. Debugging Support # 9. Example Usage + ###################################################################### ## 1. Utility Functions ###################################################################### @@ -210,6 +214,7 @@ def _extract_future_flags(globs): flags |= feature.compiler_flag return flags + def _normalize_module(module, depth=2): """ Return the module specified by `module`. In particular: @@ -235,10 +240,12 @@ def _normalize_module(module, depth=2): else: raise TypeError("Expected a module, string, or None") + def _newline_convert(data): # The IO module provides a handy decoder for universal newline conversion return IncrementalNewlineDecoder(None, True).decode(data, True) + def _load_testfile(filename, package, module_relative, encoding): if module_relative: package = _normalize_module(package, 3) @@ -257,6 +264,7 @@ def _load_testfile(filename, package, module_relative, encoding): with open(filename, encoding=encoding) as f: return f.read(), filename + def _indent(s, indent=4): """ Add the given number of space characters to the beginning of @@ -265,6 +273,7 @@ def _indent(s, indent=4): # This regexp matches the start of non-blank lines: return re.sub('(?m)^(?!$)', indent*' ', s) + def _exception_traceback(exc_info): """ Return a string containing a traceback message for the given @@ -276,6 +285,7 @@ def _exception_traceback(exc_info): traceback.print_exception(exc_type, exc_val, exc_tb, file=excout) return excout.getvalue() + # Override some StringIO methods. class _SpoofOut(StringIO): def getvalue(self): @@ -291,6 +301,7 @@ def truncate(self, size=None): self.seek(size) StringIO.truncate(self) + # Worst-case linear-time ellipsis matching. def _ellipsis_match(want, got): """ @@ -341,6 +352,7 @@ def _ellipsis_match(want, got): return True + def _comment_line(line): "Return a commented form of the given line" line = line.rstrip() @@ -349,6 +361,7 @@ def _comment_line(line): else: return '#' + def _strip_exception_details(msg): # Support for IGNORE_EXCEPTION_DETAIL. # Get rid of everything except the exception name; in particular, drop @@ -375,6 +388,7 @@ def _strip_exception_details(msg): start = i+1 return msg[start: end] + class _OutputRedirectingPdb(pdb.Pdb): """ A specialized version of the python debugger that redirects stdout @@ -411,6 +425,7 @@ def trace_dispatch(self, *args): finally: sys.stdout = save_stdout + # [XX] Normalize with respect to os.path.pardir? def _module_relative_path(module, test_path): if not inspect.ismodule(module): @@ -446,6 +461,7 @@ def _module_relative_path(module, test_path): # Combine the base directory and the test path. return os.path.join(basedir, test_path) + ###################################################################### ## 2. Example & DocTest ###################################################################### @@ -526,6 +542,7 @@ def __hash__(self): return hash((self.source, self.want, self.lineno, self.indent, self.exc_msg)) + class DocTest: """ A collection of doctest examples that should be run in a single @@ -599,6 +616,7 @@ def __lt__(self, other): < (other.name, other.filename, other_lno, id(other))) + ###################################################################### ## 3. DocTestParser ###################################################################### @@ -1164,6 +1182,7 @@ def _find_lineno(self, obj, source_lines): # We couldn't find the line number. return None + ###################################################################### ## 5. DocTest Runner ###################################################################### @@ -1483,6 +1502,7 @@ def __record_outcome(self, test, failures, tries, skips): __LINECACHE_FILENAME_RE = re.compile(r'.+)' r'\[(?P\d+)\]>$') + def __patched_linecache_getlines(self, filename, module_globals=None): m = self.__LINECACHE_FILENAME_RE.match(filename) if m and m.group('name') == self.test.name: @@ -1819,6 +1839,7 @@ def output_difference(self, example, got, optionflags): else: return 'Expected nothing\nGot nothing\n' + class DocTestFailure(Exception): """A DocTest example has failed in debugging mode. @@ -1838,6 +1859,7 @@ def __init__(self, test, example, got): def __str__(self): return str(self.test) + class UnexpectedException(Exception): """A DocTest example has encountered an unexpected exception @@ -1857,6 +1879,7 @@ def __init__(self, test, example, exc_info): def __str__(self): return str(self.test) + class DebugRunner(DocTestRunner): r"""Run doc tests but raise an exception as soon as there is a failure. @@ -1960,6 +1983,7 @@ def report_unexpected_exception(self, out, test, example, exc_info): def report_failure(self, out, test, example, got): raise DocTestFailure(test, example, got) + ###################################################################### ## 6. Test Functions ###################################################################### @@ -1969,6 +1993,7 @@ def report_failure(self, out, test, example, got): # class, updated by testmod. master = None + def testmod(m=None, name=None, globs=None, verbose=None, report=True, optionflags=0, extraglobs=None, raise_on_error=False, exclude_empty=False): @@ -2221,12 +2246,14 @@ def run_docstring_examples(f, globs, verbose=False, name="NoName", for test in finder.find(f, name, globs=globs): runner.run(test, compileflags=compileflags) + ###################################################################### ## 7. Unittest Support ###################################################################### _unittest_reportflags = 0 + def set_unittest_reportflags(flags): """Sets the unittest option flags. @@ -2427,6 +2454,7 @@ def __repr__(self): def shortDescription(self): return "Doctest: " + self._dt_test.name + class SkipDocTestCase(DocTestCase): def __init__(self, module): self.module = module @@ -2514,6 +2542,7 @@ def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, return suite + class DocFileCase(DocTestCase): def id(self): @@ -2527,6 +2556,7 @@ def format_failure(self, err): % (self._dt_test.name, self._dt_test.filename, err) ) + def DocFileTest(path, module_relative=True, package=None, globs=None, parser=DocTestParser(), encoding=None, **options): @@ -2553,6 +2583,7 @@ def DocFileTest(path, module_relative=True, package=None, test = parser.get_doctest(doc, globs, name, path, 0) return DocFileCase(test, **options) + def DocFileSuite(*paths, **kw): """A unittest suite for one or more doctest files. @@ -2622,6 +2653,7 @@ def DocFileSuite(*paths, **kw): return suite + ###################################################################### ## 8. Debugging Support ###################################################################### @@ -2708,6 +2740,7 @@ def script_from_examples(s): # Add a courtesy newline to prevent exec from choking (see bug #1172785) return '\n'.join(output) + '\n' + def testsource(module, name): """Extract the test sources from a doctest docstring as a script. @@ -2724,11 +2757,13 @@ def testsource(module, name): testsrc = script_from_examples(test.docstring) return testsrc + def debug_src(src, pm=False, globs=None): """Debug a single doctest docstring, in argument `src`'""" testsrc = script_from_examples(src) debug_script(testsrc, pm, globs) + def debug_script(src, pm=False, globs=None): "Debug a test script. `src` is the script, as a string." import pdb @@ -2749,6 +2784,7 @@ def debug_script(src, pm=False, globs=None): else: pdb.Pdb(nosigint=True).run("exec(%r)" % src, globs, globs) + def debug(module, name, pm=False): """Debug a single doctest docstring. @@ -2760,6 +2796,7 @@ def debug(module, name, pm=False): testsrc = testsource(module, name) debug_script(testsrc, pm, module.__dict__) + ###################################################################### ## 9. Example Usage ###################################################################### @@ -2807,6 +2844,7 @@ def get(self): return self.val + __test__ = {"_TestClass": _TestClass, "string": r""" Example of a string object, searched as-is. diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 13ab3c9e6ca200..e1727aa9b17b2d 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -43,6 +43,7 @@ def sample_func(v): """ return v+v + class SampleClass: """ >>> print(1) @@ -135,11 +136,14 @@ def __init__(self, val=0): 0 """ self.val = val + def square(self): return SampleClass.NestedClass(self.val*self.val) + def get(self): return self.val + class SampleNewStyleClass(object): r""" >>> print('1\n2\n3') @@ -168,6 +172,7 @@ def get(self): """ return self.val + ###################################################################### ## Test Cases ###################################################################### @@ -293,6 +298,7 @@ def test_Example(): r""" True """ + def test_DocTest(): r""" Unit tests for the `DocTest` class. @@ -443,6 +449,7 @@ def test_DocTest(): r""" """ + class test_DocTestFinder: def basics(): r""" Unit tests for the `DocTestFinder` class. @@ -828,6 +835,7 @@ def test_empty_namespace_package(self): self.assertEqual(len(include_empty_finder.find(mod)), 1) self.assertEqual(len(exclude_empty_finder.find(mod)), 0) + def test_DocTestParser(): r""" Unit tests for the `DocTestParser` class. @@ -883,6 +891,7 @@ def test_DocTestParser(): r""" ('x+y\n', '5\n', 9) """ + class test_DocTestRunner: def basics(): r""" Unit tests for the `DocTestRunner` class. @@ -1928,6 +1937,7 @@ def option_directives(): r""" ValueError: line 0 of the doctest for s has an option directive on a line with no example: '# doctest: +ELLIPSIS' """ + def test_testsource(): r""" Unit tests for `testsource()`. @@ -1968,6 +1978,7 @@ def test_testsource(): r""" """ + def test_debug(): r""" Create a docstring that we want to debug: @@ -1998,6 +2009,7 @@ def test_debug(): r""" """ + if not hasattr(sys, 'gettrace') or not sys.gettrace(): def test_pdb_set_trace(): """Using pdb.set_trace from a doctest. @@ -2219,6 +2231,7 @@ def test_pdb_set_trace_nested(): TestResults(failed=0, attempted=2) """ + def test_DocTestSuite(): """DocTestSuite creates a unittest test suite from a doctest. @@ -2346,6 +2359,7 @@ def test_DocTestSuite(): automatically cleared for us after a test. """ + def test_DocFileSuite(): """We can test tests found in text files using a DocFileSuite. @@ -2525,6 +2539,7 @@ def test_DocFileSuite(): """ + def test_trailing_space_in_test(): """ Trailing spaces in expected output are significant: @@ -2534,6 +2549,7 @@ def test_trailing_space_in_test(): foo \n """ + class Wrapper: def __init__(self, func): self.func = func @@ -2542,6 +2558,7 @@ def __init__(self, func): def __call__(self, *args, **kwargs): self.func(*args, **kwargs) + @Wrapper def test_look_in_unwrapped(): """ @@ -2551,6 +2568,7 @@ def test_look_in_unwrapped(): 'one other test' """ + def test_unittest_reportflags(): """Default unittest reporting flags can be set to control reporting @@ -2630,6 +2648,7 @@ def test_unittest_reportflags(): """ + def test_testfile(): r""" Tests for the `testfile()` function. This function runs all the doctest examples in a given file. In its simple invocation, it is @@ -2809,6 +2828,7 @@ def test_testfile(): r""" >>> _colorize._COLORIZE = save_colorize """ + class TestImporter(importlib.abc.MetaPathFinder, importlib.abc.ResourceLoader): def find_spec(self, fullname, path, target=None): @@ -2818,6 +2838,7 @@ def get_data(self, path): with open(path, mode='rb') as f: return f.read() + class TestHook: def __init__(self, pathdir): @@ -2922,6 +2943,7 @@ def test_lineendings(): r""" """ + def test_testmod(): r""" Tests for the testmod function. More might be useful, but for now we're just testing the case raised by Issue 6195, where trying to doctest a C module would @@ -2933,6 +2955,7 @@ def test_testmod(): r""" TestResults(failed=0, attempted=0) """ + try: os.fsencode("foo-bär@baz.py") supports_unicode = True @@ -2971,6 +2994,7 @@ def test_unicode(): """ TestResults(failed=1, attempted=1) """ + def test_CLI(): r""" The doctest module can be used to run doctests against an arbitrary file. These tests test this CLI functionality. @@ -3201,6 +3225,7 @@ def test_CLI(): r""" """ + def test_no_trailing_whitespace_stripping(): r""" The fancy reports had a bug for a long time where any trailing whitespace on diff --git a/Lib/test/test_doctest/test_doctest2.py b/Lib/test/test_doctest/test_doctest2.py index ea9b430fad80a4..73bc04ba11571c 100644 --- a/Lib/test/test_doctest/test_doctest2.py +++ b/Lib/test/test_doctest/test_doctest2.py @@ -19,6 +19,7 @@ if sys.flags.optimize >= 2: raise unittest.SkipTest("Cannot test docstrings with -O2") + class C(object): """Class C. From 42079be22a157b34db9d89ac81158f149fab8f81 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 17:19:10 +0300 Subject: [PATCH 04/26] Use f-strings --- Lib/doctest.py | 14 +++++++------- Lib/test/test_doctest/test_doctest2.py | 7 ++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 749a187bc3742f..f537eca715bfd6 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1050,7 +1050,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): # Look for tests in a module's contained objects. if inspect.ismodule(obj) and self._recurse: for valname, val in obj.__dict__.items(): - valname = '%s.%s' % (name, valname) + valname = f'{name}.{valname}' # Recurse to functions & classes. if ((self._is_routine(val) or inspect.isclass(val)) and @@ -1071,7 +1071,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): "must be strings, functions, methods, " "classes, or modules: %r" % (type(val),)) - valname = '%s.__test__.%s' % (name, valname) + valname = f'{name}.__test__.{valname}' self._find(tests, val, valname, module, source_lines, globs, seen) @@ -1086,7 +1086,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): if ((inspect.isroutine(val) or inspect.isclass(val) or isinstance(val, property)) and self._from_module(module, val)): - valname = '%s.%s' % (name, valname) + valname = f'{name}.{valname}' self._find(tests, val, valname, module, source_lines, globs, seen) @@ -1335,7 +1335,7 @@ def _failure_header(self, test, example): out.append('File "%s", line %s, in %s' % (test.filename, lineno, test.name)) else: - out.append('Line %s, in %s' % (example.lineno+1, test.name)) + out.append(f'Line {example.lineno+1}, in {test.name}') out.append('Failed example:') source = example.source out.append(_indent(source)) @@ -1831,7 +1831,7 @@ def output_difference(self, example, got, optionflags): # If we're not using diff, then simply list the expected # output followed by the actual output. if want and got: - return 'Expected:\n%sGot:\n%s' % (_indent(want), _indent(got)) + return f'Expected:\n{_indent(want)}Got:\n{_indent(got)}' elif want: return 'Expected:\n%sGot nothing\n' % _indent(want) elif got: @@ -2071,7 +2071,7 @@ class doctest.Tester, then merges the results into (or creates) # Check that we were actually given a module. if not inspect.ismodule(m): - raise TypeError("testmod: module required; %r" % (m,)) + raise TypeError(f"testmod: module required; {m!r}") # If no name was given, then use the module's name. if name is None: @@ -2447,7 +2447,7 @@ def __hash__(self): def __repr__(self): name = self._dt_test.name.split('.') - return "%s (%s)" % (name[-1], '.'.join(name[:-1])) + return f"{name[-1]} ({'.'.join(name[:-1])})" __str__ = object.__str__ diff --git a/Lib/test/test_doctest/test_doctest2.py b/Lib/test/test_doctest/test_doctest2.py index 73bc04ba11571c..a7765e05eafd78 100644 --- a/Lib/test/test_doctest/test_doctest2.py +++ b/Lib/test/test_doctest/test_doctest2.py @@ -122,13 +122,14 @@ def tearDown(self): _colorize._COLORIZE = self.colorize def test_testmod(self): - import doctest, sys + import doctest + import sys EXPECTED = 19 f, t = doctest.testmod(sys.modules[__name__]) if f: - self.fail("%d of %d doctests failed" % (f, t)) + self.fail(f"{f} of {t} doctests failed") if t != EXPECTED: - self.fail("expected %d tests to run, not %d" % (EXPECTED, t)) + self.fail(f"expected {EXPECTED} tests to run, not {t}") # Pollute the namespace with a bunch of imported functions and classes, From 0088579e2760a53242816b7d94a99602bff964a2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 11:48:21 -0600 Subject: [PATCH 05/26] Remove underscores from members of an underscored module Co-authored-by: Alex Waygood --- Lib/_colorize.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 412ead5d35f4e4..b984480a9af270 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -2,10 +2,10 @@ import os import sys -_COLORIZE = True +COLORIZE = True -class _ANSIColors: +class ANSIColors: BOLD_GREEN = "\x1b[1;32m" BOLD_MAGENTA = "\x1b[1;35m" BOLD_RED = "\x1b[1;31m" @@ -17,7 +17,7 @@ class _ANSIColors: YELLOW = "\x1b[33m" -def _can_colorize(): +def can_colorize(): if sys.platform == "win32": try: import nt From d3034fa57a6f2ac95ef6f22c0d4d9e72bc71190f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 18:41:50 +0300 Subject: [PATCH 06/26] Add blurb --- .../next/Library/2024-04-06-18-41-36.gh-issue-117225.tJh1Hw.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2024-04-06-18-41-36.gh-issue-117225.tJh1Hw.rst diff --git a/Misc/NEWS.d/next/Library/2024-04-06-18-41-36.gh-issue-117225.tJh1Hw.rst b/Misc/NEWS.d/next/Library/2024-04-06-18-41-36.gh-issue-117225.tJh1Hw.rst new file mode 100644 index 00000000000000..6a0da1c3bc9388 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-04-06-18-41-36.gh-issue-117225.tJh1Hw.rst @@ -0,0 +1 @@ +Add colour to doctest output. Patch by Hugo van Kemenade. From 39780cb079fabc629d5afd69f2e3159983d01574 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 20:53:23 +0300 Subject: [PATCH 07/26] Remove underscores from members of an underscored module --- Lib/_colorize.py | 2 +- Lib/doctest.py | 22 +++++------ Lib/test/test_doctest/test_doctest.py | 6 +-- Lib/test/test_doctest/test_doctest2.py | 6 +-- Lib/test/test_traceback.py | 52 +++++++++++++------------- Lib/traceback.py | 52 +++++++++++++------------- 6 files changed, 70 insertions(+), 70 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index b984480a9af270..4590062c30b357 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -33,7 +33,7 @@ def can_colorize(): return True if "NO_COLOR" in os.environ: return False - if not _COLORIZE: + if not COLORIZE: return False if "FORCE_COLOR" in os.environ: return True diff --git a/Lib/doctest.py b/Lib/doctest.py index f537eca715bfd6..7b98b932f04fa3 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1193,8 +1193,8 @@ class DocTestRunner: The `run` method is used to process a single DocTest case. It returns a TestResults instance. - >>> save_colorize = _colorize._COLORIZE - >>> _colorize._COLORIZE = False + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) @@ -1247,7 +1247,7 @@ class DocTestRunner: overriding the methods `report_start`, `report_success`, `report_unexpected_exception`, and `report_failure`. - >>> _colorize._COLORIZE = save_colorize + >>> _colorize.COLORIZE = save_colorize """ # This divider string is used to separate failure messages, and to # separate sections of the summary. @@ -1593,7 +1593,7 @@ def summarize(self, verbose=None): summary is. If the verbosity is not specified, then the DocTestRunner's verbosity is used. """ - from _colorize import _ANSIColors, _can_colorize + from _colorize import ANSIColors, can_colorize if verbose is None: verbose = self._verbose @@ -1614,13 +1614,13 @@ def summarize(self, verbose=None): 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 + 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 = "" diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index e1727aa9b17b2d..e5fb82880cb201 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -2657,8 +2657,8 @@ def test_testfile(): r""" We don't want colour or `-v` in sys.argv for these tests. - >>> save_colorize = _colorize._COLORIZE - >>> _colorize._COLORIZE = False + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False >>> save_argv = sys.argv >>> if '-v' in sys.argv: ... sys.argv = [arg for arg in save_argv if arg != '-v'] @@ -2825,7 +2825,7 @@ def test_testfile(): r""" TestResults(failed=0, attempted=2) >>> doctest.master = None # Reset master. >>> sys.argv = save_argv - >>> _colorize._COLORIZE = save_colorize + >>> _colorize.COLORIZE = save_colorize """ diff --git a/Lib/test/test_doctest/test_doctest2.py b/Lib/test/test_doctest/test_doctest2.py index a7765e05eafd78..472fac7f21e337 100644 --- a/Lib/test/test_doctest/test_doctest2.py +++ b/Lib/test/test_doctest/test_doctest2.py @@ -114,12 +114,12 @@ def clsm(cls, val): class Test(unittest.TestCase): def setUp(self): super().setUp() - self.colorize = _colorize._COLORIZE - _colorize._COLORIZE = False + self.colorize = _colorize.COLORIZE + _colorize.COLORIZE = False def tearDown(self): super().tearDown() - _colorize._COLORIZE = self.colorize + _colorize.COLORIZE = self.colorize def test_testmod(self): import doctest diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 2f55d2392c484b..d267b8cd10a497 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -47,12 +47,12 @@ class TracebackCases(unittest.TestCase): # formatting of SyntaxErrors works based on changes for 2.1. def setUp(self): super().setUp() - self.colorize = _colorize._COLORIZE - _colorize._COLORIZE = False + self.colorize = _colorize.COLORIZE + _colorize.COLORIZE = False def tearDown(self): super().tearDown() - _colorize._COLORIZE = self.colorize + _colorize.COLORIZE = self.colorize def get_exception_format(self, func, exc): try: @@ -4293,9 +4293,9 @@ def bar(): e, capture_locals=True ) lines = "".join(exc.format(colorize=True)) - red = _colorize._ANSIColors.RED - boldr = _colorize._ANSIColors.BOLD_RED - reset = _colorize._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 + "foo" + reset + boldr + "(1,2,3,4)" + reset, lines) self.assertIn("return " + red + "baz" + reset + boldr + "(1," + reset, lines) @@ -4311,11 +4311,11 @@ def test_colorized_syntax_error(self): e, capture_locals=True ) actual = "".join(exc.format(colorize=True)) - red = _colorize._ANSIColors.RED - magenta = _colorize._ANSIColors.MAGENTA - boldm = _colorize._ANSIColors.BOLD_MAGENTA - boldr = _colorize._ANSIColors.BOLD_RED - reset = _colorize._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}""{reset}, line {magenta}1{reset}\n', f' a {boldr}${reset} b\n', @@ -4334,15 +4334,15 @@ def foo(): self.fail("No exception thrown.") except Exception as e: with captured_output("stderr") as tbstderr: - with unittest.mock.patch('_colorize._can_colorize', return_value=True): + with unittest.mock.patch('_colorize.can_colorize', return_value=True): exception_print(e) actual = tbstderr.getvalue().splitlines() - red = _colorize._ANSIColors.RED - boldr = _colorize._ANSIColors.BOLD_RED - magenta = _colorize._ANSIColors.MAGENTA - boldm = _colorize._ANSIColors.BOLD_MAGENTA - reset = _colorize._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}, ' @@ -4365,23 +4365,23 @@ def test_colorized_detection_checks_for_environment_variables(self): with unittest.mock.patch("os.isatty") as isatty_mock: isatty_mock.return_value = True with unittest.mock.patch("os.environ", {'TERM': 'dumb'}): - self.assertEqual(_colorize._can_colorize(), False) + self.assertEqual(_colorize.can_colorize(), False) with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '1'}): - self.assertEqual(_colorize._can_colorize(), True) + self.assertEqual(_colorize.can_colorize(), True) with unittest.mock.patch("os.environ", {'PYTHON_COLORS': '0'}): - self.assertEqual(_colorize._can_colorize(), False) + self.assertEqual(_colorize.can_colorize(), False) with unittest.mock.patch("os.environ", {'NO_COLOR': '1'}): - self.assertEqual(_colorize._can_colorize(), False) + self.assertEqual(_colorize.can_colorize(), False) with unittest.mock.patch("os.environ", {'NO_COLOR': '1', "PYTHON_COLORS": '1'}): - self.assertEqual(_colorize._can_colorize(), True) + self.assertEqual(_colorize.can_colorize(), True) with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1'}): - self.assertEqual(_colorize._can_colorize(), True) + self.assertEqual(_colorize.can_colorize(), True) with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', 'NO_COLOR': '1'}): - self.assertEqual(_colorize._can_colorize(), False) + self.assertEqual(_colorize.can_colorize(), False) with unittest.mock.patch("os.environ", {'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}): - self.assertEqual(_colorize._can_colorize(), False) + self.assertEqual(_colorize.can_colorize(), False) isatty_mock.return_value = False - self.assertEqual(_colorize._can_colorize(), False) + self.assertEqual(_colorize.can_colorize(), False) if __name__ == "__main__": diff --git a/Lib/traceback.py b/Lib/traceback.py index a294fe21574c5a..c70fefcf069915 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -8,7 +8,7 @@ import warnings from contextlib import suppress -from _colorize import _ANSIColors +from _colorize import ANSIColors __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', @@ -134,10 +134,10 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ def _print_exception_bltin(exc, /): - from _colorize import _can_colorize + from _colorize import can_colorize file = sys.stderr if sys.stderr is not None else sys.__stderr__ - colorize = _can_colorize() + colorize = can_colorize() return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize) @@ -184,9 +184,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}" @@ -523,15 +523,15 @@ def format_frame_summary(self, frame_summary, **kwargs): filename = "" 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: @@ -658,11 +658,11 @@ def output_line(lineno): 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)) @@ -1238,12 +1238,12 @@ def _format_syntax_error(self, stype, **kwargs): if self.lineno is not None: if colorize: yield ' File {}"{}"{}, line {}{}{}\n'.format( - _ANSIColors.MAGENTA, + ANSIColors.MAGENTA, self.filename or "", - _ANSIColors.RESET, - _ANSIColors.MAGENTA, + ANSIColors.RESET, + ANSIColors.MAGENTA, self.lineno, - _ANSIColors.RESET, + ANSIColors.RESET, ) else: yield ' File "{}", line {}\n'.format( @@ -1283,11 +1283,11 @@ def _format_syntax_error(self, stype, **kwargs): # 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), @@ -1300,12 +1300,12 @@ def _format_syntax_error(self, stype, **kwargs): msg = self.msg or "" 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) From c5aec154928269d878e3f6fd7d6c54f2e535cca1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 20:54:03 +0300 Subject: [PATCH 08/26] Revert "Fix whitespace" This reverts commit bb591b62dd8f6b5c573403b28f03a3dc8f4a4f38. --- Lib/doctest.py | 38 -------------------------- Lib/test/test_doctest/test_doctest.py | 25 ----------------- Lib/test/test_doctest/test_doctest2.py | 1 - 3 files changed, 64 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 7b98b932f04fa3..6a361d28703260 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -147,13 +147,10 @@ def __repr__(self): # Option constants. OPTIONFLAGS_BY_NAME = {} - - def register_optionflag(name): # Create a new flag unless `name` is already known. return OPTIONFLAGS_BY_NAME.setdefault(name, 1 << len(OPTIONFLAGS_BY_NAME)) - DONT_ACCEPT_TRUE_FOR_1 = register_optionflag('DONT_ACCEPT_TRUE_FOR_1') DONT_ACCEPT_BLANKLINE = register_optionflag('DONT_ACCEPT_BLANKLINE') NORMALIZE_WHITESPACE = register_optionflag('NORMALIZE_WHITESPACE') @@ -197,7 +194,6 @@ def register_optionflag(name): # 8. Debugging Support # 9. Example Usage - ###################################################################### ## 1. Utility Functions ###################################################################### @@ -214,7 +210,6 @@ def _extract_future_flags(globs): flags |= feature.compiler_flag return flags - def _normalize_module(module, depth=2): """ Return the module specified by `module`. In particular: @@ -240,12 +235,10 @@ def _normalize_module(module, depth=2): else: raise TypeError("Expected a module, string, or None") - def _newline_convert(data): # The IO module provides a handy decoder for universal newline conversion return IncrementalNewlineDecoder(None, True).decode(data, True) - def _load_testfile(filename, package, module_relative, encoding): if module_relative: package = _normalize_module(package, 3) @@ -264,7 +257,6 @@ def _load_testfile(filename, package, module_relative, encoding): with open(filename, encoding=encoding) as f: return f.read(), filename - def _indent(s, indent=4): """ Add the given number of space characters to the beginning of @@ -273,7 +265,6 @@ def _indent(s, indent=4): # This regexp matches the start of non-blank lines: return re.sub('(?m)^(?!$)', indent*' ', s) - def _exception_traceback(exc_info): """ Return a string containing a traceback message for the given @@ -285,7 +276,6 @@ def _exception_traceback(exc_info): traceback.print_exception(exc_type, exc_val, exc_tb, file=excout) return excout.getvalue() - # Override some StringIO methods. class _SpoofOut(StringIO): def getvalue(self): @@ -301,7 +291,6 @@ def truncate(self, size=None): self.seek(size) StringIO.truncate(self) - # Worst-case linear-time ellipsis matching. def _ellipsis_match(want, got): """ @@ -352,7 +341,6 @@ def _ellipsis_match(want, got): return True - def _comment_line(line): "Return a commented form of the given line" line = line.rstrip() @@ -361,7 +349,6 @@ def _comment_line(line): else: return '#' - def _strip_exception_details(msg): # Support for IGNORE_EXCEPTION_DETAIL. # Get rid of everything except the exception name; in particular, drop @@ -388,7 +375,6 @@ def _strip_exception_details(msg): start = i+1 return msg[start: end] - class _OutputRedirectingPdb(pdb.Pdb): """ A specialized version of the python debugger that redirects stdout @@ -425,7 +411,6 @@ def trace_dispatch(self, *args): finally: sys.stdout = save_stdout - # [XX] Normalize with respect to os.path.pardir? def _module_relative_path(module, test_path): if not inspect.ismodule(module): @@ -461,7 +446,6 @@ def _module_relative_path(module, test_path): # Combine the base directory and the test path. return os.path.join(basedir, test_path) - ###################################################################### ## 2. Example & DocTest ###################################################################### @@ -542,7 +526,6 @@ def __hash__(self): return hash((self.source, self.want, self.lineno, self.indent, self.exc_msg)) - class DocTest: """ A collection of doctest examples that should be run in a single @@ -616,7 +599,6 @@ def __lt__(self, other): < (other.name, other.filename, other_lno, id(other))) - ###################################################################### ## 3. DocTestParser ###################################################################### @@ -1182,7 +1164,6 @@ def _find_lineno(self, obj, source_lines): # We couldn't find the line number. return None - ###################################################################### ## 5. DocTest Runner ###################################################################### @@ -1502,7 +1483,6 @@ def __record_outcome(self, test, failures, tries, skips): __LINECACHE_FILENAME_RE = re.compile(r'.+)' r'\[(?P\d+)\]>$') - def __patched_linecache_getlines(self, filename, module_globals=None): m = self.__LINECACHE_FILENAME_RE.match(filename) if m and m.group('name') == self.test.name: @@ -1839,7 +1819,6 @@ def output_difference(self, example, got, optionflags): else: return 'Expected nothing\nGot nothing\n' - class DocTestFailure(Exception): """A DocTest example has failed in debugging mode. @@ -1859,7 +1838,6 @@ def __init__(self, test, example, got): def __str__(self): return str(self.test) - class UnexpectedException(Exception): """A DocTest example has encountered an unexpected exception @@ -1879,7 +1857,6 @@ def __init__(self, test, example, exc_info): def __str__(self): return str(self.test) - class DebugRunner(DocTestRunner): r"""Run doc tests but raise an exception as soon as there is a failure. @@ -1983,7 +1960,6 @@ def report_unexpected_exception(self, out, test, example, exc_info): def report_failure(self, out, test, example, got): raise DocTestFailure(test, example, got) - ###################################################################### ## 6. Test Functions ###################################################################### @@ -1993,7 +1969,6 @@ def report_failure(self, out, test, example, got): # class, updated by testmod. master = None - def testmod(m=None, name=None, globs=None, verbose=None, report=True, optionflags=0, extraglobs=None, raise_on_error=False, exclude_empty=False): @@ -2246,14 +2221,12 @@ def run_docstring_examples(f, globs, verbose=False, name="NoName", for test in finder.find(f, name, globs=globs): runner.run(test, compileflags=compileflags) - ###################################################################### ## 7. Unittest Support ###################################################################### _unittest_reportflags = 0 - def set_unittest_reportflags(flags): """Sets the unittest option flags. @@ -2454,7 +2427,6 @@ def __repr__(self): def shortDescription(self): return "Doctest: " + self._dt_test.name - class SkipDocTestCase(DocTestCase): def __init__(self, module): self.module = module @@ -2542,7 +2514,6 @@ def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, return suite - class DocFileCase(DocTestCase): def id(self): @@ -2556,7 +2527,6 @@ def format_failure(self, err): % (self._dt_test.name, self._dt_test.filename, err) ) - def DocFileTest(path, module_relative=True, package=None, globs=None, parser=DocTestParser(), encoding=None, **options): @@ -2583,7 +2553,6 @@ def DocFileTest(path, module_relative=True, package=None, test = parser.get_doctest(doc, globs, name, path, 0) return DocFileCase(test, **options) - def DocFileSuite(*paths, **kw): """A unittest suite for one or more doctest files. @@ -2653,7 +2622,6 @@ def DocFileSuite(*paths, **kw): return suite - ###################################################################### ## 8. Debugging Support ###################################################################### @@ -2740,7 +2708,6 @@ def script_from_examples(s): # Add a courtesy newline to prevent exec from choking (see bug #1172785) return '\n'.join(output) + '\n' - def testsource(module, name): """Extract the test sources from a doctest docstring as a script. @@ -2757,13 +2724,11 @@ def testsource(module, name): testsrc = script_from_examples(test.docstring) return testsrc - def debug_src(src, pm=False, globs=None): """Debug a single doctest docstring, in argument `src`'""" testsrc = script_from_examples(src) debug_script(testsrc, pm, globs) - def debug_script(src, pm=False, globs=None): "Debug a test script. `src` is the script, as a string." import pdb @@ -2784,7 +2749,6 @@ def debug_script(src, pm=False, globs=None): else: pdb.Pdb(nosigint=True).run("exec(%r)" % src, globs, globs) - def debug(module, name, pm=False): """Debug a single doctest docstring. @@ -2796,7 +2760,6 @@ def debug(module, name, pm=False): testsrc = testsource(module, name) debug_script(testsrc, pm, module.__dict__) - ###################################################################### ## 9. Example Usage ###################################################################### @@ -2844,7 +2807,6 @@ def get(self): return self.val - __test__ = {"_TestClass": _TestClass, "string": r""" Example of a string object, searched as-is. diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index e5fb82880cb201..6a368915746695 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -43,7 +43,6 @@ def sample_func(v): """ return v+v - class SampleClass: """ >>> print(1) @@ -136,14 +135,11 @@ def __init__(self, val=0): 0 """ self.val = val - def square(self): return SampleClass.NestedClass(self.val*self.val) - def get(self): return self.val - class SampleNewStyleClass(object): r""" >>> print('1\n2\n3') @@ -172,7 +168,6 @@ def get(self): """ return self.val - ###################################################################### ## Test Cases ###################################################################### @@ -298,7 +293,6 @@ def test_Example(): r""" True """ - def test_DocTest(): r""" Unit tests for the `DocTest` class. @@ -449,7 +443,6 @@ def test_DocTest(): r""" """ - class test_DocTestFinder: def basics(): r""" Unit tests for the `DocTestFinder` class. @@ -835,7 +828,6 @@ def test_empty_namespace_package(self): self.assertEqual(len(include_empty_finder.find(mod)), 1) self.assertEqual(len(exclude_empty_finder.find(mod)), 0) - def test_DocTestParser(): r""" Unit tests for the `DocTestParser` class. @@ -891,7 +883,6 @@ def test_DocTestParser(): r""" ('x+y\n', '5\n', 9) """ - class test_DocTestRunner: def basics(): r""" Unit tests for the `DocTestRunner` class. @@ -1937,7 +1928,6 @@ def option_directives(): r""" ValueError: line 0 of the doctest for s has an option directive on a line with no example: '# doctest: +ELLIPSIS' """ - def test_testsource(): r""" Unit tests for `testsource()`. @@ -1978,7 +1968,6 @@ def test_testsource(): r""" """ - def test_debug(): r""" Create a docstring that we want to debug: @@ -2009,7 +1998,6 @@ def test_debug(): r""" """ - if not hasattr(sys, 'gettrace') or not sys.gettrace(): def test_pdb_set_trace(): """Using pdb.set_trace from a doctest. @@ -2231,7 +2219,6 @@ def test_pdb_set_trace_nested(): TestResults(failed=0, attempted=2) """ - def test_DocTestSuite(): """DocTestSuite creates a unittest test suite from a doctest. @@ -2359,7 +2346,6 @@ def test_DocTestSuite(): automatically cleared for us after a test. """ - def test_DocFileSuite(): """We can test tests found in text files using a DocFileSuite. @@ -2539,7 +2525,6 @@ def test_DocFileSuite(): """ - def test_trailing_space_in_test(): """ Trailing spaces in expected output are significant: @@ -2549,7 +2534,6 @@ def test_trailing_space_in_test(): foo \n """ - class Wrapper: def __init__(self, func): self.func = func @@ -2558,7 +2542,6 @@ def __init__(self, func): def __call__(self, *args, **kwargs): self.func(*args, **kwargs) - @Wrapper def test_look_in_unwrapped(): """ @@ -2568,7 +2551,6 @@ def test_look_in_unwrapped(): 'one other test' """ - def test_unittest_reportflags(): """Default unittest reporting flags can be set to control reporting @@ -2648,7 +2630,6 @@ def test_unittest_reportflags(): """ - def test_testfile(): r""" Tests for the `testfile()` function. This function runs all the doctest examples in a given file. In its simple invocation, it is @@ -2828,7 +2809,6 @@ def test_testfile(): r""" >>> _colorize.COLORIZE = save_colorize """ - class TestImporter(importlib.abc.MetaPathFinder, importlib.abc.ResourceLoader): def find_spec(self, fullname, path, target=None): @@ -2838,7 +2818,6 @@ def get_data(self, path): with open(path, mode='rb') as f: return f.read() - class TestHook: def __init__(self, pathdir): @@ -2943,7 +2922,6 @@ def test_lineendings(): r""" """ - def test_testmod(): r""" Tests for the testmod function. More might be useful, but for now we're just testing the case raised by Issue 6195, where trying to doctest a C module would @@ -2955,7 +2933,6 @@ def test_testmod(): r""" TestResults(failed=0, attempted=0) """ - try: os.fsencode("foo-bär@baz.py") supports_unicode = True @@ -2994,7 +2971,6 @@ def test_unicode(): """ TestResults(failed=1, attempted=1) """ - def test_CLI(): r""" The doctest module can be used to run doctests against an arbitrary file. These tests test this CLI functionality. @@ -3225,7 +3201,6 @@ def test_CLI(): r""" """ - def test_no_trailing_whitespace_stripping(): r""" The fancy reports had a bug for a long time where any trailing whitespace on diff --git a/Lib/test/test_doctest/test_doctest2.py b/Lib/test/test_doctest/test_doctest2.py index 472fac7f21e337..9ed2b7aaa2118d 100644 --- a/Lib/test/test_doctest/test_doctest2.py +++ b/Lib/test/test_doctest/test_doctest2.py @@ -19,7 +19,6 @@ if sys.flags.optimize >= 2: raise unittest.SkipTest("Cannot test docstrings with -O2") - class C(object): """Class C. From 7e40133f94a4797802c6f3486670f4f2850f553a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 20:58:11 +0300 Subject: [PATCH 09/26] Move _colorize to stdlib block, colour->color --- Lib/doctest.py | 1 - Lib/test/test_doctest/test_doctest.py | 3 +-- Lib/test/test_doctest/test_doctest2.py | 1 - Lib/test/test_traceback.py | 1 - Lib/traceback.py | 1 - 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 6a361d28703260..11db44e234b60b 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -104,7 +104,6 @@ def _test(): import unittest from io import StringIO, IncrementalNewlineDecoder from collections import namedtuple - import _colorize # Used in doctests diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 6a368915746695..54b0392848c670 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -16,7 +16,6 @@ import tempfile import types import contextlib - import _colorize # used in doctests @@ -2636,7 +2635,7 @@ def test_testfile(): r""" called with the name of a file, which is taken to be relative to the calling module. The return value is (#failures, #tests). -We don't want colour or `-v` in sys.argv for these tests. +We don't want color or `-v` in sys.argv for these tests. >>> save_colorize = _colorize.COLORIZE >>> _colorize.COLORIZE = False diff --git a/Lib/test/test_doctest/test_doctest2.py b/Lib/test/test_doctest/test_doctest2.py index 9ed2b7aaa2118d..8194cf036eaaa5 100644 --- a/Lib/test/test_doctest/test_doctest2.py +++ b/Lib/test/test_doctest/test_doctest2.py @@ -13,7 +13,6 @@ import sys import unittest - import _colorize if sys.flags.optimize >= 2: diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index d267b8cd10a497..48b38bb7256bd7 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -28,7 +28,6 @@ import contextlib from functools import partial from pathlib import Path - import _colorize MODULE_PREFIX = f'{__name__}.' if __name__ == '__main__' else '' diff --git a/Lib/traceback.py b/Lib/traceback.py index c70fefcf069915..fb9e762134677c 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -7,7 +7,6 @@ import textwrap import warnings from contextlib import suppress - from _colorize import ANSIColors __all__ = ['extract_stack', 'extract_tb', 'format_exception', From e484465124a04ef95ab3585942c2cb6d946f953d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:03:26 +0300 Subject: [PATCH 10/26] Move imports together --- Lib/traceback.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index fb9e762134677c..6c92e971c12907 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -7,7 +7,8 @@ import textwrap import warnings from contextlib import suppress -from _colorize import ANSIColors + +from _colorize import ANSIColors, can_colorize __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', @@ -133,8 +134,6 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ def _print_exception_bltin(exc, /): - from _colorize import can_colorize - file = sys.stderr if sys.stderr is not None else sys.__stderr__ colorize = can_colorize() return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize) From 1c7b025f91daee5a57381acf1cd1dc56e4e9dd8a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:09:51 +0300 Subject: [PATCH 11/26] Move imports together --- Lib/doctest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 11db44e234b60b..205455feede07d 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -105,6 +105,7 @@ def _test(): from io import StringIO, IncrementalNewlineDecoder from collections import namedtuple import _colorize # Used in doctests +from _colorize import ANSIColors, can_colorize class TestResults(namedtuple('TestResults', 'failed attempted')): @@ -1572,8 +1573,6 @@ def summarize(self, verbose=None): summary is. If the verbosity is not specified, then the DocTestRunner's verbosity is used. """ - from _colorize import ANSIColors, can_colorize - if verbose is None: verbose = self._verbose From ab2c94c82fb8dc9ca8c1855abe560c1c8c2429c7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:12:03 +0300 Subject: [PATCH 12/26] Move imports together --- Lib/test/test_doctest/test_doctest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 54b0392848c670..20c4673c673acb 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -467,7 +467,7 @@ def basics(): r""" >>> tests = finder.find(sample_func) >>> print(tests) # doctest: +ELLIPSIS - [] + [] The exact name depends on how test_doctest was invoked, so allow for leading path components. From 1aaeab85010b92868544c5232e3d6c95549223b8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:12:14 +0300 Subject: [PATCH 13/26] Revert notests -> no_tests --- Lib/doctest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 205455feede07d..9f5550bcff3ef7 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1576,7 +1576,7 @@ def summarize(self, verbose=None): if verbose is None: verbose = self._verbose - no_tests, passed, failed = [], [], [] + notests, passed, failed = [], [], [] total_tries = total_failures = total_skips = 0 for name, (failures, tries, skips) in self._stats.items(): @@ -1586,7 +1586,7 @@ def summarize(self, verbose=None): total_skips += skips if tries == 0: - no_tests.append(name) + notests.append(name) elif failures == 0: passed.append((name, tries)) else: @@ -1608,10 +1608,10 @@ def summarize(self, verbose=None): yellow = "" if verbose: - if no_tests: - print(f"{_n_items(no_tests)} had no tests:") - no_tests.sort() - for name in no_tests: + if notests: + print(f"{_n_items(notests)} had no tests:") + notests.sort() + for name in notests: print(f" {name}") if passed: From cd02e4abecf8df66a09106ca4be260c78b3652ca Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:12:41 +0300 Subject: [PATCH 14/26] Revert "Use f-strings" This reverts commit 42079be22a157b34db9d89ac81158f149fab8f81. --- Lib/doctest.py | 14 +++++++------- Lib/test/test_doctest/test_doctest2.py | 7 +++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 9f5550bcff3ef7..00ff1a107c466a 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1032,7 +1032,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): # Look for tests in a module's contained objects. if inspect.ismodule(obj) and self._recurse: for valname, val in obj.__dict__.items(): - valname = f'{name}.{valname}' + valname = '%s.%s' % (name, valname) # Recurse to functions & classes. if ((self._is_routine(val) or inspect.isclass(val)) and @@ -1053,7 +1053,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): "must be strings, functions, methods, " "classes, or modules: %r" % (type(val),)) - valname = f'{name}.__test__.{valname}' + valname = '%s.__test__.%s' % (name, valname) self._find(tests, val, valname, module, source_lines, globs, seen) @@ -1068,7 +1068,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): if ((inspect.isroutine(val) or inspect.isclass(val) or isinstance(val, property)) and self._from_module(module, val)): - valname = f'{name}.{valname}' + valname = '%s.%s' % (name, valname) self._find(tests, val, valname, module, source_lines, globs, seen) @@ -1316,7 +1316,7 @@ def _failure_header(self, test, example): out.append('File "%s", line %s, in %s' % (test.filename, lineno, test.name)) else: - out.append(f'Line {example.lineno+1}, in {test.name}') + out.append('Line %s, in %s' % (example.lineno+1, test.name)) out.append('Failed example:') source = example.source out.append(_indent(source)) @@ -1809,7 +1809,7 @@ def output_difference(self, example, got, optionflags): # If we're not using diff, then simply list the expected # output followed by the actual output. if want and got: - return f'Expected:\n{_indent(want)}Got:\n{_indent(got)}' + return 'Expected:\n%sGot:\n%s' % (_indent(want), _indent(got)) elif want: return 'Expected:\n%sGot nothing\n' % _indent(want) elif got: @@ -2044,7 +2044,7 @@ class doctest.Tester, then merges the results into (or creates) # Check that we were actually given a module. if not inspect.ismodule(m): - raise TypeError(f"testmod: module required; {m!r}") + raise TypeError("testmod: module required; %r" % (m,)) # If no name was given, then use the module's name. if name is None: @@ -2418,7 +2418,7 @@ def __hash__(self): def __repr__(self): name = self._dt_test.name.split('.') - return f"{name[-1]} ({'.'.join(name[:-1])})" + return "%s (%s)" % (name[-1], '.'.join(name[:-1])) __str__ = object.__str__ diff --git a/Lib/test/test_doctest/test_doctest2.py b/Lib/test/test_doctest/test_doctest2.py index 8194cf036eaaa5..cce77a695efce0 100644 --- a/Lib/test/test_doctest/test_doctest2.py +++ b/Lib/test/test_doctest/test_doctest2.py @@ -120,14 +120,13 @@ def tearDown(self): _colorize.COLORIZE = self.colorize def test_testmod(self): - import doctest - import sys + import doctest, sys EXPECTED = 19 f, t = doctest.testmod(sys.modules[__name__]) if f: - self.fail(f"{f} of {t} doctests failed") + self.fail("%d of %d doctests failed" % (f, t)) if t != EXPECTED: - self.fail(f"expected {EXPECTED} tests to run, not {t}") + self.fail("expected %d tests to run, not %d" % (EXPECTED, t)) # Pollute the namespace with a bunch of imported functions and classes, From 06543ffe916f1ff436dfe2f200f207308df9188c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:22:30 +0300 Subject: [PATCH 15/26] Fix local tests --- Lib/traceback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 6c92e971c12907..1922fd8caee762 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -7,8 +7,8 @@ import textwrap import warnings from contextlib import suppress - -from _colorize import ANSIColors, can_colorize +import _colorize +from _colorize import ANSIColors __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', @@ -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 = can_colorize() + colorize = _colorize.can_colorize() return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize) From 31c6647f36c7e1148d47a8b4dd39eebbb22b8ec1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:01:36 +0300 Subject: [PATCH 16/26] Use red divider for failed test --- Lib/doctest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 00ff1a107c466a..80d0b70592e8cc 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1307,7 +1307,10 @@ def report_unexpected_exception(self, out, test, example, exc_info): 'Exception raised:\n' + _indent(_exception_traceback(exc_info))) def _failure_header(self, test, example): - out = [self.DIVIDER] + red, reset = ( + (ANSIColors.RED, ANSIColors.RESET) if can_colorize() else ("", "") + ) + out = [f"{red}{self.DIVIDER}{reset}"] if test.filename: if test.lineno is not None and example.lineno is not None: lineno = test.lineno + example.lineno + 1 @@ -1621,7 +1624,7 @@ def summarize(self, verbose=None): print(f" {green}{count:3d} test{s} in {name}{reset}") if failed: - print(self.DIVIDER) + print(f"{red}{self.DIVIDER}{reset}") print(f"{red}{_n_items(failed)} had failures:{reset}") for name, (failures, tries, skips) in sorted(failed): print(f"{red} {failures:3d} of {tries:3d} in {name}{reset}") From 9be3d810130ab9fa2cb21a1175bad6acac140d4d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:58:06 +0300 Subject: [PATCH 17/26] Fix local tests --- Lib/test/test_doctest/test_doctest.py | 43 ++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 20c4673c673acb..1457a3790b68be 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -889,6 +889,9 @@ def basics(): r""" DocTestRunner is used to run DocTest test cases, and to accumulate statistics. Here's a simple DocTest case we can use: + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False + >>> def f(x): ... ''' ... >>> x = 12 @@ -943,6 +946,8 @@ def basics(): r""" 6 ok TestResults(failed=1, attempted=3) + + >>> _colorize.COLORIZE = save_colorize """ def verbose_flag(): r""" The `verbose` flag makes the test runner generate more detailed @@ -1018,6 +1023,9 @@ def exceptions(): r""" lines between the first line and the type/value may be omitted or replaced with any other string: + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False + >>> def f(x): ... ''' ... >>> x = 12 @@ -1248,6 +1256,8 @@ def exceptions(): r""" ... ZeroDivisionError: integer division or modulo by zero TestResults(failed=1, attempted=1) + + >>> _colorize.COLORIZE = save_colorize """ def displayhook(): r""" Test that changing sys.displayhook doesn't matter for doctest. @@ -1289,6 +1299,9 @@ def optionflags(): r""" The DONT_ACCEPT_TRUE_FOR_1 flag disables matches between True/False and 1/0: + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False + >>> def f(x): ... '>>> True\n1\n' @@ -1708,6 +1721,7 @@ def optionflags(): r""" Clean up. >>> del doctest.OPTIONFLAGS_BY_NAME[unlikely] + >>> _colorize.COLORIZE = save_colorize """ @@ -1718,6 +1732,9 @@ def option_directives(): r""" single example. To turn an option on for an example, follow that example with a comment of the form ``# doctest: +OPTION``: + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False + >>> def f(x): r''' ... >>> print(list(range(10))) # should fail: no ellipsis ... [0, 1, ..., 9] @@ -1925,6 +1942,8 @@ def option_directives(): r""" >>> test = doctest.DocTestParser().get_doctest(s, {}, 's', 's.py', 0) 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' + + >>> _colorize.COLORIZE = save_colorize """ def test_testsource(): r""" @@ -2008,6 +2027,9 @@ def test_pdb_set_trace(): with a version that restores stdout. This is necessary for you to see debugger output. + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False + >>> doc = ''' ... >>> x = 42 ... >>> raise Exception('clé') @@ -2062,7 +2084,7 @@ def test_pdb_set_trace(): ... finally: ... sys.stdin = real_stdin --Return-- - > (3)calls_set_trace()->None + > (3)calls_set_trace()->None -> import pdb; pdb.set_trace() (Pdb) print(y) 2 @@ -2130,6 +2152,8 @@ def test_pdb_set_trace(): Got: 9 TestResults(failed=1, attempted=3) + + >>> _colorize.COLORIZE = save_colorize """ def test_pdb_set_trace_nested(): @@ -2639,6 +2663,7 @@ def test_testfile(): r""" >>> save_colorize = _colorize.COLORIZE >>> _colorize.COLORIZE = False + >>> save_argv = sys.argv >>> if '-v' in sys.argv: ... sys.argv = [arg for arg in save_argv if arg != '-v'] @@ -2943,6 +2968,9 @@ def test_testmod(): r""" def test_unicode(): """ Check doctest with a non-ascii filename: + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False + >>> doc = ''' ... >>> raise Exception('clé') ... ''' @@ -2968,8 +2996,11 @@ def test_unicode(): """ raise Exception('clé') Exception: clé TestResults(failed=1, attempted=1) + + >>> _colorize.COLORIZE = save_colorize """ + def test_CLI(): r""" The doctest module can be used to run doctests against an arbitrary file. These tests test this CLI functionality. @@ -3260,6 +3291,9 @@ def test_run_doctestsuite_multiple_times(): def test_exception_with_note(note): """ + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False + >>> test_exception_with_note('Note') Traceback (most recent call last): ... @@ -3309,6 +3343,8 @@ def test_exception_with_note(note): ValueError: message note TestResults(failed=1, attempted=...) + + >>> _colorize.COLORIZE = save_colorize """ exc = ValueError('Text') exc.add_note(note) @@ -3389,6 +3425,9 @@ def test_syntax_error_subclass_from_stdlib(): def test_syntax_error_with_incorrect_expected_note(): """ + >>> save_colorize = _colorize.COLORIZE + >>> _colorize.COLORIZE = False + >>> def f(x): ... r''' ... >>> exc = SyntaxError("error", ("x.py", 23, None, "bad syntax")) @@ -3417,6 +3456,8 @@ def test_syntax_error_with_incorrect_expected_note(): note1 note2 TestResults(failed=1, attempted=...) + + >>> _colorize.COLORIZE = save_colorize """ From e4ff3e3dc0d4e085b740955724693dc80849e1d5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:08:59 +0300 Subject: [PATCH 18/26] Less red --- Lib/doctest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 80d0b70592e8cc..7ea71b0d11ce66 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1625,9 +1625,9 @@ def summarize(self, verbose=None): if failed: print(f"{red}{self.DIVIDER}{reset}") - print(f"{red}{_n_items(failed)} had failures:{reset}") + print(f"{_n_items(failed)} had failures:") for name, (failures, tries, skips) in sorted(failed): - print(f"{red} {failures:3d} of {tries:3d} in {name}{reset}") + print(f" {failures:3d} of {tries:3d} in {name}") if verbose: s = "" if total_tries == 1 else "s" From b62500accf60865be8f176df05c41927da88ff30 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:28:06 +0300 Subject: [PATCH 19/26] Revert unnecessary changes --- Lib/test/test_doctest/test_doctest2.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Lib/test/test_doctest/test_doctest2.py b/Lib/test/test_doctest/test_doctest2.py index cce77a695efce0..ab8a0696736e23 100644 --- a/Lib/test/test_doctest/test_doctest2.py +++ b/Lib/test/test_doctest/test_doctest2.py @@ -13,8 +13,6 @@ import sys import unittest -import _colorize - if sys.flags.optimize >= 2: raise unittest.SkipTest("Cannot test docstrings with -O2") @@ -110,15 +108,6 @@ def clsm(cls, val): class Test(unittest.TestCase): - def setUp(self): - super().setUp() - self.colorize = _colorize.COLORIZE - _colorize.COLORIZE = False - - def tearDown(self): - super().tearDown() - _colorize.COLORIZE = self.colorize - def test_testmod(self): import doctest, sys EXPECTED = 19 From eb4f8dc23fdf148f9d63e6c6b0d3831486c7e2f7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:53:38 +0300 Subject: [PATCH 20/26] Move colour tests to test__colorize.py --- Lib/test/test__colorize.py | 128 +++++++++++++++++++++++++++++++++++++ Lib/test/test_traceback.py | 112 -------------------------------- 2 files changed, 128 insertions(+), 112 deletions(-) create mode 100644 Lib/test/test__colorize.py diff --git a/Lib/test/test__colorize.py b/Lib/test/test__colorize.py new file mode 100644 index 00000000000000..2a7bdeafadbab9 --- /dev/null +++ b/Lib/test/test__colorize.py @@ -0,0 +1,128 @@ +import contextlib +import sys +import traceback +import unittest +import unittest.mock +import _colorize +from test.support import captured_output + + +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 = _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 + "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", "", "exec") + except SyntaxError as e: + exc = traceback.TracebackException.from_exception( + e, capture_locals=True + ) + actual = "".join(exc.format(colorize=True)) + magenta = _colorize.ANSIColors.MAGENTA + boldm = _colorize.ANSIColors.BOLD_MAGENTA + boldr = _colorize.ANSIColors.BOLD_RED + reset = _colorize.ANSIColors.RESET + expected = "".join( + [ + f' File {magenta}""{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('_colorize.can_colorize', return_value=True): + exception_print(e) + actual = tbstderr.getvalue().splitlines() + + 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}, ' + f'line {magenta}{lno_foo+6}{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(_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 + self.assertEqual(_colorize.can_colorize(), False) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 48b38bb7256bd7..57cf9adf059a3b 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -7,7 +7,6 @@ import types import inspect import builtins -import unittest import unittest.mock import re import tempfile @@ -25,7 +24,6 @@ import json import textwrap import traceback -import contextlib from functools import partial from pathlib import Path import _colorize @@ -4272,116 +4270,6 @@ def test_levenshtein_distance_short_circuit(self): 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 = _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 + "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", "", "exec") - except SyntaxError as e: - exc = traceback.TracebackException.from_exception( - e, capture_locals=True - ) - actual = "".join(exc.format(colorize=True)) - 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}""{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('_colorize.can_colorize', return_value=True): - exception_print(e) - actual = tbstderr.getvalue().splitlines() - - 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}, ' - 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(_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 - self.assertEqual(_colorize.can_colorize(), False) - if __name__ == "__main__": unittest.main() From 976bfb4c11d8b82733ff4536206572622eab5d5e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 17:10:38 +0300 Subject: [PATCH 21/26] Refactor asserts --- Lib/test/test__colorize.py | 50 +++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/Lib/test/test__colorize.py b/Lib/test/test__colorize.py index 2a7bdeafadbab9..f23fc0e1593afb 100644 --- a/Lib/test/test__colorize.py +++ b/Lib/test/test__colorize.py @@ -30,12 +30,17 @@ def bar(): 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 + "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) + + expected = [ + "y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, + "return " + red + "foo" + reset + boldr + "(1,2,3,4)" + reset, + "return " + red + "baz" + reset + boldr + "(1," + reset, + boldr + "2,3" + reset, + boldr + ",4)" + reset, + red + "bar" + reset + boldr + "()" + reset, + ] + for line in expected: + self.assertIn(line, lines) def test_colorized_syntax_error(self): try: @@ -101,25 +106,26 @@ def test_colorized_detection_checks_for_environment_variables(self): ) else: virtual_patching = contextlib.nullcontext() + + env_vars_expected = [ + ({'TERM': 'dumb'}, False), + ({'PYTHON_COLORS': '1'}, True), + ({'PYTHON_COLORS': '0'}, False), + ({'NO_COLOR': '1'}, False), + ({'NO_COLOR': '1', "PYTHON_COLORS": '1'}, True), + ({'FORCE_COLOR': '1'}, True), + ({'FORCE_COLOR': '1', 'NO_COLOR': '1'}, False), + ({'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}, False), + ] + 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(_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) + + for env_vars, expected in env_vars_expected: + with unittest.mock.patch("os.environ", env_vars): + self.assertEqual(_colorize.can_colorize(), expected) + isatty_mock.return_value = False self.assertEqual(_colorize.can_colorize(), False) From ad7a946c7124da73a2e82cb455ac06c125db38a6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 17:58:14 +0300 Subject: [PATCH 22/26] Add missing captured_output to test.support's __all__ to fix IDE warning --- Lib/test/support/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 2be9cd099a68d6..fb4b0a5071d71f 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -26,7 +26,7 @@ "Error", "TestFailed", "TestDidNotRun", "ResourceDenied", # io "record_original_stdout", "get_original_stdout", "captured_stdout", - "captured_stdin", "captured_stderr", + "captured_stdin", "captured_stderr", "captured_output", # unittest "is_resource_enabled", "requires", "requires_freebsd_version", "requires_gil_enabled", "requires_linux_version", "requires_mac_ver", From 796e9f228a70da282296309cf71aab63d72d7aa3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 19:49:44 +0300 Subject: [PATCH 23/26] Only move test_colorized_detection_checks_for_environment_variables from test_traceback to test__colorize --- Lib/test/test__colorize.py | 91 -------------------------------------- Lib/test/test_traceback.py | 84 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 91 deletions(-) diff --git a/Lib/test/test__colorize.py b/Lib/test/test__colorize.py index f23fc0e1593afb..f9794de2efdb9b 100644 --- a/Lib/test/test__colorize.py +++ b/Lib/test/test__colorize.py @@ -8,97 +8,6 @@ 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 = _colorize.ANSIColors.RED - boldr = _colorize.ANSIColors.BOLD_RED - reset = _colorize.ANSIColors.RESET - - expected = [ - "y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, - "return " + red + "foo" + reset + boldr + "(1,2,3,4)" + reset, - "return " + red + "baz" + reset + boldr + "(1," + reset, - boldr + "2,3" + reset, - boldr + ",4)" + reset, - red + "bar" + reset + boldr + "()" + reset, - ] - for line in expected: - self.assertIn(line, lines) - - def test_colorized_syntax_error(self): - try: - compile("a $ b", "", "exec") - except SyntaxError as e: - exc = traceback.TracebackException.from_exception( - e, capture_locals=True - ) - actual = "".join(exc.format(colorize=True)) - magenta = _colorize.ANSIColors.MAGENTA - boldm = _colorize.ANSIColors.BOLD_MAGENTA - boldr = _colorize.ANSIColors.BOLD_RED - reset = _colorize.ANSIColors.RESET - expected = "".join( - [ - f' File {magenta}""{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('_colorize.can_colorize', return_value=True): - exception_print(e) - actual = tbstderr.getvalue().splitlines() - - 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}, ' - f'line {magenta}{lno_foo+6}{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( diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 57cf9adf059a3b..2e174a7184f2bc 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -7,6 +7,7 @@ import types import inspect import builtins +import unittest import unittest.mock import re import tempfile @@ -4270,6 +4271,89 @@ def test_levenshtein_distance_short_circuit(self): 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 = _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 + "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", "", "exec") + except SyntaxError as e: + exc = traceback.TracebackException.from_exception( + e, capture_locals=True + ) + actual = "".join(exc.format(colorize=True)) + 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}""{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('_colorize.can_colorize', return_value=True): + exception_print(e) + actual = tbstderr.getvalue().splitlines() + + 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}, ' + 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) + if __name__ == "__main__": unittest.main() From 99d4d0c5b1d5ff076ce8c207d3f41e27e0ed6da5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:50:57 -0600 Subject: [PATCH 24/26] Apply suggestions from code review Co-authored-by: Alex Waygood --- Lib/test/test__colorize.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/test/test__colorize.py b/Lib/test/test__colorize.py index f9794de2efdb9b..8abbb680ca2de5 100644 --- a/Lib/test/test__colorize.py +++ b/Lib/test/test__colorize.py @@ -7,7 +7,7 @@ from test.support import captured_output -class TestColorizedTraceback(unittest.TestCase): +class TestColorizeFunction(unittest.TestCase): def test_colorized_detection_checks_for_environment_variables(self): if sys.platform == "win32": virtual_patching = unittest.mock.patch( @@ -32,8 +32,9 @@ def test_colorized_detection_checks_for_environment_variables(self): isatty_mock.return_value = True for env_vars, expected in env_vars_expected: - with unittest.mock.patch("os.environ", env_vars): - self.assertEqual(_colorize.can_colorize(), expected) + with self.subTest(env_vars=env_vars, expected_color=expected): + with unittest.mock.patch("os.environ", env_vars): + self.assertEqual(_colorize.can_colorize(), expected) isatty_mock.return_value = False self.assertEqual(_colorize.can_colorize(), False) From 95b983134099aa29f00d9cf3ffe9690e2fd1ece8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Apr 2024 20:54:55 +0300 Subject: [PATCH 25/26] Use unittest's enterContext Co-authored-by: Alex Waygood --- Lib/test/test__colorize.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/Lib/test/test__colorize.py b/Lib/test/test__colorize.py index 8abbb680ca2de5..4415459f0254ae 100644 --- a/Lib/test/test__colorize.py +++ b/Lib/test/test__colorize.py @@ -1,21 +1,11 @@ -import contextlib import sys -import traceback import unittest import unittest.mock import _colorize -from test.support import captured_output class TestColorizeFunction(unittest.TestCase): 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() - env_vars_expected = [ ({'TERM': 'dumb'}, False), ({'PYTHON_COLORS': '1'}, True), @@ -27,17 +17,23 @@ def test_colorized_detection_checks_for_environment_variables(self): ({'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}, False), ] - with virtual_patching: - with unittest.mock.patch("os.isatty") as isatty_mock: - isatty_mock.return_value = True + if sys.platform == "win32": + self.enterContext( + unittest.mock.patch( + "nt._supports_virtual_terminal", return_value=True + ) + ) + + isatty_mock = self.enterContext(unittest.mock.patch("os.isatty")) + isatty_mock.return_value = True - for env_vars, expected in env_vars_expected: - with self.subTest(env_vars=env_vars, expected_color=expected): - with unittest.mock.patch("os.environ", env_vars): - self.assertEqual(_colorize.can_colorize(), expected) + for env_vars, expected in env_vars_expected: + with self.subTest(env_vars=env_vars, expected_color=expected): + with unittest.mock.patch("os.environ", env_vars): + self.assertEqual(_colorize.can_colorize(), expected) - isatty_mock.return_value = False - self.assertEqual(_colorize.can_colorize(), False) + isatty_mock.return_value = False + self.assertEqual(_colorize.can_colorize(), False) if __name__ == "__main__": From ece3ce0f70d5a44e682d1545dd7d5146cba2d39a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:37:17 +0300 Subject: [PATCH 26/26] Keep colorize functionality in traceback module for now --- Lib/_colorize.py | 45 ------------- Lib/doctest.py | 25 ++++---- Lib/test/test__colorize.py | 40 ------------ Lib/test/test_doctest/test_doctest.py | 58 ++++++++--------- Lib/test/test_traceback.py | 62 ++++++++++++------ Lib/traceback.py | 91 +++++++++++++++++++-------- Python/stdlib_module_names.h | 1 - 7 files changed, 149 insertions(+), 173 deletions(-) delete mode 100644 Lib/_colorize.py delete mode 100644 Lib/test/test__colorize.py diff --git a/Lib/_colorize.py b/Lib/_colorize.py deleted file mode 100644 index 4590062c30b357..00000000000000 --- a/Lib/_colorize.py +++ /dev/null @@ -1,45 +0,0 @@ -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" - - -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() diff --git a/Lib/doctest.py b/Lib/doctest.py index e003e30786ed93..a3b42fdfb12254 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -104,8 +104,7 @@ def _test(): import unittest from io import StringIO, IncrementalNewlineDecoder from collections import namedtuple -import _colorize # Used in doctests -from _colorize import ANSIColors, can_colorize +from traceback import _ANSIColors, _can_colorize class TestResults(namedtuple('TestResults', 'failed attempted')): @@ -1181,8 +1180,8 @@ class DocTestRunner: The `run` method is used to process a single DocTest case. It returns a TestResults instance. - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) @@ -1235,7 +1234,7 @@ class DocTestRunner: overriding the methods `report_start`, `report_success`, `report_unexpected_exception`, and `report_failure`. - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ # This divider string is used to separate failure messages, and to # separate sections of the summary. @@ -1315,7 +1314,7 @@ def report_unexpected_exception(self, out, test, example, exc_info): 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: @@ -1602,13 +1601,13 @@ def summarize(self, verbose=None): 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 + 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 = "" diff --git a/Lib/test/test__colorize.py b/Lib/test/test__colorize.py deleted file mode 100644 index 4415459f0254ae..00000000000000 --- a/Lib/test/test__colorize.py +++ /dev/null @@ -1,40 +0,0 @@ -import sys -import unittest -import unittest.mock -import _colorize - - -class TestColorizeFunction(unittest.TestCase): - def test_colorized_detection_checks_for_environment_variables(self): - env_vars_expected = [ - ({'TERM': 'dumb'}, False), - ({'PYTHON_COLORS': '1'}, True), - ({'PYTHON_COLORS': '0'}, False), - ({'NO_COLOR': '1'}, False), - ({'NO_COLOR': '1', "PYTHON_COLORS": '1'}, True), - ({'FORCE_COLOR': '1'}, True), - ({'FORCE_COLOR': '1', 'NO_COLOR': '1'}, False), - ({'FORCE_COLOR': '1', "PYTHON_COLORS": '0'}, False), - ] - - if sys.platform == "win32": - self.enterContext( - unittest.mock.patch( - "nt._supports_virtual_terminal", return_value=True - ) - ) - - isatty_mock = self.enterContext(unittest.mock.patch("os.isatty")) - isatty_mock.return_value = True - - for env_vars, expected in env_vars_expected: - with self.subTest(env_vars=env_vars, expected_color=expected): - with unittest.mock.patch("os.environ", env_vars): - self.assertEqual(_colorize.can_colorize(), expected) - - isatty_mock.return_value = False - self.assertEqual(_colorize.can_colorize(), False) - - -if __name__ == "__main__": - unittest.main() diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 6da6999cac7c0c..2ad16558d5574c 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -16,7 +16,7 @@ import tempfile import types import contextlib -import _colorize # used in doctests +import traceback def doctest_skip_if(condition): @@ -471,7 +471,7 @@ def basics(): r""" >>> tests = finder.find(sample_func) >>> print(tests) # doctest: +ELLIPSIS - [] + [] The exact name depends on how test_doctest was invoked, so allow for leading path components. @@ -893,8 +893,8 @@ def basics(): r""" DocTestRunner is used to run DocTest test cases, and to accumulate statistics. Here's a simple DocTest case we can use: - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> def f(x): ... ''' @@ -951,7 +951,7 @@ def basics(): r""" ok TestResults(failed=1, attempted=3) - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ def verbose_flag(): r""" The `verbose` flag makes the test runner generate more detailed @@ -1027,8 +1027,8 @@ def exceptions(): r""" lines between the first line and the type/value may be omitted or replaced with any other string: - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> def f(x): ... ''' @@ -1261,7 +1261,7 @@ def exceptions(): r""" ZeroDivisionError: integer division or modulo by zero TestResults(failed=1, attempted=1) - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ def displayhook(): r""" Test that changing sys.displayhook doesn't matter for doctest. @@ -1303,8 +1303,8 @@ def optionflags(): r""" The DONT_ACCEPT_TRUE_FOR_1 flag disables matches between True/False and 1/0: - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> def f(x): ... '>>> True\n1\n' @@ -1725,7 +1725,7 @@ def optionflags(): r""" Clean up. >>> del doctest.OPTIONFLAGS_BY_NAME[unlikely] - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ @@ -1736,8 +1736,8 @@ def option_directives(): r""" single example. To turn an option on for an example, follow that example with a comment of the form ``# doctest: +OPTION``: - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> def f(x): r''' ... >>> print(list(range(10))) # should fail: no ellipsis @@ -1947,7 +1947,7 @@ def option_directives(): r""" 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' - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ def test_testsource(): r""" @@ -2031,8 +2031,8 @@ def test_pdb_set_trace(): with a version that restores stdout. This is necessary for you to see debugger output. - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> doc = ''' ... >>> x = 42 @@ -2157,7 +2157,7 @@ def test_pdb_set_trace(): 9 TestResults(failed=1, attempted=3) - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ def test_pdb_set_trace_nested(): @@ -2679,8 +2679,8 @@ def test_testfile(): r""" We don't want color or `-v` in sys.argv for these tests. - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> save_argv = sys.argv >>> if '-v' in sys.argv: @@ -2848,7 +2848,7 @@ def test_testfile(): r""" TestResults(failed=0, attempted=2) >>> doctest.master = None # Reset master. >>> sys.argv = save_argv - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ class TestImporter(importlib.abc.MetaPathFinder, importlib.abc.ResourceLoader): @@ -2986,8 +2986,8 @@ def test_testmod(): r""" def test_unicode(): """ Check doctest with a non-ascii filename: - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> doc = ''' ... >>> raise Exception('clé') @@ -3015,7 +3015,7 @@ def test_unicode(): """ Exception: clé TestResults(failed=1, attempted=1) - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ @@ -3310,8 +3310,8 @@ def test_run_doctestsuite_multiple_times(): def test_exception_with_note(note): """ - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> test_exception_with_note('Note') Traceback (most recent call last): @@ -3363,7 +3363,7 @@ def test_exception_with_note(note): note TestResults(failed=1, attempted=...) - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ exc = ValueError('Text') exc.add_note(note) @@ -3444,8 +3444,8 @@ def test_syntax_error_subclass_from_stdlib(): def test_syntax_error_with_incorrect_expected_note(): """ - >>> save_colorize = _colorize.COLORIZE - >>> _colorize.COLORIZE = False + >>> save_colorize = traceback._COLORIZE + >>> traceback._COLORIZE = False >>> def f(x): ... r''' @@ -3476,7 +3476,7 @@ def test_syntax_error_with_incorrect_expected_note(): note2 TestResults(failed=1, attempted=...) - >>> _colorize.COLORIZE = save_colorize + >>> traceback._COLORIZE = save_colorize """ diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 2e174a7184f2bc..dd9b1850adf086 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -25,9 +25,9 @@ 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 '' @@ -45,12 +45,12 @@ class TracebackCases(unittest.TestCase): # formatting of SyntaxErrors works based on changes for 2.1. def setUp(self): super().setUp() - self.colorize = _colorize.COLORIZE - _colorize.COLORIZE = False + self.colorize = traceback._COLORIZE + traceback._COLORIZE = False def tearDown(self): super().tearDown() - _colorize.COLORIZE = self.colorize + traceback._COLORIZE = self.colorize def get_exception_format(self, func, exc): try: @@ -4291,9 +4291,9 @@ def bar(): e, capture_locals=True ) lines = "".join(exc.format(colorize=True)) - red = _colorize.ANSIColors.RED - boldr = _colorize.ANSIColors.BOLD_RED - reset = _colorize.ANSIColors.RESET + 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) @@ -4309,11 +4309,11 @@ def test_colorized_syntax_error(self): e, capture_locals=True ) actual = "".join(exc.format(colorize=True)) - red = _colorize.ANSIColors.RED - magenta = _colorize.ANSIColors.MAGENTA - boldm = _colorize.ANSIColors.BOLD_MAGENTA - boldr = _colorize.ANSIColors.BOLD_RED - reset = _colorize.ANSIColors.RESET + 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}""{reset}, line {magenta}1{reset}\n', f' a {boldr}${reset} b\n', @@ -4332,15 +4332,15 @@ def foo(): self.fail("No exception thrown.") except Exception as e: with captured_output("stderr") as tbstderr: - with unittest.mock.patch('_colorize.can_colorize', return_value=True): + with unittest.mock.patch('traceback._can_colorize', return_value=True): exception_print(e) actual = tbstderr.getvalue().splitlines() - red = _colorize.ANSIColors.RED - boldr = _colorize.ANSIColors.BOLD_RED - magenta = _colorize.ANSIColors.MAGENTA - boldm = _colorize.ANSIColors.BOLD_MAGENTA - reset = _colorize.ANSIColors.RESET + 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}, ' @@ -4354,6 +4354,32 @@ def foo(): 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() diff --git a/Lib/traceback.py b/Lib/traceback.py index 1922fd8caee762..054def57c21482 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1,5 +1,7 @@ """Extract, format and print information about Python stack traces.""" +import os +import io import collections.abc import itertools import linecache @@ -7,8 +9,6 @@ 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', @@ -17,11 +17,12 @@ 'FrameSummary', 'StackSummary', 'TracebackException', 'walk_stack', 'walk_tb'] - # # 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.""" @@ -132,10 +133,35 @@ 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 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__ - colorize = _colorize.can_colorize() + colorize = _can_colorize() return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize) @@ -182,9 +208,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}" @@ -417,6 +443,17 @@ 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.""" @@ -521,15 +558,15 @@ def format_frame_summary(self, frame_summary, **kwargs): filename = "" 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: @@ -656,11 +693,11 @@ def output_line(lineno): 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)) @@ -1236,12 +1273,12 @@ def _format_syntax_error(self, stype, **kwargs): if self.lineno is not None: if colorize: yield ' File {}"{}"{}, line {}{}{}\n'.format( - ANSIColors.MAGENTA, + _ANSIColors.MAGENTA, self.filename or "", - ANSIColors.RESET, - ANSIColors.MAGENTA, + _ANSIColors.RESET, + _ANSIColors.MAGENTA, self.lineno, - ANSIColors.RESET, + _ANSIColors.RESET, ) else: yield ' File "{}", line {}\n'.format( @@ -1281,11 +1318,11 @@ def _format_syntax_error(self, stype, **kwargs): # 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), @@ -1298,12 +1335,12 @@ def _format_syntax_error(self, stype, **kwargs): msg = self.msg or "" 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) diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index b8fbb4f43434e7..ac9d91b5e12885 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -19,7 +19,6 @@ static const char* _Py_stdlib_module_names[] = { "_codecs_tw", "_collections", "_collections_abc", -"_colorize", "_compat_pickle", "_compression", "_contextvars",