diff --git a/nox/virtualenv.py b/nox/virtualenv.py index dcf2b0f7..87df09ec 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -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 @@ -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( @@ -74,7 +79,6 @@ def __dir__() -> list[str]: "UV_SYSTEM_PYTHON", ] ) -_SYSTEM = platform.system() def find_uv() -> tuple[bool, str]: @@ -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"), @@ -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 @@ -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")] diff --git a/noxfile.py b/noxfile.py index 75dd7e3f..7c23c90f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -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" diff --git a/tests/test_command.py b/tests/test_command.py index dd1af5f1..3811587e 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -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." ) @@ -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: @@ -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) @@ -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) diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index a05885cd..e364ea8d 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -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" @@ -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 [ @@ -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: @@ -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]], @@ -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, @@ -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, @@ -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]] @@ -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]] @@ -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]] @@ -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( @@ -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( @@ -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) @@ -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]], @@ -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, @@ -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]] @@ -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]], @@ -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]] @@ -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]]