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
2 changes: 2 additions & 0 deletions docs/changelog/+uv-minor-link-order.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Rank uv's minor version links behind the concrete install on Windows too, where uv creates them as directory junctions
that ``Path.is_symlink()`` does not report.
5 changes: 3 additions & 2 deletions src/python_discovery/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,9 @@ def _uv_sort_key(install: _UvInstall) -> tuple[int, ...]:
version = key.version
return (
impl_order.index(key.implementation) if key.implementation in impl_order else len(impl_order),
# uv's minor version links point at a concrete install that moves on upgrade, so report the target instead
int(install.path.is_symlink()),
# uv's minor version links point at a concrete install that moves on upgrade, so report the target instead;
# spot them by the name they resolve to, as on Windows they are junctions, which is_symlink() does not report
int(install.path.resolve().name != install.path.name),
-version.major,
-version.minor,
-version.micro,
Expand Down
36 changes: 36 additions & 0 deletions tests/test_iter_interpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,42 @@ def test_iter_interpreters_uv_minor_version_link_loses_to_its_target(
assert uv_probed("3.15") == ["cpython-3.15.0b3-macos-aarch64-none/bin/python"]


@pytest.mark.parametrize(
("concrete", "link"),
[
pytest.param("cpython-3.15.0b3-windows-x86_64-none", "cpython-3.15-windows-x86_64-none", id="prerelease"),
# the link ties with its target on version, so directory name order decides, and "-" sorts before "."
pytest.param("cpython-3.14.0-windows-x86_64-none", "cpython-3.14-windows-x86_64-none", id="first-release"),
],
)
def test_iter_interpreters_uv_minor_version_junction_loses_to_its_target(
uv_install: Callable[..., None],
uv_probed: Callable[[str | None], list[str]],
uv_junction: Callable[[str, str], None],
concrete: str,
link: str,
) -> None:
uv_install(f"{concrete}/python.exe")
uv_junction(concrete, link)

assert uv_probed("3") == [f"{concrete}/python.exe"]


@pytest.fixture
def uv_junction(uv_dir: Path, mocker: MockerFixture) -> Callable[[str, str], None]:
def _junction(target: str, link: str) -> None:
if IS_WIN: # pragma: win32 cover
import _winapi

_winapi.CreateJunction(str(uv_dir / target), str(uv_dir / link))
else: # pragma: win32 no cover
# stand in for the junction with a symlink that does not report as one
(uv_dir / link).symlink_to(uv_dir / target)
mocker.patch.object(Path, "is_symlink", return_value=False)

return _junction


def test_iter_interpreters_uv_probes_one_executable_per_install(
uv_dir: Path, uv_install: Callable[..., None], monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture
) -> None:
Expand Down
Loading