Skip to content

Commit 3e7c112

Browse files
authored
Fix for nested errors are not correctly resolved (#62)
When an exception is thrown within a WCI, the last frame is used as context to add the input lines. This does not work if the exception is thrown within a function within WCI. Therefore we now iterate through all traceback frames to find the one corresponding to WCI. The corresponding PR in WCI osscar-org/widget-code-input#26 to solve it there.
1 parent d5994ff commit 3e7c112

File tree

2 files changed

+111
-1
lines changed

2 files changed

+111
-1
lines changed

src/scwidgets/code/_widget_code_input.py

+110
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import inspect
22
import re
3+
import sys
4+
import traceback
35
import types
6+
import warnings
7+
from functools import wraps
48
from typing import List, Optional
59

610
from widget_code_input import WidgetCodeInput
11+
from widget_code_input.utils import (
12+
CodeValidationError,
13+
format_syntax_error_msg,
14+
is_valid_variable_name,
15+
)
716

817
from ..check import Check
918

@@ -127,3 +136,104 @@ def get_code(func: types.FunctionType) -> str:
127136
)
128137

129138
return source
139+
140+
def get_function_object(self):
141+
"""
142+
Return the compiled function object.
143+
144+
This can be assigned to a variable and then called, for instance::
145+
146+
func = widget.get_function_object() # This can raise a SyntaxError
147+
retval = func(parameters)
148+
149+
:raise SyntaxError: if the function code has syntax errors (or if
150+
the function name is not a valid identifier)
151+
"""
152+
globals_dict = {
153+
"__builtins__": globals()["__builtins__"],
154+
"__name__": "__main__",
155+
"__doc__": None,
156+
"__package__": None,
157+
}
158+
159+
if not is_valid_variable_name(self.function_name):
160+
raise SyntaxError("Invalid function name '{}'".format(self.function_name))
161+
162+
# Optionally one could do a ast.parse here already, to check syntax
163+
# before execution
164+
try:
165+
exec(
166+
compile(self.full_function_code, __name__, "exec", dont_inherit=True),
167+
globals_dict,
168+
)
169+
except SyntaxError as exc:
170+
raise CodeValidationError(
171+
format_syntax_error_msg(exc), orig_exc=exc
172+
) from exc
173+
174+
function_object = globals_dict[self.function_name]
175+
176+
def catch_exceptions(func):
177+
@wraps(func)
178+
def wrapper(*args, **kwargs):
179+
"""Wrap and check exceptions to return a longer and clearer
180+
exception."""
181+
182+
try:
183+
return func(*args, **kwargs)
184+
except Exception as exc:
185+
err_msg = format_generic_error_msg(exc, code_widget=self)
186+
raise CodeValidationError(err_msg, orig_exc=exc) from exc
187+
188+
return wrapper
189+
190+
return catch_exceptions(function_object)
191+
192+
193+
# Temporary fix until https://github.com/osscar-org/widget-code-input/pull/26
194+
# is merged
195+
def format_generic_error_msg(exc, code_widget):
196+
"""
197+
Return a string reproducing the traceback of a typical error.
198+
This includes line numbers, as well as neighboring lines.
199+
200+
It will require also the code_widget instance, to get the actual source code.
201+
202+
:note: this must be called from withou the exception, as it will get the
203+
current traceback state.
204+
205+
:param exc: The exception that is being processed.
206+
:param code_widget: the instance of the code widget with the code that
207+
raised the exception.
208+
"""
209+
error_class, _, tb = sys.exc_info()
210+
frame_summaries = traceback.extract_tb(tb)
211+
# The correct frame summary corresponding to widget_code_intput is not
212+
# always at the end therefore we loop through all of them
213+
wci_frame_summary = None
214+
for frame_summary in frame_summaries:
215+
if frame_summary.filename == "widget_code_input":
216+
wci_frame_summary = frame_summary
217+
if wci_frame_summary is None:
218+
warnings.warn(
219+
"Could not find traceback frame corresponding to "
220+
"widget_code_input, we output whole error message.",
221+
stacklevel=2,
222+
)
223+
224+
return exc
225+
line_number = wci_frame_summary[1]
226+
code_lines = code_widget.full_function_code.splitlines()
227+
228+
err_msg = f"{error_class.__name__} in code input: {str(exc)}\n"
229+
if line_number > 2:
230+
err_msg += f" {line_number - 2:4d} {code_lines[line_number - 3]}\n"
231+
if line_number > 1:
232+
err_msg += f" {line_number - 1:4d} {code_lines[line_number - 2]}\n"
233+
err_msg += f"---> {line_number:4d} {code_lines[line_number - 1]}\n"
234+
if line_number < len(code_lines):
235+
err_msg += f" {line_number + 1:4d} {code_lines[line_number]}\n"
236+
if line_number < len(code_lines) - 1:
237+
err_msg += f" {line_number + 2:4d} {code_lines[line_number + 1]}\n"
238+
239+
return err_msg

tests/test_code.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def test_run_code(self, code_ex):
225225
def test_erroneous_run_code(self, code_ex):
226226
with pytest.raises(
227227
CodeValidationError,
228-
match="NameError in code input: name 'bug' is not defined.*",
228+
match="name 'bug' is not defined.*",
229229
):
230230
code_ex.run_code(**code_ex.parameters)
231231

0 commit comments

Comments
 (0)