From a5271379a0ef16a3d0432407566fce628369ec63 Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Sun, 26 Sep 2021 09:57:48 -0400 Subject: [PATCH 1/5] add _format_syntax_error() to doctest, add unit test for it --- Lib/doctest.py | 41 ++++++++++++++++++- Lib/test/test_doctest3.py | 21 ++++++++++ .../2021-09-26-09-56-46.bpo-45249.RG5p25.rst | 2 + 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 Lib/test/test_doctest3.py create mode 100644 Misc/NEWS.d/next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst diff --git a/Lib/doctest.py b/Lib/doctest.py index ba898f65403df1..9005fb377e003f 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -101,6 +101,7 @@ def _test(): import re import sys import traceback +import types import unittest from io import StringIO, IncrementalNewlineDecoder from collections import namedtuple @@ -241,6 +242,44 @@ def _indent(s, indent=4): # This regexp matches the start of non-blank lines: return re.sub('(?m)^(?!$)', indent*' ', s) +def _traceback_format_syntax_error(self, stype): + """Format SyntaxError exceptions (internal helper). Copied from + traceback.py + """ + # Show exactly where the problem was found. + filename_suffix = '' + if self.lineno is not None: + yield ' File "{}", line {}\n'.format( + self.filename or "", self.lineno) + elif self.filename is not None: + filename_suffix = ' ({})'.format(self.filename) + + text = self.text + if text is not None: + # text = " foo\n" + # rtext = " foo" + # ltext = "foo" + rtext = text.rstrip('\n') + ltext = rtext.lstrip(' \n\f') + spaces = len(rtext) - len(ltext) + yield ' {}\n'.format(ltext) + # Convert 1-based column offset to 0-based index into stripped text + caret = (self.offset or 0) - 1 - spaces + if caret >= 0: + # non-space whitespace (likes tabs) must be kept for alignment + caretspace = ((c if c.isspace() else ' ') for c in ltext[:caret]) + yield ' {}{}\n'.format(''.join(caretspace), '^'*(self.exc_value.end_offset-self.offset)) + msg = self.msg or "" + yield "{}: {}{}\n".format(stype, msg, filename_suffix) + +def _print_exception(exc, /, value, tb, limit=None, file=None, chain=True): + """Copied from traceback.py""" + value, tb = traceback._parse_value_tb(exc, value, tb) + te = traceback.TracebackException(type(value), value, tb, limit=limit, compact=True) + te._format_syntax_error = types.MethodType(_traceback_format_syntax_error, te) + te.exc_value = value + te.print(file=file, chain=chain) + def _exception_traceback(exc_info): """ Return a string containing a traceback message for the given @@ -249,7 +288,7 @@ def _exception_traceback(exc_info): # Get a traceback message. excout = StringIO() exc_type, exc_val, exc_tb = exc_info - traceback.print_exception(exc_type, exc_val, exc_tb, file=excout) + _print_exception(exc_type, exc_val, exc_tb, file=excout) return excout.getvalue() # Override some StringIO methods. diff --git a/Lib/test/test_doctest3.py b/Lib/test/test_doctest3.py new file mode 100644 index 00000000000000..0c6eeb8bc12fe1 --- /dev/null +++ b/Lib/test/test_doctest3.py @@ -0,0 +1,21 @@ +import unittest +import io +import contextlib +import doctest + +class Test(unittest.TestCase): + def test_syntax_error(self): + f = io.StringIO() + with contextlib.redirect_stdout(f): + try: + doctest.run_docstring_examples(""" + >>> 1 1 + """, globals()) + except Exception as e: + print('!!!!!', str(e)) + self.assertIn(''' + 1 1 + ^^^ + SyntaxError: invalid syntax. Perhaps you forgot a comma?''', + f.getvalue()) + diff --git a/Misc/NEWS.d/next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst b/Misc/NEWS.d/next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst new file mode 100644 index 00000000000000..e477611cf744a0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst @@ -0,0 +1,2 @@ +Fixed display of :exc:`SyntaxError` errors in :mod:`doctest` to show full +error range indicator rather than a single caret (``^``). From b59ce9b1c8abdba972c5e2959f7974a5b26ecdfe Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Sun, 26 Sep 2021 10:07:16 -0400 Subject: [PATCH 2/5] simplify unit test --- Lib/test/test_doctest3.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_doctest3.py b/Lib/test/test_doctest3.py index 0c6eeb8bc12fe1..95ea38201b06fe 100644 --- a/Lib/test/test_doctest3.py +++ b/Lib/test/test_doctest3.py @@ -3,16 +3,11 @@ import contextlib import doctest -class Test(unittest.TestCase): +class TestDoctest(unittest.TestCase): def test_syntax_error(self): f = io.StringIO() with contextlib.redirect_stdout(f): - try: - doctest.run_docstring_examples(""" - >>> 1 1 - """, globals()) - except Exception as e: - print('!!!!!', str(e)) + doctest.run_docstring_examples('>>> 1 1', globals()) self.assertIn(''' 1 1 ^^^ From 84a8093aaca2f2314f58a55140d025a31425f638 Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Mon, 27 Sep 2021 21:34:01 -0400 Subject: [PATCH 3/5] revert changes to doctest.py --- Lib/doctest.py | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 9005fb377e003f..a4417aa640fc87 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -242,44 +242,6 @@ def _indent(s, indent=4): # This regexp matches the start of non-blank lines: return re.sub('(?m)^(?!$)', indent*' ', s) -def _traceback_format_syntax_error(self, stype): - """Format SyntaxError exceptions (internal helper). Copied from - traceback.py - """ - # Show exactly where the problem was found. - filename_suffix = '' - if self.lineno is not None: - yield ' File "{}", line {}\n'.format( - self.filename or "", self.lineno) - elif self.filename is not None: - filename_suffix = ' ({})'.format(self.filename) - - text = self.text - if text is not None: - # text = " foo\n" - # rtext = " foo" - # ltext = "foo" - rtext = text.rstrip('\n') - ltext = rtext.lstrip(' \n\f') - spaces = len(rtext) - len(ltext) - yield ' {}\n'.format(ltext) - # Convert 1-based column offset to 0-based index into stripped text - caret = (self.offset or 0) - 1 - spaces - if caret >= 0: - # non-space whitespace (likes tabs) must be kept for alignment - caretspace = ((c if c.isspace() else ' ') for c in ltext[:caret]) - yield ' {}{}\n'.format(''.join(caretspace), '^'*(self.exc_value.end_offset-self.offset)) - msg = self.msg or "" - yield "{}: {}{}\n".format(stype, msg, filename_suffix) - -def _print_exception(exc, /, value, tb, limit=None, file=None, chain=True): - """Copied from traceback.py""" - value, tb = traceback._parse_value_tb(exc, value, tb) - te = traceback.TracebackException(type(value), value, tb, limit=limit, compact=True) - te._format_syntax_error = types.MethodType(_traceback_format_syntax_error, te) - te.exc_value = value - te.print(file=file, chain=chain) - def _exception_traceback(exc_info): """ Return a string containing a traceback message for the given @@ -288,7 +250,7 @@ def _exception_traceback(exc_info): # Get a traceback message. excout = StringIO() exc_type, exc_val, exc_tb = exc_info - _print_exception(exc_type, exc_val, exc_tb, file=excout) + traceback.print_exception(exc_type, exc_val, exc_tb, file=excout) return excout.getvalue() # Override some StringIO methods. From 141174a6676482ee30a2b0958e13deb2c7e5547c Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Mon, 27 Sep 2021 21:35:28 -0400 Subject: [PATCH 4/5] remove types import --- Lib/doctest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index a4417aa640fc87..ba898f65403df1 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -101,7 +101,6 @@ def _test(): import re import sys import traceback -import types import unittest from io import StringIO, IncrementalNewlineDecoder from collections import namedtuple From dd363bbd40cff8813a315c3970371047990088e3 Mon Sep 17 00:00:00 2001 From: andrei kulakov Date: Mon, 27 Sep 2021 21:37:37 -0400 Subject: [PATCH 5/5] update news entry --- .../next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst b/Misc/NEWS.d/next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst index e477611cf744a0..871de19a86d14d 100644 --- a/Misc/NEWS.d/next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst +++ b/Misc/NEWS.d/next/Library/2021-09-26-09-56-46.bpo-45249.RG5p25.rst @@ -1,2 +1,2 @@ -Fixed display of :exc:`SyntaxError` errors in :mod:`doctest` to show full -error range indicator rather than a single caret (``^``). +Added regression test for caret indicators of :exc:`SyntaxError` errors +in :mod:`doctest`.