diff --git a/bt5/erp5_data_notebook/ExtensionTemplateItem/portal_components/extension.erp5.JupyterCompile.py b/bt5/erp5_data_notebook/ExtensionTemplateItem/portal_components/extension.erp5.JupyterCompile.py index 96d12aef5b2e06daed8dc6ba2da6d9ac1ad5c98f..e49fc9223949eadf783c057ad043591a9b2ddbc6 100644 --- a/bt5/erp5_data_notebook/ExtensionTemplateItem/portal_components/extension.erp5.JupyterCompile.py +++ b/bt5/erp5_data_notebook/ExtensionTemplateItem/portal_components/extension.erp5.JupyterCompile.py @@ -195,7 +195,16 @@ def Base_runJupyterCode(self, jupyter_code, old_notebook_context): import_fixer = ImportFixer() environment_collector = EnvironmentParser() ast_node = import_fixer.visit(ast_node) - ast_node = environment_collector.visit(ast_node) + + # The collector also raises errors when environment.define and undefine + # calls are made incorrectly, so we need to capture them to propagate + # to Jupyter for rendering. + try: + ast_node = environment_collector.visit(ast_node) + except (EnvironmentDefinitionError, EnvironmentUndefineError) as e: + transaction.abort() + return getErrorMessageForException(self, e, notebook_context) + # Get the node list from the parsed tree nodelist = ast_node.body @@ -277,7 +286,17 @@ def Base_runJupyterCode(self, jupyter_code, old_notebook_context): break if not found: transaction.abort() - raise Exception("Trying to remove non existing function/variable from environment: '%s'\nEnvironment: %s" % (func_alias, str(notebook_context['setup']))) + result = { + 'result_string': "EnvironmentUndefineError: Trying to remove non existing function/variable from environment: '%s'\n" % func_alias, + 'notebook_context': notebook_context, + 'status': 'ok', + 'mime_type': 'text/plain', + 'evalue': None, + 'ename': None, + 'traceback': None, + } + return result + # Removing all the setup functions if user call environment.clearAll() if environment_collector.clearAll(): @@ -402,6 +421,14 @@ def Base_runJupyterCode(self, jupyter_code, old_notebook_context): return result +class EnvironmentUndefineError(TypeError): + pass + + +class EnvironmentDefinitionError(TypeError): + pass + + def canSerialize(obj): result = False @@ -429,7 +456,7 @@ def canSerialize(obj): try: writer.serialize(obj) # Because writer.serialize(obj) relies on the implementation of __getstate__ - # of obj, all errors can happen, so the "except all" is necessary here. + # of obj, all errors can happen, so the "except all" is necessary here. except: return False return True @@ -503,11 +530,20 @@ class EnvironmentParser(ast.NodeTransformer): name = attribute.id if name == 'environment' and function.attr == 'define' and not value.keywords: if not len(value.args) == 2: - message = ( - 'Not enough arguments for environment definition. Function ' - 'name and alias are required.' - ) - raise Exception(message) + raise EnvironmentDefinitionError('environment.define calls receive 2 arguments') + + self._ensureType( + obj=value.args[0], + klass=ast.Name, + error_message='Type mismatch. environment.define receives a function as first argument.' + ) + + self._ensureType( + obj=value.args[1], + klass=ast.Str, + error_message='Type mismatch. environment.define receives a string as second argument.' + ) + func_name = value.args[0].id func_alias = value.args[1].s function_node = self.function_dict[func_name] @@ -532,6 +568,13 @@ class EnvironmentParser(ast.NodeTransformer): arg_value = node_value_dict[type(arg_value_node)](arg_value_node) self.environment_var_dict[arg_name] = arg_value elif name == 'environment' and function.attr == 'undefine': + self._ensureType( + obj=value.args[0], + klass=ast.Str, + call_type='undefine', + error_message='Type mismatch. environment.undefine receives only a string as argument.' + ) + func_alias = value.args[0].s self.environment_remove_list.append(func_alias) elif name == 'environment' and function.attr == 'clearAll': @@ -540,6 +583,14 @@ class EnvironmentParser(ast.NodeTransformer): self.show_environment_setup = True return node + def _ensureType(self, obj=None, klass=None, error_message=None, call_type='define'): + if not isinstance(obj, klass): + if call_type == 'define': + error_class = EnvironmentDefinitionError + elif call_type == 'undefine': + error_class = EnvironmentUndefineError + raise error_class(error_message) + def clearAll(self): return self.environment_clear_all @@ -912,4 +963,3 @@ def erp5PivotTableUI(self, df): iframe_host = self.REQUEST['HTTP_X_FORWARDED_HOST'].split(',')[0] url = "https://%s/erp5/Base_displayPivotTableFrame?key=%s" % (iframe_host, key) return IFrame(src=url, width='100%', height='500') - diff --git a/bt5/erp5_data_notebook/TestTemplateItem/portal_components/test.erp5.testExecuteJupyter.py b/bt5/erp5_data_notebook/TestTemplateItem/portal_components/test.erp5.testExecuteJupyter.py index 62a89e23d1717b8d178b397bc23bcc49897851a8..a97ccf2153dc2d543417b9023c31358878e57661 100644 --- a/bt5/erp5_data_notebook/TestTemplateItem/portal_components/test.erp5.testExecuteJupyter.py +++ b/bt5/erp5_data_notebook/TestTemplateItem/portal_components/test.erp5.testExecuteJupyter.py @@ -614,6 +614,69 @@ print random.randint(1,1) self.assertEquals(result['status'], 'ok') self.assertEquals(result['code_result'].strip(), '1') + def testEnvorinmentUndefineErrors(self): + """ + Tests if environment.undefine wrong usage errors are correctly captured + and rendered in Jupyter. + """ + self.login('dev_user') + undefine_not_found = 'environment.undefine("foobar")' + + reference = 'Test.Notebook.EnvironmentObject.Errors.Undefine' + result = self.portal.Base_executeJupyter( + reference=reference, + python_expression=undefine_not_found + ) + self.tic() + + error_substring = 'EnvironmentUndefineError: Trying to remove non existing' + self.assertTrue(error_substring in result) + + not_string_code = 'def foobar(): pass\nenvironment.undefine(foobar)' + + reference = 'Test.Notebook.EnvironmentObject.Errors.Undefine' + result = self.portal.Base_executeJupyter( + reference=reference, + python_expression=not_string_code + ) + self.tic() + + error_substring = 'EnvironmentUndefineError: Type mismatch.' + self.assertTrue(error_substring in result) + + def testEnvironmentDefineErrrors(self): + """ + Tests if environment.define wrong usage errors are correctly captured + and rendered in Jupyter. + """ + self.login('dev_user') + + first_arg_type_code = "environment.define('foobar', 'foobar')" + + reference = 'Test.Notebook.EnvironmentObject.Errors.Define' + result = self.portal.Base_executeJupyter( + reference=reference, + python_expression=first_arg_type_code + ) + self.tic() + + error_substring = 'EnvironmentDefinitionError: Type mismatch' + self.assertTrue(error_substring in result) + self.assertTrue('first argument' in result) + + second_arg_type_code = 'def couscous(): pass\nenvironment.define(couscous, 123)' + + reference = 'Test.Notebook.EnvironmentObject.Errors.Define' + result = self.portal.Base_executeJupyter( + reference=reference, + python_expression=second_arg_type_code + ) + self.tic() + + error_substring = 'EnvironmentDefinitionError: Type mismatch' + self.assertTrue(error_substring in result) + self.assertTrue('second argument' in result) + def testPivotTableJsIntegration(self): ''' This test ensures the PivotTableJs user interface is correctly integrated diff --git a/bt5/erp5_data_notebook/TestTemplateItem/portal_components/test.erp5.testExecuteJupyter.xml b/bt5/erp5_data_notebook/TestTemplateItem/portal_components/test.erp5.testExecuteJupyter.xml index 0ee0ffb3cb46d9d4d38509db33c49f74dab2b563..da6b536c2413c01ff648198f598d59d563591d52 100644 --- a/bt5/erp5_data_notebook/TestTemplateItem/portal_components/test.erp5.testExecuteJupyter.xml +++ b/bt5/erp5_data_notebook/TestTemplateItem/portal_components/test.erp5.testExecuteJupyter.xml @@ -44,7 +44,7 @@ <key> <string>text_content_warning_message</string> </key> <value> <tuple> - <string>W:457, 4: Unused variable \'notebook\' (unused-variable)</string> + <string>W:697, 4: Unused variable \'notebook\' (unused-variable)</string> </tuple> </value> </item>