From 0fdd475d259dc77c57ae08e1abdaf26699f8ea46 Mon Sep 17 00:00:00 2001 From: monchin Date: Fri, 6 Jun 2025 13:12:42 +0800 Subject: [PATCH 1/2] Add support for uv installed python(#31) --- README.md | 1 + pyproject.toml | 3 ++ src/findpython/providers/__init__.py | 2 ++ src/findpython/providers/uv.py | 46 ++++++++++++++++++++++++++++ tests/test_posix.py | 32 +++++++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 src/findpython/providers/uv.py 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..49e9179 --- /dev/null +++ b/src/findpython/providers/uv.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import os +import typing as t +from pathlib import Path + +from findpython.providers.base import BaseProvider +from findpython.python import PythonVersion +from findpython.utils import WINDOWS, safe_iter_dir + + +class UvProvider(BaseProvider): + def __init__(self, root: Path) -> None: + self.root = root + + @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() + print(f"{root = }") + return cls(root) + + def find_pythons(self) -> t.Iterable[PythonVersion]: + if not self.root.exists(): + return + for child in safe_iter_dir(self.root): + for intermediate in ("", "install/"): + if WINDOWS: + python_bin = child / (intermediate + "python.exe") + else: + python_bin = child / (intermediate + "bin/python3") + if python_bin.exists(): + yield self.version_maker(python_bin, _interpreter=python_bin) + break 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 From 71e1e81844737a59b40494f0c22384e96d359c32 Mon Sep 17 00:00:00 2001 From: monchin Date: Fri, 6 Jun 2025 17:00:56 +0800 Subject: [PATCH 2/2] eliminate repeating codes and remove debug "print" --- src/findpython/providers/uv.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/findpython/providers/uv.py b/src/findpython/providers/uv.py index 49e9179..96bdbb9 100644 --- a/src/findpython/providers/uv.py +++ b/src/findpython/providers/uv.py @@ -4,15 +4,11 @@ import typing as t from pathlib import Path -from findpython.providers.base import BaseProvider -from findpython.python import PythonVersion -from findpython.utils import WINDOWS, safe_iter_dir +from findpython.providers.rye import RyeProvider +from findpython.utils import WINDOWS -class UvProvider(BaseProvider): - def __init__(self, root: Path) -> None: - self.root = root - +class UvProvider(RyeProvider): @classmethod def create(cls) -> t.Self | None: # See uv#13877(https://github.com/astral-sh/uv/issues/13877) @@ -29,18 +25,4 @@ def create(cls) -> t.Self | None: root = Path(root_str).expanduser() / "uv" / "python" else: root = Path(root_str).expanduser() - print(f"{root = }") return cls(root) - - def find_pythons(self) -> t.Iterable[PythonVersion]: - if not self.root.exists(): - return - for child in safe_iter_dir(self.root): - for intermediate in ("", "install/"): - if WINDOWS: - python_bin = child / (intermediate + "python.exe") - else: - python_bin = child / (intermediate + "bin/python3") - if python_bin.exists(): - yield self.version_maker(python_bin, _interpreter=python_bin) - break