Jupyter Kernel: added automatic error rendering
@kirr, @Tyagov and @tatuya, please review. Now the ERP5 Jupyter kernel automatically renders errors that happens in the user-side code. Errors are captured during the AST tree creation (to be able to detect syntax errors) and at execution time. The current transaction is automatically aborted on error detection. /reviewed-on nexedi/erp5!85
Showing
... | ... | @@ -8,6 +8,9 @@ import sys |
import ast | ||
import types | ||
import inspect | ||
import traceback | ||
import transaction | ||
mime_type = 'text/plain' | ||
# IPython expects 2 status message - 'ok', 'error' | ||
... | ... | @@ -79,7 +82,13 @@ def Base_compileJupyterCode(self, jupyter_code, old_local_variable_dict): |
if jupyter_code: | ||
# Create ast parse tree | ||
ast_node = ast.parse(jupyter_code) | ||
try: | ||
ast_node = ast.parse(jupyter_code) | ||
except Exception as e: | ||
# It's not necessary to abort the current transaction here 'cause the | ||
# user's code wasn't executed at all yet. | ||
return getErrorMessageForException(self, e, local_variable_dict) | ||
# Get the node list from the parsed tree | ||
nodelist = ast_node.body | ||
... | ... | @@ -116,13 +125,35 @@ def Base_compileJupyterCode(self, jupyter_code, old_local_variable_dict): |
for node in to_run_exec: | ||
mod = ast.Module([node]) | ||
code = compile(mod, '<string>', "exec") | ||
exec(code, g, g) | ||
try: | ||
exec(code, g, g) | ||
except Exception as e: | ||
# Abort the current transaction. As a consequence, the notebook lines | ||
# are not added if an exception occurs. | ||
# | ||
# TODO: store which notebook line generated which exception. | ||
|
||
# | ||
transaction.abort() | ||
# Clear the portal cache from previous transaction | ||
self.getPortalObject().portal_caches.clearAllCache() | ||
return getErrorMessageForException(self, e, local_variable_dict) | ||
# Execute the interactive nodes with 'single' mode | ||
for node in to_run_interactive: | ||
mod = ast.Interactive([node]) | ||
code = compile(mod, '<string>', "single") | ||
exec(code, g, g) | ||
try: | ||
exec(code, g, g) | ||
except Exception as e: | ||
# Abort the current transaction. As a consequence, the notebook lines | ||
# are not added if an exception occurs. | ||
# | ||
# TODO: store which notebook line generated which exception. | ||
# | ||
transaction.abort() | ||
# Clear the portal cache from previous transaction | ||
self.getPortalObject().portal_caches.clearAllCache() | ||
return getErrorMessageForException(self, e, local_variable_dict) | ||
# Letting the code fail in case of error while executing the python script/code | ||
# XXX: Need to be refactored so to acclimitize transactions failure as well as | ||
... | ... | @@ -168,6 +199,24 @@ def Base_compileJupyterCode(self, jupyter_code, old_local_variable_dict): |
return result | ||
def getErrorMessageForException(self, exception, local_variable_dict): | ||
''' | ||
getErrorMessageForException receives an Expcetion object and a context for | ||
code execution (local_variable_dict) and will return a dict as Jupyter | ||
requires for error rendering. | ||
''' | ||
etype, value, tb = sys.exc_info() | ||
traceback_text = traceback.format_exc().split('\n')[:-1] | ||
return { | ||
'status': 'error', | ||
'result_string': None, | ||
'local_variable_dict': local_variable_dict, | ||
'mime_type': 'text/plain', | ||
'evalue': str(value), | ||
'ename': exception.__class__.__name__, | ||
'traceback': traceback_text | ||
} | ||
def AddNewLocalVariableDict(self): | ||
""" | ||
Function to add a new Local Variable for a Data Notebook | ||
... | ... | @@ -323,4 +372,4 @@ def getError(self, previous=1): |
evalue = unicode(error['value']) | ||
tb_list = [l+'\n' for l in error['tb_text'].split('\n')] | ||
return None | ||
return None | ||
\ No newline at end of file |