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):