From bc7a91a5ad0c3d2db52d0976f08c791604cba9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=81ajszczak?= Date: Tue, 14 Jan 2025 03:19:50 +0100 Subject: [PATCH] Merge pull request #2821 from filiplajszczak/cli-precedence-2285 Makes --python command-line flag take precedence over env var --- src/virtualenv/discovery/builtin.py | 6 ++- tests/unit/discovery/test_discovery.py | 51 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/virtualenv/discovery/builtin.py b/src/virtualenv/discovery/builtin.py index 4c20832af..a4c621715 100644 --- a/src/virtualenv/discovery/builtin.py +++ b/src/virtualenv/discovery/builtin.py @@ -3,6 +3,7 @@ import logging import os import sys +from collections import deque from contextlib import suppress from pathlib import Path from typing import TYPE_CHECKING @@ -22,13 +23,16 @@ class Builtin(Discover): - python_spec: Sequence[str] + python_spec: Sequence[str] | deque[str] app_data: AppData try_first_with: Sequence[str] def __init__(self, options) -> None: super().__init__(options) self.python_spec = options.python or [sys.executable] + if self._env.get("VIRTUALENV_PYTHON"): + self.python_spec = deque(self.python_spec) + self.python_spec.rotate(-1) self.app_data = options.app_data self.try_first_with = options.try_first_with diff --git a/tests/unit/discovery/test_discovery.py b/tests/unit/discovery/test_discovery.py index 680131e7d..8597c6651 100644 --- a/tests/unit/discovery/test_discovery.py +++ b/tests/unit/discovery/test_discovery.py @@ -99,3 +99,54 @@ def test_discovery_fallback_ok(session_app_data, caplog): assert result.executable == sys.executable, caplog.text assert "accepted" in caplog.text + + +@pytest.fixture +def mock_get_interpreter(mocker): + return mocker.patch( + "virtualenv.discovery.builtin.get_interpreter", + lambda key, *args, **kwargs: getattr(mocker.sentinel, key), # noqa: ARG005 + ) + + +@pytest.mark.usefixtures("mock_get_interpreter") +def test_returns_first_python_specified_when_only_env_var_one_is_specified(mocker, monkeypatch, session_app_data): + monkeypatch.setenv("VIRTUALENV_PYTHON", "python_from_env_var") + builtin = Builtin( + Namespace(app_data=session_app_data, try_first_with=[], python=["python_from_env_var"], env=os.environ), + ) + + result = builtin.run() + + assert result == mocker.sentinel.python_from_env_var + + +@pytest.mark.usefixtures("mock_get_interpreter") +def test_returns_second_python_specified_when_more_than_one_is_specified_and_env_var_is_specified( + mocker, monkeypatch, session_app_data +): + monkeypatch.setenv("VIRTUALENV_PYTHON", "python_from_env_var") + builtin = Builtin( + Namespace( + app_data=session_app_data, + try_first_with=[], + python=["python_from_env_var", "python_from_cli"], + env=os.environ, + ), + ) + + result = builtin.run() + + assert result == mocker.sentinel.python_from_cli + + +@pytest.mark.usefixtures("mock_get_interpreter") +def test_returns_first_python_specified_when_no_env_var_is_specified(mocker, monkeypatch, session_app_data): + monkeypatch.delenv("VIRTUALENV_PYTHON", raising=False) + builtin = Builtin( + Namespace(app_data=session_app_data, try_first_with=[], python=["python_from_cli"], env=os.environ), + ) + + result = builtin.run() + + assert result == mocker.sentinel.python_from_cli