Skip to content
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: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ lint.select = [
"ALL",
]
lint.ignore = [
"ANN101", # no type annotation for self
"ANN401", # allow Any as type annotation
"COM812", # Conflict with formatter
"CPY", # No copyright statements
Expand Down
71 changes: 54 additions & 17 deletions src/pre_commit_uv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@

# only import built-ins at top level to avoid interpreter startup overhead
import os
import pathlib
import sys
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast

if TYPE_CHECKING:
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from typing import Protocol

from pre_commit.prefix import Prefix

class _PatchablePython(Protocol):
install_environment: Callable[[Prefix, str, Sequence[str]], None]
health_check: Callable[[Prefix, str], str | None]


_original_main = None

Expand Down Expand Up @@ -57,9 +66,6 @@ def _new_main(argv: Sequence[str] | None = None) -> int:

from pre_commit.languages import python # noqa: PLC0415

if TYPE_CHECKING:
from pre_commit.prefix import Prefix # noqa: PLC0415

def _install_environment(
prefix: Prefix,
version: str,
Expand Down Expand Up @@ -121,17 +127,48 @@ def uv_version() -> str:

return _metadata_version("uv")

@cache
def _version_info(exe: str) -> str:
from pre_commit.util import CalledProcessError, cmd_output # noqa: PLC0415

prog = 'import sys;print(".".join(str(p) for p in sys.version_info[0:3]))'
try:
return cmd_output(exe, "-S", "-c", prog)[1].strip()
except CalledProcessError:
return f"<<error retrieving version from {exe}>>"

python.install_environment = _install_environment # ty: ignore[invalid-assignment]
python._version_info = _version_info # noqa: SLF001
patched = cast("_PatchablePython", python)
patched.install_environment = _install_environment
patched.health_check = _health_check
assert _original_main is not None # noqa: S101
return _original_main(argv)


def _version_info(exe: str) -> str:
from pre_commit.util import CalledProcessError, cmd_output # noqa: PLC0415

prog = 'import sys;print(".".join(str(p) for p in sys.version_info[0:3]))'
try:
return cmd_output(exe, "-S", "-c", prog)[1].strip()
except CalledProcessError:
return f"<<error retrieving version from {exe}>>"


def _health_check(prefix: Prefix, version: str) -> str | None:
# uv may record fewer version components in pyvenv.cfg than pre-commit expects (e.g. "3.14" vs "3.14.6"),
# so compare only the components uv actually wrote rather than requiring an exact string match
from pre_commit.lang_base import environment_dir # noqa: PLC0415
from pre_commit.languages import python # noqa: PLC0415
from pre_commit.util import win_exe # noqa: PLC0415

pyvenv_cfg = pathlib.Path(environment_dir(prefix, python.ENVIRONMENT_DIR, version)) / "pyvenv.cfg"
if not pyvenv_cfg.exists():
return "pyvenv.cfg does not exist (old virtualenv?)"

cfg = python._read_pyvenv_cfg(str(pyvenv_cfg)) # noqa: SLF001
if "version_info" not in cfg:
return "created virtualenv's pyvenv.cfg is missing `version_info`"
expected = cfg["version_info"].split(".")

py_exe = prefix.path(python.bin_dir(str(pyvenv_cfg.parent)), win_exe("python"))
targets = [("virtualenv python", py_exe)]
if "base-executable" in cfg:
targets.append(("base executable", cfg["base-executable"]))
for label, exe in targets:
if (actual := _version_info(exe)).split(".")[: len(expected)] != expected:
return (
f"{label} version did not match created version:\n"
f"- actual version: {actual}\n"
f"- expected version: {cfg['version_info']}\n"
)
return None
77 changes: 77 additions & 0 deletions tests/test_health_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from __future__ import annotations

import sys
from typing import TYPE_CHECKING

from pre_commit.languages import python
from pre_commit.prefix import Prefix

from pre_commit_uv import _health_check

if TYPE_CHECKING:
from pathlib import Path

version = "default"
major_minor = f"{sys.version_info[0]}.{sys.version_info[1]}"
full_version = ".".join(str(p) for p in sys.version_info[0:3])
wrong_version = f"{sys.version_info[0]}.{sys.version_info[1] + 1}"


def _make_env(tmp_path: Path, pyvenv_cfg: str | None) -> Prefix:
envdir = tmp_path / f"py_env-{version}"
bin_dir = python.bin_dir(str(envdir))
(tmp_path / bin_dir).mkdir(parents=True)
(tmp_path / bin_dir / python.win_exe("python")).symlink_to(sys.executable)
if pyvenv_cfg is not None:
(envdir / "pyvenv.cfg").write_text(pyvenv_cfg)
return Prefix(str(tmp_path))


def test_health_check_truncated_version_passes(tmp_path: Path) -> None:
"""uv >=0.11.22 writes only the major.minor into pyvenv.cfg (see issue #152)."""
prefix = _make_env(tmp_path, f"version_info = {major_minor}\nbase-executable = {sys.executable}\n")

assert _health_check(prefix, version) is None


def test_health_check_full_version_passes(tmp_path: Path) -> None:
prefix = _make_env(tmp_path, f"version_info = {full_version}\nbase-executable = {sys.executable}\n")

assert _health_check(prefix, version) is None


def test_health_check_no_base_executable_passes(tmp_path: Path) -> None:
prefix = _make_env(tmp_path, f"version_info = {major_minor}\n")

assert _health_check(prefix, version) is None


def test_health_check_version_mismatch_fails(tmp_path: Path) -> None:
prefix = _make_env(tmp_path, f"version_info = {wrong_version}\n")

result = _health_check(prefix, version)

assert result is not None
assert "virtualenv python version did not match created version" in result
assert f"- expected version: {wrong_version}" in result


def test_health_check_base_executable_mismatch_fails(tmp_path: Path) -> None:
prefix = _make_env(tmp_path, f"version_info = {major_minor}\nbase-executable = /does/not/exist/python\n")

result = _health_check(prefix, version)

assert result is not None
assert "base executable version did not match created version" in result


def test_health_check_missing_pyvenv_cfg(tmp_path: Path) -> None:
prefix = _make_env(tmp_path, None)

assert _health_check(prefix, version) == "pyvenv.cfg does not exist (old virtualenv?)"


def test_health_check_missing_version_info(tmp_path: Path) -> None:
prefix = _make_env(tmp_path, "home = /usr\n")

assert _health_check(prefix, version) == "created virtualenv's pyvenv.cfg is missing `version_info`"