File indexing completed on 2025-03-09 04:10:20

0001 """
0002 SPDX-FileCopyrightText: 2017 Eliakin Costa <eliakim170@gmail.com>
0003 
0004 SPDX-License-Identifier: GPL-2.0-or-later
0005 """
0006 from PyQt5.QtWidgets import QAction
0007 from PyQt5.QtGui import QIcon, QKeySequence
0008 from PyQt5.QtCore import Qt
0009 import sys
0010 import traceback
0011 import inspect
0012 from . import docwrapper
0013 from .... import utils
0014 import krita
0015 
0016 if sys.version_info[0] > 2:
0017     import importlib
0018     from importlib.machinery import SourceFileLoader
0019 else:
0020     import imp
0021 
0022 PYTHON27 = sys.version_info.major == 2 and sys.version_info.minor == 7
0023 PYTHON33 = sys.version_info.major == 3 and sys.version_info.minor == 3
0024 PYTHON34 = sys.version_info.major == 3 and sys.version_info.minor == 4
0025 EXEC_NAMESPACE = "__main__"  # namespace that user scripts will run in
0026 
0027 
0028 class RunAction(QAction):
0029 
0030     def __init__(self, scripter, parent=None):
0031         super(RunAction, self).__init__(parent)
0032         self.scripter = scripter
0033 
0034         self.editor = self.scripter.uicontroller.editor
0035         self.output = self.scripter.uicontroller.findTabWidget(i18n('Output'), 'OutPutTextEdit')
0036 
0037         self.triggered.connect(self.run)
0038 
0039         self.setText(i18n("Run"))
0040         self.setToolTip(i18n('Run Ctrl+R'))
0041         self.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
0042         self.setIcon(utils.getThemedIcon(':/icons/run.svg'))
0043 
0044     @property
0045     def parent(self):
0046         return 'toolBar',
0047 
0048     def run(self):
0049         """ This method execute python code from an activeDocument (file) or direct
0050             from editor (ui_scripter/editor/pythoneditor.py). When executing code
0051             from a file, we use importlib to load this module/file and with
0052             "users_script" name. That's implementation seeks for a "main()" function in the script.
0053             When executing code from editor without creating a file, we compile
0054             this script to bytecode and we execute this in an empty scope. That's
0055             faster than use exec directly and cleaner, because we are using an empty scope. """
0056 
0057         self.scripter.uicontroller.setActiveWidget(i18n('Output'))
0058         stdout = sys.stdout
0059         stderr = sys.stderr
0060         output = docwrapper.DocWrapper(self.output.document())
0061         if (self.editor._documentModified is True):
0062             output.write("==== Warning: Script not saved! ====\n")
0063         else:
0064             output.write("======================================\n")
0065         sys.stdout = output
0066         sys.stderr = output
0067 
0068         script = self.editor.document().toPlainText()
0069         document = self.scripter.documentcontroller.activeDocument
0070 
0071         try:
0072             if document and self.editor._documentModified is False:
0073                 if PYTHON27:
0074                     users_module = self.run_py2_document(document)
0075                 else:
0076                     users_module = self.run_py3_document(document)
0077 
0078                 # maybe script is to be execed, maybe main needs to be invoked
0079                 # if there is a main() then execute it, otherwise don't worry...
0080                 if hasattr(users_module, "main") and inspect.isfunction(users_module.main):
0081                     users_module.main()
0082             else:
0083                 code = compile(script, '<string>', 'exec')
0084                 globals_dict = {"__name__": EXEC_NAMESPACE}
0085                 exec(code, globals_dict)
0086 
0087         except SystemExit:
0088             # user typed quit() or exit()
0089             self.scripter.uicontroller.closeScripter()
0090         except Exception:
0091             # Provide context (line number and text) for an error that is caught.
0092             # Ordinarily, syntax and Indent errors are caught during initial
0093             # compilation in exec(), and the traceback traces back to this file.
0094             # So these need to be treated separately.
0095             # Other errors trace back to the file/script being run.
0096             type_, value_, traceback_ = sys.exc_info()
0097             if type_ == SyntaxError:
0098                 errorMessage = "%s\n%s" % (value_.text.rstrip(), " " * (value_.offset - 1) + "^")
0099                 # rstrip to remove trailing \n, output needs to be fixed width font for the ^ to align correctly
0100                 errorText = "Syntax Error on line %s" % value_.lineno
0101             elif type_ == IndentationError:
0102                 # (no offset is provided for an IndentationError
0103                 errorMessage = value_.text.rstrip()
0104                 errorText = "Unexpected Indent on line %s" % value_.lineno
0105             else:
0106                 errorText = traceback.format_exception_only(type_, value_)[0]
0107                 format_string = "In file: {0}\nIn function: {2} at line: {1}. Line with error:\n{3}"
0108                 tbList = traceback.extract_tb(traceback_)
0109                 tb = tbList[-1]
0110                 errorMessage = format_string.format(*tb)
0111             m = "\n**********************\n%s\n%s\n**********************\n" % (errorText, errorMessage)
0112             output.write(m)
0113 
0114         sys.stdout = stdout
0115         sys.stderr = stderr
0116 
0117         # scroll to bottom of output
0118         bottom = self.output.verticalScrollBar().maximum()
0119         self.output.verticalScrollBar().setValue(bottom)
0120 
0121     def run_py2_document(self, document):
0122         """ Loads and executes an external script using Python 2 specific operations
0123         and returns the loaded module for further execution if needed.
0124         """
0125         try:
0126             user_module = imp.load_source(EXEC_NAMESPACE, document.filePath)
0127         except Exception as e:
0128             raise e
0129 
0130         return user_module
0131 
0132     def run_py3_document(self, document):
0133         """ Loads and executes an external script using Python 3 specific operations
0134         and returns the loaded module for further execution if needed.
0135         """
0136         spec = importlib.util.spec_from_file_location(EXEC_NAMESPACE, document.filePath)
0137         try:
0138             users_module = importlib.util.module_from_spec(spec)
0139             spec.loader.exec_module(users_module)
0140 
0141         except AttributeError as e:  # no module from spec
0142             if PYTHON34 or PYTHON33:
0143                 loader = SourceFileLoader(EXEC_NAMESPACE, document.filePath)
0144                 users_module = loader.load_module()
0145             else:
0146                 raise e
0147 
0148         return users_module