Skip to content

Commit

Permalink
fix(venv): respect base_python passed as absolute path (#69)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
paveldikov and pre-commit-ci[bot] authored Jul 26, 2024
1 parent c664d31 commit 0f5cfa4
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 3 deletions.
23 changes: 21 additions & 2 deletions src/tox_uv/_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
import os
import sys
from abc import ABC
from functools import cached_property
Expand All @@ -21,6 +22,8 @@
from tox.execute.request import StdinSource
from tox.tox_env.python.api import Python, PythonInfo, VersionInfo
from uv import find_uv_bin
from virtualenv import app_data
from virtualenv.discovery import cached_py_info
from virtualenv.discovery.py_spec import PythonSpec

from ._installer import UvInstaller
Expand Down Expand Up @@ -83,6 +86,18 @@ def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR
platform=sys.platform,
extra={},
)
if Path(base).is_absolute():
info = cached_py_info.from_exe(
cached_py_info.PythonInfo, app_data.make_app_data(None, read_only=False, env=os.environ), base
)
return PythonInfo(
implementation=info.implementation,
version_info=VersionInfo(*info.version_info),
version=info.version,
is_64=info.architecture == 64, # noqa: PLR2004
platform=info.platform,
extra={"executable": base},
)
spec = PythonSpec.from_string_spec(base)
return PythonInfo(
implementation=spec.implementation or "CPython",
Expand Down Expand Up @@ -124,8 +139,12 @@ def _default_pass_env(self) -> list[str]:
return env

def create_python_env(self) -> None:
base, imp = self.base_python.version_info, self.base_python.impl_lower
if (base.major, base.minor) == sys.version_info[:2] and (sys.implementation.name.lower() == imp):
base = self.base_python.version_info
imp = self.base_python.impl_lower
executable = self.base_python.extra.get("executable")
if executable:
version_spec = executable
elif (base.major, base.minor) == sys.version_info[:2] and (sys.implementation.name.lower() == imp):
version_spec = sys.executable
else:
uv_imp = "" if (imp and imp == "cpython") else imp
Expand Down
66 changes: 65 additions & 1 deletion tests/test_tox_uv_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
import os
import os.path
import pathlib
import platform
import subprocess # noqa: S404
import sys
from configparser import ConfigParser
from importlib.metadata import version
from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
import pytest
from tox.pytest import ToxProjectCreator


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


@pytest.fixture
def other_interpreter_exe() -> pathlib.Path: # pragma: no cover
"""Returns an interpreter executable path that is not the exact same as `sys.executable`.
Necessary because `sys.executable` gets short-circuited when used as `base_python`."""

exe = pathlib.Path(sys.executable)
base_python: pathlib.Path | None = None
if exe.name == "python":
# python -> pythonX.Y
ver = sys.version_info
base_python = exe.with_name(f"python{ver.major}.{ver.minor}")
elif exe.name[-1].isdigit():
# python X[.Y] -> python
base_python = exe.with_name(exe.stem[:-1])
elif exe.suffix == ".exe":
# python.exe <-> pythonw.exe
base_python = (
exe.with_name(exe.stem[:-1] + ".exe") if exe.stem.endswith("w") else exe.with_name(exe.stem + "w.exe")
)
if not base_python or not base_python.is_file():
pytest.fail("Tried to pick a base_python that is not sys.executable, but failed.")
return base_python


def test_uv_venv_spec_abs_path(tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path) -> None:
project = tox_project({"tox.ini": f"[testenv]\npackage=skip\nbase_python={other_interpreter_exe}"})
result = project.run("-vv")
result.assert_success()


def test_uv_venv_spec_abs_path_conflict_ver(
tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path
) -> None:
# py27 is long gone, but still matches the testenv capture regex, so we know it will fail
project = tox_project({"tox.ini": f"[testenv:py27]\npackage=skip\nbase_python={other_interpreter_exe}"})
result = project.run("-vv", "-e", "py27")
result.assert_failed()
assert f"failed with env name py27 conflicting with base python {other_interpreter_exe}" in result.out


def test_uv_venv_spec_abs_path_conflict_impl(
tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path
) -> None:
env = "pypy" if platform.python_implementation() == "CPython" else "cpython"
project = tox_project({"tox.ini": f"[testenv:{env}]\npackage=skip\nbase_python={other_interpreter_exe}"})
result = project.run("-vv", "-e", env)
result.assert_failed()
assert f"failed with env name {env} conflicting with base python {other_interpreter_exe}" in result.out


def test_uv_venv_spec_abs_path_conflict_platform(
tox_project: ToxProjectCreator, other_interpreter_exe: pathlib.Path
) -> None:
ver = sys.version_info
env = f"py{ver.major}{ver.minor}-linux" if sys.platform == "win32" else f"py{ver.major}{ver.minor}-win32"
project = tox_project({"tox.ini": f"[testenv:{env}]\npackage=skip\nbase_python={other_interpreter_exe}"})
result = project.run("-vv", "-e", env)
result.assert_failed()
assert f"failed with env name {env} conflicting with base python {other_interpreter_exe}" in result.out


def test_uv_venv_na(tox_project: ToxProjectCreator) -> None:
project = tox_project({"tox.ini": "[testenv]\npackage=skip\nbase_python=1.0"})
result = project.run("-vv")
Expand Down

0 comments on commit 0f5cfa4

Please sign in to comment.