From 58936b9c3d52e373b3389d76057676573288401c Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Thu, 19 Dec 2024 20:16:36 +0000 Subject: [PATCH] Use uv venv instead of python venv when available Adds a huge speed boost in creating virtualenv by taking advantage of uv when it is found as being installed. --- .config/dictionary.txt | 1 + .config/requirements-test.in | 1 + .pre-commit-config.yaml | 5 +-- docs/index.md | 1 + pyproject.toml | 5 ++- src/ansible_dev_environment/config.py | 45 +++++++++++++++++-- .../subcommands/installer.py | 5 +-- src/ansible_dev_environment/utils.py | 2 +- tests/unit/test_config.py | 45 ++++++++++++++----- tests/unit/test_installer.py | 2 +- 10 files changed, 87 insertions(+), 25 deletions(-) diff --git a/.config/dictionary.txt b/.config/dictionary.txt index 78f558e..4f174ea 100644 --- a/.config/dictionary.txt +++ b/.config/dictionary.txt @@ -3,6 +3,7 @@ argvalues bindep bindir bthornto +caplog capsys cauthor cdescription diff --git a/.config/requirements-test.in b/.config/requirements-test.in index 8d493c0..44b0895 100644 --- a/.config/requirements-test.in +++ b/.config/requirements-test.in @@ -12,3 +12,4 @@ ruff toml-sort tox types-PyYAML +uv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e351d9..104ffe5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,8 +21,6 @@ repos: rev: v3.1.0 hooks: - id: add-trailing-comma - args: - - --py36-plus - repo: https://github.com/Lucas-C/pre-commit-hooks.git rev: v1.5.5 @@ -101,11 +99,12 @@ repos: hooks: - id: mypy additional_dependencies: - - pytest - pip + - pytest - subprocess_tee - types-pyyaml - types-setuptools + - uv # Override default pre-commit '--ignore-missing-imports' args: [--strict] diff --git a/docs/index.md b/docs/index.md index 2c3cfb1..810fb61 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,7 @@ For more information about communication, see the [Ansible communication guide]( - Checks for missing system packages - Symlinks the current collection into the current python interpreter's site-packages - Install all collection collection dependencies into the current python interpreter's site-packages +- Uses `uv env` instead of python's venv when available to boost performance. Can be disabled with `SKIP_UV=1` By placing collections into the python site-packages directory they are discoverable by ansible as well as python and pytest. diff --git a/pyproject.toml b/pyproject.toml index ac41da5..9e29aaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ allow-init-docstring = true arg-type-hints-in-docstring = false baseline = ".config/pydoclint-baseline.txt" check-return-types = false -exclude = '\.git|\.tox|build|out|venv' +exclude = '\.git|\.tox|\.venv|build|out|venv' should-document-private-class-attributes = true show-filenames-in-every-violation-message = true skip-checking-short-docstrings = false @@ -356,6 +356,9 @@ required-imports = ["from __future__ import annotations"] [tool.ruff.lint.pydocstyle] convention = "google" +[tool.ruff.lint.pylint] +max-args = 6 # 5 is the default + [tool.setuptools.dynamic] dependencies = {file = [".config/requirements.in"]} optional-dependencies.docs = {file = [".config/requirements-docs.in"]} diff --git a/src/ansible_dev_environment/config.py b/src/ansible_dev_environment/config.py index a44482a..b72a650 100644 --- a/src/ansible_dev_environment/config.py +++ b/src/ansible_dev_environment/config.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import logging import os import shutil import subprocess @@ -21,10 +22,39 @@ from .utils import TermFeatures -class Config: - """The application configuration.""" +_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 - # pylint: disable=too-many-instance-attributes def __init__( self, args: Namespace, @@ -144,11 +174,18 @@ 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 not self.venv.exists(): if self._create_venv: msg = f"Creating virtual environment: {self.venv}" self._output.debug(msg) - command = f"python -m venv {self.venv}" + command = f"{self.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/installer.py b/src/ansible_dev_environment/subcommands/installer.py index 370701a..b95d37c 100644 --- a/src/ansible_dev_environment/subcommands/installer.py +++ b/src/ansible_dev_environment/subcommands/installer.py @@ -505,10 +505,7 @@ def _pip_install(self) -> None: msg = "Installing python requirements." self._output.info(msg) - command = ( - f"{self._config.venv_interpreter} -m pip install" - f" -r {self._config.discovered_python_reqs}" - ) + command = f"{self._config.pip_cmd} install" f" -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 4eaf872..9cc2d62 100644 --- a/src/ansible_dev_environment/utils.py +++ b/src/ansible_dev_environment/utils.py @@ -112,7 +112,7 @@ class Ansi: GREY = "\x1b[90m" -def subprocess_run( # noqa: PLR0913 # pylint: disable=too-many-positional-arguments +def subprocess_run( # pylint: disable=too-many-positional-arguments command: str, verbose: int, msg: str, diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 8d86648..342a6c4 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -42,14 +42,22 @@ def gen_args( @pytest.mark.parametrize( - "system_site_packages", - ((True, False)), - ids=["ssp_true", "ssp_false"], + ("system_site_packages", "uv"), + ( + pytest.param(True, False, id="ssp1-uv0"), + pytest.param(False, False, id="ssp0-uv0"), + pytest.param(True, True, id="ssp1-uv1"), + pytest.param(False, True, id="ssp0-uv1"), + ), ) def test_paths( + *, tmpdir: Path, - system_site_packages: bool, # noqa: FBT001 + system_site_packages: bool, + uv: bool, + monkeypatch: pytest.MonkeyPatch, output: Output, + caplog: pytest.LogCaptureFixture, ) -> None: """Test the paths. @@ -58,16 +66,31 @@ def test_paths( Args: tmpdir: A temporary directory. system_site_packages: Whether to include system site packages. + 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. """ - venv = tmpdir / "test_venv" - args = gen_args( - venv=str(venv), - system_site_packages=system_site_packages, - ) + 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() - 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 + else: + assert len(caplog.messages) == 0 + assert "-m venv" in config.venv_cmd + assert "-m pip" in config.pip_cmd assert config.venv == venv for attr in ( diff --git a/tests/unit/test_installer.py b/tests/unit/test_installer.py index 8323590..216d2e6 100644 --- a/tests/unit/test_installer.py +++ b/tests/unit/test_installer.py @@ -739,7 +739,7 @@ def test_collection_pre_install( @pytest.mark.parametrize("first", (True, False), ids=["editable", "not_editable"]) @pytest.mark.parametrize("second", (True, False), ids=["editable", "not_editable"]) -def test_reinstall_local_collection( # noqa: PLR0913 # pylint: disable=too-many-positional-arguments +def test_reinstall_local_collection( # pylint: disable=too-many-positional-arguments first: bool, # noqa: FBT001 second: bool, # noqa: FBT001 tmp_path: Path,