diff --git a/README.md b/README.md index 1978f91..b8dfa3b 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ FindPython finds Python from the following places: - pyenv install root - asdf python install root - [rye](https://rye-up.com) toolchain install root +- [uv](https://docs.astral.sh/uv/) toolchain install root - `/Library/Frameworks/Python.framework/Versions` (MacOS) - Windows registry (Windows only) diff --git a/pyproject.toml b/pyproject.toml index b8c2e30..cd5ecfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,9 @@ package-dir = "src" [tool.pdm.dev-dependencies] tests = ["pytest"] +[tool.pdm.scripts] +test = "pytest tests" + [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" diff --git a/src/findpython/providers/__init__.py b/src/findpython/providers/__init__.py index 036ae10..e4716bb 100644 --- a/src/findpython/providers/__init__.py +++ b/src/findpython/providers/__init__.py @@ -10,6 +10,7 @@ from findpython.providers.path import PathProvider from findpython.providers.pyenv import PyenvProvider from findpython.providers.rye import RyeProvider +from findpython.providers.uv import UvProvider from findpython.providers.winreg import WinregProvider _providers: list[type[BaseProvider]] = [ @@ -19,6 +20,7 @@ AsdfProvider, PyenvProvider, RyeProvider, + UvProvider, # Windows only: WinregProvider, # MacOS only: diff --git a/src/findpython/providers/uv.py b/src/findpython/providers/uv.py new file mode 100644 index 0000000..96bdbb9 --- /dev/null +++ b/src/findpython/providers/uv.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import os +import typing as t +from pathlib import Path + +from findpython.providers.rye import RyeProvider +from findpython.utils import WINDOWS + + +class UvProvider(RyeProvider): + @classmethod + def create(cls) -> t.Self | None: + # See uv#13877(https://github.com/astral-sh/uv/issues/13877) + if WINDOWS: + default_root_str = os.getenv("APPDATA") + else: + default_root_str = "~/.local/share" + assert default_root_str is not None + root_str = os.getenv("UV_PYTHON_INSTALL_DIR") + if root_str is None: + root_str = os.getenv("XDG_DATA_HOME") + if root_str is None: + root_str = default_root_str + root = Path(root_str).expanduser() / "uv" / "python" + else: + root = Path(root_str).expanduser() + return cls(root) diff --git a/tests/test_posix.py b/tests/test_posix.py index e6fd4d8..9ae1a6f 100644 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -9,6 +9,7 @@ from findpython.providers.asdf import AsdfProvider from findpython.providers.pyenv import PyenvProvider from findpython.providers.rye import RyeProvider +from findpython.providers.uv import UvProvider if sys.platform == "win32": pytest.skip("Skip POSIX tests on Windows", allow_module_level=True) @@ -93,3 +94,34 @@ def test_find_python_from_rye_provider(mocked_python, tmp_path, monkeypatch): find_311 = Finder(selected_providers=["rye"]).find_all(3, 11) assert python311 in find_311 + + +def test_find_python_from_uv_provider(mocked_python, tmp_path, monkeypatch): + python310 = mocked_python.add_python( + tmp_path / ".local/share/uv/python/cpython@3.10.9/install/bin/python3", "3.10.9" + ) + python311 = mocked_python.add_python( + tmp_path / ".local/share/uv/python/cpython@3.11.8/bin/python3", "3.11.8" + ) + monkeypatch.setenv("HOME", str(tmp_path)) + + register_provider(UvProvider) + find_310 = Finder(selected_providers=["uv"]).find_all(3, 10) + assert python310 in find_310 + + find_311 = Finder(selected_providers=["uv"]).find_all(3, 11) + assert python311 in find_311 + + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg")) + python310_xdg = mocked_python.add_python( + tmp_path / "xdg/uv/python/cpython@3.10.9/install/bin/python3", "3.10.9" + ) + find_310 = Finder(selected_providers=["uv"]).find_all(3, 10) + assert python310_xdg in find_310 + + monkeypatch.setenv("UV_PYTHON_INSTALL_DIR", str(tmp_path / "uv_dir")) + python311_uv_dir = mocked_python.add_python( + tmp_path / "uv_dir/cpython@3.11.8/bin/python3", "3.11.8" + ) + find_311 = Finder(selected_providers=["uv"]).find_all(3, 11) + assert python311_uv_dir in find_311