Skip to content
Snippets Groups Projects
execution.py 4.64 KiB
Newer Older
#  This file is part of the mantidqt package
#
#  Copyright (C) 2017 mantidproject
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, unicode_literals)

# std imports

# 3rdparty imports
from IPython.core.inputsplitter import InputSplitter
from qtpy.QtCore import QObject, Signal

# local imports
from mantidqt.utils.async import AsyncTask
class PythonCodeExecution(QObject):
    """Provides the ability to execute arbitrary
    strings of Python code. It supports
    reporting progress updates in asynchronous execution
    """
    sig_exec_success = Signal(object)
    sig_exec_error = Signal(object)
    sig_exec_progress = Signal(int)
    def __init__(self):
        """Initialize the object"""
        super(PythonCodeExecution, self).__init__()
        self._globals_ns, self._locals_ns = None, None
        self.reset_context()

    @property
    def globals_ns(self):
        return self._globals_ns

    @property
    def locals_ns(self):
        return self._locals_ns

    def reset_context(self):
        # create new context for execution
        self._globals_ns, self._locals_ns = {}, {}

    def execute_async(self, code_str):
        """
        Execute the given code string on a separate thread. This function
        returns as soon as the new thread starts

        :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
        :returns: The created async task
        """
        # Stack is chopped on error to avoid the  AsyncTask.run->_do_exec->exec calls appearing
        # as these are not useful in this context
        t = AsyncTask(self.execute, args=(code_str,),
Martyn Gigg's avatar
Martyn Gigg committed
                      success_cb=self._on_success, error_cb=self._on_error)
        t.start()
        return t

    def execute(self, code_str):
        """Execute the given code on the calling thread
        within the provided context.

        :param code_str: A string containing code to execute
        :raises: Any error that the code generates
        """
        compile(code_str, "<string>", mode='exec')

        sig_progress = self.sig_exec_progress
        for block in code_blocks(code_str):
            sig_progress.emit(block.lineno)
            exec(block.code_obj, self.globals_ns, self.locals_ns)

    # ---------------------------------------------------------------
    # Callbacks
    # ---------------------------------------------------------------
    def _on_success(self, task_result):
        self.sig_exec_success.emit(task_result)
    def _on_error(self, task_error):
        self.sig_exec_error.emit(task_error)

    def _on_progress_updated(self, lineno):
        self.sig_exec_progress(lineno)


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_cur = 0
    lines = code_str.splitlines()
    line_count = len(lines)
    isp = InputSplitter()
        lineno_cur += 1
        isp.push(line)
        # If we need more input to form a complete statement
        # or we are not at the end of the code then keep
        # going
        if isp.push_accepts_more() and lineno_cur != line_count:
        else:
            # Now we have a complete set of executable statements
            # throw them back
            code = isp.source
            yield CodeBlock(code, lineno_cur)
            # In order to keep the line numbering in error stack traces
            # consistent each executed block needs to have the statements
            # on the same line as they are in the real code so we prepend
            # blank lines to make this so
            isp.push('\n'*lineno_cur)