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