diff --git a/.config/requirements.in b/.config/requirements.in index ede2f8f..36c4302 100644 --- a/.config/requirements.in +++ b/.config/requirements.in @@ -1,3 +1,4 @@ ansible-builder +bindep pyyaml subprocess-tee diff --git a/src/ansible_dev_environment/config.py b/src/ansible_dev_environment/config.py index b72a650..a23a117 100644 --- a/src/ansible_dev_environment/config.py +++ b/src/ansible_dev_environment/config.py @@ -3,12 +3,12 @@ from __future__ import annotations import json -import logging import os import shutil import subprocess import sys +from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING @@ -22,38 +22,8 @@ from .utils import TermFeatures -_logger = logging.getLogger(__name__) - - -def use_uv() -> bool: - """Return whether to use uv commands like venv or pip. - - Returns: - True if uv is to be used. - """ - if int(os.environ.get("SKIP_UV", "0")): - return False - try: - import uv # noqa: F401 - except ImportError: # pragma: no cover - return False - else: - _logger.info( - "UV detected and will be used instead of venv/pip. To disable that define SKIP_UP=1 in your environment.", - ) - return True - - class Config: # pylint: disable=too-many-instance-attributes - """The application configuration. - - Attributes: - pip_cmd: The pip command. - venv_cmd: The venv command. - """ - - pip_cmd: str - venv_cmd: str + """The application configuration.""" def __init__( self, @@ -114,6 +84,38 @@ def venv_cache_dir(self) -> Path: """Return the virtual environment cache directory.""" return self.cache_dir + @property + def venv_pip_install_cmd(self) -> str: + """Return the pip command for the virtual environment. + + Returns: + The pip install command for the virtual environment. + """ + if self.uv_available: + return f"uv pip install --python {self.venv_interpreter}" + return f"{self.venv}/bin/python -m pip install" + + @cached_property + def uv_available(self) -> bool: + """Return whether to use uv commands like venv or pip. + + Returns: + True if uv is to be used. + """ + if int(os.environ.get("SKIP_UV", "0")): + self._output.debug("uv is disabled by SKIP_UV=1 in the environment.") + return False + + if not (uv_path := shutil.which("uv")): + self._output.debug("uv is not available in the environment.") + return False + + self._output.debug(f"uv is available at {uv_path}") + self._output.info( + "uv is available and will be used instead of venv/pip. To disable that define SKIP_UV=1 in your environment.", + ) + return True + @property def discovered_python_reqs(self) -> Path: """Return the discovered python requirements file.""" @@ -174,18 +176,16 @@ def _set_interpreter( self, ) -> None: """Set the interpreter.""" - self.pip_cmd = f"{sys.executable} -m pip" - self.venv_cmd = f"{sys.executable} -m venv" - if use_uv(): - self.pip_cmd = f"{sys.executable} -m uv pip" - # seed and python-preference make uv venv match python -m venv behavior: - self.venv_cmd = f"{sys.executable} -m uv venv --seed --python-preference=system" + if self.uv_available: + venv_cmd = "uv venv --seed --python-preference=system" + else: + venv_cmd = f"{sys.executable} -m venv" if not self.venv.exists(): if self._create_venv: msg = f"Creating virtual environment: {self.venv}" self._output.debug(msg) - command = f"{self.venv_cmd} {self.venv}" + command = f"{venv_cmd} {self.venv}" msg = f"Creating virtual environment: {self.venv}" if self.args.system_site_packages: command = f"{command} --system-site-packages" diff --git a/src/ansible_dev_environment/subcommands/checker.py b/src/ansible_dev_environment/subcommands/checker.py index c91cfc2..fc4837c 100644 --- a/src/ansible_dev_environment/subcommands/checker.py +++ b/src/ansible_dev_environment/subcommands/checker.py @@ -4,6 +4,7 @@ import json import subprocess +import sys from typing import TYPE_CHECKING @@ -183,7 +184,7 @@ def system_deps(self) -> None: msg = "Checking system packages." self._output.info(msg) - command = f"bindep -b -f {self._config.discovered_bindep_reqs}" + command = f"{sys.executable} -m bindep -b -f {self._config.discovered_bindep_reqs}" work = "Checking system package requirements" try: subprocess_run( diff --git a/src/ansible_dev_environment/subcommands/installer.py b/src/ansible_dev_environment/subcommands/installer.py index bc0e0f2..4e5eb8d 100644 --- a/src/ansible_dev_environment/subcommands/installer.py +++ b/src/ansible_dev_environment/subcommands/installer.py @@ -124,7 +124,7 @@ def _install_core(self) -> None: return msg = "Installing ansible-core." self._output.debug(msg) - command = f"{self._config.venv_interpreter} -m pip install ansible-core" + command = f"{self._config.venv_pip_install_cmd} ansible-core" try: subprocess_run( command=command, @@ -146,7 +146,7 @@ def _install_dev_tools(self) -> None: return msg = "Installing ansible-dev-tools." self._output.debug(msg) - command = f"{self._config.venv_interpreter} -m pip install ansible-dev-tools" + command = f"{self._config.venv_pip_install_cmd} ansible-dev-tools" try: subprocess_run( command=command, @@ -522,7 +522,7 @@ def _pip_install(self) -> None: msg = "Installing python requirements." self._output.info(msg) - command = f"{self._config.pip_cmd} install -r {self._config.discovered_python_reqs}" + command = f"{self._config.venv_pip_install_cmd} -r {self._config.discovered_python_reqs}" msg = f"Installing python requirements from {self._config.discovered_python_reqs}" self._output.debug(msg) diff --git a/src/ansible_dev_environment/utils.py b/src/ansible_dev_environment/utils.py index 9cc2d62..66616ce 100644 --- a/src/ansible_dev_environment/utils.py +++ b/src/ansible_dev_environment/utils.py @@ -280,12 +280,15 @@ def collect_manifests( # noqa: C901 def builder_introspect(config: Config, output: Output) -> None: """Introspect a collection. + Use the sys executable to run builder, since it is a direct dependency + it should be accessible to the current interpreter. + Args: config: The configuration object. output: The output object. """ command = ( - f"ansible-builder introspect {config.site_pkg_path}" + f"{sys.executable} -m ansible_builder introspect {config.site_pkg_path}" f" --write-pip {config.discovered_python_reqs}" f" --write-bindep {config.discovered_bindep_reqs}" " --sanitize" @@ -316,7 +319,7 @@ def builder_introspect(config: Config, output: Output) -> None: ) except subprocess.CalledProcessError as exc: err = f"Failed to discover requirements: {exc} {exc.stderr}" - logger.critical(err) + output.critical(err) if not config.discovered_python_reqs.exists(): config.discovered_python_reqs.touch() diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 342a6c4..0201f82 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -57,7 +57,6 @@ def test_paths( uv: bool, monkeypatch: pytest.MonkeyPatch, output: Output, - caplog: pytest.LogCaptureFixture, ) -> None: """Test the paths. @@ -69,28 +68,23 @@ def test_paths( uv: Whether to use the uv module. monkeypatch: A pytest fixture for monkey patching. output: The output fixture. - caplog: A pytest fixture for capturing logs. """ - with caplog.at_level(10): - monkeypatch.setenv("SKIP_UV", "0" if uv else "1") - venv = tmpdir / "test_venv" - args = gen_args( - venv=str(venv), - system_site_packages=system_site_packages, - ) - - config = Config(args=args, output=output, term_features=output.term_features) - config.init() + monkeypatch.setenv("SKIP_UV", str(int(not uv))) + venv = tmpdir / "test_venv" + args = gen_args( + venv=str(venv), + system_site_packages=system_site_packages, + ) + + config = Config(args=args, output=output, term_features=output.term_features) + config.init() if uv: - assert len(caplog.messages) == 1 - assert "UV detected" in caplog.records[0].msg - assert "-m uv venv" in config.venv_cmd - assert "-m uv pip" in config.pip_cmd + assert config.uv_available + assert "uv pip install --python" in config.venv_pip_install_cmd else: - assert len(caplog.messages) == 0 - assert "-m venv" in config.venv_cmd - assert "-m pip" in config.pip_cmd + assert not config.uv_available + assert "-m pip install" in config.venv_pip_install_cmd assert config.venv == venv for attr in ( diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index a51e8d2..0494e07 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -13,7 +13,7 @@ ) from ansible_dev_environment.config import Config from ansible_dev_environment.output import Output -from ansible_dev_environment.utils import TermFeatures +from ansible_dev_environment.utils import TermFeatures, builder_introspect term_features = TermFeatures(color=False, links=False) @@ -123,3 +123,43 @@ def test_parse_collection_request(scenario: tuple[str, Collection | None]) -> No parse_collection_request(string=string, config=config, output=output) else: assert parse_collection_request(string=string, config=config, output=output) == spec + + +def test_builder_found(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that builder is found. + + Args: + tmp_path: A temporary path + monkeypatch: The pytest Monkeypatch fixture + + Raises: + AssertionError: if either file is not found + """ + + @property # type: ignore[misc] + def cache_dir(_self: Config) -> Path: + """Return a temporary cache directory. + + Args: + _self: The Config object + + Returns: + A temporary cache directory. + """ + return tmp_path + + monkeypatch.setattr(Config, "cache_dir", cache_dir) + + args = Namespace(venv=str(tmp_path / ".venv"), system_site_packages=False, verbose=0) + + cfg = Config( + args=args, + term_features=term_features, + output=output, + ) + cfg.init() + + builder_introspect(cfg, output) + + assert cfg.discovered_bindep_reqs.exists() is True + assert cfg.discovered_python_reqs.exists() is True