Commit 7310ae85 authored by Douglas's avatar Douglas

Jupyter Kernel: added automatic error rendering

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, thus notebook lines are not added.

TODO: on error detection store somehow what line caused the error and
information about the error itself.
parent d0dfdeab
...@@ -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
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 # 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")
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 # 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")
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 # 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
...@@ -323,4 +372,4 @@ def getError(self, previous=1): ...@@ -323,4 +372,4 @@ def getError(self, previous=1):
evalue = unicode(error['value']) evalue = unicode(error['value'])
tb_list = [l+'\n' for l in error['tb_text'].split('\n')] tb_list = [l+'\n' for l in error['tb_text'].split('\n')]
return None return None
\ No newline at end of file
...@@ -46,12 +46,14 @@ ...@@ -46,12 +46,14 @@
<key> <string>text_content_warning_message</string> </key> <key> <string>text_content_warning_message</string> </key>
<value> <value>
<tuple> <tuple>
<string>W: 55, 2: Using the global statement (global-statement)</string> <string>W: 58, 2: Using the global statement (global-statement)</string>
<string>W: 95, 8: Use of exec (exec-used)</string> <string>W:104, 8: Use of exec (exec-used)</string>
<string>W:119, 8: Use of exec (exec-used)</string> <string>W:129, 10: Use of exec (exec-used)</string>
<string>W:125, 8: Use of exec (exec-used)</string> <string>W:146, 10: Use of exec (exec-used)</string>
<string>W:220, 4: Using the global statement (global-statement)</string> <string>W:208, 2: Unused variable \'etype\' (unused-variable)</string>
<string>W:320, 2: Using the global statement (global-statement)</string> <string>W:208, 16: Unused variable \'tb\' (unused-variable)</string>
<string>W:269, 4: Using the global statement (global-statement)</string>
<string>W:369, 2: Using the global statement (global-statement)</string>
</tuple> </tuple>
</value> </value>
</item> </item>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>renderAsHtml</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>JupyterCompile</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Base_renderAsHtml</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -32,7 +32,7 @@ from Products.ERP5Type.tests.utils import createZODBPythonScript, removeZODBPyth ...@@ -32,7 +32,7 @@ from Products.ERP5Type.tests.utils import createZODBPythonScript, removeZODBPyth
import time import time
import json import json
import base64 import base64
import transaction
class TestExecuteJupyter(ERP5TypeTestCase): class TestExecuteJupyter(ERP5TypeTestCase):
...@@ -100,21 +100,25 @@ print an_undefined_variable ...@@ -100,21 +100,25 @@ print an_undefined_variable
portal = context.getPortalObject() portal = context.getPortalObject()
portal.%s() portal.%s()
"""%script_id """%script_id
# Make call to Base_runJupyter to run the jupyter code which is making # Make call to Base_runJupyter to run the jupyter code which is making
# a call to the newly created ZODB python_script and assert if the call raises # a call to the newly created ZODB python_script and assert if the call
# NameError as we are sending an invalid python_code to it # processes correctly the NameError as we are sending an invalid
self.assertRaises( # python_code to it.
NameError, #
portal.Base_runJupyter, result = portal.Base_runJupyter(
jupyter_code=jupyter_code, jupyter_code=jupyter_code,
old_local_variable_dict=portal.Base_addLocalVariableDict() old_local_variable_dict=portal.Base_addLocalVariableDict()
) )
# Abort the current transaction of test so that we can proceed to new one
transaction.abort() self.assertEquals(result['ename'], 'NameError')
# Clear the portal cache from previous transaction self.assertEquals(result['result_string'], None)
self.portal.portal_caches.clearAllCache()
# Remove the ZODB python script created above # There's no need to abort the current transaction. The error handling code
# should be responsible for this, so we check the script's title
script_title = script_container.JupyterCompile_errorResult.getTitle()
self.assertNotEqual(script_title, new_test_title)
removeZODBPythonScript(script_container, script_id) removeZODBPythonScript(script_container, script_id)
# Test that calling Base_runJupyter shouldn't change the context Title # Test that calling Base_runJupyter shouldn't change the context Title
...@@ -248,13 +252,14 @@ portal.%s() ...@@ -248,13 +252,14 @@ portal.%s()
reference = 'Test.Notebook.ExecuteJupyterErrorHandling %s' % time.time() reference = 'Test.Notebook.ExecuteJupyterErrorHandling %s' % time.time()
title = 'Test NB Title %s' % time.time() title = 'Test NB Title %s' % time.time()
self.assertRaises( result = json.loads(portal.Base_executeJupyter(
NameError, title=title,
portal.Base_executeJupyter, reference=reference,
title=title, python_expression=python_expression
reference=reference, ))
python_expression=python_expression
) self.assertEquals(result['ename'], 'NameError')
self.assertEquals(result['code_result'], None)
def testBaseExecuteJupyterSaveActiveResult(self): def testBaseExecuteJupyterSaveActiveResult(self):
""" """
...@@ -429,4 +434,4 @@ context.Base_displayImage(image_object=image) ...@@ -429,4 +434,4 @@ context.Base_displayImage(image_object=image)
reference=reference, reference=reference,
python_expression=jupyter_code2 python_expression=jupyter_code2
) )
self.assertEquals(json.loads(result)['code_result'].rstrip(), 'sys') self.assertEquals(json.loads(result)['code_result'].rstrip(), 'sys')
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment