Addon Manager: Pylint and Black cleanup

This commit is contained in:
Chris Hennes
2023-03-26 19:01:33 -05:00
parent fcda1ffc25
commit a5ddf3d255
3 changed files with 85 additions and 76 deletions

View File

@@ -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

View File

@@ -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

View File

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