diff --git a/qt/python/mantidqt/widgets/codeeditor/execution.py b/qt/python/mantidqt/widgets/codeeditor/execution.py index 8f015af554a9bda126fb2242e94c071293098ba7..4d0b0fe39073d6178f04011484dda634fa28cd57 100644 --- a/qt/python/mantidqt/widgets/codeeditor/execution.py +++ b/qt/python/mantidqt/widgets/codeeditor/execution.py @@ -54,8 +54,10 @@ class PythonCodeExecution(object): def error_cb_wrapped(_): pass self.on_error = error_cb_wrapped + self.on_progress_update = progress_cb + def execute_async(self, code_str, user_globals, - user_locals, ): + user_locals): """ Execute the given code string on a separate thread. This function returns as soon as the new thread starts @@ -65,7 +67,6 @@ class PythonCodeExecution(object): :param user_locals: A mutable mapping type to store local variables :returns: The created async task """ - t = AsyncTask(self.execute, args=(code_str, user_globals, user_locals), success_cb=self.on_success, error_cb=self.on_error) t.start() @@ -74,12 +75,59 @@ class PythonCodeExecution(object): def execute(self, code_str, user_globals, user_locals): """Execute the given code on the calling thread - within the provided context. All exceptions are caught - and stored with the returned result + within the provided context. :param code_str: A string containing code to execute :param user_globals: A mutable mapping type to store global variables :param user_locals: A mutable mapping type to store local variables :raises: Any error that the code generates """ - exec(code_str, user_globals, user_locals) + # execute whole string if no reporting is required + if self.on_progress_update is None: + self._do_exec(code_str, user_globals, user_locals) + else: + self._execute_as_blocks(code_str, user_globals, user_locals, + self.on_progress_update) + + def _execute_as_blocks(self, code_str, user_globals, user_locals, + progress_cb): + """Execute the code in the supplied context and report the progress + using the supplied callback""" + # will raise a SyntaxError if all of the code is invalid + compile(code_str, "<string>", mode='exec') + + for block in code_blocks(code_str): + progress_cb(block.lineno) + self._do_exec(block.code_obj, user_globals, user_locals) + + def _do_exec(self, code, user_globals, user_locals): + exec (code, user_globals, user_locals) + + +class CodeBlock(object): + """Holds an executable code object. It also stores the line number + of the first line within a larger group of code blocks""" + + def __init__(self, code_obj, lineno): + self.code_obj = code_obj + self.lineno = lineno + + +def code_blocks(code_str): + """Generator to produce blocks of executable code + from the given code string. + """ + lineno = 1 + lines = code_str.splitlines() + cur_block = [] + for line in lines: + cur_block.append(line) + code_block = "\n".join(cur_block) + try: + code_obj = compile(code_block, "<string>", mode='exec') + yield CodeBlock(code_obj, lineno) + lineno += len(cur_block) + cur_block = [] + except (SyntaxError, TypeError): + # assume we don't have a full block yet + continue diff --git a/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py b/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py index df8bdcbaaf7ec88af3a7bb2a67f4dfc4883dbff6..09497f412642a85cbe96988a49f3024c642976a8 100644 --- a/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py +++ b/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py @@ -30,13 +30,28 @@ class PythonCodeExecutionTest(unittest.TestCase): error_cb_called = False task_exc = None - def on_success(self, task_result): + def on_success(self): self.success_cb_called = True def on_error(self, exc): self.error_cb_called = True self.task_exc = exc + class ReceiverWithProgress(Receiver): + + def __init__(self): + self.lines_received = [] + + def on_progess_update(self, lineno): + self.lines_received.append(lineno) + + def on_error(self, exc): + self.error_cb_called = True + self.task_exc = exc + + # --------------------------------------------------------------------------- + # Successful execution tests + # --------------------------------------------------------------------------- def test_execute_places_output_in_provided_mapping_object(self): code = "_local=100" namespace = {} @@ -60,6 +75,9 @@ class PythonCodeExecutionTest(unittest.TestCase): task = executor.execute_async(code, {}, {}) task.join() + # --------------------------------------------------------------------------- + # Error execution tests + # --------------------------------------------------------------------------- def test_execute_raises_syntax_error_on_bad_code(self): code = "if:" self._verify_failed_serial_execute(SyntaxError, code, {}, {}) @@ -80,6 +98,85 @@ class PythonCodeExecutionTest(unittest.TestCase): code = "x = _local + 1" self._verify_failed_serial_execute(NameError, code, {}, {}) + # --------------------------------------------------------------------------- + # Progress tests + # --------------------------------------------------------------------------- + def test_progress_cb_is_not_called_for_empty_string(self): + code = "" + recv = PythonCodeExecutionTest.ReceiverWithProgress() + executor = PythonCodeExecution(success_cb=recv.on_success, error_cb=recv.on_error, + progress_cb=recv.on_progess_update) + task = executor.execute_async(code, {}, {}) + task.join() + self.assertEqual(0, len(recv.lines_received)) + + def test_progress_cb_is_not_called_for_code_with_syntax_errors(self): + code = """x = 1 +y = +""" + recv = PythonCodeExecutionTest.ReceiverWithProgress() + executor = PythonCodeExecution(success_cb=recv.on_success, error_cb=recv.on_error, + progress_cb=recv.on_progess_update) + task = executor.execute_async(code, {}, {}) + task.join() + self.assertFalse(recv.success_cb_called) + self.assertTrue(recv.error_cb_called) + self.assertEqual(0, len(recv.lines_received)) + + def test_progress_cb_is_called_for_single_line(self): + code = "x = 1" + recv = PythonCodeExecutionTest.ReceiverWithProgress() + executor = PythonCodeExecution(success_cb=recv.on_success, error_cb=recv.on_error, + progress_cb=recv.on_progess_update) + task = executor.execute_async(code, {}, {}) + task.join() + if not recv.success_cb_called: + self.assertTrue(recv.error_cb_called) + self.fail("Execution failed with error:\n" + str(recv.task_exc)) + + self.assertEqual([1], recv.lines_received) + + def test_progress_cb_is_called_for_multiple_single_lines(self): + code = """x = 1 +y = 2 +""" + recv = PythonCodeExecutionTest.ReceiverWithProgress() + executor = PythonCodeExecution(success_cb=recv.on_success, error_cb=recv.on_error, + progress_cb=recv.on_progess_update) + task = executor.execute_async(code, {}, {}) + task.join() + if not recv.success_cb_called: + self.assertTrue(recv.error_cb_called) + self.fail("Execution failed with error:\n" + str(recv.task_exc)) + + self.assertEqual([1, 2], recv.lines_received) + + def test_progress_cb_is_called_for_mix_single_lines_and_blocks(self): + code = """x = 1 +# comment line + +sum = 0 +for i in range(10): + if i %2 == 0: + sum += i + +squared = sum*sum +""" + recv = PythonCodeExecutionTest.ReceiverWithProgress() + executor = PythonCodeExecution(success_cb=recv.on_success, error_cb=recv.on_error, + progress_cb=recv.on_progess_update) + context = {} + task = executor.execute_async(code, context, context) + task.join() + if not recv.success_cb_called: + self.assertTrue(recv.error_cb_called) + self.fail("Execution failed with error:\n" + str(recv.task_exc)) + + self.assertEqual(20, context['sum']) + self.assertEqual(20*20, context['squared']) + self.assertEqual(1, context['x']) + self.assertEqual([1, 2, 3, 4, 5, 8, 9], recv.lines_received) + # ------------------------------------------------------------------------- # Helpers # -------------------------------------------------------------------------