Skip to content
Snippets Groups Projects
Commit 45118765 authored by Martyn Gigg's avatar Martyn Gigg
Browse files

Add support for progress updates during code execution.

Refs #21251
parent 570a0c48
No related branches found
No related tags found
No related merge requests found
......@@ -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
......@@ -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
# -------------------------------------------------------------------------
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment