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 | ... | @@ -8,6 +8,9 @@ import sys |
import ast | import ast | ||
import types | import types | ||
import inspect | import inspect | ||
import traceback | |||
import transaction | |||
mime_type = 'text/plain' | mime_type = 'text/plain' | ||
# IPython expects 2 status message - 'ok', 'error' | # IPython expects 2 status message - 'ok', 'error' | ||
... | @@ -79,7 +82,13 @@ def Base_compileJupyterCode(self, jupyter_code, old_local_variable_dict): | ... | @@ -79,7 +82,13 @@ def Base_compileJupyterCode(self, jupyter_code, old_local_variable_dict): |
if jupyter_code: | if jupyter_code: | ||
# Create ast parse tree | # Create ast parse tree | ||
try: | |||
ast_node = ast.parse(jupyter_code) | 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 | # Get the node list from the parsed tree | ||
nodelist = ast_node.body | nodelist = ast_node.body | ||
... | @@ -116,13 +125,35 @@ def Base_compileJupyterCode(self, jupyter_code, old_local_variable_dict): | ... | @@ -116,13 +125,35 @@ def Base_compileJupyterCode(self, jupyter_code, old_local_variable_dict): |
for node in to_run_exec: | for node in to_run_exec: | ||
mod = ast.Module([node]) | mod = ast.Module([node]) | ||
code = compile(mod, '<string>', "exec") | code = compile(mod, '<string>', "exec") | ||
try: | |||
exec(code, g, g) | 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 | # Execute the interactive nodes with 'single' mode | ||
for node in to_run_interactive: | for node in to_run_interactive: | ||
mod = ast.Interactive([node]) | mod = ast.Interactive([node]) | ||
code = compile(mod, '<string>', "single") | code = compile(mod, '<string>', "single") | ||
try: | |||
exec(code, g, g) | 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 | # 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 | # 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): | ... | @@ -168,6 +199,24 @@ def Base_compileJupyterCode(self, jupyter_code, old_local_variable_dict): |
return result | 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): | def AddNewLocalVariableDict(self): | ||
""" | """ | ||
Function to add a new Local Variable for a Data Notebook | Function to add a new Local Variable for a Data Notebook | ||
... | ... |