From 0d3241792609a28ac316d7492172626f61e53268 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 28 May 2025 20:49:39 +0300 Subject: [PATCH 1/2] gh-134857: Improve error report for doctests run with unittest Remove doctest module frames from tracebacks and redundant newline character from a failure message. --- Lib/doctest.py | 23 ++-- Lib/test/test_doctest/test_doctest.py | 130 +----------------- ...-05-28-20-49-29.gh-issue-134857.dVYXVO.rst | 3 + 3 files changed, 22 insertions(+), 134 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-05-28-20-49-29.gh-issue-134857.dVYXVO.rst diff --git a/Lib/doctest.py b/Lib/doctest.py index 2acb6cb79f394d..faa294e8fe54f4 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -108,6 +108,8 @@ def _test(): from _colorize import ANSIColors, can_colorize +__unittest = True + class TestResults(namedtuple('TestResults', 'failed attempted')): def __new__(cls, failed, attempted, *, skipped=0): results = super().__new__(cls, failed, attempted) @@ -1395,11 +1397,12 @@ def __run(self, test, compileflags, out): exec(compile(example.source, filename, "single", compileflags, True), test.globs) self.debugger.set_continue() # ==== Example Finished ==== - exception = None + exc_info = None except KeyboardInterrupt: raise except: - exception = sys.exc_info() + exc_info = sys.exc_info() + exc_info = (*exc_info[:2], exc_info[2].tb_next) self.debugger.set_continue() # ==== Example Finished ==== got = self._fakeout.getvalue() # the actual output @@ -1408,21 +1411,21 @@ def __run(self, test, compileflags, out): # If the example executed without raising any exceptions, # verify its output. - if exception is None: + if exc_info is None: if check(example.want, got, self.optionflags): outcome = SUCCESS # The example raised an exception: check if it was expected. else: - formatted_ex = traceback.format_exception_only(*exception[:2]) - if issubclass(exception[0], SyntaxError): + formatted_ex = traceback.format_exception_only(*exc_info[:2]) + if issubclass(exc_info[0], SyntaxError): # SyntaxError / IndentationError is special: # we don't care about the carets / suggestions / etc # We only care about the error message and notes. # They start with `SyntaxError:` (or any other class name) exception_line_prefixes = ( - f"{exception[0].__qualname__}:", - f"{exception[0].__module__}.{exception[0].__qualname__}:", + f"{exc_info[0].__qualname__}:", + f"{exc_info[0].__module__}.{exc_info[0].__qualname__}:", ) exc_msg_index = next( index @@ -1433,7 +1436,7 @@ def __run(self, test, compileflags, out): exc_msg = "".join(formatted_ex) if not quiet: - got += _exception_traceback(exception) + got += _exception_traceback(exc_info) # If `example.exc_msg` is None, then we weren't expecting # an exception. @@ -1462,7 +1465,7 @@ def __run(self, test, compileflags, out): elif outcome is BOOM: if not quiet: self.report_unexpected_exception(out, test, example, - exception) + exc_info) failures += 1 else: assert False, ("unknown outcome", outcome) @@ -2324,7 +2327,7 @@ def runTest(self): sys.stdout = old if results.failed: - raise self.failureException(self.format_failure(new.getvalue())) + raise self.failureException(self.format_failure(new.getvalue().rstrip('\n'))) def format_failure(self, err): test = self._dt_test diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index c5b247797c321d..2bfaa6c599cd47 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -2411,9 +2411,6 @@ def test_DocTestSuite_errors(): >>> result >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - Traceback (most recent call last): - File ... - raise self.failureException(self.format_failure(new.getvalue())) AssertionError: Failed doctest test for test.test_doctest.sample_doctest_errors File "...sample_doctest_errors.py", line 0, in sample_doctest_errors @@ -2431,21 +2428,12 @@ def test_DocTestSuite_errors(): 1/0 Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in 1/0 ~^~ ZeroDivisionError: division by zero - >>> print(result.failures[1][1]) # doctest: +ELLIPSIS - Traceback (most recent call last): - File ... - raise self.failureException(self.format_failure(new.getvalue())) AssertionError: Failed doctest test for test.test_doctest.sample_doctest_errors.__test__.bad File "...sample_doctest_errors.py", line unknown line number, in bad @@ -2463,21 +2451,12 @@ def test_DocTestSuite_errors(): 1/0 Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in 1/0 ~^~ ZeroDivisionError: division by zero - >>> print(result.failures[2][1]) # doctest: +ELLIPSIS - Traceback (most recent call last): - File ... - raise self.failureException(self.format_failure(new.getvalue())) AssertionError: Failed doctest test for test.test_doctest.sample_doctest_errors.errors File "...sample_doctest_errors.py", line 14, in errors @@ -2495,11 +2474,6 @@ def test_DocTestSuite_errors(): 1/0 Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in 1/0 ~^~ @@ -2510,11 +2484,6 @@ def test_DocTestSuite_errors(): f() Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in f() ~^^ @@ -2528,11 +2497,6 @@ def test_DocTestSuite_errors(): g() Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in g() ~^^ @@ -2541,11 +2505,7 @@ def test_DocTestSuite_errors(): ~~^^^ IndexError: list index out of range - >>> print(result.failures[3][1]) # doctest: +ELLIPSIS - Traceback (most recent call last): - File ... - raise self.failureException(self.format_failure(new.getvalue())) AssertionError: Failed doctest test for test.test_doctest.sample_doctest_errors.syntax_error File "...sample_doctest_errors.py", line 29, in syntax_error @@ -2554,18 +2514,11 @@ def test_DocTestSuite_errors(): Failed example: 2+*3 Exception raised: - Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^ File "", line 1 2+*3 ^ SyntaxError: invalid syntax - """ def test_DocFileSuite(): @@ -2740,9 +2693,6 @@ def test_DocFileSuite_errors(): >>> result >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - Traceback (most recent call last): - File ... - raise self.failureException(self.format_failure(new.getvalue())) AssertionError: Failed doctest test for test_doctest_errors.txt File "...test_doctest_errors.txt", line 0 @@ -2760,11 +2710,6 @@ def test_DocFileSuite_errors(): 1/0 Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in 1/0 ~^~ @@ -2775,11 +2720,6 @@ def test_DocFileSuite_errors(): f() Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in f() ~^^ @@ -2792,18 +2732,11 @@ def test_DocFileSuite_errors(): Failed example: 2+*3 Exception raised: - Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^ File "", line 1 2+*3 ^ SyntaxError: invalid syntax - """ def test_trailing_space_in_test(): @@ -2876,7 +2809,8 @@ def test_unittest_reportflags(): >>> result >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - Traceback ... + AssertionError: Failed doctest test for test_doctest.txt + ... Failed example: favorite_color ... @@ -2895,14 +2829,14 @@ def test_unittest_reportflags(): >>> result >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - Traceback ... + AssertionError: Failed doctest test for test_doctest.txt + ... Failed example: favorite_color Exception raised: ... NameError: name 'favorite_color' is not defined - We get only the first failure. @@ -2922,7 +2856,8 @@ def test_unittest_reportflags(): the trailing whitespace using `\x20` in the diff below. >>> print(result.failures[0][1]) # doctest: +ELLIPSIS - Traceback ... + AssertionError: Failed doctest test for test_doctest.txt + ... Failed example: favorite_color ... @@ -2937,7 +2872,6 @@ def test_unittest_reportflags(): +\x20 b - Test runners can restore the formatting flags after they run: @@ -3145,11 +3079,6 @@ def test_testfile_errors(): r""" 1/0 Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in 1/0 ~^~ @@ -3160,11 +3089,6 @@ def test_testfile_errors(): r""" f() Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in f() ~^^ @@ -3177,12 +3101,6 @@ def test_testfile_errors(): r""" Failed example: 2+*3 Exception raised: - Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^ File "", line 1 2+*3 ^ @@ -3343,11 +3261,6 @@ def test_testmod_errors(): r""" 1/0 Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in 1/0 ~^~ @@ -3366,11 +3279,6 @@ def test_testmod_errors(): r""" 1/0 Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in 1/0 ~^~ @@ -3389,11 +3297,6 @@ def test_testmod_errors(): r""" 1/0 Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in 1/0 ~^~ @@ -3404,11 +3307,6 @@ def test_testmod_errors(): r""" f() Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in f() ~^^ @@ -3422,11 +3320,6 @@ def test_testmod_errors(): r""" g() Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in g() ~^^ @@ -3439,12 +3332,6 @@ def test_testmod_errors(): r""" Failed example: 2+*3 Exception raised: - Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^ File "", line 1 2+*3 ^ @@ -3490,11 +3377,6 @@ def test_unicode(): """ raise Exception('clé') Exception raised: Traceback (most recent call last): - File ... - exec(compile(example.source, filename, "single", - ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - compileflags, True), test.globs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in raise Exception('clé') Exception: clé diff --git a/Misc/NEWS.d/next/Library/2025-05-28-20-49-29.gh-issue-134857.dVYXVO.rst b/Misc/NEWS.d/next/Library/2025-05-28-20-49-29.gh-issue-134857.dVYXVO.rst new file mode 100644 index 00000000000000..92e38c0bb5ac87 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-28-20-49-29.gh-issue-134857.dVYXVO.rst @@ -0,0 +1,3 @@ +Improve error report for :mod:`doctest`\ s run with :mod:`unittest`. Remove +:mod:`!doctest` module frames from tracebacks and redundant newline +character from a failure message. From e6e91bf336fe6f3d3b8ff08701b041a3a8d779f6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 29 May 2025 22:25:20 +0300 Subject: [PATCH 2/2] Get rid of sys.exc_info(). --- Lib/doctest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index faa294e8fe54f4..dec10a345165da 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1400,9 +1400,8 @@ def __run(self, test, compileflags, out): exc_info = None except KeyboardInterrupt: raise - except: - exc_info = sys.exc_info() - exc_info = (*exc_info[:2], exc_info[2].tb_next) + except BaseException as exc: + exc_info = type(exc), exc, exc.__traceback__.tb_next self.debugger.set_continue() # ==== Example Finished ==== got = self._fakeout.getvalue() # the actual output