|
1 | 1 | import inspect
|
2 | 2 | import re
|
| 3 | +import sys |
| 4 | +import traceback |
3 | 5 | import types
|
| 6 | +import warnings |
| 7 | +from functools import wraps |
4 | 8 | from typing import List, Optional
|
5 | 9 |
|
6 | 10 | 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 | +) |
7 | 16 |
|
8 | 17 | from ..check import Check
|
9 | 18 |
|
@@ -127,3 +136,104 @@ def get_code(func: types.FunctionType) -> str:
|
127 | 136 | )
|
128 | 137 |
|
129 | 138 | 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 |
0 commit comments