From 46fc605fca8cd7ba3289383f1e345edeedb251d7 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 21 Aug 2022 14:00:26 -0500 Subject: [PATCH] Addon Manager: Refactoring and pylint cleanup --- src/Mod/AddonManager/Addon.py | 16 +- src/Mod/AddonManager/AddonManager.py | 4 +- .../AddonManagerTest/app/test_git.py | 3 +- .../AddonManagerTest/gui/test_gui.py | 5 +- .../gui/test_workers_installation.py | 179 +++++++++++++- .../gui/test_workers_startup.py | 231 +++++++++--------- .../gui/test_workers_utility.py | 17 +- src/Mod/AddonManager/NetworkManager.py | 4 +- src/Mod/AddonManager/TestAddonManagerGui.py | 2 +- src/Mod/AddonManager/addonmanager_git.py | 17 ++ .../AddonManager/addonmanager_utilities.py | 3 +- .../addonmanager_workers_installation.py | 72 ++---- .../addonmanager_workers_startup.py | 56 +++-- .../addonmanager_workers_utility.py | 5 +- src/Mod/AddonManager/package_details.py | 4 +- 15 files changed, 386 insertions(+), 232 deletions(-) diff --git a/src/Mod/AddonManager/Addon.py b/src/Mod/AddonManager/Addon.py index e3ae0ad44d..1393adc91a 100644 --- a/src/Mod/AddonManager/Addon.py +++ b/src/Mod/AddonManager/Addon.py @@ -462,9 +462,7 @@ class Addon: ) # Required path separator in the metadata.xml file to local separator _, file_extension = os.path.splitext(real_icon) - store = os.path.join( - self.cache_directory, "PackageMetadata" - ) + store = os.path.join(self.cache_directory, "PackageMetadata") self.cached_icon_filename = os.path.join( store, self.name, "cached_icon" + file_extension ) @@ -517,17 +515,13 @@ class Addon: def is_disabled(self): """Check to see if the disabling stopfile exists""" - stopfile = os.path.join( - self.mod_directory, self.name, "ADDON_DISABLED" - ) + stopfile = os.path.join(self.mod_directory, self.name, "ADDON_DISABLED") return os.path.exists(stopfile) def disable(self): """Disable this addon from loading when FreeCAD starts up by creating a stopfile""" - stopfile = os.path.join( - mod_directory, self.name, "ADDON_DISABLED" - ) + stopfile = os.path.join(mod_directory, self.name, "ADDON_DISABLED") with open(stopfile, "w", encoding="utf-8") as f: f.write( "The existence of this file prevents FreeCAD from loading this Addon. To re-enable, delete the file." @@ -536,9 +530,7 @@ class Addon: def enable(self): """Re-enable loading this addon by deleting the stopfile""" - stopfile = os.path.join( - self.mod_directory, self.name, "ADDON_DISABLED" - ) + stopfile = os.path.join(self.mod_directory, self.name, "ADDON_DISABLED") try: os.unlink(stopfile) except FileNotFoundError: diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index f3a7f0ad23..3ff0b08ece 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -702,7 +702,9 @@ class CommandAddonManager: if not self.update_cache: self.update_cache = True # Make sure to trigger the other cache updates, if the json file was missing self.create_addon_list_worker = CreateAddonListWorker() - self.create_addon_list_worker.status_message.connect(self.show_information) + self.create_addon_list_worker.status_message.connect( + self.show_information + ) self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo) self.update_progress_bar(10, 100) self.create_addon_list_worker.finished.connect( diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_git.py b/src/Mod/AddonManager/AddonManagerTest/app/test_git.py index 46bc937631..e70822e8d8 100644 --- a/src/Mod/AddonManager/AddonManagerTest/app/test_git.py +++ b/src/Mod/AddonManager/AddonManagerTest/app/test_git.py @@ -30,8 +30,6 @@ import time from zipfile import ZipFile import FreeCAD -from typing import Dict - from addonmanager_git import GitManager, NoGitFound, GitFailed @@ -89,6 +87,7 @@ class TestGit(unittest.TestCase): status = self.git.status(checkout_dir).strip() expected_status = "## HEAD (no branch)" self.assertEqual(status, expected_status) + self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started") def test_update(self): diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_gui.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_gui.py index bfc0676585..e78896939d 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/test_gui.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_gui.py @@ -25,9 +25,10 @@ import unittest import FreeCAD + class TestGui(unittest.TestCase): - MODULE = 'test_gui' # file name without extension + MODULE = "test_gui" # file name without extension def setUp(self): - pass \ No newline at end of file + pass diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_installation.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_installation.py index f86d44e105..637cf1cd93 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_installation.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_installation.py @@ -22,15 +22,190 @@ # * * # *************************************************************************** -import unittest +import json import os +import shutil +import stat import tempfile +import unittest import FreeCAD +from addonmanager_git import initialize_git + +from PySide2 import QtCore + +import NetworkManager +from Addon import Addon +from addonmanager_workers_startup import ( + CreateAddonListWorker, + UpdateChecker, +) +from addonmanager_workers_installation import InstallWorkbenchWorker class TestWorkersInstallation(unittest.TestCase): MODULE = "test_workers_installation" # file name without extension + addon_list = ( + [] + ) # Cache at the class level so only the first test has to download it + def setUp(self): - pass \ No newline at end of file + """Set up the test""" + self.test_dir = os.path.join( + FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data" + ) + + self.saved_mod_directory = Addon.mod_directory + self.saved_cache_directory = Addon.cache_directory + Addon.mod_directory = os.path.join( + tempfile.gettempdir(), "FreeCADTesting", "Mod" + ) + Addon.cache_directory = os.path.join( + tempfile.gettempdir(), "FreeCADTesting", "Cache" + ) + + os.makedirs(Addon.mod_directory, mode=0o777, exist_ok=True) + os.makedirs(Addon.cache_directory, mode=0o777, exist_ok=True) + + url = "https://api.github.com/zen" + NetworkManager.InitializeNetworkManager() + result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url) + if result is None: + self.skipTest("No active internet connection detected") + + self.macro_counter = 0 + self.workbench_counter = 0 + self.prefpack_counter = 0 + self.addon_from_cache_counter = 0 + self.macro_from_cache_counter = 0 + + self.package_cache = {} + self.macro_cache = [] + + self.package_cache_filename = os.path.join( + Addon.cache_directory, "packages.json" + ) + self.macro_cache_filename = os.path.join(Addon.cache_directory, "macros.json") + + if not TestWorkersInstallation.addon_list: + self._create_addon_list() + + # Workbench: use the FreeCAD-Help workbench for testing purposes + self.help_addon = None + for addon in self.addon_list: + if addon.name == "Help": + self.help_addon = addon + break + if not self.help_addon: + print("Unable to locate the FreeCAD-Help addon to test with") + self.skipTest("No active internet connection detected") + + # Store the user's preference for whether git is enabled or disabled + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + self.saved_git_disabled_status = pref.GetBool("disableGit", False) + + def tearDown(self): + mod_dir = os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod") + if os.path.exists(mod_dir): + self._rmdir(mod_dir) + macro_dir = os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod") + if os.path.exists(macro_dir): + self._rmdir(macro_dir) + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + pref.SetBool("disableGit", self.saved_git_disabled_status) + + def test_workbench_installation(self): + addon_location = os.path.join( + tempfile.gettempdir(), "FreeCADTesting", "Mod", self.help_addon.name + ) + worker = InstallWorkbenchWorker(self.help_addon, addon_location) + worker.run() # Synchronous call, blocks until complete + self.assertTrue(os.path.exists(addon_location)) + self.assertTrue(os.path.exists(os.path.join(addon_location, "package.xml"))) + + def test_workbench_installation_git_disabled(self): + """If the testing user has git enabled, also test the addon manager with git disabled""" + if self.saved_git_disabled_status: + self.skipTest("Git is disabled, this test is redundant") + + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + pref.SetBool("disableGit", True) + + self.test_workbench_installation() + + pref.SetBool("disableGit", False) + + def test_workbench_update_checker(self): + + git_manager = initialize_git() + + if not git_manager: + return + + # Workbench: use the FreeCAD-Help workbench for testing purposes + help_addon = None + for addon in self.addon_list: + if addon.name == "Help": + help_addon = addon + break + if not help_addon: + print("Unable to locate the FreeCAD-Help addon to test with") + return + + addon_location = os.path.join( + tempfile.gettempdir(), "FreeCADTesting", "Mod", self.help_addon.name + ) + worker = InstallWorkbenchWorker(addon, addon_location) + worker.run() # Synchronous call, blocks until complete + self.assertEqual(help_addon.status(), Addon.Status.PENDING_RESTART) + + # Back up one revision + git_manager.reset(addon_location, ["--hard", "HEAD~1"]) + + # At this point the addon should be "out of date", checked out to one revision behind + # the most recent. + + worker = UpdateChecker() + worker.override_mod_directory( + os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod") + ) + worker.check_workbench(help_addon) # Synchronous call + self.assertEqual(help_addon.status(), Addon.Status.UPDATE_AVAILABLE) + + # Now try to "update" it (which is really done via the install worker) + worker = InstallWorkbenchWorker(addon, addon_location) + worker.run() # Synchronous call, blocks until complete + self.assertEqual(help_addon.status(), Addon.Status.PENDING_RESTART) + + def _rmdir(self, path): + try: + shutil.rmtree(path, onerror=self._remove_readonly) + except Exception as e: + print(e) + + def _remove_readonly(self, func, path, _) -> None: + """Remove a read-only file.""" + + os.chmod(path, stat.S_IWRITE) + func(path) + + def _create_addon_list(self): + """Create the list of addons""" + worker = CreateAddonListWorker() + worker.addon_repo.connect(self._addon_added) + worker.start() + while worker.isRunning(): + QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) + QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) + + def _addon_added(self, addon: Addon): + """Callback for adding an Addon: tracks the list, and counts the various types""" + print(f"Addon added: {addon.name}") + TestWorkersInstallation.addon_list.append(addon) + if addon.contains_workbench(): + self.workbench_counter += 1 + if addon.contains_macro(): + self.macro_counter += 1 + if addon.contains_preference_pack(): + self.prefpack_counter += 1 diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_startup.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_startup.py index 90e1d7a3e5..0043741529 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_startup.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_startup.py @@ -27,11 +27,7 @@ import unittest import os import tempfile -have_git = True -try: - import git -except ImportError: - have_git = False +from addonmanager_git import initialize_git import FreeCAD @@ -44,24 +40,31 @@ from addonmanager_workers_startup import ( LoadPackagesFromCacheWorker, LoadMacrosFromCacheWorker, CheckSingleUpdateWorker, - ) +) from addonmanager_workers_installation import ( InstallWorkbenchWorker, - ) +) + class TestWorkersStartup(unittest.TestCase): MODULE = "test_workers_startup" # file name without extension def setUp(self): - """ Set up the test """ - self.test_dir = os.path.join(FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data") - + """Set up the test""" + self.test_dir = os.path.join( + FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data" + ) + self.saved_mod_directory = Addon.mod_directory self.saved_cache_directory = Addon.cache_directory - Addon.mod_directory = os.path.join(tempfile.gettempdir(),"FreeCADTesting","Mod") - Addon.cache_directory = os.path.join(tempfile.gettempdir(),"FreeCADTesting","Cache") + Addon.mod_directory = os.path.join( + tempfile.gettempdir(), "FreeCADTesting", "Mod" + ) + Addon.cache_directory = os.path.join( + tempfile.gettempdir(), "FreeCADTesting", "Cache" + ) os.makedirs(Addon.mod_directory, mode=0o777, exist_ok=True) os.makedirs(Addon.cache_directory, mode=0o777, exist_ok=True) @@ -78,23 +81,29 @@ class TestWorkersStartup(unittest.TestCase): self.prefpack_counter = 0 self.addon_from_cache_counter = 0 self.macro_from_cache_counter = 0 - - # Populated when the addon list is created in the first test + self.package_cache = {} self.macro_cache = [] - self.package_cache_filename = os.path.join(Addon.cache_directory,"packages.json") - self.macro_cache_filename = os.path.join(Addon.cache_directory,"macros.json") + self.package_cache_filename = os.path.join( + Addon.cache_directory, "packages.json" + ) + self.macro_cache_filename = os.path.join(Addon.cache_directory, "macros.json") + + # Store the user's preference for whether git is enabled or disabled + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + self.saved_git_disabled_status = pref.GetBool("disableGit", False) def tearDown(self): - """ Tear down the test """ + """Tear down the test""" Addon.mod_directory = self.saved_mod_directory Addon.cache_directory = self.saved_cache_directory + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + pref.SetBool("disableGit", self.saved_git_disabled_status) def test_create_addon_list_worker(self): - """ Test whether any addons are added: runs the full query, so this potentially is a SLOW - test. Note that this test must be run before any of the other tests, so that the cache gets - created. """ + """Test whether any addons are added: runs the full query, so this potentially is a SLOW + test.""" worker = CreateAddonListWorker() worker.addon_repo.connect(self._addon_added) worker.start() @@ -102,21 +111,94 @@ class TestWorkersStartup(unittest.TestCase): QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) - self.assertGreater(self.macro_counter,0, "No macros returned") - self.assertGreater(self.workbench_counter,0, "No workbenches returned") - self.assertGreater(self.prefpack_counter,0, "No preference packs returned") + self.assertGreater(self.macro_counter, 0, "No macros returned") + self.assertGreater(self.workbench_counter, 0, "No workbenches returned") + + # Make sure there are no duplicates: + addon_name_set = set() + for addon in self.addon_list: + addon_name_set.add(addon.name) + self.assertEqual( + len(addon_name_set), len(self.addon_list), "Duplicate names are not allowed" + ) # Write the cache data if hasattr(self, "package_cache"): - with open(self.package_cache_filename,"w",encoding="utf-8") as f: + with open(self.package_cache_filename, "w", encoding="utf-8") as f: f.write(json.dumps(self.package_cache, indent=" ")) if hasattr(self, "macro_cache"): - with open(self.macro_cache_filename,"w",encoding="utf-8") as f: + with open(self.macro_cache_filename, "w", encoding="utf-8") as f: f.write(json.dumps(self.macro_cache, indent=" ")) - def _addon_added(self, addon:Addon): - """ Callback for adding an Addon: tracks the list, and counts the various types """ - print (f"Addon Test: {addon.name}") + original_macro_counter = self.macro_counter + original_workbench_counter = self.workbench_counter + original_addon_list = self.addon_list.copy() + self.macro_counter = 0 + self.workbench_counter = 0 + self.addon_list.clear() + + # Now try loading the same data from the cache we just created + worker = LoadPackagesFromCacheWorker(self.package_cache_filename) + worker.override_metadata_cache_path( + os.path.join(Addon.cache_directory, "PackageMetadata") + ) + worker.addon_repo.connect(self._addon_added) + + worker.start() + while worker.isRunning(): + QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) + QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) + + worker = LoadMacrosFromCacheWorker(self.macro_cache_filename) + worker.add_macro_signal.connect(self._addon_added) + + worker.start() + while worker.isRunning(): + QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) + QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) + + # Make sure that every addon in the original list is also in the new list + fail_counter = 0 + for original_addon in original_addon_list: + found = False + for addon in self.addon_list: + if addon.name == original_addon.name: + found = True + break + if not found: + print(f"Failed to load {addon.name} from cache") + fail_counter += 1 + self.assertEqual(fail_counter, 0) + + # Make sure there are no duplicates: + addon_name_set.clear() + for addon in self.addon_list: + addon_name_set.add(addon.name) + + self.assertEqual(len(addon_name_set), len(self.addon_list)) + self.assertEqual(len(original_addon_list), len(self.addon_list)) + + self.assertEqual( + original_macro_counter, + self.macro_counter, + "Cache loaded a different number of macros", + ) + # We can't check workbench and preference pack counting at this point, because that relies + # on the package.xml metadata file, which this test does not download. + + def test_create_addon_list_git_disabled(self): + """If the user has git enabled, also test the addon manager with git disabled""" + if self.saved_git_disabled_status: + self.skipTest("Git is disabled, this test is redundant") + + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + pref.SetBool("disableGit", True) + + self.test_create_addon_list_worker() + + def _addon_added(self, addon: Addon): + """Callback for adding an Addon: tracks the list, and counts the various types""" + print(f"Addon added: {addon.name}") self.addon_list.append(addon) if addon.contains_workbench(): self.workbench_counter += 1 @@ -126,94 +208,7 @@ class TestWorkersStartup(unittest.TestCase): self.prefpack_counter += 1 # Also record the information for cache purposes - self.package_cache[addon.name] = addon.to_cache() - - if addon.macro is not None: + if addon.macro is None: + self.package_cache[addon.name] = addon.to_cache() + else: self.macro_cache.append(addon.macro.to_cache()) - - def test_load_packages_from_cache_worker(self): - """ Test loading packages from the cache """ - worker = LoadPackagesFromCacheWorker(self.package_cache_filename) - worker.override_metadata_cache_path(os.path.join(Addon.cache_directory,"PackageMetadata")) - worker.addon_repo.connect(self._addon_added_from_cache) - self.addon_from_cache_counter = 0 - - worker.start() - while worker.isRunning(): - QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) - QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) - - self.assertGreater(self.addon_from_cache_counter,0, "No addons in the cache") - - def _addon_added_from_cache(self, addon:Addon): - """ Callback when addon added from cache """ - print (f"Addon Cache Test: {addon.name}") - self.addon_from_cache_counter += 1 - - def test_load_macros_from_cache_worker(self): - """ Test loading macros from the cache """ - worker = LoadMacrosFromCacheWorker(self.macro_cache_filename) - worker.add_macro_signal.connect(self._macro_added_from_cache) - self.macro_from_cache_counter = 0 - - worker.start() - while worker.isRunning(): - QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) - QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) - - self.assertGreater(self.macro_from_cache_counter,0, "No macros in the cache") - - def _macro_added_from_cache(self, addon:Addon): - """ Callback for adding macros from the cache """ - print (f"Macro Cache Test: {addon.name}") - self.macro_from_cache_counter += 1 - - def test_update_checker(self): - """ Test the code that checks a single addon for available updates. """ - - if not have_git: - return - - # Populate the test's Addon List: - self.test_create_addon_list_worker() - - # First, install a specific Addon of each kind into a temp location - location = os.path.join(tempfile.gettempdir(),"FreeCADTesting") - - self._test_workbench_update_checker(location) - - - - # Preference Pack - # Macro - - # Arrange for those addons to be out-of-date - - # Check for updates - - def _test_workbench_update_checker(self, location): - - # Workbench: use the FreeCAD-Help workbench for testing purposes - help_addon = None - for addon in self.addon_list: - if addon.name == "Help": - help_addon = addon - break - if not help_addon: - print("Unable to locate the FreeCAD-Help addon to test with") - return - - addon_location = os.path.join(location, help_addon.name) - worker = InstallWorkbenchWorker(addon, addon_location) - worker.run() # Synchronous call, blocks until complete - gitrepo = git.Git(addon_location) - gitrepo.reset("--hard", "HEAD^") - print (addon_location) - - # At this point the addon should be "out of date", checked out to one revision behind - # the most recent. - - worker = CheckSingleUpdateWorker(help_addon) - worker.do_work() # Synchronous call - - self.assertEqual(help_addon.status(), Addon.Status.UPDATE_AVAILABLE) \ No newline at end of file diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_utility.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_utility.py index 45890c76e8..b5b174a42b 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_utility.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_utility.py @@ -30,12 +30,15 @@ from PySide2 import QtCore import NetworkManager + class TestWorkersUtility(unittest.TestCase): MODULE = "test_workers_utility" # file name without extension def setUp(self): - self.test_dir = os.path.join(FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data") + self.test_dir = os.path.join( + FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data" + ) self.last_result = None url = "https://api.github.com/zen" @@ -45,7 +48,7 @@ class TestWorkersUtility(unittest.TestCase): self.skipTest("No active internet connection detected") def test_connection_checker_basic(self): - """ Tests the connection checking worker's basic operation: does not exit until worker thread completes """ + """Tests the connection checking worker's basic operation: does not exit until worker thread completes""" worker = ConnectionChecker() worker.success.connect(self.connection_succeeded) worker.failure.connect(self.connection_failed) @@ -54,8 +57,8 @@ class TestWorkersUtility(unittest.TestCase): while worker.isRunning(): QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) - self.assertEqual(self.last_result,"SUCCESS") - + self.assertEqual(self.last_result, "SUCCESS") + def test_connection_checker_thread_interrupt(self): worker = ConnectionChecker() worker.success.connect(self.connection_succeeded) @@ -66,8 +69,10 @@ class TestWorkersUtility(unittest.TestCase): while worker.isRunning(): QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) - self.assertIsNone(self.last_result, "Requesting interruption of thread failed to interrupt") - + self.assertIsNone( + self.last_result, "Requesting interruption of thread failed to interrupt" + ) + def connection_succeeded(self): self.last_result = "SUCCESS" diff --git a/src/Mod/AddonManager/NetworkManager.py b/src/Mod/AddonManager/NetworkManager.py index b28813bf67..140bf515cb 100644 --- a/src/Mod/AddonManager/NetworkManager.py +++ b/src/Mod/AddonManager/NetworkManager.py @@ -156,7 +156,9 @@ if HAVE_QTNETWORK: # Make sure we exit nicely on quit if QtCore.QCoreApplication.instance() is not None: - QtCore.QCoreApplication.instance().aboutToQuit.connect(self.__aboutToQuit) + QtCore.QCoreApplication.instance().aboutToQuit.connect( + self.__aboutToQuit + ) # Create the QNAM on this thread: self.QNAM = QtNetwork.QNetworkAccessManager() diff --git a/src/Mod/AddonManager/TestAddonManagerGui.py b/src/Mod/AddonManager/TestAddonManagerGui.py index de864d0974..7cb913b305 100644 --- a/src/Mod/AddonManager/TestAddonManagerGui.py +++ b/src/Mod/AddonManager/TestAddonManagerGui.py @@ -40,4 +40,4 @@ from AddonManagerTest.gui.test_workers_installation import ( False if AddonManagerTestGui.__name__ else True False if AddonManagerTestWorkersUtility.__name__ else True False if AddonManagerTestWorkersStartup.__name__ else True -False if AddonManagerTestWorkersInstallation.__name__ else True \ No newline at end of file +False if AddonManagerTestWorkersInstallation.__name__ else True diff --git a/src/Mod/AddonManager/addonmanager_git.py b/src/Mod/AddonManager/addonmanager_git.py index e297457b3e..240efd7694 100644 --- a/src/Mod/AddonManager/addonmanager_git.py +++ b/src/Mod/AddonManager/addonmanager_git.py @@ -35,6 +35,23 @@ import FreeCAD translate = FreeCAD.Qt.translate +def initialize_git() -> object: + """If git is enabled, locate the git executable if necessary and return a new + GitManager object. The executable location is saved in user preferences for reuse, + and git can be disabled by setting the disableGit parameter in the Addons + 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") + disable_git = pref.GetBool("disableGit", False) + if not disable_git: + try: + git_manager = GitManager() + except NoGitFound: + pass + return git_manager + + class NoGitFound(RuntimeError): """Could not locate the git executable on this system.""" diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index d7bcaa2e52..bbe0911a2b 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -382,8 +382,9 @@ def get_python_exe() -> str: prefs.SetString("PythonExecutableForPip", python_exe) return python_exe + def get_cache_file_name(file: str) -> str: cache_path = FreeCAD.getUserCachePath() am_path = os.path.join(cache_path, "AddonManager") os.makedirs(am_path, exist_ok=True) - return os.path.join(am_path, file) \ No newline at end of file + return os.path.join(am_path, file) diff --git a/src/Mod/AddonManager/addonmanager_workers_installation.py b/src/Mod/AddonManager/addonmanager_workers_installation.py index a683f22546..4fe222171e 100644 --- a/src/Mod/AddonManager/addonmanager_workers_installation.py +++ b/src/Mod/AddonManager/addonmanager_workers_installation.py @@ -49,17 +49,7 @@ import addonmanager_utilities as utils from addonmanager_macro import Macro from Addon import Addon import NetworkManager -from addonmanager_git import GitManager, GitFailed, NoGitFound - - -git_manager = None -pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") -disable_git = pref.GetBool("disableGit", False) -if not disable_git: - try: - git_manager = GitManager() - except NoGitFound: - pass # A log messsage was already printed by the startup code +from addonmanager_git import initialize_git translate = FreeCAD.Qt.translate @@ -95,33 +85,26 @@ class InstallWorkbenchWorker(QtCore.QThread): if not os.path.exists(self.clone_directory): os.makedirs(self.clone_directory) + self.git_manager = initialize_git() + def run(self): "installs or updates the selected addon" if not self.repo: return - if not git_manager: + if not self.git_manager: FreeCAD.Console.PrintLog( translate( "AddonsInstaller", - "GitPython not found. Using ZIP file download instead.", + "Git disabled - using ZIP file download instead.", ) + "\n" ) - if not have_zip: - FreeCAD.Console.PrintError( - translate( - "AddonsInstaller", - "Your version of Python doesn't appear to support ZIP files. Unable to proceed.", - ) - + "\n" - ) - return target_dir = self.clone_directory - if git_manager: + if self.git_manager: # Do the git process... self.run_git(target_dir) else: @@ -135,6 +118,8 @@ class InstallWorkbenchWorker(QtCore.QThread): return QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) + self.repo.set_status(Addon.Status.PENDING_RESTART) + def update_status(self) -> None: if hasattr(self, "git_progress") and self.isRunning(): self.progress_made.emit(self.git_progress.current, self.git_progress.total) @@ -142,18 +127,16 @@ class InstallWorkbenchWorker(QtCore.QThread): def run_git(self, clonedir: str) -> None: - if not git_manager: + if not self.git_manager: FreeCAD.Console.PrintLog( translate( "AddonsInstaller", - "No Git Python installed, skipping git operations", + "Git disabled, skipping git operations", ) + "\n" ) return - self.git_progress = GitProgressMonitor() - if os.path.exists(clonedir): self.run_git_update(clonedir) else: @@ -163,9 +146,9 @@ class InstallWorkbenchWorker(QtCore.QThread): self.status_message.emit("Updating module...") with self.repo.git_lock: if not os.path.exists(clonedir + os.sep + ".git"): - git_manager.repair(self.repo.url, clonedir) + self.git_manager.repair(self.repo.url, clonedir) try: - git_manager.update(clonedir) + self.git_manager.update(clonedir) if self.repo.contains_workbench(): answer = translate( "AddonsInstaller", @@ -207,7 +190,7 @@ class InstallWorkbenchWorker(QtCore.QThread): with self.repo.git_lock: FreeCAD.Console.PrintMessage("Lock acquired...\n") - git_manager.clone(self.repo.url, clonedir) + self.git_manager.clone(self.repo.url, clonedir) FreeCAD.Console.PrintMessage("Initial clone complete...\n") if current_thread.isInterruptionRequested(): return @@ -706,30 +689,6 @@ class UpdateMetadataCacheWorker(QtCore.QThread): repo.cached_icon_filename = cache_file -if git_manager: - - class GitProgressMonitor: - """An object that receives git progress updates and stores them for later display""" - - def __init__(self): - self.current = 0 - self.total = 100 - self.message = "" - - def update( - self, - _: int, - cur_count: Union[str, float], - max_count: Union[str, float, None] = None, - message: str = "", - ) -> None: - if max_count: - self.current = int(cur_count) - self.total = int(max_count) - if message: - self.message = message - - class UpdateAllWorker(QtCore.QThread): """Update all listed packages, of any kind""" @@ -804,9 +763,10 @@ class UpdateSingleWorker(QtCore.QThread): success = QtCore.Signal(Addon) failure = QtCore.Signal(Addon) - def __init__(self, repo_queue: queue.Queue): + def __init__(self, repo_queue: queue.Queue, location=None): super().__init__() self.repo_queue = repo_queue + self.location = location def run(self): current_thread = QtCore.QThread.currentThread() @@ -858,7 +818,7 @@ class UpdateSingleWorker(QtCore.QThread): def update_package(self, repo: Addon): """Updating a package re-uses the package installation worker, so actually spawns another thread that we block on""" - worker = InstallWorkbenchWorker(repo) + worker = InstallWorkbenchWorker(repo, location=self.location) worker.success.connect(lambda repo, _: self.success.emit(repo)) worker.failure.connect(lambda repo, _: self.failure.emit(repo)) worker.start() diff --git a/src/Mod/AddonManager/addonmanager_workers_startup.py b/src/Mod/AddonManager/addonmanager_workers_startup.py index 012cbaf708..192131bdf6 100644 --- a/src/Mod/AddonManager/addonmanager_workers_startup.py +++ b/src/Mod/AddonManager/addonmanager_workers_startup.py @@ -40,21 +40,10 @@ import addonmanager_utilities as utils from addonmanager_macro import Macro from Addon import Addon import NetworkManager -from addonmanager_git import GitManager, GitFailed, NoGitFound +from addonmanager_git import initialize_git translate = FreeCAD.Qt.translate -git_manager = None -pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") -disable_git = pref.GetBool("disableGit", False) -if not disable_git: - try: - git_manager = GitManager() - except NoGitFound: - FreeCAD.Console.PrintWarning( - translate("AddonsInstaller", "Could not locate a suitable git executable") - ) - # Workers only have one public method by design # pylint: disable=too-few-public-methods @@ -83,6 +72,8 @@ class CreateAddonListWorker(QtCore.QThread): self.moddir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod") self.current_thread = None + self.git_manager = initialize_git() + def run(self): "populates the list of addons" @@ -262,12 +253,12 @@ class CreateAddonListWorker(QtCore.QThread): macro_cache_location = utils.get_cache_file_name("Macros") - if not git_manager: + if not self.git_manager: message = translate( "AddonsInstaller", - "Failed to execute git command: check installation of git", + "Git is disabled, skipping git macros", ) - self.status_message_signal.emit(message) + self.status_message.emit(message) FreeCAD.Console.PrintWarning(message + "\n") return @@ -290,6 +281,12 @@ class CreateAddonListWorker(QtCore.QThread): return if filename.lower().endswith(".fcmacro"): macro = Macro(filename[:-8]) # Remove ".FCMacro". + if macro.name in self.package_names: + FreeCAD.Console.PrintLog( + f"Ignoring second macro named {macro.name} (found on git)\n" + ) + continue # We already have a macro with this name + self.package_names.append(macro.name) macro.on_git = True macro.src_filename = os.path.join(dirpath, filename) macro.fill_details_from_file(macro.src_filename) @@ -310,13 +307,13 @@ class CreateAddonListWorker(QtCore.QThread): "Attempting to change non-git Macro setup to use git\n", ) ) - git_manager.repair( + self.git_manager.repair( "https://github.com/FreeCAD/FreeCAD-macros.git", macro_cache_location, ) - git_manager.update(macro_cache_location) + self.git_manager.update(macro_cache_location) else: - git_manager.clone( + self.git_manager.clone( "https://github.com/FreeCAD/FreeCAD-macros.git", macro_cache_location, ) @@ -335,7 +332,7 @@ class CreateAddonListWorker(QtCore.QThread): ) try: shutil.rmtree(macro_cache_location, onerror=self._remove_readonly) - git_manager.clone( + self.git_manager.clone( "https://github.com/FreeCAD/FreeCAD-macros.git", macro_cache_location, ) @@ -394,6 +391,12 @@ class CreateAddonListWorker(QtCore.QThread): ): macro_names.append(macname) macro = Macro(macname) + if macro.name in self.package_names: + FreeCAD.Console.PrintLog( + f"Ignoring second macro named {macro.name} (found on wiki)\n" + ) + continue # We already have a macro with this name + self.package_names.append(macro.name) macro.on_wiki = True macro.parsed = False repo = Addon.from_macro(macro) @@ -548,6 +551,7 @@ class UpdateChecker: def __init__(self): self.basedir = FreeCAD.getUserAppDataDir() self.moddir = os.path.join(self.basedir, "Mod") + self.git_manager = initialize_git() def override_mod_directory(self, moddir): """Primarily for use when testing, sets an alternate directory to use for mods""" @@ -556,7 +560,7 @@ class UpdateChecker: def check_workbench(self, wb): """Given a workbench Addon wb, check it for updates using git. If git is not available, does nothing.""" - if not git_manager: + if not self.git_manager: wb.set_status(Addon.Status.CANNOT_CHECK) return clonedir = os.path.join(self.moddir, wb.name) @@ -564,15 +568,15 @@ class UpdateChecker: # mark as already installed AND already checked for updates if not os.path.exists(os.path.join(clonedir, ".git")): with wb.git_lock: - git_manager.repair(wb.url, clonedir) + self.git_manager.repair(wb.url, clonedir) with wb.git_lock: try: - status = git_manager.status(clonedir) - if "(no branch)" in git_manager.status(clonedir): + status = self.git_manager.status(clonedir) + if "(no branch)" in self.git_manager.status(clonedir): # By definition, in a detached-head state we cannot # update, so don't even bother checking. wb.set_status(Addon.Status.NO_UPDATE_AVAILABLE) - wb.branch = git_manager.current_branch(clonedir) + wb.branch = self.git_manager.current_branch(clonedir) return except GitFailed as e: FreeCAD.Console.PrintWarning( @@ -587,7 +591,7 @@ class UpdateChecker: wb.set_status(Addon.Status.CANNOT_CHECK) else: try: - if git_manager.update_available(clonedir): + if self.git_manager.update_available(clonedir): wb.set_status(Addon.Status.UPDATE_AVAILABLE) else: wb.set_status(Addon.Status.NO_UPDATE_AVAILABLE) @@ -609,7 +613,7 @@ class UpdateChecker: if os.path.exists(clonedir): # First, try to just do a git-based update, which will give the most accurate results: - if git_manager: + if self.git_manager: self.check_workbench(package) if package.status() != Addon.Status.CANNOT_CHECK: # It worked, just exit now diff --git a/src/Mod/AddonManager/addonmanager_workers_utility.py b/src/Mod/AddonManager/addonmanager_workers_utility.py index 12a7700338..1f612ab842 100644 --- a/src/Mod/AddonManager/addonmanager_workers_utility.py +++ b/src/Mod/AddonManager/addonmanager_workers_utility.py @@ -27,6 +27,7 @@ import FreeCAD from PySide2 import QtCore import NetworkManager + class ConnectionChecker(QtCore.QThread): """A worker thread for checking the connection to GitHub as a proxy for overall network connectivity. It has two signals: success() and failure(str). The failure @@ -59,8 +60,8 @@ class ConnectionChecker(QtCore.QThread): self.success.emit() def check_network_connection(self) -> Optional[str]: - """ The main work of this object: returns the decoded result of the connection request, or - None if the request failed """ + """The main work of this object: returns the decoded result of the connection request, or + None if the request failed""" url = "https://api.github.com/zen" result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url) if result: diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 7c1075e0e4..b916a445c8 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -170,7 +170,7 @@ class PackageDetails(QWidget): self.display_repo_status(self.repo.update_status) def display_repo_status(self, status): - """ Updates the contents of the widget to display the current install status of the widget. """ + """Updates the contents of the widget to display the current install status of the widget.""" repo = self.repo self.set_change_branch_button_state() self.set_disable_button_state() @@ -599,7 +599,7 @@ class PackageDetails(QWidget): self.ui.progressBar.setValue(progress) def load_finished(self, load_succeeded: bool): - """ Once loading is complete, update the display of the progress bar and loading widget. """ + """Once loading is complete, update the display of the progress bar and loading widget.""" self.ui.loadingLabel.hide() self.ui.slowLoadLabel.hide() self.ui.webView.show()