Skip to content

Commit 0f5cfa4

Browse files
fix(venv): respect base_python passed as absolute path (#69)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c664d31 commit 0f5cfa4

File tree

2 files changed

+86
-3
lines changed

2 files changed

+86
-3
lines changed

src/tox_uv/_venv.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import json
6+
import os
67
import sys
78
from abc import ABC
89
from functools import cached_property
@@ -21,6 +22,8 @@
2122
from tox.execute.request import StdinSource
2223
from tox.tox_env.python.api import Python, PythonInfo, VersionInfo
2324
from uv import find_uv_bin
25+
from virtualenv import app_data
26+
from virtualenv.discovery import cached_py_info
2427
from virtualenv.discovery.py_spec import PythonSpec
2528

2629
from ._installer import UvInstaller
@@ -83,6 +86,18 @@ def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR
8386
platform=sys.platform,
8487
extra={},
8588
)
89+
if Path(base).is_absolute():
90+
info = cached_py_info.from_exe(
91+
cached_py_info.PythonInfo, app_data.make_app_data(None, read_only=False, env=os.environ), base
92+
)
93+
return PythonInfo(
94+
implementation=info.implementation,
95+
version_info=VersionInfo(*info.version_info),
96+
version=info.version,
97+
is_64=info.architecture == 64, # noqa: PLR2004
98+
platform=info.platform,
99+
extra={"executable": base},
100+
)
86101
spec = PythonSpec.from_string_spec(base)
87102
return PythonInfo(
88103
implementation=spec.implementation or "CPython",
@@ -124,8 +139,12 @@ def _default_pass_env(self) -> list[str]:
124139
return env
125140

126141
def create_python_env(self) -> None:
127-
base, imp = self.base_python.version_info, self.base_python.impl_lower
128-
if (base.major, base.minor) == sys.version_info[:2] and (sys.implementation.name.lower() == imp):
142+
base = self.base_python.version_info
143+
imp = self.base_python.impl_lower
144+
executable = self.base_python.extra.get("executable")
145+
if executable:
146+
version_spec = executable
147+
elif (base.major, base.minor) == sys.version_info[:2] and (sys.implementation.name.lower() == imp):
129148
version_spec = sys.executable
130149
else:
131150
uv_imp = "" if (imp and imp == "cpython") else imp

tests/test_tox_uv_venv.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
import os
55
import os.path
66
import pathlib
7+
import platform
78
import subprocess # noqa: S404
89
import sys
910
from configparser import ConfigParser
1011
from importlib.metadata import version
1112
from typing import TYPE_CHECKING
1213

14+
import pytest
15+
1316
if TYPE_CHECKING:
14-
import pytest
1517
from tox.pytest import ToxProjectCreator
1618

1719

@@ -50,6 +52,68 @@ def test_uv_venv_spec_major_only(tox_project: ToxProjectCreator) -> None:
5052
result.assert_success()
5153

5254

55+
@pytest.fixture
56+
def other_interpreter_exe() -> pathlib.Path: # pragma: no cover
57+
"""Returns an interpreter executable path that is not the exact same as `sys.executable`.
58+
59+
Necessary because `sys.executable` gets short-circuited when used as `base_python`."""
60+
61+
exe = pathlib.Path(sys.executable)
62+
base_python: pathlib.Path | None = None
63+
if exe.name == "python":
64+
# python -> pythonX.Y
65+
ver = sys.version_info
66+
base_python = exe.with_name(f"python{ver.major}.{ver.minor}")
67+
elif exe.name[-1].isdigit():
68+
# python X[.Y] -> python
69+
base_python = exe.with_name(exe.stem[:-1])
70+
elif exe.suffix == ".exe":
71+
# python.exe <-> pythonw.exe
72+
base_python = (
73+
exe.with_name(exe.stem[:-1] + ".exe") if exe.stem.endswith("w") else exe.with_name(exe.stem + "w.exe")
74+
)
75+
if not base_python or not base_python.is_file():
76+
pytest.fail("Tried to pick a base_python that is not sys.executable, but failed.")
77+
return base_python
78+
79+
80+
def test_uv_venv_spec_abs_path(tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path) -> None:
81+
project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={other_interpreter_exe}"})
82+
result = project.run("-vv")
83+
result.assert_success()
84+
85+
86+
def test_uv_venv_spec_abs_path_conflict_ver(
87+
tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path
88+
) -> None:
89+
# py27 is long gone, but still matches the testenv capture regex, so we know it will fail
90+
project = tox_project({"tox.ini": f"[testenv:py27]\npackage=skip\nbase_python={other_interpreter_exe}"})
91+
result = project.run("-vv", "-e", "py27")
92+
result.assert_failed()
93+
assert f"failed with env name py27 conflicting with base python {other_interpreter_exe}" in result.out
94+
95+
96+
def test_uv_venv_spec_abs_path_conflict_impl(
97+
tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path
98+
) -> None:
99+
env = "pypy" if platform.python_implementation() == "CPython" else "cpython"
100+
project = tox_project({"tox.ini": f"[testenv:{env}]\npackage=skip\nbase_python={other_interpreter_exe}"})
101+
result = project.run("-vv", "-e", env)
102+
result.assert_failed()
103+
assert f"failed with env name {env} conflicting with base python {other_interpreter_exe}" in result.out
104+
105+
106+
def test_uv_venv_spec_abs_path_conflict_platform(
107+
tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path
108+
) -> None:
109+
ver = sys.version_info
110+
env = f"py{ver.major}{ver.minor}-linux" if sys.platform == "win32" else f"py{ver.major}{ver.minor}-win32"
111+
project = tox_project({"tox.ini": f"[testenv:{env}]\npackage=skip\nbase_python={other_interpreter_exe}"})
112+
result = project.run("-vv", "-e", env)
113+
result.assert_failed()
114+
assert f"failed with env name {env} conflicting with base python {other_interpreter_exe}" in result.out
115+
116+
53117
def test_uv_venv_na(tox_project: ToxProjectCreator) -> None:
54118
project = tox_project({"tox.ini": "[testenv]\npackage=skip\nbase_python=1.0"})
55119
result = project.run("-vv")

0 commit comments

Comments
 (0)