diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index 2d9b3e3376..7cad775fa4 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -26,6 +26,7 @@ SET(AddonManager_SRCS addonmanager_macro.py addonmanager_macro_parser.py addonmanager_metadata.py + addonmanager_pyside_interface.py addonmanager_update_all_gui.py addonmanager_uninstaller.py addonmanager_uninstaller_gui.py diff --git a/src/Mod/AddonManager/addonmanager_dependency_installer.py b/src/Mod/AddonManager/addonmanager_dependency_installer.py index 7ef1307c27..f066d527f4 100644 --- a/src/Mod/AddonManager/addonmanager_dependency_installer.py +++ b/src/Mod/AddonManager/addonmanager_dependency_installer.py @@ -28,8 +28,8 @@ import subprocess from typing import List import addonmanager_freecad_interface as fci +from addonmanager_pyside_interface import QObject, Signal, is_interruption_requested -from PySide import QtCore import addonmanager_utilities as utils from addonmanager_installer import AddonInstaller, MacroInstaller from Addon import Addon @@ -37,14 +37,14 @@ from Addon import Addon translate = fci.translate -class DependencyInstaller(QtCore.QObject): +class DependencyInstaller(QObject): """Install Python dependencies using pip. Intended to be instantiated and then moved into a QThread: connect the run() function to the QThread's started() signal.""" - no_python_exe = QtCore.Signal() - no_pip = QtCore.Signal(str) # Attempted command - failure = QtCore.Signal(str, str) # Short message, detailed message - finished = QtCore.Signal() + no_python_exe = Signal() + no_pip = Signal(str) # Attempted command + failure = Signal(str, str) # Short message, detailed message + finished = Signal() def __init__( self, @@ -69,9 +69,9 @@ class DependencyInstaller(QtCore.QObject): signal.""" if self._verify_pip(): if self.python_requires or self.python_optional: - if not QtCore.QThread.currentThread().isInterruptionRequested(): + if not is_interruption_requested(): self._install_python_packages() - if not QtCore.QThread.currentThread().isInterruptionRequested(): + if not is_interruption_requested(): self._install_addons() self.finished.emit() @@ -107,7 +107,7 @@ class DependencyInstaller(QtCore.QObject): signal is emitted and the function exits without proceeding with any additional installations.""" for pymod in self.python_requires: - if QtCore.QThread.currentThread().isInterruptionRequested(): + if is_interruption_requested(): return False try: proc = self._run_pip( @@ -136,7 +136,7 @@ class DependencyInstaller(QtCore.QObject): """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: - if QtCore.QThread.currentThread().isInterruptionRequested(): + if is_interruption_requested(): return try: proc = self._run_pip( @@ -179,7 +179,7 @@ class DependencyInstaller(QtCore.QObject): def _install_addons(self): for addon in self.addons: - if QtCore.QThread.currentThread().isInterruptionRequested(): + if is_interruption_requested(): return fci.Console.PrintMessage( translate( diff --git a/src/Mod/AddonManager/addonmanager_git.py b/src/Mod/AddonManager/addonmanager_git.py index ab3e6a665e..560d2517a0 100644 --- a/src/Mod/AddonManager/addonmanager_git.py +++ b/src/Mod/AddonManager/addonmanager_git.py @@ -31,11 +31,11 @@ import shutil import subprocess from typing import List, Optional import time -import FreeCAD import addonmanager_utilities as utils +import addonmanager_freecad_interface as fci -translate = FreeCAD.Qt.translate +translate = fci.translate class NoGitFound(RuntimeError): @@ -90,7 +90,7 @@ class GitManager: self._synchronous_call_git(["pull"]) self._synchronous_call_git(["submodule", "update", "--init", "--recursive"]) except GitFailed as e: - FreeCAD.Console.PrintWarning( + fci.Console.PrintWarning( translate( "AddonsInstaller", "Basic git update failed with the following message:", @@ -98,7 +98,7 @@ class GitManager: + str(e) + "\n" ) - FreeCAD.Console.PrintWarning( + fci.Console.PrintWarning( translate( "AddonsInstaller", "Backing up the original directory and re-cloning", @@ -209,7 +209,7 @@ class GitManager: try: self.clone(remote, local_path) except GitFailed as e: - FreeCAD.Console.PrintError( + fci.Console.PrintError( translate( "AddonsInstaller", "Failed to clone {} into {} using git" ).format(remote, local_path) @@ -240,10 +240,10 @@ class GitManager: if len(segments) == 3: result = segments[1] break - FreeCAD.Console.PrintWarning( + fci.Console.PrintWarning( "Error parsing the results from git remote -v show:\n" ) - FreeCAD.Console.PrintWarning(line + "\n") + fci.Console.PrintWarning(line + "\n") os.chdir(old_dir) return result @@ -320,10 +320,10 @@ class GitManager: # 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 a shutil search for your system's "git" executable - prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + prefs = fci.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): - fc_dir = FreeCAD.getHomePath() + fc_dir = fci.DataPaths().home_dir() git_exe = os.path.join(fc_dir, "bin", "git") if "Windows" in platform.system(): git_exe += ".exe" @@ -361,7 +361,7 @@ def initialize_git() -> Optional[GitManager]: 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") + pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons") disable_git = pref.GetBool("disableGit", False) if not disable_git: try: diff --git a/src/Mod/AddonManager/addonmanager_pyside_interface.py b/src/Mod/AddonManager/addonmanager_pyside_interface.py new file mode 100644 index 0000000000..a6c99a6603 --- /dev/null +++ b/src/Mod/AddonManager/addonmanager_pyside_interface.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2022 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD 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 * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +"""Wrap QtCore imports so that can be replaced when running outside of FreeCAD (e.g. for +unit tests, etc. Only provides wrappers for the things commonly used by the Addon +Manager.""" + +try: + from PySide import QtCore + QObject = QtCore.QObject + Signal = QtCore.Signal + + def is_interruption_requested() -> bool: + return QtCore.QThread.currentThread().isInterruptionRequested() + +except ImportError: + QObject = object + + class Signal: + """A purely synchronous signal. emit() does not use queued slots so cannot be + used across threads.""" + + def __init__(self, *args): + self.expected_types = args + self.connections = [] + + def connect(self, func): + self.connections.append(func) + + def disconnect(self, func): + if func in self.connections: + self.connections.remove(func) + + def emit(self, *args): + for connection in self.connections: + connection(args) + + def is_interruption_requested() -> bool: + return False diff --git a/src/Mod/AddonManager/addonmanager_uninstaller.py b/src/Mod/AddonManager/addonmanager_uninstaller.py index 7bb1d051b2..f3d11efb8c 100644 --- a/src/Mod/AddonManager/addonmanager_uninstaller.py +++ b/src/Mod/AddonManager/addonmanager_uninstaller.py @@ -28,14 +28,13 @@ details. """ import os from typing import List -import FreeCAD - -from PySide import QtCore +import addonmanager_freecad_interface as fci +from addonmanager_pyside_interface import QObject, Signal import addonmanager_utilities as utils from Addon import Addon -translate = FreeCAD.Qt.translate +translate = fci.translate # pylint: disable=too-few-public-methods @@ -44,7 +43,7 @@ class InvalidAddon(RuntimeError): """Raised when an object that cannot be uninstalled is passed to the constructor""" -class AddonUninstaller(QtCore.QObject): +class AddonUninstaller(QObject): """The core, non-GUI uninstaller class for non-macro addons. Usually instantiated and moved to its own thread, otherwise it will block the GUI (if the GUI is running) -- since all it does is delete files this is not a huge problem, but in some cases the Addon might be quite large, and @@ -59,7 +58,7 @@ class AddonUninstaller(QtCore.QObject): addon_to_remove = MyAddon() # Some class with 'name' attribute - self.worker_thread = QtCore.QThread() + self.worker_thread = QThread() self.uninstaller = AddonUninstaller(addon_to_remove) self.uninstaller.moveToThread(self.worker_thread) self.uninstaller.success.connect(self.removal_succeeded) @@ -83,12 +82,12 @@ class AddonUninstaller(QtCore.QObject): # Signals: success and failure # Emitted when the installation process is complete. The object emitted is the object that the # installation was requested for. - success = QtCore.Signal(object) - failure = QtCore.Signal(object, str) + success = Signal(object) + failure = Signal(object, str) # Finished: regardless of the outcome, this is emitted when all work that is going to be done # is done (i.e. whatever thread this is running in can quit). - finished = QtCore.Signal() + finished = Signal() def __init__(self, addon: object): """Initialize the uninstaller.""" @@ -185,7 +184,7 @@ class AddonUninstaller(QtCore.QObject): FreeCAD.Console.PrintWarning(str(e) + "\n") -class MacroUninstaller(QtCore.QObject): +class MacroUninstaller(QObject): """The core, non-GUI uninstaller class for macro addons. May be run directly on the GUI thread if desired, since macros are intended to be relatively small and shouldn't have too many files to delete. However, it is a QObject so may also be moved into a QThread -- see AddonUninstaller @@ -200,12 +199,12 @@ class MacroUninstaller(QtCore.QObject): # Signals: success and failure # Emitted when the removal process is complete. The object emitted is the object that the # removal was requested for. - success = QtCore.Signal(object) - failure = QtCore.Signal(object, str) + success = Signal(object) + failure = Signal(object, str) # Finished: regardless of the outcome, this is emitted when all work that is going to be done # is done (i.e. whatever thread this is running in can quit). - finished = QtCore.Signal() + finished = Signal() def __init__(self, addon): super().__init__() diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 5a7822994d..c31d03f54e 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -407,7 +407,7 @@ class PackageDetails(QtWidgets.QWidget): def set_change_branch_button_state(self): """The change branch button is only available for installed Addons that have a .git directory - and in runs where the GitPython import is available.""" + and in runs where the git is available.""" self.ui.buttonChangeBranch.hide()