Skip to content

Commit eda43ef

Browse files
committed
Fix for nested errors are not correctly resolved
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. Will be fixed in wci osscar-org/widget-code-input#26
1 parent 1d182b9 commit eda43ef

File tree

1 file changed

+101
-0
lines changed

1 file changed

+101
-0
lines changed

src/scwidgets/code/_widget_code_input.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
import re
33
import types
44
from typing import List, Optional
5+
from functools import wraps
6+
import sys
7+
import traceback
8+
import warnings
59

610
from widget_code_input import WidgetCodeInput
11+
from widget_code_input.utils import CodeValidationError, is_valid_variable_name
712

813
from ..check import Check
914

@@ -127,3 +132,99 @@ def get_code(func: types.FunctionType) -> str:
127132
)
128133

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

0 commit comments

Comments
 (0)