diff --git a/src/Mod/AddonManager/addonmanager_dependency_installer.py b/src/Mod/AddonManager/addonmanager_dependency_installer.py index 05784a1f08..7ef1307c27 100644 --- a/src/Mod/AddonManager/addonmanager_dependency_installer.py +++ b/src/Mod/AddonManager/addonmanager_dependency_installer.py @@ -25,16 +25,16 @@ import os import subprocess -from time import sleep from typing import List -import FreeCAD +import addonmanager_freecad_interface as fci from PySide import QtCore import addonmanager_utilities as utils from addonmanager_installer import AddonInstaller, MacroInstaller +from Addon import Addon -translate = FreeCAD.Qt.translate +translate = fci.translate class DependencyInstaller(QtCore.QObject): @@ -48,7 +48,7 @@ class DependencyInstaller(QtCore.QObject): def __init__( self, - addons: List[object], + addons: List[Addon], python_requires: List[str], python_optional: List[str], location: os.PathLike = None, @@ -56,7 +56,8 @@ class DependencyInstaller(QtCore.QObject): """Install the various types of dependencies that might be specified. If an optional dependency fails this is non-fatal, but other failures are considered fatal. If location is specified it overrides the FreeCAD user base directory setting: this is used mostly - for testing purposes and shouldn't be set by normal code in most circumstances.""" + for testing purposes and shouldn't be set by normal code in most circumstances. + """ super().__init__() self.addons = addons self.python_requires = python_requires @@ -95,18 +96,19 @@ class DependencyInstaller(QtCore.QObject): return False try: proc = self._run_pip(["--version"]) - FreeCAD.Console.PrintMessage(proc.stdout + "\n") + fci.Console.PrintMessage(proc.stdout + "\n") except subprocess.CalledProcessError: self.no_pip.emit(f"{python_exe} -m pip --version") return False return True - def _install_required(self, vendor_path: os.PathLike) -> bool: - """Install the required Python package dependencies. If any fail a failure signal is - emitted and the function exits without proceeding with any additional installs.""" + def _install_required(self, vendor_path: str) -> bool: + """Install the required Python package dependencies. If any fail a failure + signal is emitted and the function exits without proceeding with any additional + installations.""" for pymod in self.python_requires: if QtCore.QThread.currentThread().isInterruptionRequested(): - return + return False try: proc = self._run_pip( [ @@ -117,9 +119,9 @@ class DependencyInstaller(QtCore.QObject): pymod, ] ) - FreeCAD.Console.PrintMessage(proc.stdout + "\n") + fci.Console.PrintMessage(proc.stdout + "\n") except subprocess.CalledProcessError as e: - FreeCAD.Console.PrintError(str(e) + "\n") + fci.Console.PrintError(str(e) + "\n") self.failure.emit( translate( "AddonsInstaller", @@ -127,9 +129,10 @@ class DependencyInstaller(QtCore.QObject): ).format(pymod), str(e), ) - return + return False + return True - def _install_optional(self, vendor_path: os.PathLike): + def _install_optional(self, vendor_path: str): """Install the optional Python package dependencies. If any fail a message is printed to the console, but installation of the others continues.""" for pymod in self.python_optional: @@ -145,9 +148,9 @@ class DependencyInstaller(QtCore.QObject): pymod, ] ) - FreeCAD.Console.PrintMessage(proc.stdout + "\n") + fci.Console.PrintMessage(proc.stdout + "\n") except subprocess.CalledProcessError as e: - FreeCAD.Console.PrintError( + fci.Console.PrintError( translate( "AddonsInstaller", "Installation of optional package failed" ) @@ -162,7 +165,8 @@ class DependencyInstaller(QtCore.QObject): final_args.extend(args) return self._subprocess_wrapper(final_args) - def _subprocess_wrapper(self, args) -> object: + @staticmethod + def _subprocess_wrapper(args) -> subprocess.CompletedProcess: """Wrap subprocess call so test code can mock it.""" return utils.run_interruptable_subprocess(args) @@ -177,7 +181,7 @@ class DependencyInstaller(QtCore.QObject): for addon in self.addons: if QtCore.QThread.currentThread().isInterruptionRequested(): return - FreeCAD.Console.PrintMessage( + fci.Console.PrintMessage( translate( "AddonsInstaller", "Installing required dependency {}" ).format(addon.name) diff --git a/src/Mod/AddonManager/addonmanager_devmode.py b/src/Mod/AddonManager/addonmanager_devmode.py index 9d1e88f43c..f57cc2efb0 100644 --- a/src/Mod/AddonManager/addonmanager_devmode.py +++ b/src/Mod/AddonManager/addonmanager_devmode.py @@ -110,7 +110,6 @@ class DeveloperMode: """The main Developer Mode dialog, for editing package.xml metadata graphically.""" def __init__(self): - # In the UI we want to show a translated string for the person type, but the underlying # string must be the one expected by the metadata parser, in English self.person_type_translation = { @@ -153,7 +152,7 @@ class DeveloperMode: QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg")) ) - def show(self, parent=None, path=None): + def show(self, parent=None, path: str = None): """Show the main dev mode dialog""" if parent: self.dialog.setParent(parent) @@ -178,7 +177,7 @@ class DeveloperMode: self.metadata.Date = str(now) self.metadata.write(os.path.join(self.current_mod, "package.xml")) - def _populate_dialog(self, path_to_repo): + def _populate_dialog(self, path_to_repo: str): """Populate this dialog using the best available parsing of the contents of the repo at path_to_repo. This is a multi-layered process that starts with any existing package.xml file or other known metadata files, and proceeds through examining the contents of the @@ -330,7 +329,7 @@ class DeveloperMode: predictor = Predictor() self.metadata = predictor.predict_metadata(self.current_mod) - def _scan_for_git_info(self, path): + def _scan_for_git_info(self, path: str): """Look for branch availability""" self.git_interface = AddonGitInterface(path) if self.git_interface.git_exists: @@ -383,7 +382,7 @@ class DeveloperMode: # Finally, populate the combo boxes, etc. self._populate_combo() - # Disable all of the "Remove" buttons until something is selected + # Disable all the "Remove" buttons until something is selected self.dialog.removeContentItemToolButton.setDisabled(True) def _sync_metadata_to_ui(self): @@ -445,7 +444,7 @@ class DeveloperMode: else: self.metadata.PythonMin = "0.0.0" # Code for "unset" - # Content, people, and licenses should already be sync'ed + # Content, people, and licenses should already be synchronized ############################################################################################### # DIALOG SLOTS @@ -516,7 +515,7 @@ class DeveloperMode: if entry and entry not in recent_mod_paths and os.path.exists(entry): recent_mod_paths.append(entry) - # Remove the whole thing so we can recreate it from scratch + # Remove the whole thing, so we can recreate it from scratch self.pref.RemGroup("recentModsList") if recent_mod_paths: @@ -604,11 +603,10 @@ class DeveloperMode: import vermin required_minor_version = 0 - for dirpath, _, filenames in os.walk(self.current_mod): + for dir_path, _, filenames in os.walk(self.current_mod): for filename in filenames: if filename.endswith(".py"): - - with open(os.path.join(dirpath, filename), encoding="utf-8") as f: + with open(os.path.join(dir_path, filename), encoding="utf-8") as f: contents = f.read() version_strings = vermin.version_strings( vermin.detect(contents) @@ -649,7 +647,7 @@ class DeveloperMode: translate("AddonsInstaller", "Install Vermin?"), translate( "AddonsInstaller", - "Autodetecting the required version of Python for this Addon requires Vermin (https://pypi.org/project/vermin/). OK to install?", + "Auto-detecting the required version of Python for this Addon requires Vermin (https://pypi.org/project/vermin/). OK to install?", ), QMessageBox.Yes | QMessageBox.Cancel, ) @@ -682,7 +680,7 @@ class DeveloperMode: ) FreeCAD.Console.PrintMessage(proc.stdout.decode()) if proc.returncode != 0: - response = QMessageBox.critical( + QMessageBox.critical( self.dialog, translate("AddonsInstaller", "Installation failed"), translate( @@ -697,7 +695,7 @@ class DeveloperMode: # pylint: disable=import-outside-toplevel import vermin except ImportError: - response = QMessageBox.critical( + QMessageBox.critical( self.dialog, translate("AddonsInstaller", "Installation failed"), translate( diff --git a/src/Mod/AddonManager/addonmanager_devmode_add_content.py b/src/Mod/AddonManager/addonmanager_devmode_add_content.py index 789a0d3b01..44cfad5738 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_add_content.py +++ b/src/Mod/AddonManager/addonmanager_devmode_add_content.py @@ -58,7 +58,7 @@ translate = FreeCAD.Qt.translate class AddContent: """A dialog for adding a single content item to the package metadata.""" - def __init__(self, path_to_addon: os.PathLike, toplevel_metadata: FreeCAD.Metadata): + def __init__(self, path_to_addon: str, toplevel_metadata: FreeCAD.Metadata): """path_to_addon is the full path to the toplevel directory of this Addon, and toplevel_metadata is to overall package.xml Metadata object for this Addon. This information is used to assist the use in filling out the dialog by providing @@ -147,7 +147,7 @@ class AddContent: result = self.dialog.exec() if result == QDialog.Accepted: return self._generate_metadata() - return None, None + return None def _populate_dialog(self, metadata: FreeCAD.Metadata) -> None: """Fill in the dialog with the details from the passed metadata object""" @@ -231,7 +231,7 @@ class AddContent: # Early return if this is the only addon if self.dialog.singletonCheckBox.isChecked(): - return (current_data, self.metadata) + return current_data, self.metadata # Otherwise, process the rest of the metadata (display name is already done) self.metadata.Description = ( @@ -254,13 +254,14 @@ class AddContent: licenses = [] for row in range(self.dialog.licensesTableWidget.rowCount()): - new_license = {} - new_license["name"] = self.dialog.licensesTableWidget.item(row, 0).text - new_license["file"] = self.dialog.licensesTableWidget.item(row, 1).text() + new_license = { + "name": self.dialog.licensesTableWidget.item(row, 0).text, + "file": self.dialog.licensesTableWidget.item(row, 1).text(), + } licenses.append(new_license) self.metadata.License = licenses - return (self.dialog.addonKindComboBox.currentData(), self.metadata) + return self.dialog.addonKindComboBox.currentData(), self.metadata ############################################################################################### # DIALOG SLOTS @@ -381,7 +382,8 @@ class EditTags: def exec(self): """Execute the dialog, returning a list of tags (which may be empty, but still represents - the expected list of tags to be set, e.g. the user may have removed them all).""" + the expected list of tags to be set, e.g. the user may have removed them all). + """ result = self.dialog.exec() if result == QDialog.Accepted: new_tags: List[str] = self.dialog.lineEdit.text().split(",") @@ -541,7 +543,8 @@ class EditDependency: self, dep_type="", dep_name="", dep_optional=False ) -> Tuple[str, str, bool]: """Execute the dialog, returning a tuple of the type of dependency (workbench, addon, or - python), the name of the dependency, and a boolean indicating whether this is optional.""" + python), the name of the dependency, and a boolean indicating whether this is optional. + """ # If we are editing an existing row, set up the dialog: if dep_type and dep_name: @@ -564,8 +567,8 @@ class EditDependency: dep_name = self.dialog.dependencyComboBox.currentData() if dep_name == "other": dep_name = self.dialog.lineEdit.text() - return (dep_type, dep_name, dep_optional) - return ("", "", False) + return dep_type, dep_name, dep_optional + return "", "", False def _populate_internal_workbenches(self): """Add all known internal FreeCAD Workbenches to the list""" diff --git a/src/Mod/AddonManager/addonmanager_devmode_license_selector.py b/src/Mod/AddonManager/addonmanager_devmode_license_selector.py index e53e4dad5c..7b20a4cdc8 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_license_selector.py +++ b/src/Mod/AddonManager/addonmanager_devmode_license_selector.py @@ -43,6 +43,8 @@ try: RegexWrapper = QRegularExpression RegexValidatorWrapper = QRegularExpressionValidator except ImportError: + QRegularExpressionValidator = None + QRegularExpression = None from PySide.QtGui import ( QRegExpValidator, ) @@ -150,7 +152,7 @@ class LicenseSelector: new_short_code = self.dialog.otherLineEdit.text() self.pref.SetString("devModeLastSelectedLicense", new_short_code) return new_short_code, new_license_path - return None, None + return None def set_license(self, short_code): """Set the currently-selected license.""" diff --git a/src/Mod/AddonManager/addonmanager_devmode_licenses_table.py b/src/Mod/AddonManager/addonmanager_devmode_licenses_table.py index 3526e70ee0..a30ee37028 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_licenses_table.py +++ b/src/Mod/AddonManager/addonmanager_devmode_licenses_table.py @@ -72,9 +72,9 @@ class LicensesTable: """Use the passed metadata object to populate the maintainers and authors""" self.widget.tableWidget.setRowCount(0) row = 0 - for l in self.metadata.License: - shortcode = l["name"] - path = l["file"] + for lic in self.metadata.License: + shortcode = lic["name"] + path = lic["file"] self._add_row(row, shortcode, path) row += 1 diff --git a/src/Mod/AddonManager/addonmanager_devmode_metadata_checker.py b/src/Mod/AddonManager/addonmanager_devmode_metadata_checker.py index 3850186797..3f000d7643 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_metadata_checker.py +++ b/src/Mod/AddonManager/addonmanager_devmode_metadata_checker.py @@ -65,7 +65,7 @@ class MetadataValidators: return # The package.xml standard has some required elements that the basic XML reader is not - # actually checking for. In developer mode, actually make sure that all of the rules are + # actually checking for. In developer mode, actually make sure that all the rules are # being followed for each element. errors = [] @@ -141,7 +141,8 @@ class MetadataValidators: errors.extend(self.validate_urls(urls)) return errors - def validate_urls(self, urls) -> List[str]: + @staticmethod + def validate_urls(urls) -> List[str]: """Check the URLs provided by the addon""" errors = [] if len(urls) == 0: @@ -183,14 +184,16 @@ class MetadataValidators: ) return errors - def validate_workbench_metadata(self, workbench) -> List[str]: + @staticmethod + def validate_workbench_metadata(workbench) -> List[str]: """Validate the required element(s) for a workbench""" errors = [] if not workbench.Classname or len(workbench.Classname) == 0: errors.append("No specified for workbench") return errors - def validate_preference_pack_metadata(self, pack) -> List[str]: + @staticmethod + def validate_preference_pack_metadata(pack) -> List[str]: """Validate the required element(s) for a preference pack""" errors = [] if not pack.Name or len(pack.Name) == 0: diff --git a/src/Mod/AddonManager/addonmanager_devmode_person_editor.py b/src/Mod/AddonManager/addonmanager_devmode_person_editor.py index 9f2fc1df8e..1086a7767f 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_person_editor.py +++ b/src/Mod/AddonManager/addonmanager_devmode_person_editor.py @@ -61,7 +61,7 @@ class PersonEditor: self.dialog.nameLineEdit.text(), self.dialog.emailLineEdit.text(), ) - return (None, None, None) + return "", "", "" def setup( self, person_type: str = "maintainer", name: str = "", email: str = "" diff --git a/src/Mod/AddonManager/addonmanager_devmode_predictor.py b/src/Mod/AddonManager/addonmanager_devmode_predictor.py index 647d186a7c..278ada4e87 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_predictor.py +++ b/src/Mod/AddonManager/addonmanager_devmode_predictor.py @@ -63,7 +63,7 @@ class Predictor: if not self.git_manager: raise Exception("Cannot use Developer Mode without git installed") - def predict_metadata(self, path: os.PathLike) -> FreeCAD.Metadata: + def predict_metadata(self, path: str) -> FreeCAD.Metadata: """Create a predicted Metadata object based on the contents of the passed-in directory""" if not os.path.isdir(path): return None @@ -85,7 +85,7 @@ class Predictor: committers = self.git_manager.get_last_committers(self.path) - # This is a dictionary keyed to the author's name (which can be many different + # This is a dictionary keyed to the author's name (which can be many # things, depending on the author) containing two fields, "email" and "count". It # is common for there to be multiple entries representing the same human being, # so a passing attempt is made to reconcile: diff --git a/src/Mod/AddonManager/addonmanager_devmode_validators.py b/src/Mod/AddonManager/addonmanager_devmode_validators.py index 680e57f768..baa5168944 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_validators.py +++ b/src/Mod/AddonManager/addonmanager_devmode_validators.py @@ -40,6 +40,8 @@ try: RegexWrapper = QRegularExpression RegexValidatorWrapper = QRegularExpressionValidator except ImportError: + QRegularExpressionValidator = None + QRegularExpression = None from PySide.QtGui import ( QRegExpValidator, ) @@ -163,4 +165,4 @@ class VersionValidator(QValidator): return semver_result if calver_result[0] == QValidator.Intermediate: return calver_result - return (QValidator.Invalid, value, position) + return QValidator.Invalid, value, position diff --git a/src/Mod/AddonManager/addonmanager_git.py b/src/Mod/AddonManager/addonmanager_git.py index 4f2042d8c6..ab3e6a665e 100644 --- a/src/Mod/AddonManager/addonmanager_git.py +++ b/src/Mod/AddonManager/addonmanager_git.py @@ -29,34 +29,15 @@ import os import platform import shutil import subprocess -from typing import List +from typing import List, Optional import time import FreeCAD -from PySide import QtCore # Needed to detect thread interruption - import addonmanager_utilities as utils translate = FreeCAD.Qt.translate -def initialize_git() -> object: - """If git is enabled, locate the git executable if necessary and return a new - GitManager object. The executable location is saved in user preferences for reuse, - and git can be disabled by setting the disableGit parameter in the Addons - preference group. Returns None if for any of those reasons we aren't using git.""" - - git_manager = None - pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") - disable_git = pref.GetBool("disableGit", False) - if not disable_git: - try: - git_manager = GitManager() - except NoGitFound: - pass - return git_manager - - class NoGitFound(RuntimeError): """Could not locate the git executable on this system.""" @@ -219,7 +200,7 @@ class GitManager: original_cwd = os.getcwd() - # Make sure we are not currently in that directory, otherwise on Windows the rename + # Make sure we are not currently in that directory, otherwise on Windows the "rename" # will fail. To guarantee we aren't in it, change to it, then shift up one. os.chdir(local_path) os.chdir("..") @@ -284,7 +265,7 @@ class GitManager: return branches def get_last_committers(self, local_path, n=10): - """Examine the last n entries of the commit history, and return a list of all of the + """Examine the last n entries of the commit history, and return a list of all the committers, their email addresses, and how many commits each one is responsible for.""" old_dir = os.getcwd() os.chdir(local_path) @@ -313,7 +294,7 @@ class GitManager: return result_dict def get_last_authors(self, local_path, n=10): - """Examine the last n entries of the commit history, and return a list of all of the + """Examine the last n entries of the commit history, and return a list of all the authors, their email addresses, and how many commits each one is responsible for.""" old_dir = os.getcwd() os.chdir(local_path) @@ -338,7 +319,7 @@ class GitManager: # Find git. In preference order # A) The value of the GitExecutable user preference # B) The executable located in the same bin directory as FreeCAD and called "git" - # C) The result of an shutil search for your system's "git" executable + # C) The result of a shutil search for your system's "git" executable prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") git_exe = prefs.GetString("GitExecutable", "Not set") if not git_exe or git_exe == "Not set" or not os.path.exists(git_exe): @@ -371,3 +352,20 @@ class GitManager: ) return proc.stdout + + +def initialize_git() -> Optional[GitManager]: + """If git is enabled, locate the git executable if necessary and return a new + GitManager object. The executable location is saved in user preferences for reuse, + and git can be disabled by setting the disableGit parameter in the Addons + preference group. Returns None if for any of those reasons we aren't using git.""" + + git_manager = None + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + disable_git = pref.GetBool("disableGit", False) + if not disable_git: + try: + git_manager = GitManager() + except NoGitFound: + pass + return git_manager diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 1c11f034b6..bffb9bf755 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -47,17 +47,13 @@ if FreeCAD.GuiUp: # Python urllib.request (if requests is not available). import NetworkManager # Requires an event loop, so is only available with the GUI else: - has_requests = False try: import requests - - has_requests = True except ImportError: - has_requests = False + requests = None import urllib.request import ssl - # @package AddonManager_utilities # \ingroup ADDONMANAGER # \brief Utilities to work across different platforms, providers and python versions @@ -99,7 +95,7 @@ def symlink(source, link_name): def rmdir(path: os.PathLike) -> bool: try: shutil.rmtree(path, onerror=remove_readonly) - except Exception: + except (WindowsError, PermissionError, OSError): return False return True @@ -214,10 +210,10 @@ def get_desc_regex(repo): """Returns a regex string that extracts a WB description to be displayed in the description panel of the Addon manager, if the README could not be found""" - parsedUrl = urlparse(repo.url) - if parsedUrl.netloc == "github.com": + parsed_url = urlparse(repo.url) + if parsed_url.netloc == "github.com": return r'' FreeCAD.Console.PrintLog( "Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", @@ -230,10 +226,10 @@ def get_desc_regex(repo): def get_readme_html_url(repo): """Returns the location of a html file containing readme""" - parsedUrl = urlparse(repo.url) - if parsedUrl.netloc == "github.com": + parsed_url = urlparse(repo.url) + if parsed_url.netloc == "github.com": return f"{repo.url}/blob/{repo.branch}/README.md" - if parsedUrl.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]: + if parsed_url.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]: return f"{repo.url}/-/blob/{repo.branch}/README.md" FreeCAD.Console.PrintLog( "Unrecognized git repo location '' -- guessing it is a GitLab instance..." @@ -358,8 +354,8 @@ def get_python_exe() -> str: E) The result of an shutil search for your system's "python" executable""" prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") python_exe = prefs.GetString("PythonExecutableForPip", "Not set") + fc_dir = FreeCAD.getHomePath() if not python_exe or python_exe == "Not set" or not os.path.exists(python_exe): - fc_dir = FreeCAD.getHomePath() python_exe = os.path.join(fc_dir, "bin", "python3") if "Windows" in platform.system(): python_exe += ".exe" @@ -409,7 +405,7 @@ def blocking_get(url: str, method=None) -> str: if FreeCAD.GuiUp and method is None or method == "networkmanager": NetworkManager.InitializeNetworkManager() p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url) - elif has_requests and method is None or method == "requests": + elif requests and method is None or method == "requests": response = requests.get(url) if response.status_code == 200: p = response.text @@ -420,18 +416,18 @@ def blocking_get(url: str, method=None) -> str: return p -def run_interruptable_subprocess(args) -> object: +def run_interruptable_subprocess(args) -> subprocess.CompletedProcess: """Wrap subprocess call so it can be interrupted gracefully.""" - creationflags = 0 + creation_flags = 0 if hasattr(subprocess, "CREATE_NO_WINDOW"): # Added in Python 3.7 -- only used on Windows - creationflags = subprocess.CREATE_NO_WINDOW + creation_flags = subprocess.CREATE_NO_WINDOW try: p = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - creationflags=creationflags, + creationflags=creation_flags, text=True, encoding="utf-8", ) @@ -447,13 +443,12 @@ def run_interruptable_subprocess(args) -> object: except subprocess.TimeoutExpired: if QtCore.QThread.currentThread().isInterruptionRequested(): p.kill() - stdout, stderr = p.communicate() - return_code = -1 raise ProcessInterrupted() if return_code is None or return_code != 0: raise subprocess.CalledProcessError(return_code, args, stdout, stderr) return subprocess.CompletedProcess(args, return_code, stdout, stderr) + def get_main_am_window(): windows = QtWidgets.QApplication.topLevelWidgets() for widget in windows: