diff --git a/craft_application/commands/init.py b/craft_application/commands/init.py index 53fb64671..66a65c99a 100644 --- a/craft_application/commands/init.py +++ b/craft_application/commands/init.py @@ -15,8 +15,11 @@ from __future__ import annotations +import argparse +import contextlib import importlib.resources import pathlib +import warnings from textwrap import dedent from typing import TYPE_CHECKING, cast @@ -28,6 +31,7 @@ if TYPE_CHECKING: import argparse + from collections.abc import Generator class InitCommand(base.AppCommand): @@ -87,19 +91,44 @@ def fill_parser(self, parser: argparse.ArgumentParser) -> None: @property def parent_template_dir(self) -> pathlib.Path: - """Return the path to the directory that contains all templates.""" + """Return the path to the directory that contains all templates. + + DEPRECATED: This method is deprecated and is not called by default. + Use ``template_dir_parent`` instead. + """ + warnings.warn( + "parent_template_dir is deprecated in favor of template_dir_parent.", + category=DeprecationWarning, + stacklevel=1, + ) with importlib.resources.path( self._app.name, "templates" ) as _parent_template_dir: return _parent_template_dir + @classmethod + def _parent_template_dir_override(cls) -> bool: + return cls.parent_template_dir is not InitCommand.parent_template_dir + + @contextlib.contextmanager + def template_dir_parent(self) -> Generator[pathlib.Path]: + """Return the path to the directory that contains all templates.""" + if self._parent_template_dir_override(): + yield self.parent_template_dir + return + with importlib.resources.path( + self._app.name, "templates" + ) as template_dir_parent: + yield template_dir_parent + @property def profiles(self) -> list[str]: """A list of profile names generated from template directories.""" - template_dirs = [ - path for path in self.parent_template_dir.iterdir() if path.is_dir() - ] - return sorted([template.name for template in template_dirs]) + with self.template_dir_parent() as template_dir_parent: + template_names = [ + path.name for path in template_dir_parent.iterdir() if path.is_dir() + ] + return sorted(template_names) def run(self, parsed_args: argparse.Namespace) -> None: """Run the command.""" @@ -115,20 +144,21 @@ def run(self, parsed_args: argparse.Namespace) -> None: ) project_dir = self._get_project_dir(parsed_args) - template_dir = pathlib.Path(self.parent_template_dir / parsed_args.profile) - - craft_cli.emit.progress("Checking project directory.") - self._services.init.check_for_existing_files( - project_dir=project_dir, template_dir=template_dir - ) - - craft_cli.emit.progress("Initialising project.") - self._services.init.initialise_project( - project_dir=project_dir, - project_name=project_name, - template_dir=template_dir, - ) - craft_cli.emit.message("Successfully initialised project.") + with self.template_dir_parent() as template_dir_parent: + template_dir = pathlib.Path(template_dir_parent / parsed_args.profile) + + craft_cli.emit.progress("Checking project directory.") + self._services.init.check_for_existing_files( + project_dir=project_dir, template_dir=template_dir + ) + + craft_cli.emit.progress("Initialising project.") + self._services.init.initialise_project( + project_dir=project_dir, + project_name=project_name, + template_dir=template_dir, + ) + craft_cli.emit.message("Successfully initialised project.") def _get_name(self, parsed_args: argparse.Namespace) -> str: """Get name of the package that is about to be initialised. diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index 2d8d4321d..63ac3c182 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -7,6 +7,12 @@ Changelog 5.7.0 (2025-MM-DD) ------------------ +Commands +======== + +- ``InitCommand`` now keeps the parent template directory context alive while it's in + use. + Services ======== diff --git a/tests/integration/commands/test_init.py b/tests/integration/commands/test_init.py index 65f817881..e67c71374 100644 --- a/tests/integration/commands/test_init.py +++ b/tests/integration/commands/test_init.py @@ -33,9 +33,8 @@ def mock_parent_template_dir(tmp_path, mocker): """Mock the parent template directory.""" mocker.patch.object( InitCommand, - "parent_template_dir", - pathlib.Path(tmp_path) / "templates", - ) + "template_dir_parent", + ).return_value.__enter__.return_value = pathlib.Path(tmp_path) / "templates" @pytest.fixture diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index 57299e5ee..fa535a10b 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -31,9 +31,8 @@ def init_command(app_metadata, mock_services, mocker, tmp_path): mocker.patch.object( InitCommand, - "parent_template_dir", - pathlib.Path(tmp_path) / "templates", - ) + "template_dir_parent", + ).return_value.__enter__.return_value = pathlib.Path(tmp_path) / "templates" return InitCommand({"app": app_metadata, "services": mock_services}) @@ -65,12 +64,13 @@ def test_init_in_cwd(init_command, name, new_dir, mock_services, emitter): init_command.run(parsed_args) - mock_services.init.initialise_project.assert_called_once_with( - project_dir=new_dir, - project_name=expected_name, - template_dir=init_command.parent_template_dir / "test-profile", - ) - emitter.assert_message("Successfully initialised project.") + with init_command.template_dir_parent() as template_dir_parent: + mock_services.init.initialise_project.assert_called_once_with( + project_dir=new_dir, + project_name=expected_name, + template_dir=template_dir_parent / "test-profile", + ) + emitter.assert_message("Successfully initialised project.") @pytest.mark.parametrize("name", [None, "my-project"]) @@ -87,12 +87,13 @@ def test_init_run_project_dir(init_command, name, mock_services, emitter): init_command.run(parsed_args) - mock_services.init.initialise_project.assert_called_once_with( - project_dir=project_dir.expanduser().resolve(), - project_name=expected_name, - template_dir=init_command.parent_template_dir / "test-profile", - ) - emitter.assert_message("Successfully initialised project.") + with init_command.template_dir_parent() as template_dir_parent: + mock_services.init.initialise_project.assert_called_once_with( + project_dir=project_dir.expanduser().resolve(), + project_name=expected_name, + template_dir=template_dir_parent / "test-profile", + ) + emitter.assert_message("Successfully initialised project.") @pytest.mark.usefixtures("fake_template_dirs") @@ -143,8 +144,9 @@ def _validate_project_name(_name: str, *, use_default: bool = False): init_command.run(parsed_args) - mock_services.init.initialise_project.assert_called_once_with( - project_dir=project_dir.expanduser().resolve(), - project_name="my-project", - template_dir=init_command.parent_template_dir / "simple", - ) + with init_command.template_dir_parent() as template_dir_parent: + mock_services.init.initialise_project.assert_called_once_with( + project_dir=project_dir.expanduser().resolve(), + project_name="my-project", + template_dir=template_dir_parent / "simple", + )