Repository: wntrblm/nox

Issue:
`tox-to-nox` should correctly separate the command-line arguments
### Current Behavior

I try to convert config from `tox.ini` to `nox` (noxfile.py)
`tox.ini` - https://github.com/reagento/dishka/blob/develop/tox.ini



### Expected Behavior

`tox-to-nox` tool should correctly separate the command-line arguments.

Specifically, the "-r requirements/aiogram-330.txt" argument should be split into two separate strings like this:
```py
session.install('pytest', 'pytest-cov', '-r', 'requirements/aiogram-330.txt')
```

### Steps To Reproduce

`tox.ini` - https://github.com/reagento/dishka/blob/develop/tox.ini

`uv pip install --upgrade nox[tox_to_nox]` 
`tox-to-nox`


Generated noxfile.py output:
```py
@nox.session(python='python3.13')
def aiogram_330(session):
    session.install('pytest', 'pytest-cov', '-r requirements/aiogram-330.txt')
    session.install('-e', '.')
    session.run('pytest', '--cov=dishka', '--cov-append', '--cov-report=term-missing', '-v', 'tests/integrations/aiogram')
```


### Environment

```Markdown
- OS: Windows 10
- Python: 3.13
- Nox: 2024.10.9
```


### Anything else?

_No response_

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml
index 4db5a7c..2119850 100644
--- a/.github/workflows/action.yml
+++ b/.github/workflows/action.yml
@@ -27,7 +27,7 @@ jobs:
           - macos-14
     steps:
       - uses: actions/checkout@v4
-      - uses: astral-sh/setup-uv@v4
+      - uses: astral-sh/setup-uv@v5
       - uses: ./
       - run: nox --non-interactive --error-on-missing-interpreter --session github_actions_default_tests
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d75c8fb..1f32ff1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -44,7 +44,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
           allow-prereleases: true
       - name: Setup uv
-        uses: astral-sh/setup-uv@v4
+        uses: astral-sh/setup-uv@v5
       - name: Set up Miniconda
         uses: conda-incubator/setup-miniconda@v3
         with:
@@ -76,7 +76,7 @@ jobs:
         with:
           python-version: "3.12"
       - name: Setup uv
-        uses: astral-sh/setup-uv@v4
+        uses: astral-sh/setup-uv@v5
       - name: Install Nox-under-test
         run:  uv pip install --system .
       - name: Download individual coverage reports
@@ -110,7 +110,7 @@ jobs:
         with:
           python-version: "3.12"
       - name: Setup uv
-        uses: astral-sh/setup-uv@v4
+        uses: astral-sh/setup-uv@v5
       - name: Install Nox-under-test
         run:  uv pip install --system .
       - name: Docs
diff --git a/nox/tox_to_nox.py b/nox/tox_to_nox.py
index 087585d..088f666 100644
--- a/nox/tox_to_nox.py
+++ b/nox/tox_to_nox.py
@@ -116,7 +116,12 @@ def main() -> None:
                 wrapjoin(c.split()) for c in section["commands"].strip().splitlines()
             ]
 
-            config[name]["deps"] = wrapjoin(section["deps"].strip().splitlines())
+            config[name]["deps"] = wrapjoin([
+                part for dep
+                in section["deps"].strip().splitlines()
+                for part in
+                (dep.split() if dep.startswith("-r") else [dep])
+            ])
 
             for option in "skip_install", "use_develop":
                 if section.get(option):
diff --git a/nox/virtualenv.py b/nox/virtualenv.py
index dcf2b0f..87df09e 100644
--- a/nox/virtualenv.py
+++ b/nox/virtualenv.py
@@ -19,11 +19,11 @@ import contextlib
 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 @@ _BLACKLISTED_ENV_VARS = frozenset(
         "UV_SYSTEM_PYTHON",
     ]
 )
-_SYSTEM = platform.system()
 
 
 def find_uv() -> tuple[bool, str]:
@@ -359,7 +363,7 @@ class CondaEnv(ProcessEnv):
     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 @@ class VirtualEnv(ProcessEnv):
 
         # 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 @@ class VirtualEnv(ProcessEnv):
     @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 75dd7e3..7c23c90 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -18,14 +18,13 @@ from __future__ import annotations
 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"


Test patch:
diff --git a/tests/test_command.py b/tests/test_command.py
index dd1af5f..3811587 100644
--- a/tests/test_command.py
+++ b/tests/test_command.py
@@ -36,12 +36,12 @@ import nox.popen
 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_tox_to_nox.py b/tests/test_tox_to_nox.py
index 4146ea4..24885e3 100644
--- a/tests/test_tox_to_nox.py
+++ b/tests/test_tox_to_nox.py
@@ -357,3 +357,33 @@ def test_descriptions_into_docstrings(makeconfig: Callable[[str], str]) -> None:
             """
         ).lstrip()
     )
+
+
+def test_commands_with_requirements(makeconfig: Callable[[str], str]) -> None:
+    result = makeconfig(
+        textwrap.dedent("""
+        [tox]
+        envlist = aiohttp
+
+        [testenv]
+        use_develop = true
+        deps =
+            pytest
+            pytest-cov
+            aiohttp: -r requirements/aiohttp.txt
+    """
+        )
+    )
+
+    assert (
+        result
+        == textwrap.dedent("""
+    import nox
+
+
+    @nox.session(python='python3.13')
+    def aiohttp(session):
+        session.install('pytest', 'pytest-cov', '-r', 'requirements/aiohttp.txt')
+        session.install('-e', '.')
+    """).lstrip()
+    )
diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py
index a05885c..e364ea8 100644
--- a/tests/test_virtualenv.py
+++ b/tests/test_virtualenv.py
@@ -38,7 +38,7 @@ if TYPE_CHECKING:
 
     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 test__resolved_interpreter_windows_pyexe(
     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 test__resolved_interpreter_windows_pyexe_fails(
     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]]