From d8e6260d613f0adaecca260b0b807d10ca77954f Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 26 Mar 2023 18:45:01 -0500 Subject: [PATCH 1/2] Addon Manager: Extract PySide QtCore interface --- src/Mod/AddonManager/CMakeLists.txt | 1 + .../addonmanager_dependency_installer.py | 22 +++---- src/Mod/AddonManager/addonmanager_git.py | 20 +++---- .../addonmanager_pyside_interface.py | 59 +++++++++++++++++++ .../AddonManager/addonmanager_uninstaller.py | 25 ++++---- src/Mod/AddonManager/package_details.py | 2 +- 6 files changed, 94 insertions(+), 35 deletions(-) create mode 100644 src/Mod/AddonManager/addonmanager_pyside_interface.py 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() From 506b93a4c5a091902c3cbde7037d5850b188bb20 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 26 Mar 2023 19:01:33 -0500 Subject: [PATCH 2/2] Addon Manager: Pylint and Black cleanup --- src/Mod/AddonManager/addonmanager_git.py | 44 ++++--- .../addonmanager_pyside_interface.py | 3 +- .../AddonManager/addonmanager_uninstaller.py | 114 +++++++++--------- 3 files changed, 85 insertions(+), 76 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_git.py b/src/Mod/AddonManager/addonmanager_git.py index 560d2517a0..bd89f9f292 100644 --- a/src/Mod/AddonManager/addonmanager_git.py +++ b/src/Mod/AddonManager/addonmanager_git.py @@ -47,8 +47,9 @@ class GitFailed(RuntimeError): class GitManager: - """A class to manage access to git: mostly just provides a simple wrapper around the basic - command-line calls. Provides optional asynchronous access to clone and update.""" + """A class to manage access to git: mostly just provides a simple wrapper around + the basic command-line calls. Provides optional asynchronous access to clone and + update.""" def __init__(self): self.git_exe = None @@ -110,9 +111,10 @@ class GitManager: os.path.join(local_path, "ADDON_DISABLED"), "w", encoding="utf-8" ) as f: f.write( - "This is a backup of an addon that failed to update cleanly so was re-cloned. " - + "It was disabled by the Addon Manager's git update facility and can be " - + "safely deleted if the addon is working properly." + "This is a backup of an addon that failed to update cleanly so " + "was re-cloned. It was disabled by the Addon Manager's git update " + "facility and can be safely deleted if the addon is working " + "properly." ) os.chdir("..") os.rename(local_path, local_path + ".backup" + str(time.time())) @@ -193,15 +195,16 @@ class GitManager: return branch def repair(self, remote, local_path): - """Assumes that local_path is supposed to be a local clone of the given remote, and - ensures that it is. Note that any local changes in local_path will be destroyed. This - is achieved by archiving the old path, cloning an entirely new copy, and then deleting - the old directory.""" + """Assumes that local_path is supposed to be a local clone of the given + remote, and ensures that it is. Note that any local changes in local_path + will be destroyed. This is achieved by archiving the old path, cloning an + entirely new copy, and then deleting the old directory.""" original_cwd = os.getcwd() - # 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. + # 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("..") backup_path = local_path + ".backup" + str(time.time()) @@ -232,7 +235,6 @@ class GitManager: result = "(unknown remote)" for line in lines: if line.endswith("(fetch)"): - # The line looks like: # origin https://some/sort/of/path (fetch) @@ -265,8 +267,10 @@ 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 the - committers, their email addresses, and how many commits each one is responsible for.""" + """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) authors = self._synchronous_call_git(["log", f"-{n}", "--format=%cN"]).split( @@ -294,8 +298,10 @@ 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 the - authors, their email addresses, and how many commits each one is responsible for.""" + """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) authors = self._synchronous_call_git(["log", f"-{n}", "--format=%aN"]) @@ -318,12 +324,12 @@ class GitManager: def _find_git(self): # 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" + # B) The executable located in the same directory as FreeCAD and called "git" # C) The result of a shutil search for your system's "git" executable 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 = fci.DataPaths().home_dir() + fc_dir = fci.DataPaths().home_dir git_exe = os.path.join(fc_dir, "bin", "git") if "Windows" in platform.system(): git_exe += ".exe" @@ -349,7 +355,7 @@ class GitManager: f"Git returned a non-zero exit status: {e.returncode}\n" + f"Called with: {' '.join(final_args)}\n\n" + f"Returned stderr:\n{e.stderr}" - ) + ) from e return proc.stdout diff --git a/src/Mod/AddonManager/addonmanager_pyside_interface.py b/src/Mod/AddonManager/addonmanager_pyside_interface.py index a6c99a6603..74629e9c2e 100644 --- a/src/Mod/AddonManager/addonmanager_pyside_interface.py +++ b/src/Mod/AddonManager/addonmanager_pyside_interface.py @@ -22,11 +22,12 @@ # *************************************************************************** """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 +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 diff --git a/src/Mod/AddonManager/addonmanager_uninstaller.py b/src/Mod/AddonManager/addonmanager_uninstaller.py index f3d11efb8c..87e9ac0812 100644 --- a/src/Mod/AddonManager/addonmanager_uninstaller.py +++ b/src/Mod/AddonManager/addonmanager_uninstaller.py @@ -21,9 +21,9 @@ # * * # *************************************************************************** -""" Contains the classes to manage Addon removal: intended as a stable API, safe for external -code to call and to rely upon existing. See classes AddonUninstaller and MacroUninstaller for -details. """ +""" Contains the classes to manage Addon removal: intended as a stable API, safe for +external code to call and to rely upon existing. See classes AddonUninstaller and +MacroUninstaller for details.""" import os from typing import List @@ -44,15 +44,16 @@ class InvalidAddon(RuntimeError): 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 - deletion may take a non-trivial amount of time. + """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 deletion may take a + non-trivial amount of time. - In all cases in this class, the generic Python 'object' argument to the init function is - intended to be an Addon-like object that provides, at a minimum, a 'name' attribute. The Addon - manager uses the Addon class for this purpose, but external code may use any other class that - meets that criterion. + In all cases in this class, the generic Python 'object' argument to the init + function is intended to be an Addon-like object that provides, at a minimum, + a 'name' attribute. The Addon manager uses the Addon class for this purpose, + but external code may use any other class that meets that criterion. Recommended Usage (when running with the GUI up, so you don't block the GUI thread): @@ -67,8 +68,8 @@ class AddonUninstaller(QObject): self.worker_thread.started.connect(self.uninstaller.run) self.worker_thread.start() # Returns immediately - # On success, the connections above result in self.removal_succeeded being emitted, and - # on failure, self.removal_failed is emitted. + # On success, the connections above result in self.removal_succeeded being + emitted, and # on failure, self.removal_failed is emitted. Recommended non-GUI usage (blocks until complete): @@ -79,27 +80,25 @@ class AddonUninstaller(QObject): """ - # Signals: success and failure - # Emitted when the installation process is complete. The object emitted is the object that the - # installation was requested for. + # Signals: success and failure Emitted when the installation process is complete. + # The object emitted is the object that the installation was requested for. 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: 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 = Signal() - def __init__(self, addon: object): + def __init__(self, addon: Addon): """Initialize the uninstaller.""" super().__init__() self.addon_to_remove = addon - basedir = FreeCAD.getUserAppDataDir() - self.installation_path = os.path.join(basedir, "Mod") - self.macro_installation_path = FreeCAD.getUserMacroDir(True) + self.installation_path = fci.DataPaths().mod_dir + self.macro_installation_path = fci.DataPaths().macro_dir def run(self) -> bool: - """Remove an addon. Returns True if the addon was removed cleanly, or False if not. Emits - either success or failure prior to returning.""" + """Remove an addon. Returns True if the addon was removed cleanly, or False + if not. Emits either success or failure prior to returning.""" success = False error_message = translate("AddonsInstaller", "An unknown error occurred") if hasattr(self.addon_to_remove, "name") and self.addon_to_remove.name: @@ -127,18 +126,19 @@ class AddonUninstaller(QObject): self.failure.emit(self.addon_to_remove, error_message) self.addon_to_remove.set_status(Addon.Status.NOT_INSTALLED) self.finished.emit() + return success - def run_uninstall_script(self, path_to_remove): + @staticmethod + def run_uninstall_script(path_to_remove): """Run the addon's uninstaller.py script, if it exists""" uninstall_script = os.path.join(path_to_remove, "uninstall.py") if os.path.exists(uninstall_script): - print("Running script") + # pylint: disable=broad-exception-caught try: - with open(uninstall_script) as f: + with open(uninstall_script, encoding="utf-8") as f: exec(f.read()) - print("I think I ran OK") except Exception: - FreeCAD.Console.PrintError( + fci.Console.PrintError( translate( "AddonsInstaller", "Execution of Addon's uninstall.py script failed. Proceeding with uninstall...", @@ -146,9 +146,11 @@ class AddonUninstaller(QObject): + "\n" ) - def remove_extra_files(self, path_to_remove): - """When installing, an extra file called AM_INSTALLATION_DIGEST.txt may be created, listing - extra files that the installer put into place. Remove those files.""" + @staticmethod + def remove_extra_files(path_to_remove): + """When installing, an extra file called AM_INSTALLATION_DIGEST.txt may be + created, listing extra files that the installer put into place. Remove those + files.""" digest = os.path.join(path_to_remove, "AM_INSTALLATION_DIGEST.txt") if not os.path.exists(digest): return @@ -163,7 +165,7 @@ class AddonUninstaller(QObject): ): try: os.unlink(stripped) - FreeCAD.Console.PrintMessage( + fci.Console.PrintMessage( translate( "AddonsInstaller", "Removed extra installed file {}" ).format(stripped) @@ -172,43 +174,43 @@ class AddonUninstaller(QObject): except FileNotFoundError: pass # Great, no need to remove then! except OSError as e: - # Strange error to receive here, but just continue and print out an - # error to the console - FreeCAD.Console.PrintWarning( + # Strange error to receive here, but just continue and print + # out an error to the console + fci.Console.PrintWarning( translate( "AddonsInstaller", "Error while trying to remove extra installed file {}", ).format(stripped) + "\n" ) - FreeCAD.Console.PrintWarning(str(e) + "\n") + fci.Console.PrintWarning(str(e) + "\n") 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 - documentation for details of that implementation. + """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 documentation for details of that + implementation. - The Python object passed in is expected to provide a "macro" subobject, which itself is - required to provide at least a "filename" attribute, and may also provide an "icon", "xpm", - and/or "other_files" attribute. All filenames provided by those attributes are expected to be - relative to the installed location of the "filename" macro file (usually the main FreeCAD - user macros directory).""" + The Python object passed in is expected to provide a "macro" subobject, + which itself is required to provide at least a "filename" attribute, and may also + provide an "icon", "xpm", and/or "other_files" attribute. All filenames provided + by those attributes are expected to be relative to the installed location of the + "filename" macro file (usually the main FreeCAD user macros directory).""" - # Signals: success and failure - # Emitted when the removal process is complete. The object emitted is the object that the - # removal was requested for. + # Signals: success and failure Emitted when the removal process is complete. The + # object emitted is the object that the removal was requested for. 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: 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 = Signal() def __init__(self, addon): super().__init__() - self.installation_location = FreeCAD.getUserMacroDir(True) + self.installation_location = fci.DataPaths().macro_dir self.addon_to_remove = addon if ( not hasattr(self.addon_to_remove, "macro") @@ -230,7 +232,7 @@ class MacroUninstaller(QObject): directories.add(os.path.dirname(full_path)) try: os.unlink(full_path) - FreeCAD.Console.PrintLog(f"Removed macro file {full_path}\n") + fci.Console.PrintLog(f"Removed macro file {full_path}\n") except FileNotFoundError: pass # Great, no need to remove then! except OSError as e: @@ -255,8 +257,7 @@ class MacroUninstaller(QObject): def _get_files_to_remove(self) -> List[os.PathLike]: """Get the list of files that should be removed""" - files_to_remove = [] - files_to_remove.append(self.addon_to_remove.macro.filename) + files_to_remove = [self.addon_to_remove.macro.filename] if self.addon_to_remove.macro.icon: files_to_remove.append(self.addon_to_remove.macro.icon) if self.addon_to_remove.macro.xpm: @@ -267,7 +268,8 @@ class MacroUninstaller(QObject): files_to_remove.append(f) return files_to_remove - def _cleanup_directories(self, directories): + @staticmethod + def _cleanup_directories(directories): """Clean up any extra directories that are leftover and are empty""" for directory in directories: if os.path.isdir(directory):