Skip to content

Commit

Permalink
Add support for fine-grained error location lines in Python tracebacks
Browse files Browse the repository at this point in the history
See: PEP 657

Python 3.11 makes it easier to identify where on the line a problem
occurred. This is achieved with the error location lines.
We need to skip such lines when parsing the traceback, otherwise
we won't identify the exception name correctly (See: rhbz#2137473).

Example:

Traceback (most recent call last):
  File "test.py", line 2, in <module>
    x['a']['b']['c']['d'] = 1
    ~~~~~~~~~~~^^^^^
TypeError: 'NoneType' object is not subscriptable

Signed-off-by: Michal Srb <[email protected]>
  • Loading branch information
msrb authored and mgrabovsky committed Oct 31, 2022
1 parent 7e20e30 commit 8143690
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 21 deletions.
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
unreleased
==========
Add support for fine-grained error location lines in Python tracebacks

0.39
==========
Expand Down
33 changes: 33 additions & 0 deletions lib/python_frame.c
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,39 @@ sr_python_frame_parse(const char **input,
{
const char *local_input = *input;

/*
* Skip fine-grained error location lines (See: PEP 657),
* and syntax error lines. Such lines only contain '^' and '~'
* characters.
*
* Example:
*
* Traceback (most recent call last):
* File "test.py", line 2, in <module>
* x['a']['b']['c']['d'] = 1
* ~~~~~~~~~~~^^^^^
* TypeError: 'NoneType' object is not subscriptable
*/
bool is_error_location_line = true;
const char *tmp_input = local_input;
while (*tmp_input != '\n' && *tmp_input != '\0')
{
if (*tmp_input != ' ' && *tmp_input != '^' && *tmp_input != '~')
{
is_error_location_line = false;
break;
}
++tmp_input;
}

if (is_error_location_line)
{
/* Skip the error location line */
sr_skip_char_cspan(&local_input, "\n");
++local_input;
*input = local_input;
}

if (0 == sr_skip_string(&local_input, " File \""))
{
location->message = g_strdup_printf("Frame header not found.");
Expand Down
21 changes: 0 additions & 21 deletions lib/python_stacktrace.c
Original file line number Diff line number Diff line change
Expand Up @@ -215,27 +215,6 @@ sr_python_stacktrace_parse(const char **input,
return NULL;
}

bool invalid_syntax_pointer = true;
const char *tmp_input = local_input;
while (*tmp_input != '\n' && *tmp_input != '\0')
{
if (*tmp_input != ' ' && *tmp_input != '^')
{
invalid_syntax_pointer = false;
break;
}
++tmp_input;
}

if (invalid_syntax_pointer)
{
/* Skip line " ^" pointing to the invalid code */
sr_skip_char_cspan(&local_input, "\n");
++local_input;
++location->line;
location->column = 1;
}

/* Parse exception name. */
if (!sr_parse_char_cspan(&local_input, ":\n", &stacktrace->exception_name))
{
Expand Down
16 changes: 16 additions & 0 deletions tests/python/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,22 @@ def test_invalid_syntax_imported_file(self):
self.assertTrue(f.special_function)
self.assertFalse(f.special_file)

def test_fine_grained_error_location(self):
trace = load_input_contents('../python_stacktraces/python-06')
trace = satyr.PythonStacktrace(trace)

self.assertEqual(len(trace.frames), 1)
self.assertEqual(trace.exception_name, 'ZeroDivisionError')

f = trace.frames[0]
self.assertEqual(f.file_name, '/usr/bin/will_python3_raise')
self.assertEqual(f.function_name, "module")
self.assertEqual(f.file_line, 3)
self.assertEqual(f.line_contents, '0/0')
self.assertTrue(f.special_function)
self.assertFalse(f.special_file)


class TestPythonFrame(BindingsTestCase):
def setUp(self):
self.frame = satyr.PythonStacktrace(contents).frames[-1]
Expand Down
18 changes: 18 additions & 0 deletions tests/python_stacktraces/python-06
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
will_python3_raise:3:<module>:ZeroDivisionError: division by zero

Traceback (most recent call last):
File "/usr/bin/will_python3_raise", line 3, in <module>
0/0
~^~
ZeroDivisionError: division by zero

Local variables in innermost frame:
__name__: '__main__'
__doc__: None
__package__: None
__loader__: <_frozen_importlib_external.SourceFileLoader object at 0x7fd295c62c50>
__spec__: None
__annotations__: {}
__builtins__: <module 'builtins' (built-in)>
__file__: '/usr/bin/will_python3_raise'
__cached__: None

0 comments on commit 8143690

Please sign in to comment.