| """Test cases for traceback module""" | |
| from collections import namedtuple | |
| from io import StringIO | |
| import linecache | |
| import sys | |
| import types | |
| import inspect | |
| import builtins | |
| import unittest | |
| import unittest.mock | |
| import os | |
| import re | |
| import tempfile | |
| import random | |
| import string | |
| import importlib.machinery | |
| import sysconfig | |
| from test import support | |
| import shutil | |
| from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ, | |
| requires_debug_ranges, has_no_debug_ranges, | |
| requires_subprocess, os_helper) | |
| from test.support.os_helper import TESTFN, temp_dir, unlink | |
| from test.support.script_helper import assert_python_ok, assert_python_failure, make_script | |
| from test.support.import_helper import ensure_lazy_imports, forget | |
| from test.support import force_not_colorized, force_not_colorized_test_class | |
| import json | |
| import textwrap | |
| import traceback | |
| 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']) | |
| test_code.co_positions = lambda _: iter([(6, 6, 0, 0)]) | |
| test_frame = namedtuple('frame', ['f_code', 'f_globals', 'f_locals']) | |
| test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next', 'tb_lasti']) | |
| color_overrides = {"reset": "z", "filename": "fn", "error_highlight": "E", "note": "n"} | |
| colors = { | |
| color_overrides.get(k, k[0].lower()): v | |
| for k, v in _colorize.default_theme.traceback.items() | |
| } | |
| LEVENSHTEIN_DATA_FILE = Path(__file__).parent / 'levenshtein_examples.json' | |
| class TracebackCases(unittest.TestCase): | |
| # For now, a very minimal set of tests. I want to be sure that | |
| # formatting of SyntaxErrors works based on changes for 2.1. | |
| def setUp(self): | |
| super().setUp() | |
| self.colorize = _colorize.COLORIZE | |
| _colorize.COLORIZE = False | |
| def tearDown(self): | |
| super().tearDown() | |
| _colorize.COLORIZE = self.colorize | |
| def get_exception_format(self, func, exc): | |
| try: | |
| func() | |
| except exc as value: | |
| return traceback.format_exception_only(exc, value) | |
| else: | |
| raise ValueError("call did not raise exception") | |
| def syntax_error_with_caret(self): | |
| compile("def fact(x):\n\treturn x!\n", "?", "exec") | |
| def syntax_error_with_caret_2(self): | |
| compile("1 +\n", "?", "exec") | |
| def syntax_error_with_caret_range(self): | |
| compile("f(x, y for y in range(30), z)", "?", "exec") | |
| def syntax_error_bad_indentation(self): | |
| compile("def spam():\n print(1)\n print(2)", "?", "exec") | |
| def syntax_error_with_caret_non_ascii(self): | |
| compile('Python = "\u1e54\xfd\u0163\u0125\xf2\xf1" +', "?", "exec") | |
| def syntax_error_bad_indentation2(self): | |
| compile(" print(2)", "?", "exec") | |
| def tokenizer_error_with_caret_range(self): | |
| compile("blech ( ", "?", "exec") | |
| def syntax_error_with_caret_wide_char(self): | |
| compile("女女女=1; 女女女/", "?", "exec") | |
| def syntax_error_with_caret_wide_char_range(self): | |
| compile("f(x, 女女女 for 女女女 in range(30), z)", "?", "exec") | |
| def test_caret(self): | |
| err = self.get_exception_format(self.syntax_error_with_caret, | |
| SyntaxError) | |
| self.assertEqual(len(err), 4) | |
| self.assertEqual(err[1].strip(), "return x!") | |
| self.assertIn("^", err[2]) # third line has caret | |
| self.assertEqual(err[1].find("!"), err[2].find("^")) # in the right place | |
| self.assertEqual(err[2].count("^"), 1) | |
| err = self.get_exception_format(self.syntax_error_with_caret_2, | |
| SyntaxError) | |
| self.assertIn("^", err[2]) # third line has caret | |
| self.assertEqual(err[2].count('\n'), 1) # and no additional newline | |
| self.assertEqual(err[1].find("+") + 1, err[2].find("^")) # in the right place | |
| self.assertEqual(err[2].count("^"), 1) | |
| err = self.get_exception_format(self.syntax_error_with_caret_non_ascii, | |
| SyntaxError) | |
| self.assertIn("^", err[2]) # third line has caret | |
| self.assertEqual(err[2].count('\n'), 1) # and no additional newline | |
| self.assertEqual(err[1].find("+") + 1, err[2].find("^")) # in the right place | |
| self.assertEqual(err[2].count("^"), 1) | |
| err = self.get_exception_format(self.syntax_error_with_caret_range, | |
| SyntaxError) | |
| self.assertIn("^", err[2]) # third line has caret | |
| self.assertEqual(err[2].count('\n'), 1) # and no additional newline | |
| self.assertEqual(err[1].find("y"), err[2].find("^")) # in the right place | |
| self.assertEqual(err[2].count("^"), len("y for y in range(30)")) | |
| err = self.get_exception_format(self.tokenizer_error_with_caret_range, | |
| SyntaxError) | |
| self.assertIn("^", err[2]) # third line has caret | |
| self.assertEqual(err[2].count('\n'), 1) # and no additional newline | |
| self.assertEqual(err[1].find("("), err[2].find("^")) # in the right place | |
| self.assertEqual(err[2].count("^"), 1) | |
| def test_caret_wide_char(self): | |
| err = self.get_exception_format(self.syntax_error_with_caret_wide_char, | |
| SyntaxError) | |
| self.assertIn("^", err[2]) | |
| # "女女女=1; 女女女/" has display width 17 | |
| self.assertEqual(err[2].find("^"), 4 + 17) | |
| err = self.get_exception_format(self.syntax_error_with_caret_wide_char_range, | |
| SyntaxError) | |
| self.assertIn("^", err[2]) | |
| self.assertEqual(err[2].find("^"), 4 + 5) | |
| # "女女女 for 女女女 in range(30)" has display width 30 | |
| self.assertEqual(err[2].count("^"), 30) | |
| def test_nocaret(self): | |
| exc = SyntaxError("error", ("x.py", 23, None, "bad syntax")) | |
| err = traceback.format_exception_only(SyntaxError, exc) | |
| self.assertEqual(len(err), 3) | |
| self.assertEqual(err[1].strip(), "bad syntax") | |
| @force_not_colorized | |
| def test_no_caret_with_no_debug_ranges_flag(self): | |
| # Make sure that if `-X no_debug_ranges` is used, there are no carets | |
| # in the traceback. | |
| try: | |
| with open(TESTFN, 'w') as f: | |
| f.write("x = 1 / 0\n") | |
| _, _, stderr = assert_python_failure( | |
| '-X', 'no_debug_ranges', TESTFN) | |
| lines = stderr.splitlines() | |
| self.assertEqual(len(lines), 4) | |
| self.assertEqual(lines[0], b'Traceback (most recent call last):') | |
| self.assertIn(b'line 1, in <module>', lines[1]) | |
| self.assertEqual(lines[2], b' x = 1 / 0') | |
| self.assertEqual(lines[3], b'ZeroDivisionError: division by zero') | |
| finally: | |
| unlink(TESTFN) | |
| def test_no_caret_with_no_debug_ranges_flag_python_traceback(self): | |
| code = textwrap.dedent(""" | |
| import traceback | |
| try: | |
| x = 1 / 0 | |
| except ZeroDivisionError: | |
| traceback.print_exc() | |
| """) | |
| try: | |
| with open(TESTFN, 'w') as f: | |
| f.write(code) | |
| _, _, stderr = assert_python_ok( | |
| '-X', 'no_debug_ranges', TESTFN) | |
| lines = stderr.splitlines() | |
| self.assertEqual(len(lines), 4) | |
| self.assertEqual(lines[0], b'Traceback (most recent call last):') | |
| self.assertIn(b'line 4, in <module>', lines[1]) | |
| self.assertEqual(lines[2], b' x = 1 / 0') | |
| self.assertEqual(lines[3], b'ZeroDivisionError: division by zero') | |
| finally: | |
| unlink(TESTFN) | |
| def test_recursion_error_during_traceback(self): | |
| code = textwrap.dedent(""" | |
| import sys | |
| from weakref import ref | |
| sys.setrecursionlimit(15) | |
| def f(): | |
| ref(lambda: 0, ord) | |
| f() | |
| try: | |
| f() | |
| except RecursionError: | |
| pass | |
| """) | |
| try: | |
| with open(TESTFN, 'w') as f: | |
| f.write(code) | |
| rc, _, _ = assert_python_ok(TESTFN) | |
| self.assertEqual(rc, 0) | |
| finally: | |
| unlink(TESTFN) | |
| def test_bad_indentation(self): | |
| err = self.get_exception_format(self.syntax_error_bad_indentation, | |
| IndentationError) | |
| self.assertEqual(len(err), 4) | |
| self.assertEqual(err[1].strip(), "print(2)") | |
| self.assertIn("^", err[2]) | |
| self.assertEqual(err[1].find(")") + 1, err[2].find("^")) | |
| # No caret for "unexpected indent" | |
| err = self.get_exception_format(self.syntax_error_bad_indentation2, | |
| IndentationError) | |
| self.assertEqual(len(err), 3) | |
| self.assertEqual(err[1].strip(), "print(2)") | |
| def test_base_exception(self): | |
| # Test that exceptions derived from BaseException are formatted right | |
| e = KeyboardInterrupt() | |
| lst = traceback.format_exception_only(e.__class__, e) | |
| self.assertEqual(lst, ['KeyboardInterrupt\n']) | |
| def test_format_exception_only_bad__str__(self): | |
| class X(Exception): | |
| def __str__(self): | |
| 1/0 | |
| err = traceback.format_exception_only(X, X()) | |
| self.assertEqual(len(err), 1) | |
| str_value = '<exception str() failed>' | |
| if X.__module__ in ('__main__', 'builtins'): | |
| str_name = X.__qualname__ | |
| else: | |
| str_name = '.'.join([X.__module__, X.__qualname__]) | |
| self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value)) | |
| def test_format_exception_group_without_show_group(self): | |
| eg = ExceptionGroup('A', [ValueError('B')]) | |
| err = traceback.format_exception_only(eg) | |
| self.assertEqual(err, ['ExceptionGroup: A (1 sub-exception)\n']) | |
| def test_format_exception_group(self): | |
| eg = ExceptionGroup('A', [ValueError('B')]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A (1 sub-exception)\n', | |
| ' ValueError: B\n', | |
| ]) | |
| def test_format_base_exception_group(self): | |
| eg = BaseExceptionGroup('A', [BaseException('B')]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'BaseExceptionGroup: A (1 sub-exception)\n', | |
| ' BaseException: B\n', | |
| ]) | |
| def test_format_exception_group_with_note(self): | |
| exc = ValueError('B') | |
| exc.add_note('Note') | |
| eg = ExceptionGroup('A', [exc]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A (1 sub-exception)\n', | |
| ' ValueError: B\n', | |
| ' Note\n', | |
| ]) | |
| def test_format_exception_group_explicit_class(self): | |
| eg = ExceptionGroup('A', [ValueError('B')]) | |
| err = traceback.format_exception_only(ExceptionGroup, eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A (1 sub-exception)\n', | |
| ' ValueError: B\n', | |
| ]) | |
| def test_format_exception_group_multiple_exceptions(self): | |
| eg = ExceptionGroup('A', [ValueError('B'), TypeError('C')]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A (2 sub-exceptions)\n', | |
| ' ValueError: B\n', | |
| ' TypeError: C\n', | |
| ]) | |
| def test_format_exception_group_multiline_messages(self): | |
| eg = ExceptionGroup('A\n1', [ValueError('B\n2')]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A\n1 (1 sub-exception)\n', | |
| ' ValueError: B\n', | |
| ' 2\n', | |
| ]) | |
| def test_format_exception_group_multiline2_messages(self): | |
| exc = ValueError('B\n\n2\n') | |
| exc.add_note('\nC\n\n3') | |
| eg = ExceptionGroup('A\n\n1\n', [exc, IndexError('D')]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A\n\n1\n (2 sub-exceptions)\n', | |
| ' ValueError: B\n', | |
| ' \n', | |
| ' 2\n', | |
| ' \n', | |
| ' \n', # first char of `note` | |
| ' C\n', | |
| ' \n', | |
| ' 3\n', # note ends | |
| ' IndexError: D\n', | |
| ]) | |
| def test_format_exception_group_syntax_error(self): | |
| exc = SyntaxError("error", ("x.py", 23, None, "bad syntax")) | |
| eg = ExceptionGroup('A\n1', [exc]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A\n1 (1 sub-exception)\n', | |
| ' File "x.py", line 23\n', | |
| ' bad syntax\n', | |
| ' SyntaxError: error\n', | |
| ]) | |
| def test_format_exception_group_nested_with_notes(self): | |
| exc = IndexError('D') | |
| exc.add_note('Note\nmultiline') | |
| eg = ExceptionGroup('A', [ | |
| ValueError('B'), | |
| ExceptionGroup('C', [exc, LookupError('E')]), | |
| TypeError('F'), | |
| ]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A (3 sub-exceptions)\n', | |
| ' ValueError: B\n', | |
| ' ExceptionGroup: C (2 sub-exceptions)\n', | |
| ' IndexError: D\n', | |
| ' Note\n', | |
| ' multiline\n', | |
| ' LookupError: E\n', | |
| ' TypeError: F\n', | |
| ]) | |
| def test_format_exception_group_with_tracebacks(self): | |
| def f(): | |
| try: | |
| 1 / 0 | |
| except ZeroDivisionError as e: | |
| return e | |
| def g(): | |
| try: | |
| raise TypeError('g') | |
| except TypeError as e: | |
| return e | |
| eg = ExceptionGroup('A', [ | |
| f(), | |
| ExceptionGroup('B', [g()]), | |
| ]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A (2 sub-exceptions)\n', | |
| ' ZeroDivisionError: division by zero\n', | |
| ' ExceptionGroup: B (1 sub-exception)\n', | |
| ' TypeError: g\n', | |
| ]) | |
| def test_format_exception_group_with_cause(self): | |
| def f(): | |
| try: | |
| try: | |
| 1 / 0 | |
| except ZeroDivisionError: | |
| raise ValueError(0) | |
| except Exception as e: | |
| return e | |
| eg = ExceptionGroup('A', [f()]) | |
| err = traceback.format_exception_only(eg, show_group=True) | |
| self.assertEqual(err, [ | |
| 'ExceptionGroup: A (1 sub-exception)\n', | |
| ' ValueError: 0\n', | |
| ]) | |
| def test_format_exception_group_syntax_error_with_custom_values(self): | |
| # See https://github.com/python/cpython/issues/128894 | |
| for exc in [ | |
| SyntaxError('error', 'abcd'), | |
| SyntaxError('error', [None] * 4), | |
| SyntaxError('error', (1, 2, 3, 4)), | |
| SyntaxError('error', (1, 2, 3, 4)), | |
| SyntaxError('error', (1, 'a', 'b', 2)), | |
| # with end_lineno and end_offset: | |
| SyntaxError('error', 'abcdef'), | |
| SyntaxError('error', [None] * 6), | |
| SyntaxError('error', (1, 2, 3, 4, 5, 6)), | |
| SyntaxError('error', (1, 'a', 'b', 2, 'c', 'd')), | |
| ]: | |
| with self.subTest(exc=exc): | |
| err = traceback.format_exception_only(exc, show_group=True) | |
| # Should not raise an exception: | |
| if exc.lineno is not None: | |
| self.assertEqual(len(err), 2) | |
| self.assertTrue(err[0].startswith(' File')) | |
| else: | |
| self.assertEqual(len(err), 1) | |
| self.assertEqual(err[-1], 'SyntaxError: error\n') | |
| @requires_subprocess() | |
| @force_not_colorized | |
| def test_encoded_file(self): | |
| # Test that tracebacks are correctly printed for encoded source files: | |
| # - correct line number (Issue2384) | |
| # - respect file encoding (Issue3975) | |
| import sys, subprocess | |
| # The spawned subprocess has its stdout redirected to a PIPE, and its | |
| # encoding may be different from the current interpreter, on Windows | |
| # at least. | |
| process = subprocess.Popen([sys.executable, "-c", | |
| "import sys; print(sys.stdout.encoding)"], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT) | |
| stdout, stderr = process.communicate() | |
| output_encoding = str(stdout, 'ascii').splitlines()[0] | |
| def do_test(firstlines, message, charset, lineno): | |
| # Raise the message in a subprocess, and catch the output | |
| try: | |
| with open(TESTFN, "w", encoding=charset) as output: | |
| output.write("""{0}if 1: | |
| import traceback; | |
| raise RuntimeError('{1}') | |
| """.format(firstlines, message)) | |
| process = subprocess.Popen([sys.executable, TESTFN], | |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | |
| stdout, stderr = process.communicate() | |
| stdout = stdout.decode(output_encoding).splitlines() | |
| finally: | |
| unlink(TESTFN) | |
| # The source lines are encoded with the 'backslashreplace' handler | |
| encoded_message = message.encode(output_encoding, | |
| 'backslashreplace') | |
| # and we just decoded them with the output_encoding. | |
| message_ascii = encoded_message.decode(output_encoding) | |
| err_line = "raise RuntimeError('{0}')".format(message_ascii) | |
| err_msg = "RuntimeError: {0}".format(message_ascii) | |
| self.assertIn("line %s" % lineno, stdout[1]) | |
| self.assertEndsWith(stdout[2], err_line) | |
| actual_err_msg = stdout[3] | |
| self.assertEqual(actual_err_msg, err_msg) | |
| do_test("", "foo", "ascii", 3) | |
| for charset in ("ascii", "iso-8859-1", "utf-8", "GBK"): | |
| if charset == "ascii": | |
| text = "foo" | |
| elif charset == "GBK": | |
| text = "\u4E02\u5100" | |
| else: | |
| text = "h\xe9 ho" | |
| do_test("# coding: {0}\n".format(charset), | |
| text, charset, 4) | |
| do_test("#!shebang\n# coding: {0}\n".format(charset), | |
| text, charset, 5) | |
| do_test(" \t\f\n# coding: {0}\n".format(charset), | |
| text, charset, 5) | |
| # Issue #18960: coding spec should have no effect | |
| do_test("x=0\n# coding: GBK\n", "h\xe9 ho", 'utf-8', 5) | |
| def test_print_traceback_at_exit(self): | |
| # Issue #22599: Ensure that it is possible to use the traceback module | |
| # to display an exception at Python exit | |
| code = textwrap.dedent(""" | |
| import sys | |
| import traceback | |
| class PrintExceptionAtExit(object): | |
| def __init__(self): | |
| try: | |
| x = 1 / 0 | |
| except Exception as e: | |
| self.exc = e | |
| # self.exc.__traceback__ contains frames: | |
| # explicitly clear the reference to self in the current | |
| # frame to break a reference cycle | |
| self = None | |
| def __del__(self): | |
| traceback.print_exception(self.exc) | |
| # Keep a reference in the module namespace to call the destructor | |
| # when the module is unloaded | |
| obj = PrintExceptionAtExit() | |
| """) | |
| rc, stdout, stderr = assert_python_ok('-c', code) | |
| expected = [b'Traceback (most recent call last):', | |
| b' File "<string>", line 8, in __init__', | |
| b' x = 1 / 0', | |
| b' ^^^^^', | |
| b'ZeroDivisionError: division by zero'] | |
| self.assertEqual(stderr.splitlines(), expected) | |
| @cpython_only | |
| def test_lost_io_open(self): | |
| # GH-142737: Display the traceback even if io.open is lost | |
| crasher = textwrap.dedent("""\ | |
| import io | |
| import traceback | |
| # Trigger fallback mode | |
| traceback._print_exception_bltin = None | |
| del io.open | |
| raise RuntimeError("should not crash") | |
| """) | |
| # Create a temporary script to exercise _Py_FindSourceFile | |
| with temp_dir() as script_dir: | |
| script = make_script( | |
| script_dir=script_dir, | |
| script_basename='tb_test_no_io_open', | |
| source=crasher) | |
| rc, stdout, stderr = assert_python_failure(script) | |
| self.assertEqual(rc, 1) # Make sure it's not a crash | |
| expected = [b'Traceback (most recent call last):', | |
| f' File "{script}", line 6, in <module>'.encode(), | |
| b'RuntimeError: should not crash'] | |
| self.assertEqual(stderr.splitlines(), expected) | |
| def test_print_exception(self): | |
| output = StringIO() | |
| traceback.print_exception( | |
| Exception, Exception("projector"), None, file=output | |
| ) | |
| self.assertEqual(output.getvalue(), "Exception: projector\n") | |
| def test_print_exception_exc(self): | |
| output = StringIO() | |
| traceback.print_exception(Exception("projector"), file=output) | |
| self.assertEqual(output.getvalue(), "Exception: projector\n") | |
| def test_print_last(self): | |
| with support.swap_attr(sys, 'last_exc', ValueError(42)): | |
| output = StringIO() | |
| traceback.print_last(file=output) | |
| self.assertEqual(output.getvalue(), "ValueError: 42\n") | |
| def test_format_exception_exc(self): | |
| e = Exception("projector") | |
| output = traceback.format_exception(e) | |
| self.assertEqual(output, ["Exception: projector\n"]) | |
| with self.assertRaisesRegex(ValueError, 'Both or neither'): | |
| traceback.format_exception(e.__class__, e) | |
| with self.assertRaisesRegex(ValueError, 'Both or neither'): | |
| traceback.format_exception(e.__class__, tb=e.__traceback__) | |
| with self.assertRaisesRegex(TypeError, 'required positional argument'): | |
| traceback.format_exception(exc=e) | |
| def test_format_exception_only_exc(self): | |
| output = traceback.format_exception_only(Exception("projector")) | |
| self.assertEqual(output, ["Exception: projector\n"]) | |
| def test_exception_is_None(self): | |
| NONE_EXC_STRING = 'NoneType: None\n' | |
| excfile = StringIO() | |
| traceback.print_exception(None, file=excfile) | |
| self.assertEqual(excfile.getvalue(), NONE_EXC_STRING) | |
| excfile = StringIO() | |
| traceback.print_exception(None, None, None, file=excfile) | |
| self.assertEqual(excfile.getvalue(), NONE_EXC_STRING) | |
| excfile = StringIO() | |
| traceback.print_exc(None, file=excfile) | |
| self.assertEqual(excfile.getvalue(), NONE_EXC_STRING) | |
| self.assertEqual(traceback.format_exc(None), NONE_EXC_STRING) | |
| self.assertEqual(traceback.format_exception(None), [NONE_EXC_STRING]) | |
| self.assertEqual( | |
| traceback.format_exception(None, None, None), [NONE_EXC_STRING]) | |
| self.assertEqual( | |
| traceback.format_exception_only(None), [NONE_EXC_STRING]) | |
| self.assertEqual( | |
| traceback.format_exception_only(None, None), [NONE_EXC_STRING]) | |
| def test_signatures(self): | |
| self.assertEqual( | |
| str(inspect.signature(traceback.print_exception)), | |
| ('(exc, /, value=<implicit>, tb=<implicit>, ' | |
| 'limit=None, file=None, chain=True, **kwargs)')) | |
| self.assertEqual( | |
| str(inspect.signature(traceback.format_exception)), | |
| ('(exc, /, value=<implicit>, tb=<implicit>, limit=None, ' | |
| 'chain=True, **kwargs)')) | |
| self.assertEqual( | |
| str(inspect.signature(traceback.format_exception_only)), | |
| '(exc, /, value=<implicit>, *, show_group=False, **kwargs)') | |
| class PurePythonExceptionFormattingMixin: | |
| def get_exception(self, callable, slice_start=0, slice_end=-1): | |
| try: | |
| callable() | |
| except BaseException: | |
| return traceback.format_exc().splitlines()[slice_start:slice_end] | |
| else: | |
| self.fail("No exception thrown.") | |
| callable_line = get_exception.__code__.co_firstlineno + 2 | |
| class CAPIExceptionFormattingMixin: | |
| LEGACY = 0 | |
| def get_exception(self, callable, slice_start=0, slice_end=-1): | |
| from _testcapi import exception_print | |
| try: | |
| callable() | |
| self.fail("No exception thrown.") | |
| except Exception as e: | |
| with captured_output("stderr") as tbstderr: | |
| exception_print(e, self.LEGACY) | |
| return tbstderr.getvalue().splitlines()[slice_start:slice_end] | |
| callable_line = get_exception.__code__.co_firstlineno + 3 | |
| class CAPIExceptionFormattingLegacyMixin(CAPIExceptionFormattingMixin): | |
| LEGACY = 1 | |
| @requires_debug_ranges() | |
| class TracebackErrorLocationCaretTestBase: | |
| """ | |
| Tests for printing code error expressions as part of PEP 657 | |
| """ | |
| def test_basic_caret(self): | |
| # NOTE: In caret tests, "if True:" is used as a way to force indicator | |
| # display, since the raising expression spans only part of the line. | |
| def f(): | |
| if True: raise ValueError("basic caret tests") | |
| lineno_f = f.__code__.co_firstlineno | |
| expected_f = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+1}, in f\n' | |
| ' if True: raise ValueError("basic caret tests")\n' | |
| ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' | |
| ) | |
| result_lines = self.get_exception(f) | |
| self.assertEqual(result_lines, expected_f.splitlines()) | |
| def test_line_with_unicode(self): | |
| # Make sure that even if a line contains multi-byte unicode characters | |
| # the correct carets are printed. | |
| def f_with_unicode(): | |
| if True: raise ValueError("Ĥellö Wörld") | |
| lineno_f = f_with_unicode.__code__.co_firstlineno | |
| expected_f = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+1}, in f_with_unicode\n' | |
| ' if True: raise ValueError("Ĥellö Wörld")\n' | |
| ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' | |
| ) | |
| result_lines = self.get_exception(f_with_unicode) | |
| self.assertEqual(result_lines, expected_f.splitlines()) | |
| def test_caret_in_type_annotation(self): | |
| def f_with_type(): | |
| def foo(a: THIS_DOES_NOT_EXIST ) -> int: | |
| return 0 | |
| foo.__annotations__ | |
| lineno_f = f_with_type.__code__.co_firstlineno | |
| expected_f = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+3}, in f_with_type\n' | |
| ' foo.__annotations__\n' | |
| f' File "{__file__}", line {lineno_f+1}, in __annotate__\n' | |
| ' def foo(a: THIS_DOES_NOT_EXIST ) -> int:\n' | |
| ' ^^^^^^^^^^^^^^^^^^^\n' | |
| ) | |
| result_lines = self.get_exception(f_with_type) | |
| self.assertEqual(result_lines, expected_f.splitlines()) | |
| def test_caret_multiline_expression(self): | |
| # Make sure no carets are printed for expressions spanning multiple | |
| # lines. | |
| def f_with_multiline(): | |
| if True: raise ValueError( | |
| "error over multiple lines" | |
| ) | |
| lineno_f = f_with_multiline.__code__.co_firstlineno | |
| expected_f = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+1}, in f_with_multiline\n' | |
| ' if True: raise ValueError(\n' | |
| ' ^^^^^^^^^^^^^^^^^\n' | |
| ' "error over multiple lines"\n' | |
| ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' | |
| ' )\n' | |
| ' ^' | |
| ) | |
| result_lines = self.get_exception(f_with_multiline) | |
| self.assertEqual(result_lines, expected_f.splitlines()) | |
| def test_caret_multiline_expression_syntax_error(self): | |
| # Make sure an expression spanning multiple lines that has | |
| # a syntax error is correctly marked with carets. | |
| code = textwrap.dedent(""" | |
| def foo(*args, **kwargs): | |
| pass | |
| a, b, c = 1, 2, 3 | |
| foo(a, z | |
| for z in | |
| range(10), b, c) | |
| """) | |
| def f_with_multiline(): | |
| # Need to defer the compilation until in self.get_exception(..) | |
| return compile(code, "?", "exec") | |
| lineno_f = f_with_multiline.__code__.co_firstlineno | |
| expected_f = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n' | |
| ' return compile(code, "?", "exec")\n' | |
| ' File "?", line 7\n' | |
| ' foo(a, z\n' | |
| ' ^' | |
| ) | |
| result_lines = self.get_exception(f_with_multiline) | |
| self.assertEqual(result_lines, expected_f.splitlines()) | |
| # Check custom error messages covering multiple lines | |
| code = textwrap.dedent(""" | |
| dummy_call( | |
| "dummy value" | |
| foo="bar", | |
| ) | |
| """) | |
| def f_with_multiline(): | |
| # Need to defer the compilation until in self.get_exception(..) | |
| return compile(code, "?", "exec") | |
| lineno_f = f_with_multiline.__code__.co_firstlineno | |
| expected_f = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n' | |
| ' return compile(code, "?", "exec")\n' | |
| ' File "?", line 3\n' | |
| ' "dummy value"\n' | |
| ' ^^^^^^^^^^^^^' | |
| ) | |
| result_lines = self.get_exception(f_with_multiline) | |
| self.assertEqual(result_lines, expected_f.splitlines()) | |
| def test_caret_multiline_expression_bin_op(self): | |
| # Make sure no carets are printed for expressions spanning multiple | |
| # lines. | |
| def f_with_multiline(): | |
| return ( | |
| 2 + 1 / | |
| 0 | |
| ) | |
| lineno_f = f_with_multiline.__code__.co_firstlineno | |
| expected_f = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n' | |
| ' 2 + 1 /\n' | |
| ' ~~^\n' | |
| ' 0\n' | |
| ' ~' | |
| ) | |
| result_lines = self.get_exception(f_with_multiline) | |
| self.assertEqual(result_lines, expected_f.splitlines()) | |
| def test_caret_for_binary_operators(self): | |
| def f_with_binary_operator(): | |
| divisor = 20 | |
| return 10 + divisor / 0 + 30 | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' | |
| ' return 10 + divisor / 0 + 30\n' | |
| ' ~~~~~~~~^~~\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_binary_operators_with_unicode(self): | |
| def f_with_binary_operator(): | |
| áóí = 20 | |
| return 10 + áóí / 0 + 30 | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' | |
| ' return 10 + áóí / 0 + 30\n' | |
| ' ~~~~^~~\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_binary_operators_two_char(self): | |
| def f_with_binary_operator(): | |
| divisor = 20 | |
| return 10 + divisor // 0 + 30 | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' | |
| ' return 10 + divisor // 0 + 30\n' | |
| ' ~~~~~~~~^^~~\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_binary_operators_with_spaces_and_parenthesis(self): | |
| def f_with_binary_operator(): | |
| a = 1 | |
| b = c = "" | |
| return ( a ) +b + c | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n' | |
| ' return ( a ) +b + c\n' | |
| ' ~~~~~~~~~~^~\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_binary_operators_multiline(self): | |
| def f_with_binary_operator(): | |
| b = 1 | |
| c = "" | |
| a = b \ | |
| +\ | |
| c # test | |
| return a | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n' | |
| ' a = b \\\n' | |
| ' ~~~~~~\n' | |
| ' +\\\n' | |
| ' ^~\n' | |
| ' c # test\n' | |
| ' ~\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_binary_operators_multiline_two_char(self): | |
| def f_with_binary_operator(): | |
| b = 1 | |
| c = "" | |
| a = ( | |
| (b # test + | |
| ) \ | |
| # + | |
| << (c # test | |
| \ | |
| ) # test | |
| ) | |
| return a | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+4}, in f_with_binary_operator\n' | |
| ' (b # test +\n' | |
| ' ~~~~~~~~~~~~\n' | |
| ' ) \\\n' | |
| ' ~~~~\n' | |
| ' # +\n' | |
| ' ~~~\n' | |
| ' << (c # test\n' | |
| ' ^^~~~~~~~~~~~\n' | |
| ' \\\n' | |
| ' ~\n' | |
| ' ) # test\n' | |
| ' ~\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_binary_operators_multiline_with_unicode(self): | |
| def f_with_binary_operator(): | |
| b = 1 | |
| a = ("ááá" + | |
| "áá") + b | |
| return a | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' | |
| ' a = ("ááá" +\n' | |
| ' ~~~~~~~~\n' | |
| ' "áá") + b\n' | |
| ' ~~~~~~^~~\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_subscript(self): | |
| def f_with_subscript(): | |
| some_dict = {'x': {'y': None}} | |
| return some_dict['x']['y']['z'] | |
| lineno_f = f_with_subscript.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n' | |
| " return some_dict['x']['y']['z']\n" | |
| ' ~~~~~~~~~~~~~~~~~~~^^^^^\n' | |
| ) | |
| result_lines = self.get_exception(f_with_subscript) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_subscript_unicode(self): | |
| def f_with_subscript(): | |
| some_dict = {'ó': {'á': {'í': {'theta': 1}}}} | |
| return some_dict['ó']['á']['í']['beta'] | |
| lineno_f = f_with_subscript.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n' | |
| " return some_dict['ó']['á']['í']['beta']\n" | |
| ' ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n' | |
| ) | |
| result_lines = self.get_exception(f_with_subscript) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_subscript_with_spaces_and_parenthesis(self): | |
| def f_with_binary_operator(): | |
| a = [] | |
| b = c = 1 | |
| return b [ a ] + c | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n' | |
| ' return b [ a ] + c\n' | |
| ' ~~~~~~^^^^^^^^^\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_subscript_multiline(self): | |
| def f_with_subscript(): | |
| bbbbb = {} | |
| ccc = 1 | |
| ddd = 2 | |
| b = bbbbb \ | |
| [ ccc # test | |
| + ddd \ | |
| ] # test | |
| return b | |
| lineno_f = f_with_subscript.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+4}, in f_with_subscript\n' | |
| ' b = bbbbb \\\n' | |
| ' ~~~~~~~\n' | |
| ' [ ccc # test\n' | |
| ' ^^^^^^^^^^^^^\n' | |
| ' \n' | |
| ' \n' | |
| ' + ddd \\\n' | |
| ' ^^^^^^^^\n' | |
| ' \n' | |
| ' \n' | |
| ' ] # test\n' | |
| ' ^\n' | |
| ) | |
| result_lines = self.get_exception(f_with_subscript) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_call(self): | |
| def f_with_call(): | |
| def f1(a): | |
| def f2(b): | |
| raise RuntimeError("fail") | |
| return f2 | |
| return f1("x")("y")("z") | |
| lineno_f = f_with_call.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+5}, in f_with_call\n' | |
| ' return f1("x")("y")("z")\n' | |
| ' ~~~~~~~^^^^^\n' | |
| f' File "{__file__}", line {lineno_f+3}, in f2\n' | |
| ' raise RuntimeError("fail")\n' | |
| ) | |
| result_lines = self.get_exception(f_with_call) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_call_unicode(self): | |
| def f_with_call(): | |
| def f1(a): | |
| def f2(b): | |
| raise RuntimeError("fail") | |
| return f2 | |
| return f1("ó")("á") | |
| lineno_f = f_with_call.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+5}, in f_with_call\n' | |
| ' return f1("ó")("á")\n' | |
| ' ~~~~~~~^^^^^\n' | |
| f' File "{__file__}", line {lineno_f+3}, in f2\n' | |
| ' raise RuntimeError("fail")\n' | |
| ) | |
| result_lines = self.get_exception(f_with_call) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_call_with_spaces_and_parenthesis(self): | |
| def f_with_binary_operator(): | |
| def f(a): | |
| raise RuntimeError("fail") | |
| return f ( "x" ) + 2 | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n' | |
| ' return f ( "x" ) + 2\n' | |
| ' ~~~~~~^^^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f\n' | |
| ' raise RuntimeError("fail")\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_for_call_multiline(self): | |
| def f_with_call(): | |
| class C: | |
| def y(self, a): | |
| def f(b): | |
| raise RuntimeError("fail") | |
| return f | |
| def g(x): | |
| return C() | |
| a = (g(1).y)( | |
| 2 | |
| )(3)(4) | |
| return a | |
| lineno_f = f_with_call.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+8}, in f_with_call\n' | |
| ' a = (g(1).y)(\n' | |
| ' ~~~~~~~~~\n' | |
| ' 2\n' | |
| ' ~\n' | |
| ' )(3)(4)\n' | |
| ' ~^^^\n' | |
| f' File "{__file__}", line {lineno_f+4}, in f\n' | |
| ' raise RuntimeError("fail")\n' | |
| ) | |
| result_lines = self.get_exception(f_with_call) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_many_lines(self): | |
| def f(): | |
| x = 1 | |
| if True: x += ( | |
| "a" + | |
| "a" | |
| ) # test | |
| lineno_f = f.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f\n' | |
| ' if True: x += (\n' | |
| ' ^^^^^^\n' | |
| ' ...<2 lines>...\n' | |
| ' ) # test\n' | |
| ' ^\n' | |
| ) | |
| result_lines = self.get_exception(f) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_many_lines_no_caret(self): | |
| def f(): | |
| x = 1 | |
| x += ( | |
| "a" + | |
| "a" | |
| ) | |
| lineno_f = f.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f\n' | |
| ' x += (\n' | |
| ' ...<2 lines>...\n' | |
| ' )\n' | |
| ) | |
| result_lines = self.get_exception(f) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_many_lines_binary_op(self): | |
| def f_with_binary_operator(): | |
| b = 1 | |
| c = "a" | |
| a = ( | |
| b + | |
| b | |
| ) + ( | |
| c + | |
| c + | |
| c | |
| ) | |
| return a | |
| lineno_f = f_with_binary_operator.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n' | |
| ' a = (\n' | |
| ' ~\n' | |
| ' b +\n' | |
| ' ~~~\n' | |
| ' b\n' | |
| ' ~\n' | |
| ' ) + (\n' | |
| ' ~~^~~\n' | |
| ' c +\n' | |
| ' ~~~\n' | |
| ' ...<2 lines>...\n' | |
| ' )\n' | |
| ' ~\n' | |
| ) | |
| result_lines = self.get_exception(f_with_binary_operator) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_traceback_specialization_with_syntax_error(self): | |
| bytecode = compile("1 / 0 / 1 / 2\n", TESTFN, "exec") | |
| with open(TESTFN, "w") as file: | |
| # make the file's contents invalid | |
| file.write("1 $ 0 / 1 / 2\n") | |
| self.addCleanup(unlink, TESTFN) | |
| func = partial(exec, bytecode) | |
| result_lines = self.get_exception(func) | |
| lineno_f = bytecode.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{TESTFN}", line {lineno_f}, in <module>\n' | |
| " 1 $ 0 / 1 / 2\n" | |
| ' ^^^^^\n' | |
| ) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_traceback_very_long_line(self): | |
| source = "if True: " + "a" * 256 | |
| bytecode = compile(source, TESTFN, "exec") | |
| with open(TESTFN, "w") as file: | |
| file.write(source) | |
| self.addCleanup(unlink, TESTFN) | |
| func = partial(exec, bytecode) | |
| result_lines = self.get_exception(func) | |
| lineno_f = bytecode.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{TESTFN}", line {lineno_f}, in <module>\n' | |
| f' {source}\n' | |
| f' {" "*len("if True: ") + "^"*256}\n' | |
| ) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_secondary_caret_not_elided(self): | |
| # Always show a line's indicators if they include the secondary character. | |
| def f_with_subscript(): | |
| some_dict = {'x': {'y': None}} | |
| some_dict['x']['y']['z'] | |
| lineno_f = f_with_subscript.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n' | |
| " some_dict['x']['y']['z']\n" | |
| ' ~~~~~~~~~~~~~~~~~~~^^^^^\n' | |
| ) | |
| result_lines = self.get_exception(f_with_subscript) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_caret_exception_group(self): | |
| # Notably, this covers whether indicators handle margin strings correctly. | |
| # (Exception groups use margin strings to display vertical indicators.) | |
| # The implementation must account for both "indent" and "margin" offsets. | |
| def exc(): | |
| if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) | |
| expected_error = ( | |
| f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| f' | callable()\n' | |
| f' | ~~~~~~~~^^\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n' | |
| f' | if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' | |
| f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' | |
| f' | ExceptionGroup: eg (2 sub-exceptions)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | ValueError: 1\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | TypeError: 2\n') | |
| result_lines = self.get_exception(exc) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def assertSpecialized(self, func, expected_specialization): | |
| result_lines = self.get_exception(func) | |
| specialization_line = result_lines[-1] | |
| self.assertEqual(specialization_line.lstrip(), expected_specialization) | |
| def test_specialization_variations(self): | |
| self.assertSpecialized(lambda: 1/0, | |
| "~^~") | |
| self.assertSpecialized(lambda: 1/0/3, | |
| "~^~") | |
| self.assertSpecialized(lambda: 1 / 0, | |
| "~~^~~") | |
| self.assertSpecialized(lambda: 1 / 0 / 3, | |
| "~~^~~") | |
| self.assertSpecialized(lambda: 1/ 0, | |
| "~^~~") | |
| self.assertSpecialized(lambda: 1/ 0/3, | |
| "~^~~") | |
| self.assertSpecialized(lambda: 1 / 0, | |
| "~~~~~^~~~") | |
| self.assertSpecialized(lambda: 1 / 0 / 5, | |
| "~~~~~^~~~") | |
| self.assertSpecialized(lambda: 1 /0, | |
| "~~^~") | |
| self.assertSpecialized(lambda: 1//0, | |
| "~^^~") | |
| self.assertSpecialized(lambda: 1//0//4, | |
| "~^^~") | |
| self.assertSpecialized(lambda: 1 // 0, | |
| "~~^^~~") | |
| self.assertSpecialized(lambda: 1 // 0 // 4, | |
| "~~^^~~") | |
| self.assertSpecialized(lambda: 1 //0, | |
| "~~^^~") | |
| self.assertSpecialized(lambda: 1// 0, | |
| "~^^~~") | |
| def test_decorator_application_lineno_correct(self): | |
| def dec_error(func): | |
| raise TypeError | |
| def dec_fine(func): | |
| return func | |
| def applydecs(): | |
| @dec_error | |
| @dec_fine | |
| def g(): pass | |
| result_lines = self.get_exception(applydecs) | |
| lineno_applydescs = applydecs.__code__.co_firstlineno | |
| lineno_dec_error = dec_error.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_applydescs + 1}, in applydecs\n' | |
| ' @dec_error\n' | |
| ' ^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_dec_error + 1}, in dec_error\n' | |
| ' raise TypeError\n' | |
| ) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def applydecs_class(): | |
| @dec_error | |
| @dec_fine | |
| class A: pass | |
| result_lines = self.get_exception(applydecs_class) | |
| lineno_applydescs_class = applydecs_class.__code__.co_firstlineno | |
| expected_error = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| ' callable()\n' | |
| ' ~~~~~~~~^^\n' | |
| f' File "{__file__}", line {lineno_applydescs_class + 1}, in applydecs_class\n' | |
| ' @dec_error\n' | |
| ' ^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_dec_error + 1}, in dec_error\n' | |
| ' raise TypeError\n' | |
| ) | |
| self.assertEqual(result_lines, expected_error.splitlines()) | |
| def test_multiline_method_call_a(self): | |
| def f(): | |
| (None | |
| .method | |
| )() | |
| actual = self.get_exception(f) | |
| expected = [ | |
| "Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f", | |
| " .method", | |
| " ^^^^^^", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_multiline_method_call_b(self): | |
| def f(): | |
| (None. | |
| method | |
| )() | |
| actual = self.get_exception(f) | |
| expected = [ | |
| "Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f", | |
| " method", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_multiline_method_call_c(self): | |
| def f(): | |
| (None | |
| . method | |
| )() | |
| actual = self.get_exception(f) | |
| expected = [ | |
| "Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f", | |
| " . method", | |
| " ^^^^^^", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_wide_characters_unicode_with_problematic_byte_offset(self): | |
| def f(): | |
| width | |
| actual = self.get_exception(f) | |
| expected = [ | |
| "Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " width", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_byte_offset_with_wide_characters_middle(self): | |
| def f(): | |
| width = 1 | |
| raise ValueError(width) | |
| actual = self.get_exception(f) | |
| expected = [ | |
| "Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f", | |
| " raise ValueError(width)", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_byte_offset_multiline(self): | |
| def f(): | |
| www = 1 | |
| th = 0 | |
| print(1, www( | |
| th)) | |
| actual = self.get_exception(f) | |
| expected = [ | |
| "Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 4}, in f", | |
| f" print(1, www(", | |
| f" ~~~~~~^", | |
| f" th))", | |
| f" ^^^^^", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_byte_offset_with_wide_characters_term_highlight(self): | |
| def f(): | |
| 说明说明 = 1 | |
| şçöğıĤellö = 0 # not wide but still non-ascii | |
| return 说明说明 / şçöğıĤellö | |
| actual = self.get_exception(f) | |
| expected = [ | |
| f"Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| f" callable()", | |
| f" ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 3}, in f", | |
| f" return 说明说明 / şçöğıĤellö", | |
| f" ~~~~~~~~~^~~~~~~~~~~~", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_byte_offset_with_emojis_term_highlight(self): | |
| def f(): | |
| return "✨🐍" + func_说明说明("📗🚛", | |
| "📗🚛") + "🐍" | |
| actual = self.get_exception(f) | |
| expected = [ | |
| f"Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| f" callable()", | |
| f" ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| f' return "✨🐍" + func_说明说明("📗🚛",', | |
| f" ^^^^^^^^^^^^^", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_byte_offset_wide_chars_subscript(self): | |
| def f(): | |
| my_dct = { | |
| "✨🚛✨": { | |
| "说明": { | |
| "🐍🐍🐍": None | |
| } | |
| } | |
| } | |
| return my_dct["✨🚛✨"]["说明"]["🐍"]["说明"]["🐍🐍"] | |
| actual = self.get_exception(f) | |
| expected = [ | |
| f"Traceback (most recent call last):", | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| f" callable()", | |
| f" ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 8}, in f", | |
| f' return my_dct["✨🚛✨"]["说明"]["🐍"]["说明"]["🐍🐍"]', | |
| f" ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^", | |
| ] | |
| self.assertEqual(actual, expected) | |
| def test_memory_error(self): | |
| def f(): | |
| raise MemoryError() | |
| actual = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f' File "{__file__}", line {self.callable_line}, in get_exception', | |
| ' callable()', | |
| ' ~~~~~~~~^^', | |
| f' File "{__file__}", line {f.__code__.co_firstlineno + 1}, in f', | |
| ' raise MemoryError()'] | |
| self.assertEqual(actual, expected) | |
| def test_anchors_for_simple_return_statements_are_elided(self): | |
| def g(): | |
| 1/0 | |
| def f(): | |
| return g() | |
| result_lines = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " return g()", | |
| f" File \"{__file__}\", line {g.__code__.co_firstlineno + 1}, in g", | |
| " 1/0", | |
| " ~^~" | |
| ] | |
| self.assertEqual(result_lines, expected) | |
| def g(): | |
| 1/0 | |
| def f(): | |
| return g() + 1 | |
| result_lines = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " return g() + 1", | |
| " ~^^", | |
| f" File \"{__file__}\", line {g.__code__.co_firstlineno + 1}, in g", | |
| " 1/0", | |
| " ~^~" | |
| ] | |
| self.assertEqual(result_lines, expected) | |
| def g(*args): | |
| 1/0 | |
| def f(): | |
| return g(1, | |
| 2, 4, | |
| 5) | |
| result_lines = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " return g(1,", | |
| " 2, 4,", | |
| " 5)", | |
| f" File \"{__file__}\", line {g.__code__.co_firstlineno + 1}, in g", | |
| " 1/0", | |
| " ~^~" | |
| ] | |
| self.assertEqual(result_lines, expected) | |
| def g(*args): | |
| 1/0 | |
| def f(): | |
| return g(1, | |
| 2, 4, | |
| 5) + 1 | |
| result_lines = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " return g(1,", | |
| " ~^^^", | |
| " 2, 4,", | |
| " ^^^^^", | |
| " 5) + 1", | |
| " ^^", | |
| f" File \"{__file__}\", line {g.__code__.co_firstlineno + 1}, in g", | |
| " 1/0", | |
| " ~^~" | |
| ] | |
| self.assertEqual(result_lines, expected) | |
| def test_anchors_for_simple_assign_statements_are_elided(self): | |
| def g(): | |
| 1/0 | |
| def f(): | |
| x = g() | |
| result_lines = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " x = g()", | |
| f" File \"{__file__}\", line {g.__code__.co_firstlineno + 1}, in g", | |
| " 1/0", | |
| " ~^~" | |
| ] | |
| self.assertEqual(result_lines, expected) | |
| def g(*args): | |
| 1/0 | |
| def f(): | |
| x = g(1, | |
| 2, 3, | |
| 4) | |
| result_lines = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " x = g(1,", | |
| " 2, 3,", | |
| " 4)", | |
| f" File \"{__file__}\", line {g.__code__.co_firstlineno + 1}, in g", | |
| " 1/0", | |
| " ~^~" | |
| ] | |
| self.assertEqual(result_lines, expected) | |
| def g(): | |
| 1/0 | |
| def f(): | |
| x = y = g() | |
| result_lines = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " x = y = g()", | |
| " ~^^", | |
| f" File \"{__file__}\", line {g.__code__.co_firstlineno + 1}, in g", | |
| " 1/0", | |
| " ~^~" | |
| ] | |
| self.assertEqual(result_lines, expected) | |
| def g(*args): | |
| 1/0 | |
| def f(): | |
| x = y = g(1, | |
| 2, 3, | |
| 4) | |
| result_lines = self.get_exception(f) | |
| expected = ['Traceback (most recent call last):', | |
| f" File \"{__file__}\", line {self.callable_line}, in get_exception", | |
| " callable()", | |
| " ~~~~~~~~^^", | |
| f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", | |
| " x = y = g(1,", | |
| " ~^^^", | |
| " 2, 3,", | |
| " ^^^^^", | |
| " 4)", | |
| " ^^", | |
| f" File \"{__file__}\", line {g.__code__.co_firstlineno + 1}, in g", | |
| " 1/0", | |
| " ~^~" | |
| ] | |
| self.assertEqual(result_lines, expected) | |
| class TestKeywordTypoSuggestions(unittest.TestCase): | |
| TYPO_CASES = [ | |
| ("with block ad something:\n pass", "and"), | |
| ("fur a in b:\n pass", "for"), | |
| ("for a in b:\n pass\nelso:\n pass", "else"), | |
| ("whille True:\n pass", "while"), | |
| ("iff x > 5:\n pass", "if"), | |
| ("if x:\n pass\nelseif y:\n pass", "elif"), | |
| ("tyo:\n pass\nexcept y:\n pass", "try"), | |
| ("classe MyClass:\n pass", "class"), | |
| ("impor math", "import"), | |
| ("form x import y", "from"), | |
| ("defn calculate_sum(a, b):\n return a + b", "def"), | |
| ("def foo():\n returm result", "return"), | |
| ("lamda x: x ** 2", "lambda"), | |
| ("def foo():\n yeld i", "yield"), | |
| ("def foo():\n globel counter", "global"), | |
| ("frum math import sqrt", "from"), | |
| ("asynch def fetch_data():\n pass", "async"), | |
| ("async def foo():\n awaid fetch_data()", "await"), | |
| ('raisee ValueError("Error")', "raise"), | |
| ("[x for x\nin range(3)\nof x]", "if"), | |
| ("[123 fur x\nin range(3)\nif x]", "for"), | |
| ("for x im n:\n pass", "in"), | |
| ("mach x:", "match"), | |
| ("math x:", "match"), | |
| ("match 1:\n cse 1:", "case"), | |
| ("typ x = int", "type"), | |
| ("typed x = int", "type"), | |
| ("lazi import x", "lazy"), | |
| ("lezi import x", "lazy"), | |
| ("switch x:\n case:", "match"), | |
| ("delete x", "del"), | |
| ("function f():", "def"), | |
| ("func f():", "def"), | |
| ("void f():", "def"), | |
| ] | |
| def test_keyword_suggestions_from_file(self): | |
| with tempfile.TemporaryDirectory() as script_dir: | |
| for i, (code, expected_kw) in enumerate(self.TYPO_CASES): | |
| with self.subTest(typo=expected_kw): | |
| source = textwrap.dedent(code).strip() | |
| script_name = make_script(script_dir, f"script_{i}", source) | |
| rc, stdout, stderr = assert_python_failure(script_name) | |
| stderr_text = stderr.decode('utf-8') | |
| self.assertIn(f"Did you mean '{expected_kw}'", stderr_text) | |
| def test_keyword_suggestions_from_command_string(self): | |
| for code, expected_kw in self.TYPO_CASES: | |
| with self.subTest(typo=expected_kw): | |
| source = textwrap.dedent(code).strip() | |
| rc, stdout, stderr = assert_python_failure('-c', source) | |
| stderr_text = stderr.decode('utf-8') | |
| self.assertIn(f"Did you mean '{expected_kw}'", stderr_text) | |
| def test_no_keyword_suggestion_for_comma_errors(self): | |
| # When the parser identifies a missing comma, don't suggest | |
| # bogus keyword replacements like 'print' -> 'not' | |
| code = '''\ | |
| import sys | |
| print( | |
| "line1" | |
| "line2" | |
| file=sys.stderr | |
| ) | |
| ''' | |
| source = textwrap.dedent(code).strip() | |
| rc, stdout, stderr = assert_python_failure('-c', source) | |
| stderr_text = stderr.decode('utf-8') | |
| self.assertIn("Perhaps you forgot a comma", stderr_text) | |
| self.assertNotIn("Did you mean", stderr_text) | |
| @requires_debug_ranges() | |
| @force_not_colorized_test_class | |
| class PurePythonTracebackErrorCaretTests( | |
| PurePythonExceptionFormattingMixin, | |
| TracebackErrorLocationCaretTestBase, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests as above using the pure Python implementation of | |
| traceback printing in traceback.py. | |
| """ | |
| @cpython_only | |
| @requires_debug_ranges() | |
| @force_not_colorized_test_class | |
| class CPythonTracebackErrorCaretTests( | |
| CAPIExceptionFormattingMixin, | |
| TracebackErrorLocationCaretTestBase, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests as above but with Python's internal traceback printing. | |
| """ | |
| @cpython_only | |
| @requires_debug_ranges() | |
| @force_not_colorized_test_class | |
| class CPythonTracebackLegacyErrorCaretTests( | |
| CAPIExceptionFormattingLegacyMixin, | |
| TracebackErrorLocationCaretTestBase, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests as above but with Python's legacy internal traceback printing. | |
| """ | |
| class TracebackFormatMixin: | |
| DEBUG_RANGES = True | |
| def some_exception(self): | |
| raise KeyError('blah') | |
| def _filter_debug_ranges(self, expected): | |
| return [line for line in expected if not set(line.strip()) <= set("^~")] | |
| def _maybe_filter_debug_ranges(self, expected): | |
| if not self.DEBUG_RANGES: | |
| return self._filter_debug_ranges(expected) | |
| return expected | |
| @cpython_only | |
| def check_traceback_format(self, cleanup_func=None): | |
| from _testcapi import traceback_print | |
| try: | |
| self.some_exception() | |
| except KeyError as e: | |
| tb = e.__traceback__ | |
| if cleanup_func is not None: | |
| # Clear the inner frames, not this one | |
| cleanup_func(tb.tb_next) | |
| traceback_fmt = 'Traceback (most recent call last):\n' + \ | |
| ''.join(traceback.format_tb(tb)) | |
| # clear caret lines from traceback_fmt since internal API does | |
| # not emit them | |
| traceback_fmt = "\n".join( | |
| self._filter_debug_ranges(traceback_fmt.splitlines()) | |
| ) + "\n" | |
| file_ = StringIO() | |
| traceback_print(tb, file_) | |
| python_fmt = file_.getvalue() | |
| # Call all _tb and _exc functions | |
| with captured_output("stderr") as tbstderr: | |
| traceback.print_tb(tb) | |
| tbfile = StringIO() | |
| traceback.print_tb(tb, file=tbfile) | |
| with captured_output("stderr") as excstderr: | |
| traceback.print_exc() | |
| excfmt = traceback.format_exc() | |
| excfile = StringIO() | |
| traceback.print_exc(file=excfile) | |
| else: | |
| raise Error("unable to create test traceback string") | |
| # Make sure that Python and the traceback module format the same thing | |
| self.assertEqual(traceback_fmt, python_fmt) | |
| # Now verify the _tb func output | |
| self.assertEqual(tbstderr.getvalue(), tbfile.getvalue()) | |
| # Now verify the _exc func output | |
| self.assertEqual(excstderr.getvalue(), excfile.getvalue()) | |
| self.assertEqual(excfmt, excfile.getvalue()) | |
| # Make sure that the traceback is properly indented. | |
| tb_lines = python_fmt.splitlines() | |
| banner = tb_lines[0] | |
| self.assertEqual(len(tb_lines), 5) | |
| location, source_line = tb_lines[-2], tb_lines[-1] | |
| self.assertStartsWith(banner, 'Traceback') | |
| self.assertStartsWith(location, ' File') | |
| self.assertStartsWith(source_line, ' raise') | |
| def test_traceback_format(self): | |
| self.check_traceback_format() | |
| def test_traceback_format_with_cleared_frames(self): | |
| # Check that traceback formatting also works with a clear()ed frame | |
| def cleanup_tb(tb): | |
| tb.tb_frame.clear() | |
| self.check_traceback_format(cleanup_tb) | |
| def test_stack_format(self): | |
| # Verify _stack functions. Note we have to use _getframe(1) to | |
| # compare them without this frame appearing in the output | |
| with captured_output("stderr") as ststderr: | |
| traceback.print_stack(sys._getframe(1)) | |
| stfile = StringIO() | |
| traceback.print_stack(sys._getframe(1), file=stfile) | |
| self.assertEqual(ststderr.getvalue(), stfile.getvalue()) | |
| stfmt = traceback.format_stack(sys._getframe(1)) | |
| self.assertEqual(ststderr.getvalue(), "".join(stfmt)) | |
| def test_print_stack(self): | |
| def prn(): | |
| traceback.print_stack() | |
| with captured_output("stderr") as stderr: | |
| prn() | |
| lineno = prn.__code__.co_firstlineno | |
| self.assertEqual(stderr.getvalue().splitlines()[-4:], [ | |
| ' File "%s", line %d, in test_print_stack' % (__file__, lineno+3), | |
| ' prn()', | |
| ' File "%s", line %d, in prn' % (__file__, lineno+1), | |
| ' traceback.print_stack()', | |
| ]) | |
| # issue 26823 - Shrink recursive tracebacks | |
| def _check_recursive_traceback_display(self, render_exc): | |
| # Always show full diffs when this test fails | |
| # Note that rearranging things may require adjusting | |
| # the relative line numbers in the expected tracebacks | |
| self.maxDiff = None | |
| # Check hitting the recursion limit | |
| def f(): | |
| f() | |
| with captured_output("stderr") as stderr_f: | |
| try: | |
| f() | |
| except RecursionError: | |
| render_exc() | |
| else: | |
| self.fail("no recursion occurred") | |
| lineno_f = f.__code__.co_firstlineno | |
| result_f = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' | |
| ' f()\n' | |
| ' ~^^\n' | |
| f' File "{__file__}", line {lineno_f+1}, in f\n' | |
| ' f()\n' | |
| ' ~^^\n' | |
| f' File "{__file__}", line {lineno_f+1}, in f\n' | |
| ' f()\n' | |
| ' ~^^\n' | |
| f' File "{__file__}", line {lineno_f+1}, in f\n' | |
| ' f()\n' | |
| ' ~^^\n' | |
| # XXX: The following line changes depending on whether the tests | |
| # are run through the interactive interpreter or with -m | |
| # It also varies depending on the platform (stack size) | |
| # Fortunately, we don't care about exactness here, so we use regex | |
| r' \[Previous line repeated (\d+) more times\]' '\n' | |
| 'RecursionError: maximum recursion depth exceeded\n' | |
| ) | |
| expected = self._maybe_filter_debug_ranges(result_f.splitlines()) | |
| actual = stderr_f.getvalue().splitlines() | |
| # Check the output text matches expectations | |
| # 2nd last line contains the repetition count | |
| self.assertEqual(actual[:-2], expected[:-2]) | |
| self.assertRegex(actual[-2], expected[-2]) | |
| # last line can have additional text appended | |
| self.assertIn(expected[-1], actual[-1]) | |
| # Check the recursion count is roughly as expected | |
| rec_limit = sys.getrecursionlimit() | |
| self.assertIn(int(re.search(r"\d+", actual[-2]).group()), range(rec_limit-60, rec_limit)) | |
| # Check a known (limited) number of recursive invocations | |
| def g(count=10): | |
| if count: | |
| return g(count-1) + 1 | |
| raise ValueError | |
| with captured_output("stderr") as stderr_g: | |
| try: | |
| g() | |
| except ValueError: | |
| render_exc() | |
| else: | |
| self.fail("no value error was raised") | |
| lineno_g = g.__code__.co_firstlineno | |
| result_g = ( | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| ' [Previous line repeated 7 more times]\n' | |
| f' File "{__file__}", line {lineno_g+3}, in g\n' | |
| ' raise ValueError\n' | |
| 'ValueError\n' | |
| ) | |
| tb_line = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n' | |
| ' g()\n' | |
| ' ~^^\n' | |
| ) | |
| expected = self._maybe_filter_debug_ranges((tb_line + result_g).splitlines()) | |
| actual = stderr_g.getvalue().splitlines() | |
| self.assertEqual(actual, expected) | |
| # Check 2 different repetitive sections | |
| def h(count=10): | |
| if count: | |
| return h(count-1) | |
| g() | |
| with captured_output("stderr") as stderr_h: | |
| try: | |
| h() | |
| except ValueError: | |
| render_exc() | |
| else: | |
| self.fail("no value error was raised") | |
| lineno_h = h.__code__.co_firstlineno | |
| result_h = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n' | |
| ' h()\n' | |
| ' ~^^\n' | |
| f' File "{__file__}", line {lineno_h+2}, in h\n' | |
| ' return h(count-1)\n' | |
| f' File "{__file__}", line {lineno_h+2}, in h\n' | |
| ' return h(count-1)\n' | |
| f' File "{__file__}", line {lineno_h+2}, in h\n' | |
| ' return h(count-1)\n' | |
| ' [Previous line repeated 7 more times]\n' | |
| f' File "{__file__}", line {lineno_h+3}, in h\n' | |
| ' g()\n' | |
| ' ~^^\n' | |
| ) | |
| expected = self._maybe_filter_debug_ranges((result_h + result_g).splitlines()) | |
| actual = stderr_h.getvalue().splitlines() | |
| self.assertEqual(actual, expected) | |
| # Check the boundary conditions. First, test just below the cutoff. | |
| with captured_output("stderr") as stderr_g: | |
| try: | |
| g(traceback._RECURSIVE_CUTOFF) | |
| except ValueError: | |
| render_exc() | |
| else: | |
| self.fail("no error raised") | |
| result_g = ( | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_g+3}, in g\n' | |
| ' raise ValueError\n' | |
| 'ValueError\n' | |
| ) | |
| tb_line = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {lineno_g+77}, in _check_recursive_traceback_display\n' | |
| ' g(traceback._RECURSIVE_CUTOFF)\n' | |
| ' ~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' | |
| ) | |
| expected = self._maybe_filter_debug_ranges((tb_line + result_g).splitlines()) | |
| actual = stderr_g.getvalue().splitlines() | |
| self.assertEqual(actual, expected) | |
| # Second, test just above the cutoff. | |
| with captured_output("stderr") as stderr_g: | |
| try: | |
| g(traceback._RECURSIVE_CUTOFF + 1) | |
| except ValueError: | |
| render_exc() | |
| else: | |
| self.fail("no error raised") | |
| result_g = ( | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| f' File "{__file__}", line {lineno_g+2}, in g\n' | |
| ' return g(count-1) + 1\n' | |
| ' ~^^^^^^^^^\n' | |
| ' [Previous line repeated 1 more time]\n' | |
| f' File "{__file__}", line {lineno_g+3}, in g\n' | |
| ' raise ValueError\n' | |
| 'ValueError\n' | |
| ) | |
| tb_line = ( | |
| 'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {lineno_g+109}, in _check_recursive_traceback_display\n' | |
| ' g(traceback._RECURSIVE_CUTOFF + 1)\n' | |
| ' ~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' | |
| ) | |
| expected = self._maybe_filter_debug_ranges((tb_line + result_g).splitlines()) | |
| actual = stderr_g.getvalue().splitlines() | |
| self.assertEqual(actual, expected) | |
| @requires_debug_ranges() | |
| def test_recursive_traceback(self): | |
| if self.DEBUG_RANGES: | |
| self._check_recursive_traceback_display(traceback.print_exc) | |
| else: | |
| from _testcapi import exception_print | |
| def render_exc(): | |
| exception_print(sys.exception()) | |
| self._check_recursive_traceback_display(render_exc) | |
| def test_format_stack(self): | |
| def fmt(): | |
| return traceback.format_stack() | |
| result = fmt() | |
| lineno = fmt.__code__.co_firstlineno | |
| self.assertEqual(result[-2:], [ | |
| ' File "%s", line %d, in test_format_stack\n' | |
| ' result = fmt()\n' % (__file__, lineno+2), | |
| ' File "%s", line %d, in fmt\n' | |
| ' return traceback.format_stack()\n' % (__file__, lineno+1), | |
| ]) | |
| @cpython_only | |
| def test_unhashable(self): | |
| from _testcapi import exception_print | |
| class UnhashableException(Exception): | |
| def __eq__(self, other): | |
| return True | |
| ex1 = UnhashableException('ex1') | |
| ex2 = UnhashableException('ex2') | |
| try: | |
| raise ex2 from ex1 | |
| except UnhashableException: | |
| try: | |
| raise ex1 | |
| except UnhashableException as e: | |
| exc_val = e | |
| with captured_output("stderr") as stderr_f: | |
| exception_print(exc_val) | |
| tb = stderr_f.getvalue().strip().splitlines() | |
| self.assertEqual(11, len(tb)) | |
| self.assertEqual(context_message.strip(), tb[5]) | |
| self.assertIn('UnhashableException: ex2', tb[3]) | |
| self.assertIn('UnhashableException: ex1', tb[10]) | |
| def deep_eg(self): | |
| e = TypeError(1) | |
| for i in range(2000): | |
| e = ExceptionGroup('eg', [e]) | |
| return e | |
| @cpython_only | |
| @support.skip_emscripten_stack_overflow() | |
| def test_exception_group_deep_recursion_capi(self): | |
| from _testcapi import exception_print | |
| LIMIT = 75 | |
| eg = self.deep_eg() | |
| with captured_output("stderr") as stderr_f: | |
| with support.infinite_recursion(max_depth=LIMIT): | |
| exception_print(eg) | |
| output = stderr_f.getvalue() | |
| self.assertIn('ExceptionGroup', output) | |
| self.assertLessEqual(output.count('ExceptionGroup'), LIMIT) | |
| @support.skip_emscripten_stack_overflow() | |
| def test_exception_group_deep_recursion_traceback(self): | |
| LIMIT = 75 | |
| eg = self.deep_eg() | |
| with captured_output("stderr") as stderr_f: | |
| with support.infinite_recursion(max_depth=LIMIT): | |
| traceback.print_exception(type(eg), eg, eg.__traceback__) | |
| output = stderr_f.getvalue() | |
| self.assertIn('ExceptionGroup', output) | |
| self.assertLessEqual(output.count('ExceptionGroup'), LIMIT) | |
| @cpython_only | |
| def test_print_exception_bad_type_capi(self): | |
| from _testcapi import exception_print | |
| with captured_output("stderr") as stderr: | |
| with support.catch_unraisable_exception(): | |
| exception_print(42) | |
| self.assertEqual( | |
| stderr.getvalue(), | |
| ('TypeError: print_exception(): ' | |
| 'Exception expected for value, int found\n') | |
| ) | |
| def test_print_exception_bad_type_python(self): | |
| msg = "Exception expected for value, int found" | |
| with self.assertRaisesRegex(TypeError, msg): | |
| traceback.print_exception(42) | |
| cause_message = ( | |
| "\nThe above exception was the direct cause " | |
| "of the following exception:\n\n") | |
| context_message = ( | |
| "\nDuring handling of the above exception, " | |
| "another exception occurred:\n\n") | |
| boundaries = re.compile( | |
| '(%s|%s)' % (re.escape(cause_message), re.escape(context_message))) | |
| @force_not_colorized_test_class | |
| class TestTracebackFormat(unittest.TestCase, TracebackFormatMixin): | |
| pass | |
| @cpython_only | |
| @force_not_colorized_test_class | |
| class TestFallbackTracebackFormat(unittest.TestCase, TracebackFormatMixin): | |
| DEBUG_RANGES = False | |
| def setUp(self) -> None: | |
| self.original_unraisable_hook = sys.unraisablehook | |
| sys.unraisablehook = lambda *args: None | |
| self.original_hook = traceback._print_exception_bltin | |
| traceback._print_exception_bltin = lambda *args: 1/0 | |
| return super().setUp() | |
| def tearDown(self) -> None: | |
| traceback._print_exception_bltin = self.original_hook | |
| sys.unraisablehook = self.original_unraisable_hook | |
| return super().tearDown() | |
| class BaseExceptionReportingTests: | |
| def get_exception(self, exception_or_callable): | |
| if isinstance(exception_or_callable, BaseException): | |
| return exception_or_callable | |
| try: | |
| exception_or_callable() | |
| except Exception as e: | |
| return e | |
| callable_line = get_exception.__code__.co_firstlineno + 4 | |
| def zero_div(self): | |
| 1/0 # In zero_div | |
| def check_zero_div(self, msg): | |
| lines = msg.splitlines() | |
| if has_no_debug_ranges(): | |
| self.assertStartsWith(lines[-3], ' File') | |
| self.assertIn('1/0 # In zero_div', lines[-2]) | |
| else: | |
| self.assertStartsWith(lines[-4], ' File') | |
| self.assertIn('1/0 # In zero_div', lines[-3]) | |
| self.assertStartsWith(lines[-1], 'ZeroDivisionError') | |
| def test_simple(self): | |
| try: | |
| 1/0 # Marker | |
| except ZeroDivisionError as _: | |
| e = _ | |
| lines = self.get_report(e).splitlines() | |
| if has_no_debug_ranges(): | |
| self.assertEqual(len(lines), 4) | |
| self.assertStartsWith(lines[3], 'ZeroDivisionError') | |
| else: | |
| self.assertEqual(len(lines), 5) | |
| self.assertStartsWith(lines[4], 'ZeroDivisionError') | |
| self.assertStartsWith(lines[0], 'Traceback') | |
| self.assertStartsWith(lines[1], ' File') | |
| self.assertIn('1/0 # Marker', lines[2]) | |
| def test_cause(self): | |
| def inner_raise(): | |
| try: | |
| self.zero_div() | |
| except ZeroDivisionError as e: | |
| raise KeyError from e | |
| def outer_raise(): | |
| inner_raise() # Marker | |
| blocks = boundaries.split(self.get_report(outer_raise)) | |
| self.assertEqual(len(blocks), 3) | |
| self.assertEqual(blocks[1], cause_message) | |
| self.check_zero_div(blocks[0]) | |
| self.assertIn('inner_raise() # Marker', blocks[2]) | |
| def test_context(self): | |
| def inner_raise(): | |
| try: | |
| self.zero_div() | |
| except ZeroDivisionError: | |
| raise KeyError | |
| def outer_raise(): | |
| inner_raise() # Marker | |
| blocks = boundaries.split(self.get_report(outer_raise)) | |
| self.assertEqual(len(blocks), 3) | |
| self.assertEqual(blocks[1], context_message) | |
| self.check_zero_div(blocks[0]) | |
| self.assertIn('inner_raise() # Marker', blocks[2]) | |
| def test_context_suppression(self): | |
| try: | |
| try: | |
| raise Exception | |
| except Exception: | |
| raise ZeroDivisionError from None | |
| except ZeroDivisionError as _: | |
| e = _ | |
| lines = self.get_report(e).splitlines() | |
| self.assertEqual(len(lines), 4) | |
| self.assertStartsWith(lines[3], 'ZeroDivisionError') | |
| self.assertStartsWith(lines[0], 'Traceback') | |
| self.assertStartsWith(lines[1], ' File') | |
| self.assertIn('ZeroDivisionError from None', lines[2]) | |
| def test_cause_and_context(self): | |
| # When both a cause and a context are set, only the cause should be | |
| # displayed and the context should be muted. | |
| def inner_raise(): | |
| try: | |
| self.zero_div() | |
| except ZeroDivisionError as _e: | |
| e = _e | |
| try: | |
| xyzzy | |
| except NameError: | |
| raise KeyError from e | |
| def outer_raise(): | |
| inner_raise() # Marker | |
| blocks = boundaries.split(self.get_report(outer_raise)) | |
| self.assertEqual(len(blocks), 3) | |
| self.assertEqual(blocks[1], cause_message) | |
| self.check_zero_div(blocks[0]) | |
| self.assertIn('inner_raise() # Marker', blocks[2]) | |
| def test_cause_recursive(self): | |
| def inner_raise(): | |
| try: | |
| try: | |
| self.zero_div() | |
| except ZeroDivisionError as e: | |
| z = e | |
| raise KeyError from e | |
| except KeyError as e: | |
| raise z from e | |
| def outer_raise(): | |
| inner_raise() # Marker | |
| blocks = boundaries.split(self.get_report(outer_raise)) | |
| self.assertEqual(len(blocks), 3) | |
| self.assertEqual(blocks[1], cause_message) | |
| # The first block is the KeyError raised from the ZeroDivisionError | |
| self.assertIn('raise KeyError from e', blocks[0]) | |
| self.assertNotIn('1/0', blocks[0]) | |
| # The second block (apart from the boundary) is the ZeroDivisionError | |
| # re-raised from the KeyError | |
| self.assertIn('inner_raise() # Marker', blocks[2]) | |
| self.check_zero_div(blocks[2]) | |
| def test_syntax_error_offset_at_eol(self): | |
| # See #10186. | |
| def e(): | |
| raise SyntaxError('', ('', 0, 5, 'hello')) | |
| msg = self.get_report(e).splitlines() | |
| self.assertEqual(msg[-2], " ^") | |
| def e(): | |
| exec("x = 5 | 4 |") | |
| msg = self.get_report(e).splitlines() | |
| self.assertEqual(msg[-2], ' ^') | |
| def test_syntax_error_no_lineno(self): | |
| # See #34463. | |
| # Without filename | |
| e = SyntaxError('bad syntax') | |
| msg = self.get_report(e).splitlines() | |
| self.assertEqual(msg, | |
| ['SyntaxError: bad syntax']) | |
| e.lineno = 100 | |
| msg = self.get_report(e).splitlines() | |
| self.assertEqual(msg, | |
| [' File "<string>", line 100', 'SyntaxError: bad syntax']) | |
| # With filename | |
| e = SyntaxError('bad syntax') | |
| e.filename = 'myfile.py' | |
| msg = self.get_report(e).splitlines() | |
| self.assertEqual(msg, | |
| ['SyntaxError: bad syntax (myfile.py)']) | |
| e.lineno = 100 | |
| msg = self.get_report(e).splitlines() | |
| self.assertEqual(msg, | |
| [' File "myfile.py", line 100', 'SyntaxError: bad syntax']) | |
| def test_message_none(self): | |
| # A message that looks like "None" should not be treated specially | |
| err = self.get_report(Exception(None)) | |
| self.assertIn('Exception: None\n', err) | |
| err = self.get_report(Exception('None')) | |
| self.assertIn('Exception: None\n', err) | |
| err = self.get_report(Exception()) | |
| self.assertIn('Exception\n', err) | |
| err = self.get_report(Exception('')) | |
| self.assertIn('Exception\n', err) | |
| def test_syntax_error_various_offsets(self): | |
| for offset in range(-5, 10): | |
| for add in [0, 2]: | |
| text = " " * add + "text%d" % offset | |
| expected = [' File "file.py", line 1'] | |
| if offset < 1: | |
| expected.append(" %s" % text.lstrip()) | |
| elif offset <= 6: | |
| expected.append(" %s" % text.lstrip()) | |
| # Set the caret length to match the length of the text minus the offset. | |
| caret_length = max(1, len(text.lstrip()) - offset + 1) | |
| expected.append(" %s%s" % (" " * (offset - 1), "^" * caret_length)) | |
| else: | |
| caret_length = max(1, len(text.lstrip()) - 4) | |
| expected.append(" %s" % text.lstrip()) | |
| expected.append(" %s%s" % (" " * 5, "^" * caret_length)) | |
| expected.append("SyntaxError: msg") | |
| expected.append("") | |
| err = self.get_report(SyntaxError("msg", ("file.py", 1, offset + add, text))) | |
| exp = "\n".join(expected) | |
| self.assertEqual(exp, err) | |
| def test_exception_with_note(self): | |
| e = ValueError(123) | |
| vanilla = self.get_report(e) | |
| e.add_note('My Note') | |
| self.assertEqual(self.get_report(e), vanilla + 'My Note\n') | |
| del e.__notes__ | |
| e.add_note('') | |
| self.assertEqual(self.get_report(e), vanilla + '\n') | |
| del e.__notes__ | |
| e.add_note('Your Note') | |
| self.assertEqual(self.get_report(e), vanilla + 'Your Note\n') | |
| del e.__notes__ | |
| self.assertEqual(self.get_report(e), vanilla) | |
| def test_exception_with_invalid_notes(self): | |
| e = ValueError(123) | |
| vanilla = self.get_report(e) | |
| # non-sequence __notes__ | |
| class BadThing: | |
| def __str__(self): | |
| return 'bad str' | |
| def __repr__(self): | |
| return 'bad repr' | |
| # unprintable, non-sequence __notes__ | |
| class Unprintable: | |
| def __repr__(self): | |
| raise ValueError('bad value') | |
| e.__notes__ = BadThing() | |
| notes_repr = 'bad repr' | |
| self.assertEqual(self.get_report(e), vanilla + notes_repr + '\n') | |
| e.__notes__ = Unprintable() | |
| err_msg = '<__notes__ repr() failed>' | |
| self.assertEqual(self.get_report(e), vanilla + err_msg + '\n') | |
| # non-string item in the __notes__ sequence | |
| e.__notes__ = [BadThing(), 'Final Note'] | |
| bad_note = 'bad str' | |
| self.assertEqual(self.get_report(e), vanilla + bad_note + '\nFinal Note\n') | |
| # unprintable, non-string item in the __notes__ sequence | |
| e.__notes__ = [Unprintable(), 'Final Note'] | |
| err_msg = '<note str() failed>' | |
| self.assertEqual(self.get_report(e), vanilla + err_msg + '\nFinal Note\n') | |
| e.__notes__ = "please do not explode me" | |
| err_msg = "'please do not explode me'" | |
| self.assertEqual(self.get_report(e), vanilla + err_msg + '\n') | |
| e.__notes__ = b"please do not show me as numbers" | |
| err_msg = "b'please do not show me as numbers'" | |
| self.assertEqual(self.get_report(e), vanilla + err_msg + '\n') | |
| # an exception with a broken __getattr__ raising a non expected error | |
| class BrokenException(Exception): | |
| broken = False | |
| def __getattr__(self, name): | |
| if self.broken: | |
| raise ValueError(f'no {name}') | |
| e = BrokenException(123) | |
| vanilla = self.get_report(e) | |
| e.broken = True | |
| self.assertEqual( | |
| self.get_report(e), | |
| vanilla + "Ignored error getting __notes__: ValueError('no __notes__')\n") | |
| def test_exception_with_multiple_notes(self): | |
| for e in [ValueError(42), SyntaxError('bad syntax')]: | |
| with self.subTest(e=e): | |
| vanilla = self.get_report(e) | |
| e.add_note('Note 1') | |
| e.add_note('Note 2') | |
| e.add_note('Note 3') | |
| self.assertEqual( | |
| self.get_report(e), | |
| vanilla + 'Note 1\n' + 'Note 2\n' + 'Note 3\n') | |
| del e.__notes__ | |
| e.add_note('Note 4') | |
| del e.__notes__ | |
| e.add_note('Note 5') | |
| e.add_note('Note 6') | |
| self.assertEqual( | |
| self.get_report(e), | |
| vanilla + 'Note 5\n' + 'Note 6\n') | |
| def test_exception_qualname(self): | |
| class A: | |
| class B: | |
| class X(Exception): | |
| def __str__(self): | |
| return "I am X" | |
| err = self.get_report(A.B.X()) | |
| str_value = 'I am X' | |
| str_name = '.'.join([A.B.X.__module__, A.B.X.__qualname__]) | |
| exp = "%s: %s\n" % (str_name, str_value) | |
| self.assertEqual(exp, MODULE_PREFIX + err) | |
| def test_exception_modulename(self): | |
| class X(Exception): | |
| def __str__(self): | |
| return "I am X" | |
| for modulename in '__main__', 'builtins', 'some_module': | |
| X.__module__ = modulename | |
| with self.subTest(modulename=modulename): | |
| err = self.get_report(X()) | |
| str_value = 'I am X' | |
| if modulename in ['builtins', '__main__']: | |
| str_name = X.__qualname__ | |
| else: | |
| str_name = '.'.join([X.__module__, X.__qualname__]) | |
| exp = "%s: %s\n" % (str_name, str_value) | |
| self.assertEqual(exp, err) | |
| def test_exception_angle_bracketed_filename(self): | |
| src = textwrap.dedent(""" | |
| try: | |
| raise ValueError(42) | |
| except Exception as e: | |
| exc = e | |
| """) | |
| code = compile(src, "<does not exist>", "exec") | |
| g, l = {}, {} | |
| exec(code, g, l) | |
| err = self.get_report(l['exc']) | |
| exp = ' File "<does not exist>", line 3, in <module>\nValueError: 42\n' | |
| self.assertIn(exp, err) | |
| def test_exception_modulename_not_unicode(self): | |
| class X(Exception): | |
| def __str__(self): | |
| return "I am X" | |
| X.__module__ = 42 | |
| err = self.get_report(X()) | |
| exp = f'<unknown>.{X.__qualname__}: I am X\n' | |
| self.assertEqual(exp, err) | |
| def test_exception_bad__str__(self): | |
| class X(Exception): | |
| def __str__(self): | |
| 1/0 | |
| err = self.get_report(X()) | |
| str_value = '<exception str() failed>' | |
| str_name = '.'.join([X.__module__, X.__qualname__]) | |
| self.assertEqual(MODULE_PREFIX + err, f"{str_name}: {str_value}\n") | |
| # #### Exception Groups #### | |
| def test_exception_group_basic(self): | |
| def exc(): | |
| raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) | |
| expected = ( | |
| f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| f' | exception_or_callable()\n' | |
| f' | ~~~~~~~~~~~~~~~~~~~~~^^\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n' | |
| f' | raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' | |
| f' | ExceptionGroup: eg (2 sub-exceptions)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | ValueError: 1\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | TypeError: 2\n' | |
| f' +------------------------------------\n') | |
| report = self.get_report(exc) | |
| self.assertEqual(report, expected) | |
| def test_exception_group_cause(self): | |
| def exc(): | |
| EG = ExceptionGroup | |
| try: | |
| raise EG("eg1", [ValueError(1), TypeError(2)]) | |
| except Exception as e: | |
| raise EG("eg2", [ValueError(3), TypeError(4)]) from e | |
| expected = (f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 3}, in exc\n' | |
| f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' | |
| f' | ExceptionGroup: eg1 (2 sub-exceptions)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | ValueError: 1\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | TypeError: 2\n' | |
| f' +------------------------------------\n' | |
| f'\n' | |
| f'The above exception was the direct cause of the following exception:\n' | |
| f'\n' | |
| f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| f' | exception_or_callable()\n' | |
| f' | ~~~~~~~~~~~~~~~~~~~~~^^\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' | |
| f' | raise EG("eg2", [ValueError(3), TypeError(4)]) from e\n' | |
| f' | ExceptionGroup: eg2 (2 sub-exceptions)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | ValueError: 3\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | TypeError: 4\n' | |
| f' +------------------------------------\n') | |
| report = self.get_report(exc) | |
| self.assertEqual(report, expected) | |
| def test_exception_group_context_with_context(self): | |
| def exc(): | |
| EG = ExceptionGroup | |
| try: | |
| try: | |
| raise EG("eg1", [ValueError(1), TypeError(2)]) | |
| except EG: | |
| raise EG("eg2", [ValueError(3), TypeError(4)]) | |
| except EG: | |
| raise ImportError(5) | |
| expected = ( | |
| f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 4}, in exc\n' | |
| f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' | |
| f' | ExceptionGroup: eg1 (2 sub-exceptions)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | ValueError: 1\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | TypeError: 2\n' | |
| f' +------------------------------------\n' | |
| f'\n' | |
| f'During handling of the above exception, another exception occurred:\n' | |
| f'\n' | |
| f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n' | |
| f' | raise EG("eg2", [ValueError(3), TypeError(4)])\n' | |
| f' | ExceptionGroup: eg2 (2 sub-exceptions)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | ValueError: 3\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | TypeError: 4\n' | |
| f' +------------------------------------\n' | |
| f'\n' | |
| f'During handling of the above exception, another exception occurred:\n' | |
| f'\n' | |
| f'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| f' exception_or_callable()\n' | |
| f' ~~~~~~~~~~~~~~~~~~~~~^^\n' | |
| f' File "{__file__}", line {exc.__code__.co_firstlineno + 8}, in exc\n' | |
| f' raise ImportError(5)\n' | |
| f'ImportError: 5\n') | |
| report = self.get_report(exc) | |
| self.assertEqual(report, expected) | |
| def test_exception_group_nested(self): | |
| def exc(): | |
| EG = ExceptionGroup | |
| VE = ValueError | |
| TE = TypeError | |
| try: | |
| try: | |
| raise EG("nested", [TE(2), TE(3)]) | |
| except Exception as e: | |
| exc = e | |
| raise EG("eg", [VE(1), exc, VE(4)]) | |
| except EG: | |
| raise EG("top", [VE(5)]) | |
| expected = (f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n' | |
| f' | raise EG("eg", [VE(1), exc, VE(4)])\n' | |
| f' | ExceptionGroup: eg (3 sub-exceptions)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | ValueError: 1\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n' | |
| f' | raise EG("nested", [TE(2), TE(3)])\n' | |
| f' | ExceptionGroup: nested (2 sub-exceptions)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | TypeError: 2\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | TypeError: 3\n' | |
| f' +------------------------------------\n' | |
| f' +---------------- 3 ----------------\n' | |
| f' | ValueError: 4\n' | |
| f' +------------------------------------\n' | |
| f'\n' | |
| f'During handling of the above exception, another exception occurred:\n' | |
| f'\n' | |
| f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| f' | exception_or_callable()\n' | |
| f' | ~~~~~~~~~~~~~~~~~~~~~^^\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 11}, in exc\n' | |
| f' | raise EG("top", [VE(5)])\n' | |
| f' | ExceptionGroup: top (1 sub-exception)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | ValueError: 5\n' | |
| f' +------------------------------------\n') | |
| report = self.get_report(exc) | |
| self.assertEqual(report, expected) | |
| def test_exception_group_width_limit(self): | |
| excs = [] | |
| for i in range(1000): | |
| excs.append(ValueError(i)) | |
| eg = ExceptionGroup('eg', excs) | |
| expected = (' | ExceptionGroup: eg (1000 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 0\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ValueError: 1\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: 2\n' | |
| ' +---------------- 4 ----------------\n' | |
| ' | ValueError: 3\n' | |
| ' +---------------- 5 ----------------\n' | |
| ' | ValueError: 4\n' | |
| ' +---------------- 6 ----------------\n' | |
| ' | ValueError: 5\n' | |
| ' +---------------- 7 ----------------\n' | |
| ' | ValueError: 6\n' | |
| ' +---------------- 8 ----------------\n' | |
| ' | ValueError: 7\n' | |
| ' +---------------- 9 ----------------\n' | |
| ' | ValueError: 8\n' | |
| ' +---------------- 10 ----------------\n' | |
| ' | ValueError: 9\n' | |
| ' +---------------- 11 ----------------\n' | |
| ' | ValueError: 10\n' | |
| ' +---------------- 12 ----------------\n' | |
| ' | ValueError: 11\n' | |
| ' +---------------- 13 ----------------\n' | |
| ' | ValueError: 12\n' | |
| ' +---------------- 14 ----------------\n' | |
| ' | ValueError: 13\n' | |
| ' +---------------- 15 ----------------\n' | |
| ' | ValueError: 14\n' | |
| ' +---------------- ... ----------------\n' | |
| ' | and 985 more exceptions\n' | |
| ' +------------------------------------\n') | |
| report = self.get_report(eg) | |
| self.assertEqual(report, expected) | |
| def test_exception_group_depth_limit(self): | |
| exc = TypeError('bad type') | |
| for i in range(1000): | |
| exc = ExceptionGroup( | |
| f'eg{i}', | |
| [ValueError(i), exc, ValueError(-i)]) | |
| expected = (' | ExceptionGroup: eg999 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 999\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg998 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 998\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg997 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 997\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg996 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 996\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg995 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 995\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg994 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 994\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg993 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 993\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg992 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 992\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg991 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 991\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ExceptionGroup: eg990 (3 sub-exceptions)\n' | |
| ' +-+---------------- 1 ----------------\n' | |
| ' | ValueError: 990\n' | |
| ' +---------------- 2 ----------------\n' | |
| ' | ... (max_group_depth is 10)\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -990\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -991\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -992\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -993\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -994\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -995\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -996\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -997\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -998\n' | |
| ' +------------------------------------\n' | |
| ' +---------------- 3 ----------------\n' | |
| ' | ValueError: -999\n' | |
| ' +------------------------------------\n') | |
| report = self.get_report(exc) | |
| self.assertEqual(report, expected) | |
| def test_exception_group_with_notes(self): | |
| def exc(): | |
| try: | |
| excs = [] | |
| for msg in ['bad value', 'terrible value']: | |
| try: | |
| raise ValueError(msg) | |
| except ValueError as e: | |
| e.add_note(f'the {msg}') | |
| excs.append(e) | |
| raise ExceptionGroup("nested", excs) | |
| except ExceptionGroup as e: | |
| e.add_note(('>> Multi line note\n' | |
| '>> Because I am such\n' | |
| '>> an important exception.\n' | |
| '>> empty lines work too\n' | |
| '\n' | |
| '(that was an empty line)')) | |
| raise | |
| expected = (f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| f' | exception_or_callable()\n' | |
| f' | ~~~~~~~~~~~~~~~~~~~~~^^\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n' | |
| f' | raise ExceptionGroup("nested", excs)\n' | |
| f' | ExceptionGroup: nested (2 sub-exceptions)\n' | |
| f' | >> Multi line note\n' | |
| f' | >> Because I am such\n' | |
| f' | >> an important exception.\n' | |
| f' | >> empty lines work too\n' | |
| f' | \n' | |
| f' | (that was an empty line)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' | |
| f' | raise ValueError(msg)\n' | |
| f' | ValueError: bad value\n' | |
| f' | the bad value\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' | |
| f' | raise ValueError(msg)\n' | |
| f' | ValueError: terrible value\n' | |
| f' | the terrible value\n' | |
| f' +------------------------------------\n') | |
| report = self.get_report(exc) | |
| self.assertEqual(report, expected) | |
| def test_exception_group_with_multiple_notes(self): | |
| def exc(): | |
| try: | |
| excs = [] | |
| for msg in ['bad value', 'terrible value']: | |
| try: | |
| raise ValueError(msg) | |
| except ValueError as e: | |
| e.add_note(f'the {msg}') | |
| e.add_note(f'Goodbye {msg}') | |
| excs.append(e) | |
| raise ExceptionGroup("nested", excs) | |
| except ExceptionGroup as e: | |
| e.add_note(('>> Multi line note\n' | |
| '>> Because I am such\n' | |
| '>> an important exception.\n' | |
| '>> empty lines work too\n' | |
| '\n' | |
| '(that was an empty line)')) | |
| e.add_note('Goodbye!') | |
| raise | |
| expected = (f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| f' | exception_or_callable()\n' | |
| f' | ~~~~~~~~~~~~~~~~~~~~~^^\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 10}, in exc\n' | |
| f' | raise ExceptionGroup("nested", excs)\n' | |
| f' | ExceptionGroup: nested (2 sub-exceptions)\n' | |
| f' | >> Multi line note\n' | |
| f' | >> Because I am such\n' | |
| f' | >> an important exception.\n' | |
| f' | >> empty lines work too\n' | |
| f' | \n' | |
| f' | (that was an empty line)\n' | |
| f' | Goodbye!\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' | |
| f' | raise ValueError(msg)\n' | |
| f' | ValueError: bad value\n' | |
| f' | the bad value\n' | |
| f' | Goodbye bad value\n' | |
| f' +---------------- 2 ----------------\n' | |
| f' | Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' | |
| f' | raise ValueError(msg)\n' | |
| f' | ValueError: terrible value\n' | |
| f' | the terrible value\n' | |
| f' | Goodbye terrible value\n' | |
| f' +------------------------------------\n') | |
| report = self.get_report(exc) | |
| self.assertEqual(report, expected) | |
| def test_exception_group_wrapped_naked(self): | |
| # See gh-128799 | |
| def exc(): | |
| try: | |
| raise Exception(42) | |
| except* Exception as e: | |
| raise | |
| expected = (f' + Exception Group Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {self.callable_line}, in get_exception\n' | |
| f' | exception_or_callable()\n' | |
| f' | ~~~~~~~~~~~~~~~~~~~~~^^\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 3}, in exc\n' | |
| f' | except* Exception as e:\n' | |
| f' | raise\n' | |
| f' | ExceptionGroup: (1 sub-exception)\n' | |
| f' +-+---------------- 1 ----------------\n' | |
| f' | Traceback (most recent call last):\n' | |
| f' | File "{__file__}", line {exc.__code__.co_firstlineno + 2}, in exc\n' | |
| f' | raise Exception(42)\n' | |
| f' | Exception: 42\n' | |
| f' +------------------------------------\n') | |
| report = self.get_report(exc) | |
| self.assertEqual(report, expected) | |
| def test_KeyboardInterrupt_at_first_line_of_frame(self): | |
| # see GH-93249 | |
| def f(): | |
| return sys._getframe() | |
| tb_next = None | |
| frame = f() | |
| lasti = 0 | |
| lineno = f.__code__.co_firstlineno | |
| tb = types.TracebackType(tb_next, frame, lasti, lineno) | |
| exc = KeyboardInterrupt() | |
| exc.__traceback__ = tb | |
| expected = (f'Traceback (most recent call last):\n' | |
| f' File "{__file__}", line {lineno}, in f\n' | |
| f' def f():\n' | |
| f'\n' | |
| f'KeyboardInterrupt\n') | |
| report = self.get_report(exc) | |
| # remove trailing writespace: | |
| report = '\n'.join([l.rstrip() for l in report.split('\n')]) | |
| self.assertEqual(report, expected) | |
| @force_not_colorized_test_class | |
| class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): | |
| # | |
| # This checks reporting through the 'traceback' module, with both | |
| # format_exception() and print_exception(). | |
| # | |
| def get_report(self, e): | |
| e = self.get_exception(e) | |
| s = ''.join( | |
| traceback.format_exception(type(e), e, e.__traceback__)) | |
| with captured_output("stderr") as sio: | |
| traceback.print_exception(type(e), e, e.__traceback__) | |
| self.assertEqual(sio.getvalue(), s) | |
| return s | |
| @force_not_colorized_test_class | |
| class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): | |
| # | |
| # This checks built-in reporting by the interpreter. | |
| # | |
| @cpython_only | |
| def get_report(self, e): | |
| from _testcapi import exception_print | |
| e = self.get_exception(e) | |
| with captured_output("stderr") as s: | |
| exception_print(e) | |
| return s.getvalue() | |
| class LimitTests(unittest.TestCase): | |
| ''' Tests for limit argument. | |
| It's enough to test extact_tb, extract_stack and format_exception ''' | |
| def last_raises1(self): | |
| raise Exception('Last raised') | |
| def last_raises2(self): | |
| self.last_raises1() | |
| def last_raises3(self): | |
| self.last_raises2() | |
| def last_raises4(self): | |
| self.last_raises3() | |
| def last_raises5(self): | |
| self.last_raises4() | |
| def last_returns_frame1(self): | |
| return sys._getframe() | |
| def last_returns_frame2(self): | |
| return self.last_returns_frame1() | |
| def last_returns_frame3(self): | |
| return self.last_returns_frame2() | |
| def last_returns_frame4(self): | |
| return self.last_returns_frame3() | |
| def last_returns_frame5(self): | |
| return self.last_returns_frame4() | |
| def test_extract_stack(self): | |
| frame = self.last_returns_frame5() | |
| def extract(**kwargs): | |
| return traceback.extract_stack(frame, **kwargs) | |
| def assertEqualExcept(actual, expected, ignore): | |
| self.assertEqual(actual[:ignore], expected[:ignore]) | |
| self.assertEqual(actual[ignore+1:], expected[ignore+1:]) | |
| self.assertEqual(len(actual), len(expected)) | |
| with support.swap_attr(sys, 'tracebacklimit', 1000): | |
| nolim = extract() | |
| self.assertGreater(len(nolim), 5) | |
| self.assertEqual(extract(limit=2), nolim[-2:]) | |
| assertEqualExcept(extract(limit=100), nolim[-100:], -5-1) | |
| self.assertEqual(extract(limit=-2), nolim[:2]) | |
| assertEqualExcept(extract(limit=-100), nolim[:100], len(nolim)-5-1) | |
| self.assertEqual(extract(limit=0), []) | |
| del sys.tracebacklimit | |
| assertEqualExcept(extract(), nolim, -5-1) | |
| sys.tracebacklimit = 2 | |
| self.assertEqual(extract(), nolim[-2:]) | |
| self.assertEqual(extract(limit=3), nolim[-3:]) | |
| self.assertEqual(extract(limit=-3), nolim[:3]) | |
| sys.tracebacklimit = 0 | |
| self.assertEqual(extract(), []) | |
| sys.tracebacklimit = -1 | |
| self.assertEqual(extract(), []) | |
| def test_extract_tb(self): | |
| try: | |
| self.last_raises5() | |
| except Exception as e: | |
| tb = e.__traceback__ | |
| def extract(**kwargs): | |
| return traceback.extract_tb(tb, **kwargs) | |
| with support.swap_attr(sys, 'tracebacklimit', 1000): | |
| nolim = extract() | |
| self.assertEqual(len(nolim), 5+1) | |
| self.assertEqual(extract(limit=2), nolim[:2]) | |
| self.assertEqual(extract(limit=10), nolim) | |
| self.assertEqual(extract(limit=-2), nolim[-2:]) | |
| self.assertEqual(extract(limit=-10), nolim) | |
| self.assertEqual(extract(limit=0), []) | |
| del sys.tracebacklimit | |
| self.assertEqual(extract(), nolim) | |
| sys.tracebacklimit = 2 | |
| self.assertEqual(extract(), nolim[:2]) | |
| self.assertEqual(extract(limit=3), nolim[:3]) | |
| self.assertEqual(extract(limit=-3), nolim[-3:]) | |
| sys.tracebacklimit = 0 | |
| self.assertEqual(extract(), []) | |
| sys.tracebacklimit = -1 | |
| self.assertEqual(extract(), []) | |
| def test_format_exception(self): | |
| try: | |
| self.last_raises5() | |
| except Exception as e: | |
| exc = e | |
| # [1:-1] to exclude "Traceback (...)" header and | |
| # exception type and value | |
| def extract(**kwargs): | |
| return traceback.format_exception(exc, **kwargs)[1:-1] | |
| with support.swap_attr(sys, 'tracebacklimit', 1000): | |
| nolim = extract() | |
| self.assertEqual(len(nolim), 5+1) | |
| self.assertEqual(extract(limit=2), nolim[:2]) | |
| self.assertEqual(extract(limit=10), nolim) | |
| self.assertEqual(extract(limit=-2), nolim[-2:]) | |
| self.assertEqual(extract(limit=-10), nolim) | |
| self.assertEqual(extract(limit=0), []) | |
| del sys.tracebacklimit | |
| self.assertEqual(extract(), nolim) | |
| sys.tracebacklimit = 2 | |
| self.assertEqual(extract(), nolim[:2]) | |
| self.assertEqual(extract(limit=3), nolim[:3]) | |
| self.assertEqual(extract(limit=-3), nolim[-3:]) | |
| sys.tracebacklimit = 0 | |
| self.assertEqual(extract(), []) | |
| sys.tracebacklimit = -1 | |
| self.assertEqual(extract(), []) | |
| class MiscTracebackCases(unittest.TestCase): | |
| # | |
| # Check non-printing functions in traceback module | |
| # | |
| def test_clear(self): | |
| def outer(): | |
| middle() | |
| def middle(): | |
| inner() | |
| def inner(): | |
| i = 1 | |
| 1/0 | |
| try: | |
| outer() | |
| except BaseException as e: | |
| tb = e.__traceback__ | |
| # Initial assertion: there's one local in the inner frame. | |
| inner_frame = tb.tb_next.tb_next.tb_next.tb_frame | |
| self.assertEqual(len(inner_frame.f_locals), 1) | |
| # Clear traceback frames | |
| traceback.clear_frames(tb) | |
| # Local variable dict should now be empty. | |
| self.assertEqual(len(inner_frame.f_locals), 0) | |
| def test_extract_stack(self): | |
| def extract(): | |
| return traceback.extract_stack() | |
| result = extract() | |
| lineno = extract.__code__.co_firstlineno | |
| self.assertEqual(result[-2:], [ | |
| (__file__, lineno+2, 'test_extract_stack', 'result = extract()'), | |
| (__file__, lineno+1, 'extract', 'return traceback.extract_stack()'), | |
| ]) | |
| self.assertEqual(len(result[0]), 4) | |
| class TestFrame(unittest.TestCase): | |
| def test_basics(self): | |
| linecache.clearcache() | |
| linecache.lazycache("f", globals()) | |
| f = traceback.FrameSummary("f", 1, "dummy") | |
| self.assertEqual(f, | |
| ("f", 1, "dummy", '"""Test cases for traceback module"""')) | |
| self.assertEqual(tuple(f), | |
| ("f", 1, "dummy", '"""Test cases for traceback module"""')) | |
| self.assertEqual(f, traceback.FrameSummary("f", 1, "dummy")) | |
| self.assertEqual(f, tuple(f)) | |
| # Since tuple.__eq__ doesn't support FrameSummary, the equality | |
| # operator fallbacks to FrameSummary.__eq__. | |
| self.assertEqual(tuple(f), f) | |
| self.assertIsNone(f.locals) | |
| self.assertNotEqual(f, object()) | |
| self.assertEqual(f, ALWAYS_EQ) | |
| def test_lazy_lines(self): | |
| linecache.clearcache() | |
| f = traceback.FrameSummary("f", 1, "dummy", lookup_line=False) | |
| self.assertEqual(None, f._lines) | |
| linecache.lazycache("f", globals()) | |
| self.assertEqual( | |
| '"""Test cases for traceback module"""', | |
| f.line) | |
| def test_no_line(self): | |
| f = traceback.FrameSummary("f", None, "dummy") | |
| self.assertEqual(f.line, None) | |
| def test_explicit_line(self): | |
| f = traceback.FrameSummary("f", 1, "dummy", line="line") | |
| self.assertEqual("line", f.line) | |
| def test_len(self): | |
| f = traceback.FrameSummary("f", 1, "dummy", line="line") | |
| self.assertEqual(len(f), 4) | |
| class TestStack(unittest.TestCase): | |
| def test_walk_stack(self): | |
| def deeper(): | |
| return list(traceback.walk_stack(None)) | |
| s1, s2 = list(traceback.walk_stack(None)), deeper() | |
| self.assertEqual(len(s2) - len(s1), 1) | |
| self.assertEqual(s2[1:], s1) | |
| def test_walk_innermost_frame(self): | |
| def inner(): | |
| return list(traceback.walk_stack(None)) | |
| frames = inner() | |
| innermost_frame, _ = frames[0] | |
| self.assertEqual(innermost_frame.f_code.co_name, "inner") | |
| def test_walk_tb(self): | |
| try: | |
| 1/0 | |
| except Exception as e: | |
| tb = e.__traceback__ | |
| s = list(traceback.walk_tb(tb)) | |
| self.assertEqual(len(s), 1) | |
| def test_extract_stack(self): | |
| s = traceback.StackSummary.extract(traceback.walk_stack(None)) | |
| self.assertIsInstance(s, traceback.StackSummary) | |
| def test_extract_stack_limit(self): | |
| s = traceback.StackSummary.extract(traceback.walk_stack(None), limit=5) | |
| self.assertEqual(len(s), 5) | |
| def test_extract_stack_lookup_lines(self): | |
| linecache.clearcache() | |
| linecache.updatecache('/foo.py', globals()) | |
| c = test_code('/foo.py', 'method') | |
| f = test_frame(c, None, None) | |
| s = traceback.StackSummary.extract(iter([(f, 6)]), lookup_lines=True) | |
| linecache.clearcache() | |
| self.assertEqual(s[0].line, "import sys") | |
| def test_extract_stackup_deferred_lookup_lines(self): | |
| linecache.clearcache() | |
| c = test_code('/foo.py', 'method') | |
| f = test_frame(c, None, None) | |
| s = traceback.StackSummary.extract(iter([(f, 6)]), lookup_lines=False) | |
| self.assertEqual({}, linecache.cache) | |
| linecache.updatecache('/foo.py', globals()) | |
| self.assertEqual(s[0].line, "import sys") | |
| def test_from_list(self): | |
| s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) | |
| self.assertEqual( | |
| [' File "foo.py", line 1, in fred\n line\n'], | |
| s.format()) | |
| def test_from_list_edited_stack(self): | |
| s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) | |
| s[0] = ('foo.py', 2, 'fred', 'line') | |
| s2 = traceback.StackSummary.from_list(s) | |
| self.assertEqual( | |
| [' File "foo.py", line 2, in fred\n line\n'], | |
| s2.format()) | |
| def test_format_smoke(self): | |
| # For detailed tests see the format_list tests, which consume the same | |
| # code. | |
| s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) | |
| self.assertEqual( | |
| [' File "foo.py", line 1, in fred\n line\n'], | |
| s.format()) | |
| def test_locals(self): | |
| linecache.updatecache('/foo.py', globals()) | |
| c = test_code('/foo.py', 'method') | |
| f = test_frame(c, globals(), {'something': 1}) | |
| s = traceback.StackSummary.extract(iter([(f, 6)]), capture_locals=True) | |
| self.assertEqual(s[0].locals, {'something': '1'}) | |
| def test_no_locals(self): | |
| linecache.updatecache('/foo.py', globals()) | |
| c = test_code('/foo.py', 'method') | |
| f = test_frame(c, globals(), {'something': 1}) | |
| s = traceback.StackSummary.extract(iter([(f, 6)])) | |
| self.assertEqual(s[0].locals, None) | |
| def test_format_locals(self): | |
| def some_inner(k, v): | |
| a = 1 | |
| b = 2 | |
| return traceback.StackSummary.extract( | |
| traceback.walk_stack(None), capture_locals=True, limit=1) | |
| s = some_inner(3, 4) | |
| self.assertEqual( | |
| [' File "%s", line %d, in some_inner\n' | |
| ' return traceback.StackSummary.extract(\n' | |
| ' a = 1\n' | |
| ' b = 2\n' | |
| ' k = 3\n' | |
| ' v = 4\n' % (__file__, some_inner.__code__.co_firstlineno + 3) | |
| ], s.format()) | |
| def test_custom_format_frame(self): | |
| class CustomStackSummary(traceback.StackSummary): | |
| def format_frame_summary(self, frame_summary, colorize=False): | |
| return f'{frame_summary.filename}:{frame_summary.lineno}' | |
| def some_inner(): | |
| return CustomStackSummary.extract( | |
| traceback.walk_stack(None), limit=1) | |
| s = some_inner() | |
| self.assertEqual( | |
| s.format(), | |
| [f'{__file__}:{some_inner.__code__.co_firstlineno + 1}']) | |
| def test_dropping_frames(self): | |
| def f(): | |
| 1/0 | |
| def g(): | |
| try: | |
| f() | |
| except Exception as e: | |
| return e.__traceback__ | |
| tb = g() | |
| class Skip_G(traceback.StackSummary): | |
| def format_frame_summary(self, frame_summary, colorize=False): | |
| if frame_summary.name == 'g': | |
| return None | |
| return super().format_frame_summary(frame_summary) | |
| stack = Skip_G.extract( | |
| traceback.walk_tb(tb)).format() | |
| self.assertEqual(len(stack), 1) | |
| lno = f.__code__.co_firstlineno + 1 | |
| self.assertEqual( | |
| stack[0], | |
| f' File "{__file__}", line {lno}, in f\n 1/0\n' | |
| ) | |
| def test_summary_should_show_carets(self): | |
| # See: https://github.com/python/cpython/issues/122353 | |
| # statement to execute and to get a ZeroDivisionError for a traceback | |
| statement = "abcdef = 1 / 0 and 2.0" | |
| colno = statement.index('1 / 0') | |
| end_colno = colno + len('1 / 0') | |
| # Actual line to use when rendering the traceback | |
| # and whose AST will be extracted (it will be empty). | |
| cached_line = '# this line will be used during rendering' | |
| self.addCleanup(unlink, TESTFN) | |
| with open(TESTFN, "w") as file: | |
| file.write(cached_line) | |
| linecache.updatecache(TESTFN, {}) | |
| try: | |
| exec(compile(statement, TESTFN, "exec")) | |
| except ZeroDivisionError as exc: | |
| # This is the simplest way to create a StackSummary | |
| # whose FrameSummary items have their column offsets. | |
| s = traceback.TracebackException.from_exception(exc).stack | |
| self.assertIsInstance(s, traceback.StackSummary) | |
| with unittest.mock.patch.object(s, '_should_show_carets', | |
| wraps=s._should_show_carets) as ff: | |
| self.assertEqual(len(s), 2) | |
| self.assertListEqual( | |
| s.format_frame_summary(s[1]).splitlines(), | |
| [ | |
| f' File "{TESTFN}", line 1, in <module>', | |
| f' {cached_line}' | |
| ] | |
| ) | |
| ff.assert_called_with(colno, end_colno, [cached_line], None) | |
| class Unrepresentable: | |
| def __repr__(self) -> str: | |
| raise Exception("Unrepresentable") | |
| # Used in test_dont_swallow_cause_or_context_of_falsey_exception and | |
| # test_dont_swallow_subexceptions_of_falsey_exceptiongroup. | |
| class FalseyException(Exception): | |
| def __bool__(self): | |
| return False | |
| class FalseyExceptionGroup(ExceptionGroup): | |
| def __bool__(self): | |
| return False | |
| class TestTracebackException(unittest.TestCase): | |
| def do_test_smoke(self, exc, expected_type_str): | |
| try: | |
| raise exc | |
| except Exception as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(e) | |
| expected_stack = traceback.StackSummary.extract( | |
| traceback.walk_tb(e.__traceback__)) | |
| self.assertEqual(None, exc.__cause__) | |
| self.assertEqual(None, exc.__context__) | |
| self.assertEqual(False, exc.__suppress_context__) | |
| self.assertEqual(expected_stack, exc.stack) | |
| with self.assertWarns(DeprecationWarning): | |
| self.assertEqual(type(exc_obj), exc.exc_type) | |
| self.assertEqual(expected_type_str, exc.exc_type_str) | |
| self.assertEqual(str(exc_obj), str(exc)) | |
| def test_smoke_builtin(self): | |
| self.do_test_smoke(ValueError(42), 'ValueError') | |
| def test_smoke_user_exception(self): | |
| class MyException(Exception): | |
| pass | |
| if __name__ == '__main__': | |
| expected = ('TestTracebackException.' | |
| 'test_smoke_user_exception.<locals>.MyException') | |
| else: | |
| expected = ('test.test_traceback.TestTracebackException.' | |
| 'test_smoke_user_exception.<locals>.MyException') | |
| self.do_test_smoke(MyException('bad things happened'), expected) | |
| def test_from_exception(self): | |
| # Check all the parameters are accepted. | |
| def foo(): | |
| 1/0 | |
| try: | |
| foo() | |
| except Exception as e: | |
| exc_obj = e | |
| tb = e.__traceback__ | |
| self.expected_stack = traceback.StackSummary.extract( | |
| traceback.walk_tb(tb), limit=1, lookup_lines=False, | |
| capture_locals=True) | |
| self.exc = traceback.TracebackException.from_exception( | |
| e, limit=1, lookup_lines=False, capture_locals=True) | |
| expected_stack = self.expected_stack | |
| exc = self.exc | |
| self.assertEqual(None, exc.__cause__) | |
| self.assertEqual(None, exc.__context__) | |
| self.assertEqual(False, exc.__suppress_context__) | |
| self.assertEqual(expected_stack, exc.stack) | |
| with self.assertWarns(DeprecationWarning): | |
| self.assertEqual(type(exc_obj), exc.exc_type) | |
| self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) | |
| self.assertEqual(str(exc_obj), str(exc)) | |
| def test_cause(self): | |
| try: | |
| try: | |
| 1/0 | |
| finally: | |
| exc = sys.exception() | |
| exc_context = traceback.TracebackException.from_exception(exc) | |
| cause = Exception("cause") | |
| raise Exception("uh oh") from cause | |
| except Exception as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(e) | |
| expected_stack = traceback.StackSummary.extract( | |
| traceback.walk_tb(e.__traceback__)) | |
| exc_cause = traceback.TracebackException(Exception, cause, None) | |
| self.assertEqual(exc_cause, exc.__cause__) | |
| self.assertEqual(exc_context, exc.__context__) | |
| self.assertEqual(True, exc.__suppress_context__) | |
| self.assertEqual(expected_stack, exc.stack) | |
| with self.assertWarns(DeprecationWarning): | |
| self.assertEqual(type(exc_obj), exc.exc_type) | |
| self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) | |
| self.assertEqual(str(exc_obj), str(exc)) | |
| def test_context(self): | |
| try: | |
| try: | |
| 1/0 | |
| finally: | |
| exc = sys.exception() | |
| exc_context = traceback.TracebackException.from_exception(exc) | |
| raise Exception("uh oh") | |
| except Exception as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(e) | |
| expected_stack = traceback.StackSummary.extract( | |
| traceback.walk_tb(e.__traceback__)) | |
| self.assertEqual(None, exc.__cause__) | |
| self.assertEqual(exc_context, exc.__context__) | |
| self.assertEqual(False, exc.__suppress_context__) | |
| self.assertEqual(expected_stack, exc.stack) | |
| with self.assertWarns(DeprecationWarning): | |
| self.assertEqual(type(exc_obj), exc.exc_type) | |
| self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) | |
| self.assertEqual(str(exc_obj), str(exc)) | |
| def test_long_context_chain(self): | |
| def f(): | |
| try: | |
| 1/0 | |
| except ZeroDivisionError: | |
| f() | |
| try: | |
| f() | |
| except RecursionError as e: | |
| exc_obj = e | |
| else: | |
| self.fail("Exception not raised") | |
| te = traceback.TracebackException.from_exception(exc_obj) | |
| res = list(te.format()) | |
| # many ZeroDiv errors followed by the RecursionError | |
| self.assertGreater(len(res), sys.getrecursionlimit()) | |
| self.assertGreater( | |
| len([l for l in res if 'ZeroDivisionError:' in l]), | |
| sys.getrecursionlimit() * 0.5) | |
| self.assertIn( | |
| "RecursionError: maximum recursion depth exceeded", res[-1]) | |
| def test_compact_with_cause(self): | |
| try: | |
| try: | |
| 1/0 | |
| finally: | |
| cause = Exception("cause") | |
| raise Exception("uh oh") from cause | |
| except Exception as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(exc_obj, compact=True) | |
| expected_stack = traceback.StackSummary.extract( | |
| traceback.walk_tb(exc_obj.__traceback__)) | |
| exc_cause = traceback.TracebackException(Exception, cause, None) | |
| self.assertEqual(exc_cause, exc.__cause__) | |
| self.assertEqual(None, exc.__context__) | |
| self.assertEqual(True, exc.__suppress_context__) | |
| self.assertEqual(expected_stack, exc.stack) | |
| with self.assertWarns(DeprecationWarning): | |
| self.assertEqual(type(exc_obj), exc.exc_type) | |
| self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) | |
| self.assertEqual(str(exc_obj), str(exc)) | |
| def test_compact_no_cause(self): | |
| try: | |
| try: | |
| 1/0 | |
| finally: | |
| exc = sys.exception() | |
| exc_context = traceback.TracebackException.from_exception(exc) | |
| raise Exception("uh oh") | |
| except Exception as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(e, compact=True) | |
| expected_stack = traceback.StackSummary.extract( | |
| traceback.walk_tb(exc_obj.__traceback__)) | |
| self.assertEqual(None, exc.__cause__) | |
| self.assertEqual(exc_context, exc.__context__) | |
| self.assertEqual(False, exc.__suppress_context__) | |
| self.assertEqual(expected_stack, exc.stack) | |
| with self.assertWarns(DeprecationWarning): | |
| self.assertEqual(type(exc_obj), exc.exc_type) | |
| self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) | |
| self.assertEqual(str(exc_obj), str(exc)) | |
| def test_no_save_exc_type(self): | |
| try: | |
| 1/0 | |
| except Exception as e: | |
| exc = e | |
| te = traceback.TracebackException.from_exception( | |
| exc, save_exc_type=False) | |
| with self.assertWarns(DeprecationWarning): | |
| self.assertIsNone(te.exc_type) | |
| def test_no_refs_to_exception_and_traceback_objects(self): | |
| exc_obj = None | |
| try: | |
| 1/0 | |
| except Exception as e: | |
| exc_obj = e | |
| refcnt1 = sys.getrefcount(exc_obj) | |
| refcnt2 = sys.getrefcount(exc_obj.__traceback__) | |
| exc = traceback.TracebackException.from_exception(exc_obj) | |
| self.assertEqual(sys.getrefcount(exc_obj), refcnt1) | |
| self.assertEqual(sys.getrefcount(exc_obj.__traceback__), refcnt2) | |
| def test_comparison_basic(self): | |
| try: | |
| 1/0 | |
| except Exception as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(exc_obj) | |
| exc2 = traceback.TracebackException.from_exception(exc_obj) | |
| self.assertIsNot(exc, exc2) | |
| self.assertEqual(exc, exc2) | |
| self.assertNotEqual(exc, object()) | |
| self.assertEqual(exc, ALWAYS_EQ) | |
| def test_comparison_params_variations(self): | |
| def raise_exc(): | |
| try: | |
| raise ValueError('bad value') | |
| except ValueError: | |
| raise | |
| def raise_with_locals(): | |
| x, y = 1, 2 | |
| raise_exc() | |
| try: | |
| raise_with_locals() | |
| except Exception as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(exc_obj) | |
| exc1 = traceback.TracebackException.from_exception(exc_obj, limit=10) | |
| exc2 = traceback.TracebackException.from_exception(exc_obj, limit=2) | |
| self.assertEqual(exc, exc1) # limit=10 gets all frames | |
| self.assertNotEqual(exc, exc2) # limit=2 truncates the output | |
| # locals change the output | |
| exc3 = traceback.TracebackException.from_exception(exc_obj, capture_locals=True) | |
| self.assertNotEqual(exc, exc3) | |
| # there are no locals in the innermost frame | |
| exc4 = traceback.TracebackException.from_exception(exc_obj, limit=-1) | |
| exc5 = traceback.TracebackException.from_exception(exc_obj, limit=-1, capture_locals=True) | |
| self.assertEqual(exc4, exc5) | |
| # there are locals in the next-to-innermost frame | |
| exc6 = traceback.TracebackException.from_exception(exc_obj, limit=-2) | |
| exc7 = traceback.TracebackException.from_exception(exc_obj, limit=-2, capture_locals=True) | |
| self.assertNotEqual(exc6, exc7) | |
| def test_comparison_equivalent_exceptions_are_equal(self): | |
| excs = [] | |
| for _ in range(2): | |
| try: | |
| 1/0 | |
| except Exception as e: | |
| excs.append(traceback.TracebackException.from_exception(e)) | |
| self.assertEqual(excs[0], excs[1]) | |
| self.assertEqual(list(excs[0].format()), list(excs[1].format())) | |
| def test_unhashable(self): | |
| class UnhashableException(Exception): | |
| def __eq__(self, other): | |
| return True | |
| ex1 = UnhashableException('ex1') | |
| ex2 = UnhashableException('ex2') | |
| try: | |
| raise ex2 from ex1 | |
| except UnhashableException: | |
| try: | |
| raise ex1 | |
| except UnhashableException as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(exc_obj) | |
| formatted = list(exc.format()) | |
| self.assertIn('UnhashableException: ex2\n', formatted[2]) | |
| self.assertIn('UnhashableException: ex1\n', formatted[6]) | |
| def test_limit(self): | |
| def recurse(n): | |
| if n: | |
| recurse(n-1) | |
| else: | |
| 1/0 | |
| try: | |
| recurse(10) | |
| except Exception as e: | |
| exc = traceback.TracebackException.from_exception(e, limit=5) | |
| expected_stack = traceback.StackSummary.extract( | |
| traceback.walk_tb(e.__traceback__), limit=5) | |
| self.assertEqual(expected_stack, exc.stack) | |
| def test_lookup_lines(self): | |
| linecache.clearcache() | |
| e = Exception("uh oh") | |
| c = test_code('/foo.py', 'method') | |
| f = test_frame(c, None, None) | |
| tb = test_tb(f, 6, None, 0) | |
| exc = traceback.TracebackException(Exception, e, tb, lookup_lines=False) | |
| self.assertEqual(linecache.cache, {}) | |
| linecache.updatecache('/foo.py', globals()) | |
| self.assertEqual(exc.stack[0].line, "import sys") | |
| def test_locals(self): | |
| linecache.updatecache('/foo.py', globals()) | |
| e = Exception("uh oh") | |
| c = test_code('/foo.py', 'method') | |
| f = test_frame(c, globals(), {'something': 1, 'other': 'string', 'unrepresentable': Unrepresentable()}) | |
| tb = test_tb(f, 6, None, 0) | |
| exc = traceback.TracebackException( | |
| Exception, e, tb, capture_locals=True) | |
| self.assertEqual( | |
| exc.stack[0].locals, | |
| {'something': '1', 'other': "'string'", 'unrepresentable': '<local repr() failed>'}) | |
| def test_no_locals(self): | |
| linecache.updatecache('/foo.py', globals()) | |
| e = Exception("uh oh") | |
| c = test_code('/foo.py', 'method') | |
| f = test_frame(c, globals(), {'something': 1}) | |
| tb = test_tb(f, 6, None, 0) | |
| exc = traceback.TracebackException(Exception, e, tb) | |
| self.assertEqual(exc.stack[0].locals, None) | |
| def test_traceback_header(self): | |
| # do not print a traceback header if exc_traceback is None | |
| # see issue #24695 | |
| exc = traceback.TracebackException(Exception, Exception("haven"), None) | |
| self.assertEqual(list(exc.format()), ["Exception: haven\n"]) | |
| @requires_debug_ranges() | |
| def test_print(self): | |
| def f(): | |
| x = 12 | |
| try: | |
| x/0 | |
| except Exception as e: | |
| return e | |
| exc = traceback.TracebackException.from_exception(f(), capture_locals=True) | |
| output = StringIO() | |
| exc.print(file=output) | |
| self.assertEqual( | |
| output.getvalue().split('\n')[-5:], | |
| [' x/0', | |
| ' ~^~', | |
| ' x = 12', | |
| 'ZeroDivisionError: division by zero', | |
| '']) | |
| def test_dont_swallow_cause_or_context_of_falsey_exception(self): | |
| # see gh-132308: Ensure that __cause__ or __context__ attributes of exceptions | |
| # that evaluate as falsey are included in the output. For falsey term, | |
| # see https://docs.python.org/3/builtins/stdtypes.html#truth-value-testing. | |
| try: | |
| raise FalseyException from KeyError | |
| except FalseyException as e: | |
| self.assertIn(cause_message, traceback.format_exception(e)) | |
| try: | |
| try: | |
| 1/0 | |
| except ZeroDivisionError: | |
| raise FalseyException | |
| except FalseyException as e: | |
| self.assertIn(context_message, traceback.format_exception(e)) | |
| class TestTracebackException_ExceptionGroups(unittest.TestCase): | |
| def setUp(self): | |
| super().setUp() | |
| self.eg = self._get_exception_group() | |
| def _get_exception_group(self): | |
| def f(): | |
| 1/0 | |
| def g(v): | |
| raise ValueError(v) | |
| self.lno_f = f.__code__.co_firstlineno | |
| self.lno_g = g.__code__.co_firstlineno | |
| try: | |
| try: | |
| try: | |
| f() | |
| except Exception as e: | |
| exc1 = e | |
| try: | |
| g(42) | |
| except Exception as e: | |
| exc2 = e | |
| raise ExceptionGroup("eg1", [exc1, exc2]) | |
| except ExceptionGroup as e: | |
| exc3 = e | |
| try: | |
| g(24) | |
| except Exception as e: | |
| exc4 = e | |
| raise ExceptionGroup("eg2", [exc3, exc4]) | |
| except ExceptionGroup as eg: | |
| return eg | |
| self.fail('Exception Not Raised') | |
| def test_exception_group_construction(self): | |
| eg = self.eg | |
| teg1 = traceback.TracebackException(type(eg), eg, eg.__traceback__) | |
| teg2 = traceback.TracebackException.from_exception(eg) | |
| self.assertIsNot(teg1, teg2) | |
| self.assertEqual(teg1, teg2) | |
| def test_exception_group_format_exception_only(self): | |
| teg = traceback.TracebackException.from_exception(self.eg) | |
| formatted = ''.join(teg.format_exception_only()).split('\n') | |
| expected = "ExceptionGroup: eg2 (2 sub-exceptions)\n".split('\n') | |
| self.assertEqual(formatted, expected) | |
| def test_exception_group_format_exception_onlyi_recursive(self): | |
| teg = traceback.TracebackException.from_exception(self.eg) | |
| formatted = ''.join(teg.format_exception_only(show_group=True)).split('\n') | |
| expected = [ | |
| 'ExceptionGroup: eg2 (2 sub-exceptions)', | |
| ' ExceptionGroup: eg1 (2 sub-exceptions)', | |
| ' ZeroDivisionError: division by zero', | |
| ' ValueError: 42', | |
| ' ValueError: 24', | |
| '' | |
| ] | |
| self.assertEqual(formatted, expected) | |
| def test_exception_group_format(self): | |
| teg = traceback.TracebackException.from_exception(self.eg) | |
| formatted = ''.join(teg.format()).split('\n') | |
| lno_f = self.lno_f | |
| lno_g = self.lno_g | |
| expected = [ | |
| f' + Exception Group Traceback (most recent call last):', | |
| f' | File "{__file__}", line {lno_g+23}, in _get_exception_group', | |
| f' | raise ExceptionGroup("eg2", [exc3, exc4])', | |
| f' | ExceptionGroup: eg2 (2 sub-exceptions)', | |
| f' +-+---------------- 1 ----------------', | |
| f' | Exception Group Traceback (most recent call last):', | |
| f' | File "{__file__}", line {lno_g+16}, in _get_exception_group', | |
| f' | raise ExceptionGroup("eg1", [exc1, exc2])', | |
| f' | ExceptionGroup: eg1 (2 sub-exceptions)', | |
| f' +-+---------------- 1 ----------------', | |
| f' | Traceback (most recent call last):', | |
| f' | File "{__file__}", line {lno_g+9}, in _get_exception_group', | |
| f' | f()', | |
| f' | ~^^', | |
| f' | File "{__file__}", line {lno_f+1}, in f', | |
| f' | 1/0', | |
| f' | ~^~', | |
| f' | ZeroDivisionError: division by zero', | |
| f' +---------------- 2 ----------------', | |
| f' | Traceback (most recent call last):', | |
| f' | File "{__file__}", line {lno_g+13}, in _get_exception_group', | |
| f' | g(42)', | |
| f' | ~^^^^', | |
| f' | File "{__file__}", line {lno_g+1}, in g', | |
| f' | raise ValueError(v)', | |
| f' | ValueError: 42', | |
| f' +------------------------------------', | |
| f' +---------------- 2 ----------------', | |
| f' | Traceback (most recent call last):', | |
| f' | File "{__file__}", line {lno_g+20}, in _get_exception_group', | |
| f' | g(24)', | |
| f' | ~^^^^', | |
| f' | File "{__file__}", line {lno_g+1}, in g', | |
| f' | raise ValueError(v)', | |
| f' | ValueError: 24', | |
| f' +------------------------------------', | |
| f''] | |
| self.assertEqual(formatted, expected) | |
| def test_max_group_width(self): | |
| excs1 = [] | |
| excs2 = [] | |
| for i in range(3): | |
| excs1.append(ValueError(i)) | |
| for i in range(10): | |
| excs2.append(TypeError(i)) | |
| EG = ExceptionGroup | |
| eg = EG('eg', [EG('eg1', excs1), EG('eg2', excs2)]) | |
| teg = traceback.TracebackException.from_exception(eg, max_group_width=2) | |
| formatted = ''.join(teg.format()).split('\n') | |
| expected = [ | |
| ' | ExceptionGroup: eg (2 sub-exceptions)', | |
| ' +-+---------------- 1 ----------------', | |
| ' | ExceptionGroup: eg1 (3 sub-exceptions)', | |
| ' +-+---------------- 1 ----------------', | |
| ' | ValueError: 0', | |
| ' +---------------- 2 ----------------', | |
| ' | ValueError: 1', | |
| ' +---------------- ... ----------------', | |
| ' | and 1 more exception', | |
| ' +------------------------------------', | |
| ' +---------------- 2 ----------------', | |
| ' | ExceptionGroup: eg2 (10 sub-exceptions)', | |
| ' +-+---------------- 1 ----------------', | |
| ' | TypeError: 0', | |
| ' +---------------- 2 ----------------', | |
| ' | TypeError: 1', | |
| ' +---------------- ... ----------------', | |
| ' | and 8 more exceptions', | |
| ' +------------------------------------', | |
| ''] | |
| self.assertEqual(formatted, expected) | |
| def test_max_group_depth(self): | |
| exc = TypeError('bad type') | |
| for i in range(3): | |
| exc = ExceptionGroup('exc', [ValueError(-i), exc, ValueError(i)]) | |
| teg = traceback.TracebackException.from_exception(exc, max_group_depth=2) | |
| formatted = ''.join(teg.format()).split('\n') | |
| expected = [ | |
| ' | ExceptionGroup: exc (3 sub-exceptions)', | |
| ' +-+---------------- 1 ----------------', | |
| ' | ValueError: -2', | |
| ' +---------------- 2 ----------------', | |
| ' | ExceptionGroup: exc (3 sub-exceptions)', | |
| ' +-+---------------- 1 ----------------', | |
| ' | ValueError: -1', | |
| ' +---------------- 2 ----------------', | |
| ' | ... (max_group_depth is 2)', | |
| ' +---------------- 3 ----------------', | |
| ' | ValueError: 1', | |
| ' +------------------------------------', | |
| ' +---------------- 3 ----------------', | |
| ' | ValueError: 2', | |
| ' +------------------------------------', | |
| ''] | |
| self.assertEqual(formatted, expected) | |
| def test_comparison(self): | |
| try: | |
| raise self.eg | |
| except ExceptionGroup as e: | |
| exc = e | |
| for _ in range(5): | |
| try: | |
| raise exc | |
| except Exception as e: | |
| exc_obj = e | |
| exc = traceback.TracebackException.from_exception(exc_obj) | |
| exc2 = traceback.TracebackException.from_exception(exc_obj) | |
| exc3 = traceback.TracebackException.from_exception(exc_obj, limit=300) | |
| ne = traceback.TracebackException.from_exception(exc_obj, limit=3) | |
| self.assertIsNot(exc, exc2) | |
| self.assertEqual(exc, exc2) | |
| self.assertEqual(exc, exc3) | |
| self.assertNotEqual(exc, ne) | |
| self.assertNotEqual(exc, object()) | |
| self.assertEqual(exc, ALWAYS_EQ) | |
| def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self): | |
| # see gh-132308: Ensure that subexceptions of exception groups | |
| # that evaluate as falsey are displayed in the output. For falsey term, | |
| # see https://docs.python.org/3/builtins/stdtypes.html#truth-value-testing. | |
| try: | |
| raise FalseyExceptionGroup("Gih", (KeyError(), NameError())) | |
| except Exception as ee: | |
| str_exc = ''.join(traceback.format_exception(ee)) | |
| self.assertIn('+---------------- 1 ----------------', str_exc) | |
| self.assertIn('+---------------- 2 ----------------', str_exc) | |
| # Test with a falsey exception, in last position, as sub-exceptions. | |
| msg = 'bool' | |
| try: | |
| raise FalseyExceptionGroup("Gah", (KeyError(), FalseyException(msg))) | |
| except Exception as ee: | |
| str_exc = traceback.format_exception(ee) | |
| self.assertIn(f'{FalseyException.__name__}: {msg}', str_exc[-2]) | |
| global_for_suggestions = None | |
| class SuggestionFormattingTestMixin: | |
| attr_function = getattr | |
| def get_suggestion(self, obj, attr_name=None): | |
| if attr_name is not None: | |
| def callable(): | |
| self.attr_function(obj, attr_name) | |
| else: | |
| callable = obj | |
| result_lines = self.get_exception( | |
| callable, slice_start=-1, slice_end=None | |
| ) | |
| return result_lines[0] | |
| class BaseSuggestionTests(SuggestionFormattingTestMixin): | |
| def test_suggestions(self): | |
| class Substitution: | |
| noise = more_noise = a = bc = None | |
| blech = None | |
| class Elimination: | |
| noise = more_noise = a = bc = None | |
| blch = None | |
| class Addition: | |
| noise = more_noise = a = bc = None | |
| bluchin = None | |
| class SubstitutionOverElimination: | |
| blach = None | |
| bluc = None | |
| class SubstitutionOverAddition: | |
| blach = None | |
| bluchi = None | |
| class EliminationOverAddition: | |
| blucha = None | |
| bluc = None | |
| class CaseChangeOverSubstitution: | |
| Luch = None | |
| fluch = None | |
| BLuch = None | |
| for cls, suggestion in [ | |
| (Addition, "'.bluchin'"), | |
| (Substitution, "'.blech'"), | |
| (Elimination, "'.blch'"), | |
| (Addition, "'.bluchin'"), | |
| (SubstitutionOverElimination, "'.blach'"), | |
| (SubstitutionOverAddition, "'.blach'"), | |
| (EliminationOverAddition, "'.bluc'"), | |
| (CaseChangeOverSubstitution, "'.BLuch'"), | |
| ]: | |
| actual = self.get_suggestion(cls(), 'bluch') | |
| self.assertIn('Did you mean ' + suggestion, actual) | |
| def test_suggestions_underscored(self): | |
| class A: | |
| bluch = None | |
| self.assertIn("'.bluch'", self.get_suggestion(A(), 'blach')) | |
| self.assertIn("'.bluch'", self.get_suggestion(A(), '_luch')) | |
| self.assertIn("'.bluch'", self.get_suggestion(A(), '_bluch')) | |
| attr_function = self.attr_function | |
| class B: | |
| _bluch = None | |
| def method(self, name): | |
| attr_function(self, name) | |
| self.assertIn("'._bluch'", self.get_suggestion(B(), '_blach')) | |
| self.assertIn("'._bluch'", self.get_suggestion(B(), '_luch')) | |
| self.assertNotIn("'._bluch'", self.get_suggestion(B(), 'bluch')) | |
| self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_blach'))) | |
| self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_luch'))) | |
| self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, 'bluch'))) | |
| def test_suggestions_with_custom___dir__(self): | |
| class M(type): | |
| def __dir__(cls): | |
| return [None, "fox"] | |
| class C0: | |
| def __dir__(self): | |
| return [..., "bluch"] | |
| class C1(C0, metaclass=M): | |
| pass | |
| self.assertNotIn("'.bluch'", self.get_suggestion(C0, "blach")) | |
| self.assertIn("'.bluch'", self.get_suggestion(C0(), "blach")) | |
| self.assertIn("'.fox'", self.get_suggestion(C1, "foo")) | |
| self.assertNotIn("'.fox'", self.get_suggestion(C1(), "foo")) | |
| self.assertNotIn("'.bluch'", self.get_suggestion(C1, "blach")) | |
| self.assertIn("'.bluch'", self.get_suggestion(C1(), "blach")) | |
| def test_do_not_trigger_for_long_attributes(self): | |
| class A: | |
| blech = None | |
| actual = self.get_suggestion(A(), 'somethingverywrong') | |
| self.assertNotIn("blech", actual) | |
| def test_do_not_trigger_for_small_names(self): | |
| class MyClass: | |
| vvv = mom = w = id = pytho = None | |
| for name in ("b", "v", "m", "py"): | |
| with self.subTest(name=name): | |
| actual = self.get_suggestion(MyClass(), name) | |
| self.assertNotIn("Did you mean", actual) | |
| self.assertNotIn("'vvv", actual) | |
| self.assertNotIn("'mom'", actual) | |
| self.assertNotIn("'id'", actual) | |
| self.assertNotIn("'w'", actual) | |
| self.assertNotIn("'pytho'", actual) | |
| def test_do_not_trigger_for_big_dicts(self): | |
| class A: | |
| blech = None | |
| # A class with a very big __dict__ will not be considered | |
| # for suggestions. | |
| for index in range(2000): | |
| setattr(A, f"index_{index}", None) | |
| actual = self.get_suggestion(A(), 'bluch') | |
| self.assertNotIn("blech", actual) | |
| def test_suggestions_for_same_name(self): | |
| class A: | |
| def __dir__(self): | |
| return ['blech'] | |
| actual = self.get_suggestion(A(), 'blech') | |
| self.assertNotIn("Did you mean", actual) | |
| def test_suggestions_not_normalized(self): | |
| class A: | |
| analization = None | |
| fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None | |
| suggestion = self.get_suggestion(A(), 'fiⁿₐˡᵢᶻₐᵗᵢᵒₙ') | |
| self.assertIn("'.finalization'", suggestion) | |
| self.assertNotIn("analization", suggestion) | |
| class B: | |
| attr_a = None | |
| attr_µ = None # attr_\xb5 | |
| suggestion = self.get_suggestion(B(), 'attr_\xb5') | |
| self.assertIn( | |
| "'.attr_\u03bc' ('attr_\\u03bc') " | |
| "instead of '.attr_\xb5' ('attr_\\xb5')", | |
| suggestion) | |
| self.assertNotIn("attr_a", suggestion) | |
| class GetattrSuggestionTests(BaseSuggestionTests): | |
| def test_suggestions_no_args(self): | |
| class A: | |
| blech = None | |
| def __getattr__(self, attr): | |
| raise AttributeError() | |
| actual = self.get_suggestion(A(), 'bluch') | |
| self.assertIn("blech", actual) | |
| class A: | |
| blech = None | |
| def __getattr__(self, attr): | |
| raise AttributeError | |
| actual = self.get_suggestion(A(), 'bluch') | |
| self.assertIn("blech", actual) | |
| def test_suggestions_invalid_args(self): | |
| class NonStringifyClass: | |
| __str__ = None | |
| __repr__ = None | |
| class A: | |
| blech = None | |
| def __getattr__(self, attr): | |
| raise AttributeError(NonStringifyClass()) | |
| class B: | |
| blech = None | |
| def __getattr__(self, attr): | |
| raise AttributeError("Error", 23) | |
| class C: | |
| blech = None | |
| def __getattr__(self, attr): | |
| raise AttributeError(23) | |
| for cls in [A, B, C]: | |
| actual = self.get_suggestion(cls(), 'bluch') | |
| self.assertIn("blech", actual) | |
| class DelattrSuggestionTests(BaseSuggestionTests): | |
| attr_function = delattr | |
| class SuggestionFormattingTestBase(SuggestionFormattingTestMixin): | |
| def test_attribute_error_with_failing_dict(self): | |
| class T: | |
| bluch = 1 | |
| def __dir__(self): | |
| raise AttributeError("oh no!") | |
| actual = self.get_suggestion(T(), 'blich') | |
| self.assertNotIn("blech", actual) | |
| self.assertNotIn("oh no!", actual) | |
| def test_attribute_error_with_non_string_candidates(self): | |
| class T: | |
| bluch = 1 | |
| instance = T() | |
| instance.__dict__[0] = 1 | |
| actual = self.get_suggestion(instance, 'blich') | |
| self.assertIn("bluch", actual) | |
| def test_attribute_error_with_bad_name(self): | |
| def raise_attribute_error_with_bad_name(): | |
| raise AttributeError(name=12, obj=23) | |
| result_lines = self.get_exception( | |
| raise_attribute_error_with_bad_name, slice_start=-1, slice_end=None | |
| ) | |
| self.assertNotIn("?", result_lines[-1]) | |
| def test_attribute_error_inside_nested_getattr(self): | |
| class A: | |
| bluch = 1 | |
| class B: | |
| def __getattribute__(self, attr): | |
| a = A() | |
| return a.blich | |
| actual = self.get_suggestion(B(), 'something') | |
| self.assertIn("Did you mean", actual) | |
| self.assertIn("bluch", actual) | |
| def test_getattr_nested_attribute_suggestions(self): | |
| # Test that nested attributes are suggested when no direct match | |
| class Inner: | |
| def __init__(self): | |
| self.value = 42 | |
| self.data = "test" | |
| class Outer: | |
| def __init__(self): | |
| self.inner = Inner() | |
| # Should suggest 'inner.value' | |
| actual = self.get_suggestion(Outer(), 'value') | |
| self.assertIn("Did you mean '.inner.value' instead of '.value'", actual) | |
| # Should suggest 'inner.data' | |
| actual = self.get_suggestion(Outer(), 'data') | |
| self.assertIn("Did you mean '.inner.data' instead of '.data'", actual) | |
| def test_getattr_nested_prioritizes_direct_matches(self): | |
| # Test that direct attribute matches are prioritized over nested ones | |
| class Inner: | |
| def __init__(self): | |
| self.foo = 42 | |
| class Outer: | |
| def __init__(self): | |
| self.inner = Inner() | |
| self.fooo = 100 # Similar to 'foo' | |
| # Should suggest 'fooo' (direct) not 'inner.foo' (nested) | |
| actual = self.get_suggestion(Outer(), 'foo') | |
| self.assertIn("Did you mean '.fooo'", actual) | |
| self.assertNotIn("inner.foo", actual) | |
| def test_getattr_nested_with_property(self): | |
| # Property suggestions should not execute the property getter. | |
| class Inner: | |
| @property | |
| def computed(self): | |
| return missing_name | |
| class Outer: | |
| def __init__(self): | |
| self.inner = Inner() | |
| actual = self.get_suggestion(Outer(), 'computed') | |
| self.assertIn( | |
| "Did you mean '.inner.computed' instead of '.computed'", | |
| actual, | |
| ) | |
| def test_getattr_nested_no_suggestion_for_deep_nesting(self): | |
| # Test that deeply nested attributes (2+ levels) are not suggested | |
| class Deep: | |
| def __init__(self): | |
| self.value = 42 | |
| class Middle: | |
| def __init__(self): | |
| self.deep = Deep() | |
| class Outer: | |
| def __init__(self): | |
| self.middle = Middle() | |
| # Should not suggest 'middle.deep.value' (too deep) | |
| actual = self.get_suggestion(Outer(), 'value') | |
| self.assertNotIn("Did you mean", actual) | |
| def test_getattr_nested_ignores_private_attributes(self): | |
| # Test that nested suggestions ignore private attributes | |
| class Inner: | |
| def __init__(self): | |
| self.public_value = 42 | |
| class Outer: | |
| def __init__(self): | |
| self._private_inner = Inner() | |
| # Should not suggest '_private_inner.public_value' | |
| actual = self.get_suggestion(Outer(), 'public_value') | |
| self.assertNotIn("Did you mean", actual) | |
| def test_getattr_nested_limits_attribute_checks(self): | |
| # Test that nested suggestions are limited to checking first 20 non-private attributes | |
| class Inner: | |
| def __init__(self): | |
| self.target_value = 42 | |
| class Outer: | |
| def __init__(self): | |
| # Add many attributes before 'inner' | |
| for i in range(25): | |
| setattr(self, f'attr_{i:02d}', i) | |
| # Add the inner object after 20+ attributes | |
| self.inner = Inner() | |
| obj = Outer() | |
| # Verify that 'inner' is indeed present but after position 20 | |
| attrs = [x for x in sorted(dir(obj)) if not x.startswith('_')] | |
| inner_position = attrs.index('inner') | |
| self.assertGreater(inner_position, 19, "inner should be after position 20 in sorted attributes") | |
| # Should not suggest 'inner.target_value' because inner is beyond the first 20 attributes checked | |
| actual = self.get_suggestion(obj, 'target_value') | |
| self.assertNotIn("inner.target_value", actual) | |
| def test_getattr_nested_returns_first_match_only(self): | |
| # Test that only the first nested match is returned (not multiple) | |
| class Inner1: | |
| def __init__(self): | |
| self.value = 1 | |
| class Inner2: | |
| def __init__(self): | |
| self.value = 2 | |
| class Inner3: | |
| def __init__(self): | |
| self.value = 3 | |
| class Outer: | |
| def __init__(self): | |
| # Multiple inner objects with same attribute | |
| self.a_inner = Inner1() | |
| self.b_inner = Inner2() | |
| self.c_inner = Inner3() | |
| # Should suggest only the first match (alphabetically) | |
| actual = self.get_suggestion(Outer(), 'value') | |
| self.assertIn("'.a_inner.value'", actual) | |
| # Verify it's a single suggestion, not multiple | |
| self.assertEqual(actual.count("Did you mean"), 1) | |
| def test_getattr_nested_handles_attribute_access_exceptions(self): | |
| # Test that exceptions raised when accessing attributes don't crash the suggestion system | |
| class ExplodingProperty: | |
| @property | |
| def exploding_attr(self): | |
| raise RuntimeError("BOOM! This property always explodes") | |
| def __repr__(self): | |
| raise RuntimeError("repr also explodes") | |
| class SafeInner: | |
| def __init__(self): | |
| self.target = 42 | |
| class Outer: | |
| def __init__(self): | |
| self.exploder = ExplodingProperty() # Accessing attributes will raise | |
| self.safe_inner = SafeInner() | |
| # Should still suggest '.safe_inner.target' without crashing | |
| # even though accessing exploder.target would raise an exception | |
| actual = self.get_suggestion(Outer(), 'target') | |
| self.assertIn("'.safe_inner.target'", actual) | |
| def test_getattr_nested_handles_hasattr_exceptions(self): | |
| # Test that exceptions in hasattr don't crash the system | |
| class WeirdObject: | |
| def __getattr__(self, name): | |
| if name == 'target': | |
| raise RuntimeError("Can't check for target attribute") | |
| raise AttributeError(f"No attribute {name}") | |
| class NormalInner: | |
| def __init__(self): | |
| self.target = 100 | |
| class Outer: | |
| def __init__(self): | |
| self.weird = WeirdObject() # hasattr will raise for 'target' | |
| self.normal = NormalInner() | |
| # Should still find 'normal.target' even though weird.target check fails | |
| actual = self.get_suggestion(Outer(), 'target') | |
| self.assertIn("'.normal.target'", actual) | |
| @force_not_colorized | |
| def test_cross_language(self): | |
| cases = [ | |
| # (type, attr, hint_attr) | |
| (list, 'push', 'append'), | |
| (list, 'concat', 'extend'), | |
| (list, 'addAll', 'extend'), | |
| (str, 'toUpperCase', 'upper'), | |
| (str, 'toLowerCase', 'lower'), | |
| (str, 'trimStart', 'lstrip'), | |
| (str, 'trimEnd', 'rstrip'), | |
| (dict, 'keySet', 'keys'), | |
| (dict, 'entrySet', 'items'), | |
| (dict, 'entries', 'items'), | |
| (dict, 'putAll', 'update'), | |
| ] | |
| for test_type, attr, hint_attr in cases: | |
| with self.subTest(type=test_type.__name__, attr=attr): | |
| obj = test_type() | |
| actual = self.get_suggestion(obj, attr) | |
| self.assertEndsWith(actual, f"Did you mean '.{hint_attr}'?") | |
| cases = [ | |
| # (type, attr, hint) | |
| (list, 'contains', "Use 'x in list'."), | |
| (list, 'add', "Did you mean to use a 'set' object?"), | |
| (dict, 'put', "Use d[k] = v."), | |
| ] | |
| for test_type, attr, expected in cases: | |
| with self.subTest(type=test_type, attr=attr): | |
| obj = test_type() | |
| actual = self.get_suggestion(obj, attr) | |
| self.assertEndsWith(actual, expected) | |
| @force_not_colorized | |
| def test_cross_language_levenshtein_fallback(self): | |
| # When no cross-language entry exists, Levenshtein still works | |
| # (e.g., trim->strip is not in the table but Levenshtein catches it) | |
| actual = self.get_suggestion('', 'trim') | |
| self.assertIn("strip", actual) | |
| @force_not_colorized | |
| def test_cross_language_no_hint_for_unknown_attr(self): | |
| actual = self.get_suggestion([], 'completely_unknown_method') | |
| self.assertNotIn("Did you mean", actual) | |
| @force_not_colorized | |
| def test_cross_language_works_for_subclasses(self): | |
| # isinstance() check means subclasses also get hints | |
| class MyList(list): | |
| pass | |
| actual = self.get_suggestion(MyList(), 'push') | |
| self.assertEndsWith(actual, "Did you mean '.append'?") | |
| class MyDict(dict): | |
| pass | |
| actual = self.get_suggestion(MyDict(), 'keySet') | |
| self.assertEndsWith(actual, "Did you mean '.keys'?") | |
| @force_not_colorized | |
| def test_cross_language_mutable_on_immutable(self): | |
| # Mutable method on immutable type suggests the mutable counterpart | |
| cases = [ | |
| (tuple, 'append', "Did you mean to use a 'list' object?"), | |
| (tuple, 'extend', "Did you mean to use a 'list' object?"), | |
| (tuple, 'insert', "Did you mean to use a 'list' object?"), | |
| (tuple, 'remove', "Did you mean to use a 'list' object?"), | |
| (frozenset, 'add', "Did you mean to use a 'set' object?"), | |
| (frozenset, 'discard', "Did you mean to use a 'set' object?"), | |
| (frozenset, 'remove', "Did you mean to use a 'set' object?"), | |
| (frozenset, 'update', "Did you mean to use a 'set' object?"), | |
| (frozendict, 'update', "Did you mean to use a 'dict' object?"), | |
| (tuple, 'clear', "Did you mean to use a 'list' object?"), | |
| (frozenset, 'clear', "Did you mean to use a 'set' object?"), | |
| (frozendict, 'clear', "Did you mean to use a 'dict' object?"), | |
| ] | |
| for test_type, attr, expected in cases: | |
| with self.subTest(type=test_type.__name__, attr=attr): | |
| obj = test_type() | |
| actual = self.get_suggestion(obj, attr) | |
| self.assertEndsWith(actual, expected) | |
| @force_not_colorized | |
| def test_cross_language_float_bitwise(self): | |
| # Bitwise operators on float suggest using int | |
| cases = ['__or__', '__and__', '__xor__', '__lshift__', '__rshift__'] | |
| for attr in cases: | |
| with self.subTest(attr=attr): | |
| actual = self.get_suggestion(1.0, attr) | |
| self.assertIn("'int'", actual) | |
| self.assertIn("Bitwise operators", actual) | |
| def make_module(self, code): | |
| tmpdir = Path(tempfile.mkdtemp()) | |
| self.addCleanup(shutil.rmtree, tmpdir) | |
| sys.path.append(str(tmpdir)) | |
| self.addCleanup(sys.path.pop) | |
| mod_name = ''.join(random.choices(string.ascii_letters, k=16)) | |
| module = tmpdir / (mod_name + ".py") | |
| module.write_text(code) | |
| return mod_name | |
| def get_import_from_suggestion(self, code, name): | |
| modname = self.make_module(code) | |
| def callable(): | |
| try: | |
| exec(f"from {modname} import {name}") | |
| except ImportError as e: | |
| raise e from None | |
| except Exception as e: | |
| self.fail(f"Expected ImportError but got {type(e)}") | |
| self.addCleanup(forget, modname) | |
| result_lines = self.get_exception( | |
| callable, slice_start=-1, slice_end=None | |
| ) | |
| return result_lines[0] | |
| def test_import_from_suggestions(self): | |
| substitution = textwrap.dedent("""\ | |
| noise = more_noise = a = bc = None | |
| blech = None | |
| """) | |
| elimination = textwrap.dedent(""" | |
| noise = more_noise = a = bc = None | |
| blch = None | |
| """) | |
| addition = textwrap.dedent(""" | |
| noise = more_noise = a = bc = None | |
| bluchin = None | |
| """) | |
| substitutionOverElimination = textwrap.dedent(""" | |
| blach = None | |
| bluc = None | |
| """) | |
| substitutionOverAddition = textwrap.dedent(""" | |
| blach = None | |
| bluchi = None | |
| """) | |
| eliminationOverAddition = textwrap.dedent(""" | |
| blucha = None | |
| bluc = None | |
| """) | |
| caseChangeOverSubstitution = textwrap.dedent(""" | |
| Luch = None | |
| fluch = None | |
| BLuch = None | |
| """) | |
| for code, suggestion in [ | |
| (addition, "'bluchin'?"), | |
| (substitution, "'blech'?"), | |
| (elimination, "'blch'?"), | |
| (addition, "'bluchin'?"), | |
| (substitutionOverElimination, "'blach'?"), | |
| (substitutionOverAddition, "'blach'?"), | |
| (eliminationOverAddition, "'bluc'?"), | |
| (caseChangeOverSubstitution, "'BLuch'?"), | |
| ]: | |
| actual = self.get_import_from_suggestion(code, 'bluch') | |
| self.assertIn(suggestion, actual) | |
| def test_import_from_suggestions_underscored(self): | |
| code = "bluch = None" | |
| self.assertIn("'bluch'", self.get_import_from_suggestion(code, 'blach')) | |
| self.assertIn("'bluch'", self.get_import_from_suggestion(code, '_luch')) | |
| self.assertIn("'bluch'", self.get_import_from_suggestion(code, '_bluch')) | |
| code = "_bluch = None" | |
| self.assertIn("'_bluch'", self.get_import_from_suggestion(code, '_blach')) | |
| self.assertIn("'_bluch'", self.get_import_from_suggestion(code, '_luch')) | |
| self.assertNotIn("'_bluch'", self.get_import_from_suggestion(code, 'bluch')) | |
| def test_import_from_suggestions_non_string(self): | |
| modWithNonStringAttr = textwrap.dedent("""\ | |
| globals()[0] = 1 | |
| bluch = 1 | |
| """) | |
| self.assertIn("'bluch'", self.get_import_from_suggestion(modWithNonStringAttr, 'blech')) | |
| def test_import_from_suggestions_do_not_trigger_for_long_attributes(self): | |
| code = "blech = None" | |
| actual = self.get_suggestion(code, 'somethingverywrong') | |
| self.assertNotIn("blech", actual) | |
| def test_import_from_error_bad_suggestions_do_not_trigger_for_small_names(self): | |
| code = "vvv = mom = w = id = pytho = None" | |
| for name in ("b", "v", "m", "py"): | |
| with self.subTest(name=name): | |
| actual = self.get_import_from_suggestion(code, name) | |
| self.assertNotIn("Did you mean", actual) | |
| self.assertNotIn("'vvv'", actual) | |
| self.assertNotIn("'mom'", actual) | |
| self.assertNotIn("'id'", actual) | |
| self.assertNotIn("'w'", actual) | |
| self.assertNotIn("'pytho'", actual) | |
| def test_import_from_suggestions_do_not_trigger_for_big_namespaces(self): | |
| # A module with lots of names will not be considered for suggestions. | |
| chunks = [f"index_{index} = " for index in range(200)] | |
| chunks.append(" None") | |
| code = " ".join(chunks) | |
| actual = self.get_import_from_suggestion(code, 'bluch') | |
| self.assertNotIn("blech", actual) | |
| def test_import_from_error_with_bad_name(self): | |
| def raise_attribute_error_with_bad_name(): | |
| raise ImportError(name=12, obj=23, name_from=11) | |
| result_lines = self.get_exception( | |
| raise_attribute_error_with_bad_name, slice_start=-1, slice_end=None | |
| ) | |
| self.assertNotIn("?", result_lines[-1]) | |
| def test_name_error_suggestions(self): | |
| def Substitution(): | |
| noise = more_noise = a = bc = None | |
| blech = None | |
| print(bluch) | |
| def Elimination(): | |
| noise = more_noise = a = bc = None | |
| blch = None | |
| print(bluch) | |
| def Addition(): | |
| noise = more_noise = a = bc = None | |
| bluchin = None | |
| print(bluch) | |
| def SubstitutionOverElimination(): | |
| blach = None | |
| bluc = None | |
| print(bluch) | |
| def SubstitutionOverAddition(): | |
| blach = None | |
| bluchi = None | |
| print(bluch) | |
| def EliminationOverAddition(): | |
| blucha = None | |
| bluc = None | |
| print(bluch) | |
| for func, suggestion in [(Substitution, "'blech'?"), | |
| (Elimination, "'blch'?"), | |
| (Addition, "'bluchin'?"), | |
| (EliminationOverAddition, "'blucha'?"), | |
| (SubstitutionOverElimination, "'blach'?"), | |
| (SubstitutionOverAddition, "'blach'?")]: | |
| actual = self.get_suggestion(func) | |
| self.assertIn(suggestion, actual) | |
| def test_name_error_suggestions_from_globals(self): | |
| def func(): | |
| print(global_for_suggestio) | |
| actual = self.get_suggestion(func) | |
| self.assertIn("'global_for_suggestions'?", actual) | |
| def test_name_error_suggestions_from_builtins(self): | |
| def func(): | |
| print(ZeroDivisionErrrrr) | |
| actual = self.get_suggestion(func) | |
| self.assertIn("'ZeroDivisionError'?", actual) | |
| def test_name_error_suggestions_from_builtins_when_builtins_is_module(self): | |
| def func(): | |
| custom_globals = globals().copy() | |
| custom_globals["__builtins__"] = builtins | |
| print(eval("ZeroDivisionErrrrr", custom_globals)) | |
| actual = self.get_suggestion(func) | |
| self.assertIn("'ZeroDivisionError'?", actual) | |
| def test_name_error_suggestions_with_non_string_candidates(self): | |
| def func(): | |
| abc = 1 | |
| custom_globals = globals().copy() | |
| custom_globals[0] = 1 | |
| print(eval("abv", custom_globals, locals())) | |
| actual = self.get_suggestion(func) | |
| self.assertIn("abc", actual) | |
| def test_name_error_suggestions_do_not_trigger_for_long_names(self): | |
| def func(): | |
| somethingverywronghehehehehehe = None | |
| print(somethingverywronghe) | |
| actual = self.get_suggestion(func) | |
| self.assertNotIn("somethingverywronghehe", actual) | |
| def test_name_error_bad_suggestions_do_not_trigger_for_small_names(self): | |
| def f_b(): | |
| vvv = mom = w = id = pytho = None | |
| b | |
| def f_v(): | |
| vvv = mom = w = id = pytho = None | |
| v | |
| def f_m(): | |
| vvv = mom = w = id = pytho = None | |
| m | |
| def f_py(): | |
| vvv = mom = w = id = pytho = None | |
| py | |
| for name, func in (("b", f_b), ("v", f_v), ("m", f_m), ("py", f_py)): | |
| with self.subTest(name=name): | |
| actual = self.get_suggestion(func) | |
| self.assertNotIn("you mean", actual) | |
| self.assertNotIn("vvv", actual) | |
| self.assertNotIn("mom", actual) | |
| self.assertNotIn("'id'", actual) | |
| self.assertNotIn("'w'", actual) | |
| self.assertNotIn("'pytho'", actual) | |
| def test_name_error_suggestions_do_not_trigger_for_too_many_locals(self): | |
| def func(): | |
| # Mutating locals() is unreliable, so we need to do it by hand | |
| a1 = a2 = a3 = a4 = a5 = a6 = a7 = a8 = a9 = a10 = \ | |
| a11 = a12 = a13 = a14 = a15 = a16 = a17 = a18 = a19 = a20 = \ | |
| a21 = a22 = a23 = a24 = a25 = a26 = a27 = a28 = a29 = a30 = \ | |
| a31 = a32 = a33 = a34 = a35 = a36 = a37 = a38 = a39 = a40 = \ | |
| a41 = a42 = a43 = a44 = a45 = a46 = a47 = a48 = a49 = a50 = \ | |
| a51 = a52 = a53 = a54 = a55 = a56 = a57 = a58 = a59 = a60 = \ | |
| a61 = a62 = a63 = a64 = a65 = a66 = a67 = a68 = a69 = a70 = \ | |
| a71 = a72 = a73 = a74 = a75 = a76 = a77 = a78 = a79 = a80 = \ | |
| a81 = a82 = a83 = a84 = a85 = a86 = a87 = a88 = a89 = a90 = \ | |
| a91 = a92 = a93 = a94 = a95 = a96 = a97 = a98 = a99 = a100 = \ | |
| a101 = a102 = a103 = a104 = a105 = a106 = a107 = a108 = a109 = a110 = \ | |
| a111 = a112 = a113 = a114 = a115 = a116 = a117 = a118 = a119 = a120 = \ | |
| a121 = a122 = a123 = a124 = a125 = a126 = a127 = a128 = a129 = a130 = \ | |
| a131 = a132 = a133 = a134 = a135 = a136 = a137 = a138 = a139 = a140 = \ | |
| a141 = a142 = a143 = a144 = a145 = a146 = a147 = a148 = a149 = a150 = \ | |
| a151 = a152 = a153 = a154 = a155 = a156 = a157 = a158 = a159 = a160 = \ | |
| a161 = a162 = a163 = a164 = a165 = a166 = a167 = a168 = a169 = a170 = \ | |
| a171 = a172 = a173 = a174 = a175 = a176 = a177 = a178 = a179 = a180 = \ | |
| a181 = a182 = a183 = a184 = a185 = a186 = a187 = a188 = a189 = a190 = \ | |
| a191 = a192 = a193 = a194 = a195 = a196 = a197 = a198 = a199 = a200 = \ | |
| a201 = a202 = a203 = a204 = a205 = a206 = a207 = a208 = a209 = a210 = \ | |
| a211 = a212 = a213 = a214 = a215 = a216 = a217 = a218 = a219 = a220 = \ | |
| a221 = a222 = a223 = a224 = a225 = a226 = a227 = a228 = a229 = a230 = \ | |
| a231 = a232 = a233 = a234 = a235 = a236 = a237 = a238 = a239 = a240 = \ | |
| a241 = a242 = a243 = a244 = a245 = a246 = a247 = a248 = a249 = a250 = \ | |
| a251 = a252 = a253 = a254 = a255 = a256 = a257 = a258 = a259 = a260 = \ | |
| a261 = a262 = a263 = a264 = a265 = a266 = a267 = a268 = a269 = a270 = \ | |
| a271 = a272 = a273 = a274 = a275 = a276 = a277 = a278 = a279 = a280 = \ | |
| a281 = a282 = a283 = a284 = a285 = a286 = a287 = a288 = a289 = a290 = \ | |
| a291 = a292 = a293 = a294 = a295 = a296 = a297 = a298 = a299 = a300 = \ | |
| a301 = a302 = a303 = a304 = a305 = a306 = a307 = a308 = a309 = a310 = \ | |
| a311 = a312 = a313 = a314 = a315 = a316 = a317 = a318 = a319 = a320 = \ | |
| a321 = a322 = a323 = a324 = a325 = a326 = a327 = a328 = a329 = a330 = \ | |
| a331 = a332 = a333 = a334 = a335 = a336 = a337 = a338 = a339 = a340 = \ | |
| a341 = a342 = a343 = a344 = a345 = a346 = a347 = a348 = a349 = a350 = \ | |
| a351 = a352 = a353 = a354 = a355 = a356 = a357 = a358 = a359 = a360 = \ | |
| a361 = a362 = a363 = a364 = a365 = a366 = a367 = a368 = a369 = a370 = \ | |
| a371 = a372 = a373 = a374 = a375 = a376 = a377 = a378 = a379 = a380 = \ | |
| a381 = a382 = a383 = a384 = a385 = a386 = a387 = a388 = a389 = a390 = \ | |
| a391 = a392 = a393 = a394 = a395 = a396 = a397 = a398 = a399 = a400 = \ | |
| a401 = a402 = a403 = a404 = a405 = a406 = a407 = a408 = a409 = a410 = \ | |
| a411 = a412 = a413 = a414 = a415 = a416 = a417 = a418 = a419 = a420 = \ | |
| a421 = a422 = a423 = a424 = a425 = a426 = a427 = a428 = a429 = a430 = \ | |
| a431 = a432 = a433 = a434 = a435 = a436 = a437 = a438 = a439 = a440 = \ | |
| a441 = a442 = a443 = a444 = a445 = a446 = a447 = a448 = a449 = a450 = \ | |
| a451 = a452 = a453 = a454 = a455 = a456 = a457 = a458 = a459 = a460 = \ | |
| a461 = a462 = a463 = a464 = a465 = a466 = a467 = a468 = a469 = a470 = \ | |
| a471 = a472 = a473 = a474 = a475 = a476 = a477 = a478 = a479 = a480 = \ | |
| a481 = a482 = a483 = a484 = a485 = a486 = a487 = a488 = a489 = a490 = \ | |
| a491 = a492 = a493 = a494 = a495 = a496 = a497 = a498 = a499 = a500 = \ | |
| a501 = a502 = a503 = a504 = a505 = a506 = a507 = a508 = a509 = a510 = \ | |
| a511 = a512 = a513 = a514 = a515 = a516 = a517 = a518 = a519 = a520 = \ | |
| a521 = a522 = a523 = a524 = a525 = a526 = a527 = a528 = a529 = a530 = \ | |
| a531 = a532 = a533 = a534 = a535 = a536 = a537 = a538 = a539 = a540 = \ | |
| a541 = a542 = a543 = a544 = a545 = a546 = a547 = a548 = a549 = a550 = \ | |
| a551 = a552 = a553 = a554 = a555 = a556 = a557 = a558 = a559 = a560 = \ | |
| a561 = a562 = a563 = a564 = a565 = a566 = a567 = a568 = a569 = a570 = \ | |
| a571 = a572 = a573 = a574 = a575 = a576 = a577 = a578 = a579 = a580 = \ | |
| a581 = a582 = a583 = a584 = a585 = a586 = a587 = a588 = a589 = a590 = \ | |
| a591 = a592 = a593 = a594 = a595 = a596 = a597 = a598 = a599 = a600 = \ | |
| a601 = a602 = a603 = a604 = a605 = a606 = a607 = a608 = a609 = a610 = \ | |
| a611 = a612 = a613 = a614 = a615 = a616 = a617 = a618 = a619 = a620 = \ | |
| a621 = a622 = a623 = a624 = a625 = a626 = a627 = a628 = a629 = a630 = \ | |
| a631 = a632 = a633 = a634 = a635 = a636 = a637 = a638 = a639 = a640 = \ | |
| a641 = a642 = a643 = a644 = a645 = a646 = a647 = a648 = a649 = a650 = \ | |
| a651 = a652 = a653 = a654 = a655 = a656 = a657 = a658 = a659 = a660 = \ | |
| a661 = a662 = a663 = a664 = a665 = a666 = a667 = a668 = a669 = a670 = \ | |
| a671 = a672 = a673 = a674 = a675 = a676 = a677 = a678 = a679 = a680 = \ | |
| a681 = a682 = a683 = a684 = a685 = a686 = a687 = a688 = a689 = a690 = \ | |
| a691 = a692 = a693 = a694 = a695 = a696 = a697 = a698 = a699 = a700 = \ | |
| a701 = a702 = a703 = a704 = a705 = a706 = a707 = a708 = a709 = a710 = \ | |
| a711 = a712 = a713 = a714 = a715 = a716 = a717 = a718 = a719 = a720 = \ | |
| a721 = a722 = a723 = a724 = a725 = a726 = a727 = a728 = a729 = a730 = \ | |
| a731 = a732 = a733 = a734 = a735 = a736 = a737 = a738 = a739 = a740 = \ | |
| a741 = a742 = a743 = a744 = a745 = a746 = a747 = a748 = a749 = a750 = \ | |
| a751 = a752 = a753 = a754 = a755 = a756 = a757 = a758 = a759 = a760 = \ | |
| a761 = a762 = a763 = a764 = a765 = a766 = a767 = a768 = a769 = a770 = \ | |
| a771 = a772 = a773 = a774 = a775 = a776 = a777 = a778 = a779 = a780 = \ | |
| a781 = a782 = a783 = a784 = a785 = a786 = a787 = a788 = a789 = a790 = \ | |
| a791 = a792 = a793 = a794 = a795 = a796 = a797 = a798 = a799 = a800 \ | |
| = None | |
| print(a0) | |
| actual = self.get_suggestion(func) | |
| self.assertNotRegex(actual, r"NameError.*a1") | |
| def test_name_error_with_custom_exceptions(self): | |
| def func(): | |
| blech = None | |
| raise NameError() | |
| actual = self.get_suggestion(func) | |
| self.assertNotIn("blech", actual) | |
| def func(): | |
| blech = None | |
| raise NameError | |
| actual = self.get_suggestion(func) | |
| self.assertNotIn("blech", actual) | |
| def test_name_error_with_instance(self): | |
| class A: | |
| def __init__(self): | |
| self.blech = None | |
| def foo(self): | |
| blich = 1 | |
| x = blech | |
| instance = A() | |
| actual = self.get_suggestion(instance.foo) | |
| self.assertIn("self.blech", actual) | |
| def test_name_error_with_instance_not_normalized(self): | |
| class A: | |
| def __init__(self): | |
| self.fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None | |
| def foo(self): | |
| analization = 1 | |
| x = fiⁿₐˡᵢᶻₐᵗᵢᵒₙ | |
| instance = A() | |
| actual = self.get_suggestion(instance.foo) | |
| self.assertIn("self.finalization", actual) | |
| self.assertNotIn("fiⁿₐˡᵢᶻₐᵗᵢᵒₙ", actual) | |
| self.assertNotIn("analization", actual) | |
| class B: | |
| def __init__(self): | |
| self.attr_µ = None # attr_\xb5 | |
| def foo(self): | |
| attr_a = 1 | |
| x = attr_µ # attr_\xb5 | |
| instance = B() | |
| actual = self.get_suggestion(instance.foo) | |
| self.assertIn("self.attr_\u03bc", actual) | |
| self.assertIn(r"self.attr_\u03bc", actual) | |
| self.assertNotIn("attr_\xb5", actual) | |
| self.assertNotIn("attr_a", actual) | |
| def test_unbound_local_error_with_instance(self): | |
| class A: | |
| def __init__(self): | |
| self.blech = None | |
| def foo(self): | |
| blich = 1 | |
| x = blech | |
| blech = 1 | |
| instance = A() | |
| actual = self.get_suggestion(instance.foo) | |
| self.assertNotIn("self.blech", actual) | |
| def test_unbound_local_error_with_side_effect(self): | |
| # gh-132385 | |
| class A: | |
| def __getattr__(self, key): | |
| if key == 'foo': | |
| raise AttributeError('foo') | |
| if key == 'spam': | |
| raise ValueError('spam') | |
| def bar(self): | |
| foo | |
| def baz(self): | |
| spam | |
| suggestion = self.get_suggestion(A().bar) | |
| self.assertNotIn('self.', suggestion) | |
| self.assertIn("'foo'", suggestion) | |
| suggestion = self.get_suggestion(A().baz) | |
| self.assertNotIn('self.', suggestion) | |
| self.assertIn("'spam'", suggestion) | |
| def test_unbound_local_error_does_not_match(self): | |
| def func(): | |
| something = 3 | |
| print(somethong) | |
| somethong = 3 | |
| actual = self.get_suggestion(func) | |
| self.assertNotIn("something", actual) | |
| def test_name_error_for_stdlib_modules(self): | |
| def func(): | |
| stream = io.StringIO() | |
| actual = self.get_suggestion(func) | |
| self.assertIn("forget to import 'io'", actual) | |
| def test_name_error_for_private_stdlib_modules(self): | |
| def func(): | |
| stream = _io.StringIO() | |
| actual = self.get_suggestion(func) | |
| self.assertIn("forget to import '_io'", actual) | |
| class PurePythonSuggestionFormattingTests( | |
| PurePythonExceptionFormattingMixin, | |
| SuggestionFormattingTestBase, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests as above using the pure Python implementation of | |
| traceback printing in traceback.py. | |
| """ | |
| @cpython_only | |
| class CPythonSuggestionFormattingTests( | |
| CAPIExceptionFormattingMixin, | |
| SuggestionFormattingTestBase, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests as above but with Python's internal traceback printing. | |
| """ | |
| class PurePythonGetattrSuggestionFormattingTests( | |
| PurePythonExceptionFormattingMixin, | |
| GetattrSuggestionTests, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests (for attribute access) as above using the pure Python | |
| implementation of traceback printing in traceback.py. | |
| """ | |
| class PurePythonDelattrSuggestionFormattingTests( | |
| PurePythonExceptionFormattingMixin, | |
| DelattrSuggestionTests, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests (for attribute deletion) as above using the pure Python | |
| implementation of traceback printing in traceback.py. | |
| """ | |
| @cpython_only | |
| class CPythonGetattrSuggestionFormattingTests( | |
| CAPIExceptionFormattingMixin, | |
| GetattrSuggestionTests, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests (for attribute access) as above but with Python's | |
| internal traceback printing. | |
| """ | |
| @cpython_only | |
| class CPythonDelattrSuggestionFormattingTests( | |
| CAPIExceptionFormattingMixin, | |
| DelattrSuggestionTests, | |
| unittest.TestCase, | |
| ): | |
| """ | |
| Same set of tests (for attribute deletion) as above but with Python's | |
| internal traceback printing. | |
| """ | |
| class MiscTest(unittest.TestCase): | |
| def test_all(self): | |
| expected = set() | |
| for name in dir(traceback): | |
| if name.startswith('_'): | |
| continue | |
| module_object = getattr(traceback, name) | |
| if getattr(module_object, '__module__', None) == 'traceback': | |
| expected.add(name) | |
| self.assertCountEqual(traceback.__all__, expected) | |
| def test_levenshtein_distance(self): | |
| # copied from _testinternalcapi.test_edit_cost | |
| # to also exercise the Python implementation | |
| def CHECK(a, b, expected): | |
| actual = traceback._levenshtein_distance(a, b, 4044) | |
| self.assertEqual(actual, expected) | |
| CHECK("", "", 0) | |
| CHECK("", "a", 2) | |
| CHECK("a", "A", 1) | |
| CHECK("Apple", "Aple", 2) | |
| CHECK("Banana", "B@n@n@", 6) | |
| CHECK("Cherry", "Cherry!", 2) | |
| CHECK("---0---", "------", 2) | |
| CHECK("abc", "y", 6) | |
| CHECK("aa", "bb", 4) | |
| CHECK("aaaaa", "AAAAA", 5) | |
| CHECK("wxyz", "wXyZ", 2) | |
| CHECK("wxyz", "wXyZ123", 8) | |
| CHECK("Python", "Java", 12) | |
| CHECK("Java", "C#", 8) | |
| CHECK("AbstractFoobarManager", "abstract_foobar_manager", 3+2*2) | |
| CHECK("CPython", "PyPy", 10) | |
| CHECK("CPython", "pypy", 11) | |
| CHECK("AttributeError", "AttributeErrop", 2) | |
| CHECK("AttributeError", "AttributeErrorTests", 10) | |
| CHECK("ABA", "AAB", 4) | |
| @support.requires_resource('cpu') | |
| def test_levenshtein_distance_short_circuit(self): | |
| if not LEVENSHTEIN_DATA_FILE.is_file(): | |
| self.fail( | |
| f"{LEVENSHTEIN_DATA_FILE} is missing." | |
| f" Run `make regen-test-levenshtein`" | |
| ) | |
| with LEVENSHTEIN_DATA_FILE.open("r") as f: | |
| examples = json.load(f) | |
| for a, b, expected in examples: | |
| res1 = traceback._levenshtein_distance(a, b, 1000) | |
| self.assertEqual(res1, expected, msg=(a, b)) | |
| for threshold in [expected, expected + 1, expected + 2]: | |
| # big enough thresholds shouldn't change the result | |
| res2 = traceback._levenshtein_distance(a, b, threshold) | |
| self.assertEqual(res2, expected, msg=(a, b, threshold)) | |
| for threshold in range(expected): | |
| # for small thresholds, the only piece of information | |
| # we receive is "strings not close enough". | |
| res3 = traceback._levenshtein_distance(a, b, threshold) | |
| self.assertGreater(res3, threshold, msg=(a, b, threshold)) | |
| @cpython_only | |
| def test_suggestions_extension(self): | |
| # Check that the C extension is available | |
| import _suggestions | |
| self.assertEqual( | |
| _suggestions._generate_suggestions( | |
| ["hello", "world"], | |
| "hell" | |
| ), | |
| "hello" | |
| ) | |
| self.assertEqual( | |
| _suggestions._generate_suggestions( | |
| ["hovercraft"], | |
| "eels" | |
| ), | |
| None | |
| ) | |
| # gh-131936: _generate_suggestions() doesn't accept list subclasses | |
| class MyList(list): | |
| pass | |
| with self.assertRaises(TypeError): | |
| _suggestions._generate_suggestions(MyList(), "") | |
| def test_no_site_package_flavour(self): | |
| code = """import boo""" | |
| _, _, stderr = assert_python_failure('-S', '-c', code) | |
| self.assertIn( | |
| (b"Site initialization is disabled, did you forget to " | |
| b"add the site-packages directory to sys.path " | |
| b"or to enable your virtual environment?"), stderr | |
| ) | |
| code = """ | |
| import sys | |
| sys.stdlib_module_names = sys.stdlib_module_names + ("boo",) | |
| import boo | |
| """ | |
| _, _, stderr = assert_python_failure('-S', '-c', code) | |
| self.assertNotIn( | |
| (b"Site initialization is disabled, did you forget to " | |
| b"add the site-packages directory to sys.path " | |
| b"or to enable your virtual environment?"), stderr | |
| ) | |
| def test_missing_stdlib_module(self): | |
| code = """ | |
| import sys | |
| sys.stdlib_module_names |= {'spam'} | |
| import spam | |
| """ | |
| _, _, stderr = assert_python_failure('-S', '-c', code) | |
| self.assertIn(b"Standard library module 'spam' was not found", stderr) | |
| code = """ | |
| import sys | |
| import traceback | |
| traceback._MISSING_STDLIB_MODULE_MESSAGES = {'spam': "Install 'spam4life' for 'spam'"} | |
| sys.stdlib_module_names |= {'spam'} | |
| import spam | |
| """ | |
| _, _, stderr = assert_python_failure('-S', '-c', code) | |
| self.assertIn(b"Install 'spam4life' for 'spam'", stderr) | |
| @unittest.skipIf(sys.platform == "win32", "Non-Windows test") | |
| def test_windows_only_module_error(self): | |
| try: | |
| import msvcrt # noqa: F401 | |
| except ModuleNotFoundError: | |
| formatted = traceback.format_exc() | |
| self.assertIn("Unsupported platform for Windows-only standard library module 'msvcrt'", formatted) | |
| else: | |
| self.fail("ModuleNotFoundError was not raised") | |
| @unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform does not support extension modules') | |
| def test_find_incompatible_extension_modules(self): | |
| """_find_incompatible_extension_modules assumes the last extension in | |
| importlib.machinery.EXTENSION_SUFFIXES (defined in Python/dynload_*.c) | |
| is untagged (eg. .so, .pyd). | |
| This test exists to make sure that assumption is correct. | |
| """ | |
| last_extension_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1] | |
| if shlib_suffix := sysconfig.get_config_var('SHLIB_SUFFIX'): | |
| self.assertEqual(last_extension_suffix, shlib_suffix) | |
| else: | |
| before_dot, *extensions = last_extension_suffix.split('.') | |
| expected_prefixes = [''] | |
| if os.name == 'nt': | |
| # Windows puts the debug tag in the module file stem (eg. foo_d.pyd) | |
| expected_prefixes.append('_d') | |
| self.assertIn(before_dot, expected_prefixes, msg=( | |
| f'Unexpected prefix {before_dot!r} in extension module ' | |
| f'suffix {last_extension_suffix!r}. ' | |
| 'traceback._find_incompatible_extension_module needs to be ' | |
| 'updated to take this into account!' | |
| )) | |
| # if SHLIB_SUFFIX is not define, we assume the native | |
| # shared library suffix only contains one extension | |
| # (eg. '.so', bad eg. '.cpython-315-x86_64-linux-gnu.so') | |
| self.assertEqual(len(extensions), 1, msg=( | |
| 'The last suffix in importlib.machinery.EXTENSION_SUFFIXES ' | |
| 'contains more than one extension, so it is probably different ' | |
| 'than SHLIB_SUFFIX. It probably contains an ABI tag! ' | |
| 'If this is a false positive, define SHLIB_SUFFIX in sysconfig.' | |
| )) | |
| @unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform does not support extension modules') | |
| def test_incompatible_extension_modules_hint(self): | |
| untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1] | |
| with os_helper.temp_dir() as tmp: | |
| # create a module with a incompatible ABI tag | |
| incompatible_module = f'foo.some-abi{untagged_suffix}' | |
| open(os.path.join(tmp, incompatible_module), "wb").close() | |
| # try importing it | |
| code = f''' | |
| import sys | |
| sys.path.insert(0, {tmp!r}) | |
| import foo | |
| ''' | |
| _, _, stderr = assert_python_failure('-c', code, __cwd=tmp) | |
| hint = f'Although a module with this name was found for a different Python version ({incompatible_module}).' | |
| self.assertIn(hint, stderr.decode()) | |
| class TestColorizedTraceback(unittest.TestCase): | |
| maxDiff = None | |
| def test_colorized_traceback(self): | |
| def foo(*args): | |
| x = {'a':{'b': None}} | |
| y = x['a']['b']['c'] | |
| def baz2(*args): | |
| return (lambda *args: foo(*args))(1,2,3,4) | |
| def baz1(*args): | |
| return baz2(1,2,3,4) | |
| def bar(): | |
| return baz1(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 = colors["e"] | |
| boldr = colors["E"] | |
| reset = colors["z"] | |
| self.assertIn("y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, lines) | |
| self.assertIn("return " + red + "(lambda *args: foo(*args))" + reset + boldr + "(1,2,3,4)" + reset, lines) | |
| self.assertIn("return (lambda *args: " + red + "foo" + reset + boldr + "(*args)" + reset + ")(1,2,3,4)", lines) | |
| self.assertIn("return baz2(1,2,3,4)", lines) | |
| self.assertIn("return baz1(1,\n 2,3\n ,4)", lines) | |
| self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines) | |
| def test_colorized_exception_notes(self): | |
| def foo(): | |
| raise ValueError() | |
| try: | |
| foo() | |
| except Exception as e: | |
| e.add_note("First note") | |
| e.add_note("Second note") | |
| exc = traceback.TracebackException.from_exception(e) | |
| lines = "".join(exc.format(colorize=True)) | |
| note = colors["n"] | |
| reset = colors["z"] | |
| self.assertIn(note + "First note" + reset, lines) | |
| self.assertIn(note + "Second note" + reset, lines) | |
| def test_colorized_syntax_error(self): | |
| try: | |
| compile("a $ b", "<string>", "exec") | |
| except SyntaxError as e: | |
| exc = traceback.TracebackException.from_exception( | |
| e, capture_locals=True | |
| ) | |
| actual = "".join(exc.format(colorize=True)) | |
| def expected(t, m, fn, l, f, E, e, z, n): | |
| return "".join( | |
| [ | |
| f' File {fn}"<string>"{z}, line {l}1{z}\n', | |
| f' a {E}${z} b\n', | |
| f' {E}^{z}\n', | |
| f'{t}SyntaxError{z}: {m}invalid syntax{z}\n' | |
| ] | |
| ) | |
| self.assertIn(expected(**colors), 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() | |
| lno_foo = foo.__code__.co_firstlineno | |
| def expected(t, m, fn, l, f, E, e, z, n): | |
| return [ | |
| 'Traceback (most recent call last):', | |
| f' File {fn}"{__file__}"{z}, ' | |
| f'line {l}{lno_foo+5}{z}, in {f}test_colorized_traceback_is_the_default{z}', | |
| f' {e}foo{z}{E}(){z}', | |
| f' {e}~~~{z}{E}^^{z}', | |
| f' File {fn}"{__file__}"{z}, ' | |
| f'line {l}{lno_foo+1}{z}, in {f}foo{z}', | |
| f' {e}1{z}{E}/{z}{e}0{z}', | |
| f' {e}~{z}{E}^{z}{e}~{z}', | |
| f'{t}ZeroDivisionError{z}: {m}division by zero{z}', | |
| ] | |
| self.assertEqual(actual, expected(**colors)) | |
| def test_colorized_traceback_from_exception_group(self): | |
| def foo(): | |
| exceptions = [] | |
| try: | |
| 1 / 0 | |
| except ZeroDivisionError as inner_exc: | |
| exceptions.append(inner_exc) | |
| raise ExceptionGroup("test", exceptions) | |
| try: | |
| foo() | |
| except Exception as e: | |
| exc = traceback.TracebackException.from_exception( | |
| e, capture_locals=True | |
| ) | |
| lno_foo = foo.__code__.co_firstlineno | |
| actual = "".join(exc.format(colorize=True)).splitlines() | |
| def expected(t, m, fn, l, f, E, e, z, n): | |
| return [ | |
| f" + Exception Group Traceback (most recent call last):", | |
| f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+9}{z}, in {f}test_colorized_traceback_from_exception_group{z}', | |
| f' | {e}foo{z}{E}(){z}', | |
| f' | {e}~~~{z}{E}^^{z}', | |
| f" | e = ExceptionGroup('test', [ZeroDivisionError('division by zero')])", | |
| f" | foo = {foo}", | |
| f' | self = <{__name__}.TestColorizedTraceback testMethod=test_colorized_traceback_from_exception_group>', | |
| f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+6}{z}, in {f}foo{z}', | |
| f' | raise ExceptionGroup("test", exceptions)', | |
| f" | exceptions = [ZeroDivisionError('division by zero')]", | |
| f' | {t}ExceptionGroup{z}: {m}test (1 sub-exception){z}', | |
| f' +-+---------------- 1 ----------------', | |
| f' | Traceback (most recent call last):', | |
| f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+3}{z}, in {f}foo{z}', | |
| f' | {e}1 {z}{E}/{z}{e} 0{z}', | |
| f' | {e}~~{z}{E}^{z}{e}~~{z}', | |
| f" | exceptions = [ZeroDivisionError('division by zero')]", | |
| f' | {t}ZeroDivisionError{z}: {m}division by zero{z}', | |
| f' +------------------------------------', | |
| ] | |
| self.assertEqual(actual, expected(**colors)) | |
| def test_colorized_traceback_unicode(self): | |
| try: | |
| 啊哈=1; 啊哈/0#### | |
| except Exception as e: | |
| exc = traceback.TracebackException.from_exception(e) | |
| actual = "".join(exc.format(colorize=True)).splitlines() | |
| def expected(t, m, fn, l, f, E, e, z, n): | |
| return [ | |
| f" 啊哈=1; {e}啊哈{z}{E}/{z}{e}0{z}####", | |
| f" {e}~~~~{z}{E}^{z}{e}~{z}", | |
| ] | |
| self.assertEqual(actual[2:4], expected(**colors)) | |
| try: | |
| ééééé/0 | |
| except Exception as e: | |
| exc = traceback.TracebackException.from_exception(e) | |
| actual = "".join(exc.format(colorize=True)).splitlines() | |
| def expected(t, m, fn, l, f, E, e, z, n): | |
| return [ | |
| f" {E}ééééé{z}/0", | |
| f" {E}^^^^^{z}", | |
| ] | |
| self.assertEqual(actual[2:4], expected(**colors)) | |
| def test_colorized_syntax_error_ascii_display_width(self): | |
| """Caret alignment for ASCII edge cases handled by _wlen. | |
| The old ASCII fast track in _display_width returned the raw character | |
| offset for ASCII strings, which is wrong for CTRL-Z (display width 2) | |
| and ANSI escape sequences (display width 0). | |
| """ | |
| E = colors["E"] | |
| z = colors["z"] | |
| t = colors["t"] | |
| m = colors["m"] | |
| fn = colors["fn"] | |
| l = colors["l"] | |
| def _make_syntax_error(text, offset, end_offset): | |
| err = SyntaxError("invalid syntax") | |
| err.filename = "<string>" | |
| err.lineno = 1 | |
| err.end_lineno = 1 | |
| err.text = text | |
| err.offset = offset | |
| err.end_offset = end_offset | |
| return err | |
| # CTRL-Z (\x1a) is ASCII but displayed as ^Z (2 columns). | |
| # Verify caret aligns when CTRL-Z precedes the error. | |
| err = _make_syntax_error("a\x1a$\n", offset=3, end_offset=4) | |
| exc = traceback.TracebackException.from_exception(err) | |
| actual = "".join(exc.format(colorize=True)) | |
| # 'a' (1 col) + '\x1a' (2 cols) = 3 cols before '$' | |
| self.assertIn( | |
| f' File {fn}"<string>"{z}, line {l}1{z}\n' | |
| f' a\x1a{E}${z}\n' | |
| f' {" " * 3}{E}^{z}\n' | |
| f'{t}SyntaxError{z}: {m}invalid syntax{z}\n', | |
| actual, | |
| ) | |
| # CTRL-Z in the highlighted (error) region counts as 2 columns. | |
| err = _make_syntax_error("$\x1a\n", offset=1, end_offset=3) | |
| exc = traceback.TracebackException.from_exception(err) | |
| actual = "".join(exc.format(colorize=True)) | |
| # '$' (1 col) + '\x1a' (2 cols) = 3 columns of carets | |
| self.assertIn( | |
| f' {E}$\x1a{z}\n' | |
| f' {E}{"^" * 3}{z}\n', | |
| actual, | |
| ) | |
| # ANSI escape sequences are ASCII but take 0 display columns. | |
| err = _make_syntax_error("a\x1b[1mb$\n", offset=7, end_offset=8) | |
| exc = traceback.TracebackException.from_exception(err) | |
| actual = "".join(exc.format(colorize=True)) | |
| # 'a' (1 col) + '\x1b[1m' (0 cols) + 'b' (1 col) = 2 before '$' | |
| self.assertIn( | |
| f' a\x1b[1mb{E}${z}\n' | |
| f' {" " * 2}{E}^{z}\n', | |
| actual, | |
| ) | |
| class TestLazyImportSuggestions(unittest.TestCase): | |
| """Test that lazy imports are not reified when computing AttributeError suggestions.""" | |
| def test_attribute_error_does_not_reify_lazy_imports(self): | |
| """Printing an AttributeError should not trigger lazy import reification.""" | |
| # lazypkg.bar prints "BAR_MODULE_LOADED" when imported. | |
| # If lazy import is reified during suggestion computation, we'll see it. | |
| code = textwrap.dedent(""" | |
| lazy import test.test_lazy_import.data.lazypkg | |
| test.test_lazy_import.data.lazypkg.nonexistent | |
| """) | |
| rc, stdout, stderr = assert_python_failure('-c', code) | |
| self.assertNotIn(b"BAR_MODULE_LOADED", stdout) | |
| def test_traceback_formatting_does_not_reify_lazy_imports(self): | |
| """Formatting a traceback should not trigger lazy import reification.""" | |
| code = textwrap.dedent(""" | |
| import traceback | |
| lazy import test.test_lazy_import.data.lazypkg | |
| try: | |
| test.test_lazy_import.data.lazypkg.nonexistent | |
| except AttributeError: | |
| traceback.format_exc() | |
| print("OK") | |
| """) | |
| rc, stdout, stderr = assert_python_ok('-c', code) | |
| self.assertIn(b"OK", stdout) | |
| self.assertNotIn(b"BAR_MODULE_LOADED", stdout) | |
| def test_suggestion_still_works_for_non_lazy_attributes(self): | |
| """Suggestions should still work for non-lazy module attributes.""" | |
| code = textwrap.dedent(""" | |
| lazy import test.test_lazy_import.data.lazypkg | |
| # Typo for __name__ | |
| test.test_lazy_import.data.lazypkg.__nme__ | |
| """) | |
| rc, stdout, stderr = assert_python_failure('-c', code) | |
| self.assertIn(b"__name__", stderr) | |
| self.assertNotIn(b"BAR_MODULE_LOADED", stdout) | |
| class LazyImportTest(unittest.TestCase): | |
| @support.cpython_only | |
| def test_lazy_import(self): | |
| ensure_lazy_imports("traceback", {"_colorize"}) | |
| if __name__ == "__main__": | |
| unittest.main() |