From 99d529740cfcc298d56689c7e91665a9986fc2a8 Mon Sep 17 00:00:00 2001
From: Martyn Gigg <martyn.gigg@gmail.com>
Date: Sun, 14 Jan 2018 17:29:02 +0000
Subject: [PATCH] Use main message display for Python output

Refs #21251
---
 .../workbench/workbench/app/mainwindow.py     | 13 ++--
 .../workbench/plugins/logmessagedisplay.py    | 15 +++-
 qt/python/CMakeLists.txt                      |  7 +-
 qt/python/mantidqt/utils/async.py             | 21 ++---
 qt/python/mantidqt/utils/test/test_async.py   | 14 ++--
 .../mantidqt/utils/test/test_writetosignal.py | 51 ++++++++++++
 qt/python/mantidqt/utils/writetosignal.py     | 43 ++++++++++
 .../widgets/codeeditor/errorformatter.py      | 40 ++++++++++
 .../widgets/codeeditor/interpreter.py         | 25 ++++--
 .../codeeditor/multifileinterpreter.py        |  1 -
 .../codeeditor/test/test_errorformatter.py    | 78 +++++++++++++++++++
 .../widgets/codeeditor/test/test_execution.py |  4 +-
 .../codeeditor/test/test_interpreter.py       |  4 +-
 qt/python/mantidqt/widgets/messagedisplay.py  |  2 +-
 .../mantidqt/widgets/src/_widgetscore.sip     |  7 ++
 15 files changed, 287 insertions(+), 38 deletions(-)
 create mode 100644 qt/python/mantidqt/utils/test/test_writetosignal.py
 create mode 100644 qt/python/mantidqt/utils/writetosignal.py
 create mode 100644 qt/python/mantidqt/widgets/codeeditor/errorformatter.py
 create mode 100644 qt/python/mantidqt/widgets/codeeditor/test/test_errorformatter.py

diff --git a/qt/applications/workbench/workbench/app/mainwindow.py b/qt/applications/workbench/workbench/app/mainwindow.py
index 48e1364dc4a..8e76e79b2ad 100644
--- a/qt/applications/workbench/workbench/app/mainwindow.py
+++ b/qt/applications/workbench/workbench/app/mainwindow.py
@@ -28,7 +28,8 @@ import sys
 # Constants
 # -----------------------------------------------------------------------------
 ORIGINAL_SYS_EXIT = sys.exit
-STDERR = sys.stderr
+ORIGINAL_STDOUT = sys.stdout
+ORIGINAL_STDERR = sys.stderr
 
 # -----------------------------------------------------------------------------
 # Requirements
@@ -43,7 +44,7 @@ requirements.check_qt()
 from qtpy.QtCore import (QByteArray, QCoreApplication, QEventLoop,
                          QPoint, QSize, Qt)  # noqa
 from qtpy.QtGui import (QColor, QPixmap)  # noqa
-from qtpy.QtWidgets import (QApplication, QDockWidget, QMainWindow, QMenu,
+from qtpy.QtWidgets import (QApplication, QDockWidget, QMainWindow,
                             QSplashScreen)  # noqa
 from mantidqt.utils.qt import plugins, widget_updates_disabled  # noqa
 
@@ -126,9 +127,6 @@ class MainWindow(QMainWindow):
         self.file_menu = None
         self.file_menu_actions = None
         self.editors_menu = None
-        self.editors_menu_actions = None
-        self.editors_execute_menu = None
-        self.editors_execute_menu_actions = None
 
         # Allow splash screen text to be overridden in set_splash
         self.splash = SPLASH
@@ -145,6 +143,7 @@ class MainWindow(QMainWindow):
         self.set_splash("Loading message display")
         from workbench.plugins.logmessagedisplay import LogMessageDisplay
         self.messagedisplay = LogMessageDisplay(self)
+        # this takes over stdout/stderr
         self.messagedisplay.register_plugin()
 
         self.set_splash("Loading IPython console")
@@ -384,7 +383,7 @@ def main():
     # Prepare for mantid import
     prepare_mantid_env()
 
-    # TODO: parse command arguments
+    # todo: parse command arguments
 
     # general initialization
     app = initialize()
@@ -397,7 +396,7 @@ def main():
         # This is type of thing we want to capture and have reports
         # about. Prints to stderr as we can't really count on anything
         # else
-        traceback.print_exc(file=STDERR)
+        traceback.print_exc(file=ORIGINAL_STDERR)
 
     if main_window is None:
         # An exception occurred don't exit here
diff --git a/qt/applications/workbench/workbench/plugins/logmessagedisplay.py b/qt/applications/workbench/workbench/plugins/logmessagedisplay.py
index 86e3363304e..6a296aa7ddd 100644
--- a/qt/applications/workbench/workbench/plugins/logmessagedisplay.py
+++ b/qt/applications/workbench/workbench/plugins/logmessagedisplay.py
@@ -16,7 +16,11 @@
 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import (absolute_import, unicode_literals)
 
+# std imports
+import sys
+
 # 3rdparty imports
+from mantidqt.utils.writetosignal import WriteToSignal
 from mantidqt.widgets.messagedisplay import MessageDisplay
 from qtpy.QtWidgets import QHBoxLayout
 
@@ -34,12 +38,21 @@ class LogMessageDisplay(PluginWidget):
         layout = QHBoxLayout()
         layout.addWidget(self.display)
         self.setLayout(layout)
-
         self.setWindowTitle(self.get_plugin_title())
 
+        # output capture
+        stdout_capture, stderr_capture = WriteToSignal(), WriteToSignal()
+        stdout_capture.sig_write_received.connect(self.display.appendNotice)
+        stderr_capture.sig_write_received.connect(self.display.appendError)
+        self.stdout, self.stderr = stdout_capture, stderr_capture
     def get_plugin_title(self):
         return "Messages"
 
     def register_plugin(self, menu=None):
         self.display.attachLoggingChannel()
+        self._capture_stdout_and_stderr()
         self.main.add_dockwidget(self)
+
+    def _capture_stdout_and_stderr(self):
+        sys.stdout = self.stdout
+        sys.stderr = self.stderr
diff --git a/qt/python/CMakeLists.txt b/qt/python/CMakeLists.txt
index 75a35fe1da1..62ca0aa2d6b 100644
--- a/qt/python/CMakeLists.txt
+++ b/qt/python/CMakeLists.txt
@@ -40,12 +40,15 @@ if ( ENABLE_WORKBENCH )
   set ( PYTHON_TEST_FILES
     mantidqt/test/test_import.py
 
+    mantidqt/utils/test/test_async.py
     mantidqt/utils/test/test_qt_utils.py
+    mantidqt/utils/test/test_writetosignal.py
 
     mantidqt/widgets/codeeditor/test/test_codeeditor.py
-    mantidqt/widgets/codeeditor/test/test_interpreter.py
+    mantidqt/widgets/codeeditor/test/test_errorformatter.py
     mantidqt/widgets/codeeditor/test/test_execution.py
-    mantidqt/widgets/codeeditor/test/test_multifileeditor.py
+    mantidqt/widgets/codeeditor/test/test_interpreter.py
+    mantidqt/widgets/codeeditor/test/test_multifileinterpreter.py
 
     mantidqt/widgets/test/test_algorithmselector.py
     mantidqt/widgets/test/test_jupyterconsole.py
diff --git a/qt/python/mantidqt/utils/async.py b/qt/python/mantidqt/utils/async.py
index 26cfaebced8..fe629a0edf9 100644
--- a/qt/python/mantidqt/utils/async.py
+++ b/qt/python/mantidqt/utils/async.py
@@ -40,13 +40,13 @@ def blocking_async_task(target, args=(), kwargs=None, blocking_cb=None,
     blocking_cb = blocking_cb if blocking_cb is not None else lambda: None
 
     class Receiver(object):
-        output, exception = None, None
+        output, exc_value = None, None
 
         def on_success(self, result):
             self.output = result.output
 
         def on_error(self, result):
-            self.exception = result.exception
+            self.exc_value = result.exc_value
 
     recv = Receiver()
     task = AsyncTask(target, args, kwargs, success_cb=recv.on_success,
@@ -56,8 +56,8 @@ def blocking_async_task(target, args=(), kwargs=None, blocking_cb=None,
         time.sleep(period_secs)
         blocking_cb()
 
-    if recv.exception is not None:
-        raise recv.exception
+    if recv.exc_value is not None:
+        raise recv.exc_value
     else:
         return recv.output
 
@@ -98,7 +98,7 @@ class AsyncTask(threading.Thread):
         except SyntaxError as exc:
             # treat SyntaxErrors as special as the traceback makes no sense
             # and the lineno is part of the exception instance
-            self.error_cb(AsyncTaskFailure(exc, None))
+            self.error_cb(AsyncTaskFailure(SyntaxError, exc, None))
         except:  # noqa
             self.error_cb(AsyncTaskFailure.from_excinfo(self.stack_chop))
         else:
@@ -132,12 +132,13 @@ class AsyncTaskFailure(object):
         the top of the stack listing
         :return: A new AsyncTaskFailure object
         """
-        _, exc_value, exc_tb = sys.exc_info()
-        return AsyncTaskFailure(exc_value, traceback.extract_tb(exc_tb)[chop:])
+        exc_type, exc_value, exc_tb = sys.exc_info()
+        return AsyncTaskFailure(exc_type, exc_value, traceback.extract_tb(exc_tb)[chop:])
 
-    def __init__(self, exception, stack_entries):
-        self.exception = exception
-        self.stack_entries = stack_entries
+    def __init__(self, exc_type, exc_value, stack):
+        self.exc_type = exc_type
+        self.exc_value = exc_value
+        self.stack = stack
 
     @property
     def success(self):
diff --git a/qt/python/mantidqt/utils/test/test_async.py b/qt/python/mantidqt/utils/test/test_async.py
index 29a311a3b3a..2d52777fae0 100644
--- a/qt/python/mantidqt/utils/test/test_async.py
+++ b/qt/python/mantidqt/utils/test/test_async.py
@@ -29,7 +29,8 @@ class AsyncTaskTest(unittest.TestCase):
 
     class Receiver(object):
         success_cb_called, error_cb_called, finished_cb_called = False, False, False
-        task_output, task_exc, task_exc_stack = None, None, None
+        task_output = None,
+        task_exc_type, task_exc, task_exc_stack = None, None, None
 
         def on_success(self, task_result):
             self.success_cb_called = True
@@ -37,8 +38,9 @@ class AsyncTaskTest(unittest.TestCase):
 
         def on_error(self, task_result):
             self.error_cb_called = True
-            self.task_exc = task_result.exception
-            self.task_exc_stack = task_result.stack_entries
+            self.task_exc_type = task_result.exc_type
+            self.task_exc = task_result.exc_value
+            self.task_exc_stack = task_result.stack
 
         def on_finished(self):
             self.finished_cb_called = True
@@ -112,7 +114,7 @@ class AsyncTaskTest(unittest.TestCase):
         # line number of self.target in async.py
         self.assertEqual(97, recv.task_exc_stack[0][1])
         # line number of raise statement above
-        self.assertEqual(98, recv.task_exc_stack[1][1])
+        self.assertEqual(100, recv.task_exc_stack[1][1])
 
     def test_unsuccessful_args_and_kwargs_operation_calls_error_and_finished_callback(self):
         def foo(scale, shift):
@@ -145,8 +147,8 @@ class AsyncTaskTest(unittest.TestCase):
         self.assertTrue(recv.error_cb_called)
         self.assertTrue(isinstance(recv.task_exc, RuntimeError))
         self.assertEqual(2, len(recv.task_exc_stack))
-        self.assertEqual(137, recv.task_exc_stack[0][1])
-        self.assertEqual(136, recv.task_exc_stack[1][1])
+        self.assertEqual(139, recv.task_exc_stack[0][1])
+        self.assertEqual(138, recv.task_exc_stack[1][1])
 
     # ---------------------------------------------------------------
     # Failure cases
diff --git a/qt/python/mantidqt/utils/test/test_writetosignal.py b/qt/python/mantidqt/utils/test/test_writetosignal.py
new file mode 100644
index 00000000000..e65614158ae
--- /dev/null
+++ b/qt/python/mantidqt/utils/test/test_writetosignal.py
@@ -0,0 +1,51 @@
+#  This file is part of the mantid workbench.
+#
+#  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)
+
+# std imports
+import unittest
+
+# 3rdparty
+from qtpy.QtCore import QCoreApplication, QObject
+
+# local imports
+from mantidqt.utils.qt.testing import requires_qapp
+from mantidqt.utils.writetosignal import WriteToSignal
+
+
+class Receiver(QObject):
+    captured_txt = None
+
+    def capture_text(self, txt):
+        self.captured_txt = txt
+
+
+@requires_qapp
+class WriteToSignalTest(unittest.TestCase):
+
+    def test_connected_receiver_receives_text(self):
+        recv = Receiver()
+        writer = WriteToSignal()
+        writer.sig_write_received.connect(recv.capture_text)
+        txt = "I expect to see this"
+        writer.write(txt)
+        QCoreApplication.processEvents()
+        self.assertEqual(txt, recv.captured_txt)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/qt/python/mantidqt/utils/writetosignal.py b/qt/python/mantidqt/utils/writetosignal.py
new file mode 100644
index 00000000000..fcbdb0f75ab
--- /dev/null
+++ b/qt/python/mantidqt/utils/writetosignal.py
@@ -0,0 +1,43 @@
+#  This file is part of the mantid workbench.
+#
+#  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)
+
+# std imports
+
+# 3rdparty imports
+from qtpy.QtCore import QObject, Signal
+
+
+class WriteToSignal(QObject):
+    """Provides a minimal file-like object that can be
+    used to transform write requests to
+    Qt-signals. Mainly used to communicate
+    stdout/stderr across threads"""
+
+    sig_write_received = Signal(str)
+
+    def closed(self):
+        return False
+
+    def flush(self):
+        pass
+
+    def isatty(self):
+        return False
+
+    def write(self, txt):
+        self.sig_write_received.emit(txt)
diff --git a/qt/python/mantidqt/widgets/codeeditor/errorformatter.py b/qt/python/mantidqt/widgets/codeeditor/errorformatter.py
new file mode 100644
index 00000000000..15aef7e0373
--- /dev/null
+++ b/qt/python/mantidqt/widgets/codeeditor/errorformatter.py
@@ -0,0 +1,40 @@
+#  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
+import traceback
+
+
+class ErrorFormatter(object):
+    """Formats errors to strings"""
+
+    def format(self, exc_type, exc_value, stack):
+        """
+        Produce a formatted error message for the given
+        error information.
+
+        :param exc_type: The type of exception
+        :param exc_value: An exception object of type exc_type
+        :param stack: An optional stack trace (assumed to be part or
+        all return by traceback.extract_tb
+        :return: A formatted string.
+        """
+        lines = traceback.format_exception(exc_type, exc_value, None)
+        if stack is not None:
+            lines.extend(traceback.format_list(stack))
+        return ''.join(lines)
diff --git a/qt/python/mantidqt/widgets/codeeditor/interpreter.py b/qt/python/mantidqt/widgets/codeeditor/interpreter.py
index 1d19431193a..3601fa89bcf 100644
--- a/qt/python/mantidqt/widgets/codeeditor/interpreter.py
+++ b/qt/python/mantidqt/widgets/codeeditor/interpreter.py
@@ -16,6 +16,9 @@
 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import (absolute_import, unicode_literals)
 
+# std imports
+import sys
+
 # 3rd party imports
 from qtpy.QtCore import QObject
 from qtpy.QtGui import QColor, QFont, QFontMetrics
@@ -23,13 +26,16 @@ from qtpy.QtWidgets import QStatusBar, QVBoxLayout, QWidget
 
 # local imports
 from mantidqt.widgets.codeeditor.editor import CodeEditor
+from mantidqt.widgets.codeeditor.errorformatter import ErrorFormatter
 from mantidqt.widgets.codeeditor.execution import PythonCodeExecution
 
+# Status messages
 IDLE_STATUS_MSG = "Status: Idle"
 RUNNING_STATUS_MSG = "Status: Running"
 
 # Editor colors
-CURRENTLINE_BKGD = QColor(247, 236, 248)
+CURRENTLINE_BKGD_COLOR = QColor(247, 236, 248)
+
 
 class PythonFileInterpreter(QWidget):
 
@@ -70,7 +76,7 @@ class PythonFileInterpreter(QWidget):
         editor.setFont(font)
 
         # show current editing line but in a softer color
-        editor.setCaretLineBackgroundColor(CURRENTLINE_BKGD)
+        editor.setCaretLineBackgroundColor(CURRENTLINE_BKGD_COLOR)
         editor.setCaretLineVisible(True)
 
         # set a margin large enough for sensible file sizes < 1000 lines
@@ -93,6 +99,7 @@ class PythonFileInterpreterPresenter(QObject):
         self.model = model
         # offset of executing code from start of the file
         self._code_start_offset = 0
+        self._error_formatter = ErrorFormatter()
 
         # connect signals
         self.model.sig_exec_success.connect(self._on_exec_success)
@@ -104,6 +111,8 @@ class PythonFileInterpreterPresenter(QObject):
 
     def req_execute_async(self):
         code_str, self._code_start_offset = self._get_code_for_execution()
+        if not code_str:
+            return
         self.view.set_editor_readonly(True)
         self.view.set_status_message(RUNNING_STATUS_MSG)
         return self.model.execute_async(code_str)
@@ -123,11 +132,15 @@ class PythonFileInterpreterPresenter(QObject):
         self.view.set_status_message(IDLE_STATUS_MSG)
 
     def _on_exec_error(self, task_error):
-        if isinstance(task_error.exception, SyntaxError):
-            lineno = task_error.exception.lineno
+        exc_type, exc_value, exc_stack = task_error.exc_type, task_error.exc_value, \
+                                         task_error.stack
+        if isinstance(exc_value, SyntaxError):
+            lineno = exc_value.lineno
         else:
-            lineno = task_error.stack_entries[-1][1]
+            lineno = exc_stack[-1][1]
+
         self.view.editor.updateProgressMarker(lineno, True)
+        sys.stderr.write(self._error_formatter.format(exc_type, exc_value, exc_stack) + '\n')
         self.view.set_editor_readonly(False)
         self.view.set_status_message(IDLE_STATUS_MSG)
 
@@ -135,4 +148,4 @@ class PythonFileInterpreterPresenter(QObject):
         """Update progress on the view taking into account if a selection of code is
         running"""
         self.view.editor.updateProgressMarker(lineno + self._code_start_offset,
-                                              False)
\ No newline at end of file
+                                              False)
diff --git a/qt/python/mantidqt/widgets/codeeditor/multifileinterpreter.py b/qt/python/mantidqt/widgets/codeeditor/multifileinterpreter.py
index 7f62d5a02f5..60ca774d5c5 100644
--- a/qt/python/mantidqt/widgets/codeeditor/multifileinterpreter.py
+++ b/qt/python/mantidqt/widgets/codeeditor/multifileinterpreter.py
@@ -60,4 +60,3 @@ class MultiPythonFileInterpreter(QWidget):
         self._editors.addTab(PythonFileInterpreter(self.default_content,
                                                    parent=None),
                              title)
-
diff --git a/qt/python/mantidqt/widgets/codeeditor/test/test_errorformatter.py b/qt/python/mantidqt/widgets/codeeditor/test/test_errorformatter.py
new file mode 100644
index 00000000000..82f78a123fe
--- /dev/null
+++ b/qt/python/mantidqt/widgets/codeeditor/test/test_errorformatter.py
@@ -0,0 +1,78 @@
+#  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
+import sys
+import traceback
+import unittest
+
+# local imports
+from mantidqt.widgets.codeeditor.errorformatter import ErrorFormatter
+
+
+class ErrorFormatterTest(unittest.TestCase):
+
+    def test_syntax_error(self):
+        try:
+            exec("if:")
+        except SyntaxError:
+            exc_type, exc_value = sys.exc_info()[:2]
+            formatter = ErrorFormatter()
+            error = formatter.format(exc_type, exc_value, None)
+
+        expected = """  File "<string>", line 1
+    if:
+      ^
+SyntaxError: invalid syntax
+"""
+        self.assertEqual(expected, error)
+
+    def test_standard_exception(self):
+        code = """
+def foo():
+    def bar():
+        # raises a NameError
+        y = _local + 1
+    # call inner
+    bar()
+foo()
+"""
+        try:
+            exec(code)
+        except NameError:
+            exc_type, exc_value, tb = sys.exc_info()
+            formatter = ErrorFormatter()
+            error = formatter.format(exc_type, exc_value, traceback.extract_tb(tb))
+            del tb
+
+        # stacktrace will contain file names that are not portable so don't do equality check
+        error_lines = error.splitlines()
+        expected_lines = [
+            "NameError: global name '_local' is not defined",
+            '  File ".*test_errorformatter.py", line 56, in test_standard_exception',
+            '    exec(.*)',
+            '  File "<string>", line 8, in <module>',
+            '  File "<string>", line 7, in foo',
+            '  File "<string>", line 5, in bar',
+        ]
+        for produced, expected in zip(error_lines, expected_lines):
+            self.assertRegexpMatches(produced, expected)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py b/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py
index f5936916381..43761cc0ebc 100644
--- a/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py
+++ b/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py
@@ -36,8 +36,8 @@ class Receiver(QObject):
 
     def on_error(self, task_result):
         self.error_cb_called = True
-        self.task_exc = task_result.exception
-        self.error_stack = task_result.stack_entries
+        self.task_exc = task_result.exc_value
+        self.error_stack = task_result.stack
 
 
 class ReceiverWithProgress(Receiver):
diff --git a/qt/python/mantidqt/widgets/codeeditor/test/test_interpreter.py b/qt/python/mantidqt/widgets/codeeditor/test/test_interpreter.py
index ef51a941907..cfd49f78e19 100644
--- a/qt/python/mantidqt/widgets/codeeditor/test/test_interpreter.py
+++ b/qt/python/mantidqt/widgets/codeeditor/test/test_interpreter.py
@@ -43,7 +43,7 @@ class PythonFileInterpreterTest(unittest.TestCase):
         w = PythonFileInterpreter()
         w._presenter.model.execute_async = mock.MagicMock()
 
-        w.execute_all_async()
+        w.execute_async()
 
         w._presenter.model.execute_async.assert_not_called()
         self.assertTrue("Status: Idle", w.status.currentMessage())
@@ -51,7 +51,7 @@ class PythonFileInterpreterTest(unittest.TestCase):
     def test_successful_execution(self):
         w = PythonFileInterpreter()
         w.editor.setText("x = 1 + 2")
-        w.execute_all_async()
+        w.execute_async()
         self.assertTrue("Status: Idle", w.status.currentMessage())
 
 
diff --git a/qt/python/mantidqt/widgets/messagedisplay.py b/qt/python/mantidqt/widgets/messagedisplay.py
index 9e24fa32942..2fe0e165230 100644
--- a/qt/python/mantidqt/widgets/messagedisplay.py
+++ b/qt/python/mantidqt/widgets/messagedisplay.py
@@ -14,7 +14,7 @@
 #
 #  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)
+from __future__ import (absolute_import, unicode_literals)
 
 from mantidqt.utils.qt import import_qtlib
 
diff --git a/qt/python/mantidqt/widgets/src/_widgetscore.sip b/qt/python/mantidqt/widgets/src/_widgetscore.sip
index 67752d51f85..86d21694acf 100644
--- a/qt/python/mantidqt/widgets/src/_widgetscore.sip
+++ b/qt/python/mantidqt/widgets/src/_widgetscore.sip
@@ -53,6 +53,13 @@ class MessageDisplay : QWidget, Configurable {
 public:
   MessageDisplay(QWidget *parent = 0);
   void attachLoggingChannel();
+
+  void appendFatal(const QString &text);
+  void appendError(const QString &text);
+  void appendWarning(const QString &text);
+  void appendNotice(const QString &text);
+  void appendInformation(const QString &text);
+  void appendDebug(const QString &text);
 };
 
 class ScriptEditor : QWidget {
-- 
GitLab