diff --git a/src/molecule/config.py b/src/molecule/config.py index 5d587fbf7d..54425c42a4 100644 --- a/src/molecule/config.py +++ b/src/molecule/config.py @@ -26,7 +26,7 @@ import warnings from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from uuid import uuid4 from ansible_compat.ports import cache, cached_property @@ -38,6 +38,7 @@ from molecule.dependency import ansible_galaxy, shell from molecule.model import schema_v3 from molecule.provisioner import ansible +from molecule.types import CollectionData from molecule.util import boolean @@ -231,6 +232,18 @@ def cache_directory( """ return "molecule_parallel" if self.is_parallel else "molecule" + @property + def collection_directory(self) -> Path | None: + """Location of collection containing the molecule files. + + Returns: + Root of the collection containing the molecule files. + """ + current_dir = Path(self.project_directory) + if (current_dir / "galaxy.yml").exists(): + return current_dir + return None + @property def molecule_directory(self) -> str: """Molecule directory for this project. @@ -240,6 +253,22 @@ def molecule_directory(self) -> str: """ return molecule_directory(self.project_directory) + @cached_property + def collection(self) -> CollectionData | None: + """Collection metadata sourced from galaxy.yml. + + Returns: + A dictionary of information about the collection molecule is running inside, if any. + """ + if not self.collection_directory: + return None + + galaxy_file = self.collection_directory / "galaxy.yml" + if galaxy_file.exists(): + galaxy_data = util.safe_load_file(galaxy_file) + return cast(CollectionData, galaxy_data) + return None + @cached_property def dependency(self) -> Dependency | None: """Dependency manager in use. diff --git a/src/molecule/types.py b/src/molecule/types.py index 6004f65312..6a59f6a3e9 100644 --- a/src/molecule/types.py +++ b/src/molecule/types.py @@ -12,6 +12,32 @@ Options: TypeAlias = MutableMapping[str, str | bool] +class CollectionData(TypedDict, total=False): + """Collection metadata sourced from galaxy.yml. + + Attributes: + name: The name of the collection. + namespace: The collection namespace. + version: The collection's version. + readme: Path to the README file. + authors: List of authors of the collection. + description: Description of the collection. + repository: URL of the collection's online repository. + license_file: Path to the collection's LICENSE file. + tags: List of tags applied to the collection. + """ + + name: str + namespace: str + version: str + readme: str + authors: list[str] + description: str + repository: str + license_file: str + tags: list[str] + + class DependencyData(TypedDict, total=False): """Molecule dependency configuration. diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 9518dbe9a1..bddde78e12 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -19,8 +19,10 @@ # DEALINGS IN THE SOFTWARE. from __future__ import annotations +import copy import os +from pathlib import Path from typing import TYPE_CHECKING, Literal import pytest @@ -84,16 +86,59 @@ def test_init_calls_validate( # noqa: D103 patched_config_validate.assert_called_once_with() +def test_collection_directory_property( + config_instance: config.Config, + resources_folder_path: Path, +) -> None: + """Test collection_directory property. + + Args: + config_instance: Instance of Config. + resources_folder_path: Path to resources directory holding a valid collection. + """ + # default path is not in a collection + assert config_instance.collection_directory is None + + # Alter config_instance to start at path of a collection + config_instance = copy.copy(config_instance) + collection_path = resources_folder_path / "sample-collection" + config_instance.project_directory = str(collection_path) + assert config_instance.collection_directory == collection_path + + def test_project_directory_property(config_instance: config.Config) -> None: # noqa: D103 - assert os.getcwd() == config_instance.project_directory # noqa: PTH109 + assert str(Path.cwd()) == config_instance.project_directory def test_molecule_directory_property(config_instance: config.Config) -> None: # noqa: D103 - x = os.path.join(os.getcwd(), "molecule") # noqa: PTH109, PTH118 + x = str(Path.cwd() / "molecule") assert x == config_instance.molecule_directory +def test_collection_property( + config_instance: config.Config, + resources_folder_path: Path, +) -> None: + """Test collection property. + + Args: + config_instance: Instance of Config. + resources_folder_path: Path to resources directory holding a valid collection. + """ + modified_instance = copy.copy(config_instance) + # default path is not in a collection + assert config_instance.collection is None + + # Alter config_instance to start at path of a collection + collection_path = resources_folder_path / "sample-collection" + modified_instance.project_directory = str(collection_path) + + assert modified_instance.collection is not None + assert modified_instance.collection["name"] == "goodies" + assert modified_instance.collection["namespace"] == "acme" + + def test_dependency_property(config_instance: config.Config) -> None: # noqa: D103 assert isinstance(config_instance.dependency, ansible_galaxy.AnsibleGalaxy)