From 6db8cd30a6b5c53be270f4690a5d229794c4dd08 Mon Sep 17 00:00:00 2001 From: "Bradley A. Thornton" Date: Wed, 1 May 2024 09:23:22 -0700 Subject: [PATCH] Fix for galaxy with system site packages (#147) --- src/ansible_dev_environment/config.py | 26 +++ .../subcommands/installer.py | 8 +- tests/unit/test_config.py | 216 ++++++++++++++++-- 3 files changed, 230 insertions(+), 20 deletions(-) diff --git a/src/ansible_dev_environment/config.py b/src/ansible_dev_environment/config.py index b06677c..2a9ba74 100644 --- a/src/ansible_dev_environment/config.py +++ b/src/ansible_dev_environment/config.py @@ -4,6 +4,7 @@ import json import os +import shutil import subprocess import sys @@ -101,6 +102,31 @@ def interpreter(self: Config) -> Path: """Return the current interpreter.""" return Path(sys.executable) + @property + def galaxy_bin(self: Config) -> Path | None: + """Find the ansible galaxy command. + + Prefer the venv over the system package over the PATH. + """ + within_venv = self.venv_bindir / "ansible-galaxy" + if within_venv.exists(): + msg = f"Found ansible-galaxy in virtual environment: {within_venv}" + self._output.debug(msg) + return within_venv + system_pkg = self.site_pkg_path / "bin" / "ansible-galaxy" + if system_pkg.exists(): + msg = f"Found ansible-galaxy in system packages: {system_pkg}" + self._output.debug(msg) + return system_pkg + last_resort = shutil.which("ansible-galaxy") + if last_resort: + msg = f"Found ansible-galaxy in PATH: {last_resort}" + self._output.debug(msg) + return Path(last_resort) + msg = "Failed to find ansible-galaxy." + self._output.critical(msg) + return None + def _set_interpreter( self: Config, ) -> None: diff --git a/src/ansible_dev_environment/subcommands/installer.py b/src/ansible_dev_environment/subcommands/installer.py index 36f4034..b98dcb3 100644 --- a/src/ansible_dev_environment/subcommands/installer.py +++ b/src/ansible_dev_environment/subcommands/installer.py @@ -137,7 +137,7 @@ def _install_galaxy_collections( shutil.rmtree(collection.site_pkg_path) command = ( - f"{self._config.venv_bindir / 'ansible-galaxy'} collection" + f"{self._config.galaxy_bin} collection" f" install {collections_str}" f" -p {self._config.site_pkg_path}" " --force" @@ -191,7 +191,7 @@ def _install_galaxy_requirements(self: Installer) -> None: shutil.rmtree(cpath) command = ( - f"{self._config.venv_bindir / 'ansible-galaxy'} collection" + f"{self._config.galaxy_bin} collection" f" install -r {self._config.args.requirement}" f" -p {self._config.site_pkg_path}" " --force" @@ -359,7 +359,7 @@ def _install_local_collection( command = ( f"cd {collection.build_dir} &&" - f" {self._config.venv_bindir / 'ansible-galaxy'} collection build" + f" {self._config.galaxy_bin} collection build" f" --output-path {collection.build_dir}" " --force" ) @@ -412,7 +412,7 @@ def _install_local_collection( shutil.rmtree(info_dir) command = ( - f"{self._config.venv_bindir / 'ansible-galaxy'} collection" + f"{self._config.galaxy_bin} collection" f" install {tarball} -p {self._config.site_pkg_path}" " --force" ) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index b1f9cc2..749871a 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -3,18 +3,38 @@ from __future__ import annotations import argparse +import shutil +from pathlib import Path from typing import TYPE_CHECKING import pytest from ansible_dev_environment.config import Config -from ansible_dev_environment.output import Output -from ansible_dev_environment.utils import TermFeatures if TYPE_CHECKING: - from pathlib import Path + from ansible_dev_environment.output import Output + + +def gen_args( + venv: str, + system_site_packages: bool = False, # noqa: FBT001, FBT002 +) -> argparse.Namespace: + """Generate the arguments. + + Args: + venv: The virtual environment. + system_site_packages: Whether to include system site packages. + + Returns: + The arguments. + """ + return argparse.Namespace( + verbose=0, + venv=venv, + system_site_packages=system_site_packages, + ) @pytest.mark.parametrize( @@ -22,7 +42,11 @@ ((True, False)), ids=["ssp_true", "ssp_false"], ) -def test_paths(tmpdir: Path, system_site_packages: bool) -> None: # noqa: FBT001 +def test_paths( + tmpdir: Path, + system_site_packages: bool, # noqa: FBT001 + output: Output, +) -> None: """Test the paths. Several of the found directories should have a parent of the tmpdir / test_venv @@ -30,24 +54,15 @@ def test_paths(tmpdir: Path, system_site_packages: bool) -> None: # noqa: FBT00 Args: tmpdir: A temporary directory. system_site_packages: Whether to include system site packages. + output: The output fixture. """ venv = tmpdir / "test_venv" - args = argparse.Namespace( + args = gen_args( venv=str(venv), system_site_packages=system_site_packages, - verbose=0, - ) - term_features = TermFeatures(color=False, links=False) - - output = Output( - log_file=str(tmpdir / "test_log.log"), - log_level="debug", - log_append="false", - term_features=term_features, - verbosity=0, ) - config = Config(args=args, output=output, term_features=term_features) + config = Config(args=args, output=output, term_features=output.term_features) config.init() assert config.venv == venv @@ -59,3 +74,172 @@ def test_paths(tmpdir: Path, system_site_packages: bool) -> None: # noqa: FBT00 "venv_interpreter", ): assert venv in getattr(config, attr).parents + + +def test_galaxy_bin_venv( + tmpdir: Path, + monkeypatch: pytest.MonkeyPatch, + output: Output, +) -> None: + """Test the galaxy_bin property found in venv. + + Args: + tmpdir: A temporary directory. + monkeypatch: A pytest fixture for monkey patching. + output: The output fixture. + """ + venv = tmpdir / "test_venv" + args = gen_args(venv=str(venv)) + + config = Config(args=args, output=output, term_features=output.term_features) + config.init() + + orig_exists = Path.exists + exists_called = False + + def _exists(path: Path) -> bool: + if path.name != "ansible-galaxy": + return orig_exists(path) + if path.parent == config.venv_bindir: + nonlocal exists_called + exists_called = True + return True + return False + + monkeypatch.setattr(Path, "exists", _exists) + + assert config.galaxy_bin == venv / "bin" / "ansible-galaxy" + assert exists_called + + +def test_galaxy_bin_site( + tmpdir: Path, + monkeypatch: pytest.MonkeyPatch, + output: Output, +) -> None: + """Test the galaxy_bin property found in site. + + Args: + tmpdir: A temporary directory. + monkeypatch: A pytest fixture for monkey patching. + output: The output fixture. + """ + venv = tmpdir / "test_venv" + args = gen_args(venv=str(venv)) + + config = Config(args=args, output=output, term_features=output.term_features) + config.init() + + orig_exists = Path.exists + exists_called = False + + def _exists(path: Path) -> bool: + if path.name != "ansible-galaxy": + return orig_exists(path) + if path.parent == config.site_pkg_path / "bin": + nonlocal exists_called + exists_called = True + return True + return False + + monkeypatch.setattr(Path, "exists", _exists) + + assert config.galaxy_bin == config.site_pkg_path / "bin" / "ansible-galaxy" + assert exists_called + + +def test_galaxy_bin_path( + tmpdir: Path, + monkeypatch: pytest.MonkeyPatch, + output: Output, +) -> None: + """Test the galaxy_bin property found in path. + + Args: + tmpdir: A temporary directory. + monkeypatch: A pytest fixture for monkey patching. + output: The output fixture. + """ + venv = tmpdir / "test_venv" + args = gen_args(venv=str(venv)) + + config = Config(args=args, output=output, term_features=output.term_features) + config.init() + + orig_exists = Path.exists + exists_called = False + + def _exists(path: Path) -> bool: + if path.name != "ansible-galaxy": + return orig_exists(path) + nonlocal exists_called + exists_called = True + return False + + monkeypatch.setattr(Path, "exists", _exists) + + orig_which = shutil.which + which_called = False + + def _which(name: str) -> str | None: + if not name.endswith("ansible-galaxy"): + return orig_which(name) + nonlocal which_called + which_called = True + return "patched" + + monkeypatch.setattr(shutil, "which", _which) + + assert config.galaxy_bin == Path("patched") + assert exists_called + assert which_called + + +def test_galaxy_bin_not_found( + tmpdir: Path, + monkeypatch: pytest.MonkeyPatch, + output: Output, +) -> None: + """Test the galaxy_bin property found in venv. + + Args: + tmpdir: A temporary directory. + monkeypatch: A pytest fixture for monkey patching. + output: The output fixture. + """ + venv = tmpdir / "test_venv" + args = gen_args(venv=str(venv)) + + config = Config(args=args, output=output, term_features=output.term_features) + config.init() + + orig_exists = Path.exists + exist_called = False + + def _exists(path: Path) -> bool: + if path.name == "ansible-galaxy": + nonlocal exist_called + exist_called = True + return False + return orig_exists(path) + + monkeypatch.setattr(Path, "exists", _exists) + + orig_which = shutil.which + which_called = False + + def _which(name: str) -> str | None: + if name.endswith("ansible-galaxy"): + nonlocal which_called + which_called = True + return None + return orig_which(name) + + monkeypatch.setattr(shutil, "which", _which) + + with pytest.raises(SystemExit) as exc: + assert config.galaxy_bin is None + + assert exc.value.code == 1 + assert exist_called + assert which_called