Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

correct virtualenv bin dir under mingw python #901

Merged
merged 4 commits into from
Jan 3, 2025
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
14 changes: 9 additions & 5 deletions nox/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
import functools
import json
import os
import platform
import re
import shutil
import subprocess
import sys
import sysconfig
from pathlib import Path
from socket import gethostbyname
from typing import TYPE_CHECKING, Any, ClassVar
Expand Down Expand Up @@ -64,6 +64,11 @@ def __dir__() -> list[str]:
return __all__


# Use for test mocking and to make mypy happy
_PLATFORM = sys.platform
_IS_MINGW = sysconfig.get_platform().startswith("mingw")


# Problematic environment variables that are stripped from all commands inside
# of a virtualenv. See https://github.com/theacodes/nox/issues/44
_BLACKLISTED_ENV_VARS = frozenset(
Expand All @@ -74,7 +79,6 @@ def __dir__() -> list[str]:
"UV_SYSTEM_PYTHON",
]
)
_SYSTEM = platform.system()


def find_uv() -> tuple[bool, str]:
Expand Down Expand Up @@ -359,7 +363,7 @@ def _clean_location(self) -> bool:
def bin_paths(self) -> list[str]:
"""Returns the location of the conda env's bin folder."""
# see https://github.com/conda/conda/blob/f60f0f1643af04ed9a51da3dd4fa242de81e32f4/conda/activate.py#L563-L572
if _SYSTEM == "Windows":
if _PLATFORM.startswith("win"):
return [
self.location,
os.path.join(self.location, "Library", "mingw-w64", "bin"),
Expand Down Expand Up @@ -601,7 +605,7 @@ def _resolved_interpreter(self) -> str:

# The rest of this is only applicable to Windows, so if we don't have
# an interpreter by now, raise.
if _SYSTEM != "Windows":
if not _PLATFORM.startswith("win"):
self._resolved = InterpreterNotFound(self.interpreter)
raise self._resolved

Expand Down Expand Up @@ -630,7 +634,7 @@ def _resolved_interpreter(self) -> str:
@property
def bin_paths(self) -> list[str]:
"""Returns the location of the virtualenv's bin folder."""
if _SYSTEM == "Windows":
if _PLATFORM.startswith("win") and not _IS_MINGW:
return [os.path.join(self.location, "Scripts")]
return [os.path.join(self.location, "bin")]

Expand Down
3 changes: 1 addition & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@
import contextlib
import functools
import os
import platform
import shutil
import sqlite3
import sys

import nox

ON_WINDOWS_CI = "CI" in os.environ and platform.system() == "Windows"
ON_WINDOWS_CI = "CI" in os.environ and sys.platform.startswith("win32")

nox.needs_version = ">=2024.4.15"
nox.options.default_venv_backend = "uv|virtualenv"
Expand Down
10 changes: 5 additions & 5 deletions tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
PYTHON = sys.executable

skip_on_windows_primary_console_session = pytest.mark.skipif(
platform.system() == "Windows" and "SECONDARY_CONSOLE_SESSION" not in os.environ,
sys.platform.startswith("win") and "SECONDARY_CONSOLE_SESSION" not in os.environ,
reason="On Windows, this test must run in a separate console session.",
)

only_on_windows = pytest.mark.skipif(
platform.system() != "Windows", reason="Only run this test on Windows."
not sys.platform.startswith("win"), reason="Only run this test on Windows."
)


Expand Down Expand Up @@ -127,7 +127,7 @@ def test_run_verbosity_failed_command(


@pytest.mark.skipif(
platform.system() == "Windows",
sys.platform.startswith("win"),
reason="See https://github.com/python/cpython/issues/85815",
)
def test_run_non_str() -> None:
Expand Down Expand Up @@ -285,7 +285,7 @@ def enable_ctrl_c(*, enabled: bool) -> None:

def interrupt_process(proc: subprocess.Popen[Any]) -> None:
"""Send SIGINT or CTRL_C_EVENT to the process."""
if platform.system() == "Windows":
if sys.platform.startswith("win"):
# Disable Ctrl-C so we don't terminate ourselves.
enable_ctrl_c(enabled=False)

Expand All @@ -301,7 +301,7 @@ def command_with_keyboard_interrupt(
monkeypatch: pytest.MonkeyPatch, marker: Any
) -> None:
"""Monkeypatch Popen.communicate to raise KeyboardInterrupt."""
if platform.system() == "Windows":
if sys.platform.startswith("win"):
# Enable Ctrl-C because the child inherits the setting from us.
enable_ctrl_c(enabled=True)

Expand Down
48 changes: 30 additions & 18 deletions tests/test_virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

from nox.virtualenv import CondaEnv, ProcessEnv, VirtualEnv

IS_WINDOWS = nox.virtualenv._SYSTEM == "Windows"
IS_WINDOWS = sys.platform.startswith("win")
HAS_CONDA = shutil.which("conda") is not None
HAS_UV = shutil.which("uv") is not None
RAISE_ERROR = "RAISE_ERROR"
Expand Down Expand Up @@ -249,7 +249,7 @@ def test_conda_env_create_verbose(
assert kwargs["log"]


@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
def test_condaenv_bin_windows(make_conda: Callable[..., tuple[CondaEnv, Path]]) -> None:
venv, dir_ = make_conda()
assert [
Expand Down Expand Up @@ -406,7 +406,7 @@ def test_bin_paths(
assert str(dir_.joinpath("Scripts" if IS_WINDOWS else "bin")) == venv.bin


@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
def test_bin_windows(
make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Path]],
) -> None:
Expand All @@ -417,6 +417,18 @@ def test_bin_windows(
assert str(dir_.joinpath("Scripts")) == venv.bin


@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch("nox.virtualenv._IS_MINGW", new=True)
def test_bin_windows_mingw(
make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Path]],
) -> None:
venv, dir_ = make_one()
assert venv.bin_paths
assert len(venv.bin_paths) == 1
assert venv.bin_paths[0] == venv.bin
assert str(dir_.joinpath("bin")) == venv.bin


def test_create(
monkeypatch: pytest.MonkeyPatch,
make_one: Callable[..., tuple[VirtualEnv, Path]],
Expand Down Expand Up @@ -899,7 +911,7 @@ def test__resolved_interpreter_none(
("2.7.15", "python2.7"),
],
)
@mock.patch("nox.virtualenv._SYSTEM", new="Linux")
@mock.patch("nox.virtualenv._PLATFORM", new="linux")
@mock.patch.object(shutil, "which", return_value=True)
def test__resolved_interpreter_numerical_non_windows(
which: mock.Mock,
Expand All @@ -914,7 +926,7 @@ def test__resolved_interpreter_numerical_non_windows(


@pytest.mark.parametrize("input_", ["2.", "2.7."])
@mock.patch("nox.virtualenv._SYSTEM", new="Linux")
@mock.patch("nox.virtualenv._PLATFORM", new="linux")
@mock.patch.object(shutil, "which", return_value=False)
def test__resolved_interpreter_invalid_numerical_id(
which: mock.Mock,
Expand All @@ -929,7 +941,7 @@ def test__resolved_interpreter_invalid_numerical_id(
which.assert_called_once_with(input_)


@mock.patch("nox.virtualenv._SYSTEM", new="Linux")
@mock.patch("nox.virtualenv._PLATFORM", new="linux")
@mock.patch.object(shutil, "which", return_value=False)
def test__resolved_interpreter_32_bit_non_windows(
which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Path]]
Expand All @@ -941,7 +953,7 @@ def test__resolved_interpreter_32_bit_non_windows(
which.assert_called_once_with("3.6-32")


@mock.patch("nox.virtualenv._SYSTEM", new="Linux")
@mock.patch("nox.virtualenv._PLATFORM", new="linux")
@mock.patch.object(shutil, "which", return_value=True)
def test__resolved_interpreter_non_windows(
which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Path]]
Expand All @@ -954,7 +966,7 @@ def test__resolved_interpreter_non_windows(
which.assert_called_once_with("python3.6")


@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch.object(shutil, "which")
def test__resolved_interpreter_windows_full_path(
which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Path]]
Expand All @@ -976,7 +988,7 @@ def test__resolved_interpreter_windows_full_path(
("2.7-32", r"c:\python27\python.exe"),
],
)
@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch.object(subprocess, "run")
@mock.patch.object(shutil, "which")
def test__resolved_interpreter_windows_pyexe(
Expand Down Expand Up @@ -1011,7 +1023,7 @@ def special_run(cmd: str, *args: str, **kwargs: object) -> TextProcessResult:
which.assert_has_calls([mock.call(input_), mock.call("py")])


@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch.object(subprocess, "run")
@mock.patch.object(shutil, "which")
def test__resolved_interpreter_windows_pyexe_fails(
Expand All @@ -1020,8 +1032,8 @@ def test__resolved_interpreter_windows_pyexe_fails(
# Establish that if the py launcher fails, we give the right error.
venv, _ = make_one(interpreter="python3.6")

# Trick the nox.virtualenv._SYSTEM into thinking that it cannot find python3.6
# (it likely will on Unix). Also, when the nox.virtualenv._SYSTEM looks for the
# Trick the nox.virtualenv into thinking that it cannot find python3.6
# (it likely will on Unix). Also, when the nox.virtualenv looks for the
# py launcher, give it a dummy that fails.
def special_run(cmd: str, *args: str, **kwargs: object) -> TextProcessResult: # noqa: ARG001
return TextProcessResult("", 1)
Expand All @@ -1036,7 +1048,7 @@ def special_run(cmd: str, *args: str, **kwargs: object) -> TextProcessResult: #
which.assert_has_calls([mock.call("python3.6"), mock.call("py")])


@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch("nox.virtualenv.UV_PYTHON_SUPPORT", new=False)
def test__resolved_interpreter_windows_path_and_version(
make_one: Callable[..., tuple[VirtualEnv, Path]],
Expand Down Expand Up @@ -1065,7 +1077,7 @@ def test__resolved_interpreter_windows_path_and_version(
@pytest.mark.parametrize("input_", ["2.7", "python3.7", "goofy"])
@pytest.mark.parametrize("sysfind_result", [r"c:\python37-x64\python.exe", None])
@pytest.mark.parametrize("sysexec_result", ["3.7.3\\n", RAISE_ERROR])
@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch("nox.virtualenv.UV_PYTHON_SUPPORT", new=False)
def test__resolved_interpreter_windows_path_and_version_fails(
input_: str,
Expand All @@ -1089,7 +1101,7 @@ def test__resolved_interpreter_windows_path_and_version_fails(
print(venv._resolved_interpreter)


@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch.object(shutil, "which")
def test__resolved_interpreter_not_found(
which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Path]]
Expand All @@ -1106,7 +1118,7 @@ def test__resolved_interpreter_not_found(
print(venv._resolved_interpreter)


@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch("nox.virtualenv.locate_via_py", new=lambda _: None) # type: ignore[misc] # noqa: PT008
def test__resolved_interpreter_nonstandard(
make_one: Callable[..., tuple[VirtualEnv, Path]],
Expand All @@ -1119,7 +1131,7 @@ def test__resolved_interpreter_nonstandard(
print(venv._resolved_interpreter)


@mock.patch("nox.virtualenv._SYSTEM", new="Linux")
@mock.patch("nox.virtualenv._PLATFORM", new="linux")
@mock.patch.object(shutil, "which", return_value=True)
def test__resolved_interpreter_cache_result(
which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Path]]
Expand All @@ -1135,7 +1147,7 @@ def test__resolved_interpreter_cache_result(
assert which.call_count == 1


@mock.patch("nox.virtualenv._SYSTEM", new="Linux")
@mock.patch("nox.virtualenv._PLATFORM", new="linux")
@mock.patch.object(shutil, "which", return_value=None)
def test__resolved_interpreter_cache_failure(
which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Path]]
Expand Down