Skip to content

Utilize uv if available for all python pkg installations #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .config/requirements.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ansible-builder
bindep
pyyaml
subprocess-tee
78 changes: 39 additions & 39 deletions src/ansible_dev_environment/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion src/ansible_dev_environment/subcommands/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import subprocess
import sys

from typing import TYPE_CHECKING

Expand Down Expand Up @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions src/ansible_dev_environment/subcommands/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions src/ansible_dev_environment/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
32 changes: 13 additions & 19 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ def test_paths(
uv: bool,
monkeypatch: pytest.MonkeyPatch,
output: Output,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the paths.

Expand All @@ -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 (
Expand Down
42 changes: 41 additions & 1 deletion tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Loading