Merge pull request #9068 from chennes/addonManagerExtractPysideInterface
Addon Manager: Extract PySide QtCore interface
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
@@ -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
|
||||
@@ -90,7 +91,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 +99,7 @@ class GitManager:
|
||||
+ str(e)
|
||||
+ "\n"
|
||||
)
|
||||
FreeCAD.Console.PrintWarning(
|
||||
fci.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Backing up the original directory and re-cloning",
|
||||
@@ -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())
|
||||
@@ -209,7 +212,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)
|
||||
@@ -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)
|
||||
|
||||
@@ -240,10 +242,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
|
||||
|
||||
@@ -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 = 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"
|
||||
@@ -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
|
||||
|
||||
@@ -361,7 +367,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:
|
||||
|
||||
60
src/Mod/AddonManager/addonmanager_pyside_interface.py
Normal file
60
src/Mod/AddonManager/addonmanager_pyside_interface.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# 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 *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""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
|
||||
@@ -21,21 +21,20 @@
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
""" 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
|
||||
|
||||
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,22 +43,23 @@ class InvalidAddon(RuntimeError):
|
||||
"""Raised when an object that cannot be uninstalled is passed to the constructor"""
|
||||
|
||||
|
||||
class AddonUninstaller(QtCore.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.
|
||||
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.
|
||||
|
||||
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):
|
||||
|
||||
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)
|
||||
@@ -68,8 +68,8 @@ class AddonUninstaller(QtCore.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):
|
||||
@@ -80,27 +80,25 @@ 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)
|
||||
# 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 = QtCore.Signal()
|
||||
# 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:
|
||||
@@ -128,18 +126,19 @@ class AddonUninstaller(QtCore.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...",
|
||||
@@ -147,9 +146,11 @@ class AddonUninstaller(QtCore.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
|
||||
@@ -164,7 +165,7 @@ class AddonUninstaller(QtCore.QObject):
|
||||
):
|
||||
try:
|
||||
os.unlink(stripped)
|
||||
FreeCAD.Console.PrintMessage(
|
||||
fci.Console.PrintMessage(
|
||||
translate(
|
||||
"AddonsInstaller", "Removed extra installed file {}"
|
||||
).format(stripped)
|
||||
@@ -173,43 +174,43 @@ class AddonUninstaller(QtCore.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(QtCore.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.
|
||||
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 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.
|
||||
success = QtCore.Signal(object)
|
||||
failure = QtCore.Signal(object, str)
|
||||
# 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 = QtCore.Signal()
|
||||
# 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")
|
||||
@@ -231,7 +232,7 @@ class MacroUninstaller(QtCore.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:
|
||||
@@ -256,8 +257,7 @@ class MacroUninstaller(QtCore.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:
|
||||
@@ -268,7 +268,8 @@ class MacroUninstaller(QtCore.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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user