Addon Manager: Pylint and Black cleanup
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user