diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4802b3502..4505e1076 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: push: branches: ["main"] - tags-ignore: [ "**" ] + tags-ignore: ["**"] pull_request: schedule: - cron: "0 8 * * *" @@ -76,6 +76,7 @@ jobs: - windows-latest exclude: - { os: windows-latest, tox_env: pkg_meta } + - { os: windows-latest, tox_env: docs } steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f95a0a256..541c4a9cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: release: needs: - - build + - build runs-on: ubuntu-latest environment: name: release diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ee81e4ff..89e0e8200 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: rev: 0.29.1 hooks: - id: check-github-workflows - args: [ "--verbose" ] + args: ["--verbose"] - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: @@ -24,7 +24,7 @@ repos: hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.5.6" + rev: "v0.5.7" hooks: - id: ruff-format - id: ruff @@ -33,11 +33,18 @@ repos: rev: 1.18.0 hooks: - id: blacken-docs - additional_dependencies: [black==24.4.2] + additional_dependencies: [black==24.8] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: rst-backticks + - repo: https://github.com/rbubley/mirrors-prettier + rev: "v3.3.3" # Use the sha / tag you want to point at + hooks: + - id: prettier + additional_dependencies: + - prettier@3.3.3 + - "@prettier/plugin-xml@3.4.1" - repo: local hooks: - id: changelogs-rst diff --git a/docs/changelog/3325.bugfix.rst b/docs/changelog/3325.bugfix.rst new file mode 100644 index 000000000..51075bf6a --- /dev/null +++ b/docs/changelog/3325.bugfix.rst @@ -0,0 +1 @@ +Fix absolute base python paths conflicting - by :user:`gaborbernat`. diff --git a/pyproject.toml b/pyproject.toml index b4c923204..8fe6f1748 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,7 +128,8 @@ lint.ignore = [ "D", # ignore documentation for now "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible - "DOC201", # broken with sphinx docs + "DOC201", # no restructuredtext support yet + "DOC402", # no restructuredtext support yet "DOC501", # broken with sphinx docs "INP001", # no implicit namespaces here "ISC001", # conflicts with formatter diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py index ae4f8cb2f..d27ca45d9 100644 --- a/src/tox/tox_env/python/api.py +++ b/src/tox/tox_env/python/api.py @@ -168,9 +168,14 @@ def _validate_base_python( if env_base_python is not None: spec_name = PythonSpec.from_string_spec(env_base_python) for base_python in base_pythons: - if Path(base_python).is_absolute(): - return [base_python] spec_base = PythonSpec.from_string_spec(base_python) + if spec_base.path is not None: + path = Path(spec_base.path).absolute() + if str(spec_base.path) == sys.executable: + ver, is_64 = sys.version_info, sys.maxsize != 2**32 + spec_base = PythonSpec.from_string_spec(f"{sys.implementation}{ver.major}{ver.minor}-{is_64}") + else: + spec_base = cls.python_spec_for_path(path) if any( getattr(spec_base, key) != getattr(spec_name, key) for key in ("implementation", "major", "minor", "micro", "architecture") @@ -183,6 +188,17 @@ def _validate_base_python( raise Fail(msg) return base_pythons + @classmethod + @abstractmethod + def python_spec_for_path(cls, path: Path) -> PythonSpec: + """ + Get the spec for an absolute path to a Python executable. + + :param path: the path investigated + :return: the found spec + """ + raise NotImplementedError + @abstractmethod def env_site_package_dir(self) -> Path: """ diff --git a/src/tox/tox_env/python/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py index 34a6c56b7..733860262 100644 --- a/src/tox/tox_env/python/virtual_env/api.py +++ b/src/tox/tox_env/python/virtual_env/api.py @@ -4,11 +4,14 @@ import os import sys +from abc import ABC from pathlib import Path from typing import TYPE_CHECKING, Any, cast from virtualenv import __version__ as virtualenv_version -from virtualenv import session_via_cli +from virtualenv import app_data, session_via_cli +from virtualenv.discovery import cached_py_info +from virtualenv.discovery.py_spec import PythonSpec from tox.config.loader.str_convert import StrConvert from tox.execute.local_sub_process import LocalSubProcessExecutor @@ -17,13 +20,14 @@ if TYPE_CHECKING: from virtualenv.create.creator import Creator + from virtualenv.discovery.py_info import PythonInfo as VirtualenvPythonInfo from virtualenv.run.session import Session from tox.execute.api import Execute from tox.tox_env.api import ToxEnvCreateArgs -class VirtualEnv(Python): +class VirtualEnv(Python, ABC): """A python executor that uses the virtualenv project with pip.""" def __init__(self, create_args: ToxEnvCreateArgs) -> None: @@ -167,3 +171,30 @@ def environment_variables(self) -> dict[str, str]: environment_variables = super().environment_variables environment_variables["VIRTUAL_ENV"] = str(self.conf["env_dir"]) return environment_variables + + @classmethod + def python_spec_for_path(cls, path: Path) -> PythonSpec: + """ + Get the spec for an absolute path to a Python executable. + + :param path: the path investigated + :return: the found spec + """ + info = cls.get_virtualenv_py_info(path) + return PythonSpec.from_string_spec( + f"{info.implementation}{info.version_info.major}{info.version_info.minor}-{info.architecture}" + ) + + @staticmethod + def get_virtualenv_py_info(path: Path) -> VirtualenvPythonInfo: + """ + Get the version info for an absolute path to a Python executable. + + :param path: the path investigated + :return: the found information (cached) + """ + return cached_py_info.from_exe( + cached_py_info.PythonInfo, + app_data.make_app_data(None, read_only=False, env=os.environ), + str(path), + ) diff --git a/tests/tox_env/python/test_python_api.py b/tests/tox_env/python/test_python_api.py index 048f398c2..8690cc838 100644 --- a/tests/tox_env/python/test_python_api.py +++ b/tests/tox_env/python/test_python_api.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import sys from typing import TYPE_CHECKING, Callable from unittest.mock import patch @@ -126,24 +125,6 @@ def test_base_python_env_no_conflict(env: str, base_python: list[str], ignore_co assert result is base_python -@pytest.mark.parametrize( - ("env", "base_python", "platform"), - [ - ("py312-unix", ["/opt/python312/bin/python"], "posix"), - ("py312-win", [r"C:\Program Files\Python312\python.exe"], "nt"), - ("py311-win", [r"\\a\python311\python.exe"], "nt"), - ("py310-win", [r"\\?\UNC\a\python310\python.exe"], "nt"), - ("py310", ["//a/python310/bin/python"], None), - ], - ids=lambda a: "|".join(a) if isinstance(a, list) else str(a), -) -def test_base_python_absolute(env: str, base_python: list[str], platform: str | None) -> None: - if platform and platform != os.name: - pytest.skip(f"Not applicable to this platform. ({platform} != {os.name})") - result = Python._validate_base_python(env, base_python, False) # noqa: SLF001 - assert result == base_python - - @pytest.mark.parametrize("ignore_conflict", [True, False]) @pytest.mark.parametrize( ("env", "base_python", "expected", "conflict"),