From 11455b28c500e9c6f084bd99c366ac0539c56159 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Thu, 29 Jan 2026 16:16:08 -0500 Subject: [PATCH 1/5] feat(Application): set the plugin group on load This sets the craft-parts plugin group on application load if possible. It also ignores errors in determining the plugin group, preventing a failure to run if there is no project file or the file is invalid. --- craft_application/application.py | 24 ++++++++++++- pyproject.toml | 2 +- tests/unit/test_application.py | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/craft_application/application.py b/craft_application/application.py index bfbf2de83..c9f825e9c 100644 --- a/craft_application/application.py +++ b/craft_application/application.py @@ -32,6 +32,8 @@ import annotated_types import craft_cli +import craft_parts +import craft_platforms import craft_providers from platformdirs import user_cache_path @@ -573,7 +575,8 @@ def register_plugins(self, plugins: dict[str, PluginType]) -> None: def _register_default_plugins(self) -> None: """Register per application plugins when initializing.""" - self.register_plugins(self._get_app_plugins()) + with warnings.catch_warnings(): + self.register_plugins(self._get_app_plugins()) def _pre_run(self, dispatcher: craft_cli.Dispatcher) -> None: """Do any final setup before running the command. @@ -775,7 +778,26 @@ def _setup_logging(self) -> None: def _enable_craft_parts_features(self) -> None: """Enable any specific craft-parts Feature that the application will need.""" + @final + def _set_plugin_group(self) -> None: + """Set the plugin group from the lifecycle service. + + If no plugin group is provided or an error occurs while determining + the build info, no plugin group is registered. + """ + try: + build_plan = self.services.get("build_plan").plan() + except (craft_cli.CraftError, craft_platforms.CraftPlatformsError): + # We can do this here because when we start the lifecycle + # we actually exit the app if there's an error. + craft_cli.emit.debug("No plugin group registered due to error.") + return + group = self.services.get_class("lifecycle").get_plugin_group(build_plan[0]) + if group: + craft_parts.plugins.set_plugin_group(group) + def _initialize_craft_parts(self) -> None: """Perform craft-parts-specific initialization, like features and plugins.""" self._enable_craft_parts_features() self._register_default_plugins() + self._set_plugin_group() diff --git a/pyproject.toml b/pyproject.toml index a5fe187ec..24ac4ec19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ # the minimum minor version here, that absolutely needs to go in the release # notes. # Pygit2 changelog: https://github.com/libgit2/pygit2/blob/master/CHANGELOG.md - "pygit2>=1.13.0,<1.19.0", + "pygit2>=1.13.0,<1.20.0", "PyYaml>=6.0", "requests", "typing_extensions>=4.4.0", diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index 191eb609d..aa835cc5b 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -1027,6 +1027,64 @@ def test_register_plugins_default(mocker, app_metadata, fake_services): assert reg.call_count == 0 +def test_set_plugin_group(mocker, app_metadata, fake_services): + """Test that we set the plugin group early.""" + mock_set = mocker.patch("craft_parts.plugins.set_plugin_group") + mock_get_plugin_group = mocker.patch.object( + fake_services.get_class("lifecycle"), "get_plugin_group" + ) + + app = FakeApplication(app_metadata, fake_services) + with pytest.raises(SystemExit): + app.run() + + mock_set.assert_called_once_with(mock_get_plugin_group.return_value) + + +def test_set_plugin_group_empty(mocker, app_metadata, fake_services): + """Test that we set the plugin group early.""" + mock_set = mocker.patch("craft_parts.plugins.set_plugin_group") + mock_get_plugin_group = mocker.patch.object( + fake_services.get_class("lifecycle"), "get_plugin_group", return_value=None + ) + + app = FakeApplication(app_metadata, fake_services) + with pytest.raises(SystemExit): + app.run() + + mock_get_plugin_group.assert_called_once() + mock_set.assert_not_called() + + +@pytest.mark.parametrize( + "planning_error", + [ + craft_cli.CraftError("General craft error"), + craft_platforms.CraftPlatformsError("Platforms error"), + craft_application.errors.CraftValidationError("Validation!"), + ], +) +def test_set_plugin_group_ignores_errors( + mocker, app_metadata, fake_services, planning_error +): + """Test that we set the plugin group early.""" + mock_set = mocker.patch("craft_parts.plugins.set_plugin_group") + mock_get_plugin_group = mocker.patch.object( + fake_services.get_class("lifecycle"), "get_plugin_group", return_value=None + ) + mock_planner = mocker.patch.object( + fake_services.get("build_plan"), "plan", side_effect=planning_error + ) + + app = FakeApplication(app_metadata, fake_services) + with pytest.raises(SystemExit): + app.run() + + mock_planner.assert_called_once() + mock_get_plugin_group.assert_not_called() + mock_set.assert_not_called() + + @pytest.fixture def grammar_project_mini(tmp_path): """A project that builds on amd64 to riscv64 and s390x.""" From 3ef6fb32b10ebf47e11e40fe04a416e49e9d0b70 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Thu, 29 Jan 2026 16:19:19 -0500 Subject: [PATCH 2/5] fix: lazy load --- craft_application/application.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/craft_application/application.py b/craft_application/application.py index c9f825e9c..5b77aae19 100644 --- a/craft_application/application.py +++ b/craft_application/application.py @@ -32,7 +32,6 @@ import annotated_types import craft_cli -import craft_parts import craft_platforms import craft_providers from platformdirs import user_cache_path @@ -793,8 +792,12 @@ def _set_plugin_group(self) -> None: craft_cli.emit.debug("No plugin group registered due to error.") return group = self.services.get_class("lifecycle").get_plugin_group(build_plan[0]) + + # We don't need to import this unless we have a group to set. + from craft_parts.plugins import set_plugin_group # noqa: PLC0415 + if group: - craft_parts.plugins.set_plugin_group(group) + set_plugin_group(group) def _initialize_craft_parts(self) -> None: """Perform craft-parts-specific initialization, like features and plugins.""" From cebdd4de9345f140183ae80b9e23662a4dec7bce Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Thu, 5 Feb 2026 14:20:53 -0500 Subject: [PATCH 3/5] fix: tests and uv lock --- tests/spread/witchcraft/plugin-groups/task.yaml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/spread/witchcraft/plugin-groups/task.yaml b/tests/spread/witchcraft/plugin-groups/task.yaml index adc005790..df0f9b6df 100644 --- a/tests/spread/witchcraft/plugin-groups/task.yaml +++ b/tests/spread/witchcraft/plugin-groups/task.yaml @@ -16,7 +16,7 @@ execute: | # Check that the package referencing a plugin not used on this system does not pack. cd "invalid-${DISTRO}" - witchcraft pack --destructive-mode |& MATCH "Plugin '[a-z]+' in part 'part1' is not registered." + witchcraft pack --destructive-mode |& MATCH "plugin not registered: 'python' (in field 'parts.part1'," restore: | diff --git a/uv.lock b/uv.lock index e59b13da8..ff97a90cd 100644 --- a/uv.lock +++ b/uv.lock @@ -624,7 +624,7 @@ requires-dist = [ { name = "license-expression", specifier = ">=30.0.0" }, { name = "platformdirs", specifier = ">=3.10" }, { name = "pydantic", specifier = "~=2.0" }, - { name = "pygit2", specifier = ">=1.13.0,<1.19.0" }, + { name = "pygit2", specifier = ">=1.13.0,<1.20.0" }, { name = "pysocks", marker = "extra == 'remote'", specifier = ">=1.7.1" }, { name = "python-apt", marker = "sys_platform == 'linux' and extra == 'apt'", specifier = ">=2.4.0", index = "https://people.canonical.com/~lengau/python-apt-ubuntu-wheels/" }, { name = "pyyaml", specifier = ">=6.0" }, From c4c4137c9a858519e95cb823d34807f70b041763 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Thu, 5 Feb 2026 14:53:03 -0500 Subject: [PATCH 4/5] fix: backslash --- tests/spread/witchcraft/plugin-groups/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/spread/witchcraft/plugin-groups/task.yaml b/tests/spread/witchcraft/plugin-groups/task.yaml index df0f9b6df..ec845ea3e 100644 --- a/tests/spread/witchcraft/plugin-groups/task.yaml +++ b/tests/spread/witchcraft/plugin-groups/task.yaml @@ -16,7 +16,7 @@ execute: | # Check that the package referencing a plugin not used on this system does not pack. cd "invalid-${DISTRO}" - witchcraft pack --destructive-mode |& MATCH "plugin not registered: 'python' (in field 'parts.part1'," + witchcraft pack --destructive-mode |& MATCH "plugin not registered: 'python' \(in field 'parts.part1'," restore: | From b8c51b2d3aea008f53f390c5296ca65475306792 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Fri, 20 Feb 2026 12:19:44 -0500 Subject: [PATCH 5/5] fix: test issue --- tests/spread/witchcraft/plugin-groups/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/spread/witchcraft/plugin-groups/task.yaml b/tests/spread/witchcraft/plugin-groups/task.yaml index ec845ea3e..739d3a9e7 100644 --- a/tests/spread/witchcraft/plugin-groups/task.yaml +++ b/tests/spread/witchcraft/plugin-groups/task.yaml @@ -16,7 +16,7 @@ execute: | # Check that the package referencing a plugin not used on this system does not pack. cd "invalid-${DISTRO}" - witchcraft pack --destructive-mode |& MATCH "plugin not registered: 'python' \(in field 'parts.part1'," + witchcraft pack --destructive-mode |& MATCH "plugin not registered: '[a-z]+' \(in field 'parts.part1'," restore: |