From 61c02090be4fc4333598e19a4808e09d684d6e38 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 22 Feb 2025 22:03:32 -0600 Subject: [PATCH] Addon Manager: Add and --- src/Mod/AddonManager/Addon.py | 35 ++++++++----------- .../AddonManagerTest/app/test_addon.py | 8 +++++ .../AddonManagerTest/app/test_metadata.py | 16 +++++++++ .../AddonManagerTest/data/bundle_only.xml | 23 ++++++++++++ .../AddonManagerTest/data/combination.xml | 6 ++++ .../AddonManagerTest/data/other_only.xml | 18 ++++++++++ src/Mod/AddonManager/CMakeLists.txt | 2 ++ .../addonmanager_widget_filter_selector.py | 10 ++++++ .../addonmanager_devmode_add_content.py | 4 +++ src/Mod/AddonManager/addonmanager_metadata.py | 2 +- src/Mod/AddonManager/package_list.py | 8 ++++- 11 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 src/Mod/AddonManager/AddonManagerTest/data/bundle_only.xml create mode 100644 src/Mod/AddonManager/AddonManagerTest/data/other_only.xml diff --git a/src/Mod/AddonManager/Addon.py b/src/Mod/AddonManager/Addon.py index d028429689..8ec60ce48e 100644 --- a/src/Mod/AddonManager/Addon.py +++ b/src/Mod/AddonManager/Addon.py @@ -489,23 +489,17 @@ class Addon: if self.repo_type == Addon.Kind.WORKBENCH: return True - if self.repo_type == Addon.Kind.PACKAGE: - if self.metadata is None: - fci.Console.PrintLog( - f"Addon Manager internal error: lost metadata for package {self.name}\n" - ) - return False - content = self.metadata.content - if not content: - return False - return "workbench" in content - return False + return self.contains_packaged_content("workbench") def contains_macro(self) -> bool: """Determine if this package contains (or is) a macro""" if self.repo_type == Addon.Kind.MACRO: return True + return self.contains_packaged_content("macro") + + def contains_packaged_content(self, content_type: str): + """Determine if the package contains content_type""" if self.repo_type == Addon.Kind.PACKAGE: if self.metadata is None: fci.Console.PrintLog( @@ -513,21 +507,20 @@ class Addon: ) return False content = self.metadata.content - return "macro" in content + return content_type in content return False def contains_preference_pack(self) -> bool: """Determine if this package contains a preference pack""" + return self.contains_packaged_content("preferencepack") - if self.repo_type == Addon.Kind.PACKAGE: - if self.metadata is None: - fci.Console.PrintLog( - f"Addon Manager internal error: lost metadata for package {self.name}\n" - ) - return False - content = self.metadata.content - return "preferencepack" in content - return False + def contains_bundle(self) -> bool: + """Determine if this package contains a bundle""" + return self.contains_packaged_content("bundle") + + def contains_other(self) -> bool: + """Determine if this package contains an "other" content item""" + return self.contains_packaged_content("other") def get_best_icon_relative_path(self) -> str: """Get the path within the repo the addon's icon. Usually specified by diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py b/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py index e3a20d33d4..3872364739 100644 --- a/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py +++ b/src/Mod/AddonManager/AddonManagerTest/app/test_addon.py @@ -93,6 +93,8 @@ class TestAddon(unittest.TestCase): self.assertTrue(addon_with_workbench.contains_workbench()) self.assertFalse(addon_with_workbench.contains_macro()) self.assertFalse(addon_with_workbench.contains_preference_pack()) + self.assertFalse(addon_with_workbench.contains_bundle()) + self.assertFalse(addon_with_workbench.contains_other()) # Macros addon_with_macro = Addon( @@ -105,6 +107,8 @@ class TestAddon(unittest.TestCase): self.assertFalse(addon_with_macro.contains_workbench()) self.assertTrue(addon_with_macro.contains_macro()) self.assertFalse(addon_with_macro.contains_preference_pack()) + self.assertFalse(addon_with_workbench.contains_bundle()) + self.assertFalse(addon_with_workbench.contains_other()) # Preference Packs addon_with_prefpack = Addon( @@ -117,6 +121,8 @@ class TestAddon(unittest.TestCase): self.assertFalse(addon_with_prefpack.contains_workbench()) self.assertFalse(addon_with_prefpack.contains_macro()) self.assertTrue(addon_with_prefpack.contains_preference_pack()) + self.assertFalse(addon_with_workbench.contains_bundle()) + self.assertFalse(addon_with_workbench.contains_other()) # Combination addon_with_all = Addon( @@ -129,6 +135,8 @@ class TestAddon(unittest.TestCase): self.assertTrue(addon_with_all.contains_workbench()) self.assertTrue(addon_with_all.contains_macro()) self.assertTrue(addon_with_all.contains_preference_pack()) + self.assertTrue(addon_with_all.contains_bundle()) + self.assertTrue(addon_with_all.contains_other()) # Now do the simple, explicitly-set cases addon_wb = Addon( diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_metadata.py b/src/Mod/AddonManager/AddonManagerTest/app/test_metadata.py index b572327d79..9f6f53cecf 100644 --- a/src/Mod/AddonManager/AddonManagerTest/app/test_metadata.py +++ b/src/Mod/AddonManager/AddonManagerTest/app/test_metadata.py @@ -616,6 +616,22 @@ class TestMetadataReaderIntegration(unittest.TestCase): expected_packs.remove(wb.name) self.assertEqual(len(expected_packs), 0) + def test_bundle(self): + from addonmanager_metadata import MetadataReader + + filename = os.path.join(self.test_data_dir, "bundle_only.xml") + metadata = MetadataReader.from_file(filename) + self.assertIn("bundle", metadata.content) + self.assertEqual(len(metadata.content["bundle"]), 1) + + def test_other(self): + from addonmanager_metadata import MetadataReader + + filename = os.path.join(self.test_data_dir, "other_only.xml") + metadata = MetadataReader.from_file(filename) + self.assertIn("other", metadata.content) + self.assertEqual(len(metadata.content["other"]), 1) + def test_content_combination(self): from addonmanager_metadata import MetadataReader diff --git a/src/Mod/AddonManager/AddonManagerTest/data/bundle_only.xml b/src/Mod/AddonManager/AddonManagerTest/data/bundle_only.xml new file mode 100644 index 0000000000..0cac57f292 --- /dev/null +++ b/src/Mod/AddonManager/AddonManagerTest/data/bundle_only.xml @@ -0,0 +1,23 @@ + + + Test Bundle + A package.xml file for unit testing. + 1.0.0 + 2025-02-22 + FreeCAD Developer + LGPL-2.1 + https://github.com/chennes/FreeCAD-Package + https://github.com/chennes/FreeCAD-Package/blob/main/README.md + + + + A bunch of great addons you should install + TestAddon1 + TestAddon2 + TestAddon3 + TestAddon4 + TestAddon5 + + + + diff --git a/src/Mod/AddonManager/AddonManagerTest/data/combination.xml b/src/Mod/AddonManager/AddonManagerTest/data/combination.xml index 8f095046f5..bc72470a0e 100644 --- a/src/Mod/AddonManager/AddonManagerTest/data/combination.xml +++ b/src/Mod/AddonManager/AddonManagerTest/data/combination.xml @@ -23,6 +23,12 @@ MyFirstPack + + A bundle that bundles nothing + + + Mysterious Object + diff --git a/src/Mod/AddonManager/AddonManagerTest/data/other_only.xml b/src/Mod/AddonManager/AddonManagerTest/data/other_only.xml new file mode 100644 index 0000000000..e4401f334d --- /dev/null +++ b/src/Mod/AddonManager/AddonManagerTest/data/other_only.xml @@ -0,0 +1,18 @@ + + + Test Other + A package.xml file for unit testing. + 1.0.0 + 2025-02-22 + FreeCAD Developer + LGPL-2.1 + https://github.com/chennes/FreeCAD-Package + https://github.com/chennes/FreeCAD-Package/blob/main/README.md + + + + A thing that's not a workbench, macro, preference pack, or bundle + + + + diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index 58b15d132b..8efcf67a0e 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -118,6 +118,7 @@ SET(AddonManagerTestsGui_SRCS SET(AddonManagerTestsFiles_SRCS AddonManagerTest/data/__init__.py AddonManagerTest/data/addon_update_stats.json + AddonManagerTest/data/bundle_only.xml AddonManagerTest/data/combination.xml AddonManagerTest/data/corrupted_metadata.zip AddonManagerTest/data/depends_on_all_workbenches.xml @@ -131,6 +132,7 @@ SET(AddonManagerTestsFiles_SRCS AddonManagerTest/data/MacrosRecipesWikiPage.zip AddonManagerTest/data/metadata.zip AddonManagerTest/data/missing_macro_metadata.FCStd + AddonManagerTest/data/other_only.xml AddonManagerTest/data/prefpack_only.xml AddonManagerTest/data/test_addon_with_fcmacro.zip AddonManagerTest/data/test_github_style_repo.zip diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py index 308335e3c9..bb07ee3a28 100644 --- a/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py +++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py @@ -69,6 +69,8 @@ class ContentFilter(IntEnum): WORKBENCH = 1 MACRO = 2 PREFERENCE_PACK = 3 + BUNDLE = 4 + OTHER = 5 class Filter: @@ -116,6 +118,14 @@ class WidgetFilterSelector(QtWidgets.QComboBox): translate("AddonsInstaller", "Preference Pack"), (FilterType.PACKAGE_CONTENTS, ContentFilter.PREFERENCE_PACK), ) + self.addItem( + translate("AddonsInstaller", "Bundle"), + (FilterType.PACKAGE_CONTENTS, ContentFilter.BUNDLE), + ) + self.addItem( + translate("AddonsInstaller", "Other"), + (FilterType.PACKAGE_CONTENTS, ContentFilter.OTHER), + ) self.insertSeparator(self.count()) self.addItem(translate("AddonsInstaller", "Installation Status")) self.installation_status_index = self.count() - 1 diff --git a/src/Mod/AddonManager/addonmanager_devmode_add_content.py b/src/Mod/AddonManager/addonmanager_devmode_add_content.py index f990f643ec..2907fbc8d4 100644 --- a/src/Mod/AddonManager/addonmanager_devmode_add_content.py +++ b/src/Mod/AddonManager/addonmanager_devmode_add_content.py @@ -71,6 +71,8 @@ class AddContent: self.dialog.addonKindComboBox.setItemData(0, "macro") self.dialog.addonKindComboBox.setItemData(1, "preferencepack") self.dialog.addonKindComboBox.setItemData(2, "workbench") + self.dialog.addonKindComboBox.setItemData(3, "bundle") + self.dialog.addonKindComboBox.setItemData(4, "other") self.people_table = PeopleTable() self.licenses_table = LicensesTable() @@ -148,6 +150,8 @@ class AddContent: self.dialog.macroFileLineEdit.setText(files[0]) elif addon_kind == "preferencepack": self.dialog.prefPackNameLineEdit.setText(self.metadata.Name) + elif addon_kind == "bundle" or addon_kind == "other": + pass else: raise RuntimeError("Invalid data found for selection") diff --git a/src/Mod/AddonManager/addonmanager_metadata.py b/src/Mod/AddonManager/addonmanager_metadata.py index debe1bac27..d72f2b92c1 100644 --- a/src/Mod/AddonManager/addonmanager_metadata.py +++ b/src/Mod/AddonManager/addonmanager_metadata.py @@ -367,7 +367,7 @@ class MetadataReader: def _parse_content(namespace: str, metadata: Metadata, root: ET.Element): """Given a content node, loop over its children, and if they are a recognized element type, recurse into each one to parse it.""" - known_content_types = ["workbench", "macro", "preferencepack"] + known_content_types = ["workbench", "macro", "preferencepack", "bundle", "other"] for child in root: content_type = child.tag[len(namespace) :] if content_type in known_content_types: diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index b4590b776f..e946e14f12 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -569,7 +569,7 @@ class PackageListFilter(QtCore.QSortFilterProxyModel): def setPackageFilter( self, package_type: int - ) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs + ) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs, 4=Bundles, 5=Other """Set the package filter to package_type and refreshes.""" self.package_type = package_type self.invalidateFilter() @@ -634,6 +634,12 @@ class PackageListFilter(QtCore.QSortFilterProxyModel): elif self.package_type == 3: if not data.contains_preference_pack(): return False + elif self.package_type == 4: + if not data.contains_bundle(): + return False + elif self.package_type == 5: + if not data.contains_other(): + return False if self.status == StatusFilter.INSTALLED: if data.status() == Addon.Status.NOT_INSTALLED: