diff --git a/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/actions/runaction/runaction.py b/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/actions/runaction/runaction.py index 2a4eef6202..d5ee09ecf6 100644 --- a/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/actions/runaction/runaction.py +++ b/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/actions/runaction/runaction.py @@ -1,91 +1,114 @@ from PyQt5.QtWidgets import QAction from PyQt5.QtGui import QIcon, QKeySequence from PyQt5.QtCore import Qt import sys from . import docwrapper import importlib from importlib.machinery import SourceFileLoader +import traceback PYTHON33 = sys.version_info.major==3 and sys.version_info.minor==3 PYTHON34 = sys.version_info.major==3 and sys.version_info.minor==4 EXEC_NAMESPACE = "users_script" # namespace that user scripts will run in class RunAction(QAction): def __init__(self, scripter, parent=None): super(RunAction, self).__init__(parent) self.scripter = scripter self.editor = self.scripter.uicontroller.editor self.output = self.scripter.uicontroller.findTabWidget('OutPut', 'OutPutTextEdit') self.triggered.connect(self.run) self.setText('Run') self.setToolTip('Run Ctrl+R') self.setIcon(QIcon(':/icons/run.svg')) self.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R)) @property def parent(self): return 'toolBar' def run(self): """ This method execute python code from an activeDocument (file) or direct from editor (ui_scripter/editor/pythoneditor.py). When executing code from a file, we use importlib to load this module/file and with "users_script" name. That's implementation seeks for a "main()" function in the script. When executing code from editor without creating a file, we compile this script to bytecode and we execute this in an empty scope. That's faster than use exec directly and cleaner, because we are using an empty scope. """ self.scripter.uicontroller.setActiveWidget('OutPut') stdout = sys.stdout stderr = sys.stderr output = docwrapper.DocWrapper(self.output.document()) if (self.editor._documentModified is True): output.write("==== Warning: Script not saved! ====\n") else: output.write("======================================\n") sys.stdout = output sys.stderr = output script = self.editor.document().toPlainText() document = self.scripter.documentcontroller.activeDocument try: if document and self.editor._documentModified is False: spec = importlib.util.spec_from_file_location(EXEC_NAMESPACE, document.filePath) try: users_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(users_module) except AttributeError as e: # no module from spec if PYTHON34 or PYTHON33: loader = SourceFileLoader(EXEC_NAMESPACE, document.filePath) users_module = loader.load_module() else: raise e try: # maybe script is to be execed, maybe main needs to be invoked # if there is a main() then execute it, otherwise don't worry... users_module.main() except AttributeError: pass else: code = compile(script, '', 'exec') exec(code, {}) except Exception as e: - self.scripter.uicontroller.showException(str(e)) + """Provide context (line number and text) for an error that is caught. + Ordinarily, syntax and Indent errors are caught during initial + compilation in exec(), and the traceback traces back to this file. + So these need to be treated separately. + Other errors trace back to the file/script being run. + """ + type_, value_, traceback_ = sys.exc_info() + if type_ == SyntaxError: + errorMessage = "%s\n%s"%(value_.text.rstrip(), " "*(value_.offset-1)+"^") + # rstrip to remove trailing \n, output needs to be fixed width font for the ^ to align correctly + errorText = "Syntax Error on line %s"%value_.lineno + elif type_ == IndentationError: + # (no offset is provided for an IndentationError + errorMessage = value_.text.rstrip() + errorText = "Unexpected Indent on line %s"%value_.lineno + else: + errorText = traceback.format_exception_only(type_, value_)[0] + format_string = "In file: {0}\nIn function: {2} at line: {1}. Line with error:\n{3}" + tbList = traceback.extract_tb(traceback_) + tb = tbList[-1] + errorMessage = format_string.format(*tb) + m = "\n**********************\n%s\n%s\n**********************\n"%(errorText, errorMessage) + output.write(m) sys.stdout = stdout sys.stderr = stderr # scroll to bottom of output bottom = self.output.verticalScrollBar().maximum() self.output.verticalScrollBar().setValue(bottom)