-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Resolve script paths via shutil.which on Windows
#10894
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -559,6 +559,87 @@ def test_env_scheme_dict_returns_modified_when_read_only( | |
| ) | ||
|
|
||
|
|
||
| class _FakeWindowsSelf: | ||
| """Lightweight stand-in used to drive ``Env.execute`` on a non-Windows | ||
| host without touching the venv-creation machinery in :class:`Env`. | ||
|
|
||
| ``Env.execute`` only reads ``self._is_windows`` and calls | ||
| ``self.get_command_from_bin``, so a duck-typed object is enough. | ||
| """ | ||
|
|
||
| _is_windows = True | ||
|
|
||
| def __init__(self, command: list[str]) -> None: | ||
| self._command = command | ||
|
|
||
| def get_command_from_bin(self, bin: str) -> list[str]: | ||
| return self._command | ||
|
|
||
|
|
||
| def test_execute_resolves_full_path_on_windows(mocker: MockerFixture) -> None: | ||
| """On Windows, ``Env.execute`` must resolve the executable's full path | ||
| via ``shutil.which`` before invoking ``subprocess.Popen``, passing the | ||
| env-aware ``PATH`` so the lookup honors the caller-supplied environment | ||
| (and bypasses ``cmd.exe``'s ~8 KiB ``PATH`` parse limit). | ||
|
|
||
| ``cmd.exe`` fails to resolve bare names when ``PATH`` exceeds its | ||
| ~8 KiB parse limit (https://github.com/python/cpython/issues/137254), | ||
| affecting ``[project.scripts]`` entries (installed as ``.cmd`` shims) | ||
| that aren't found by ``Env._bin``'s venv-relative ``.exe`` lookup. | ||
|
|
||
| Regression test for https://github.com/python-poetry/poetry/issues/10482. | ||
|
Comment on lines
+579
to
+590
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Also assert how The test currently only checks that the Suggested implementation: def test_execute_resolves_full_path_on_windows(mocker: MockerFixture) -> None:
"""On Windows, ``Env.execute`` must resolve the executable's full path
via ``shutil.which`` before invoking ``subprocess.Popen``.
``cmd.exe`` fails to resolve bare names when ``PATH`` exceeds its
~8 KiB parse limit (https://github.com/python/cpython/issues/137254),
affecting ``[project.scripts]`` entries (installed as ``.cmd`` shims)
that aren't found by ``Env._bin``'s venv-relative ``.exe`` lookup.
Regression test for https://github.com/python-poetry/poetry/issues/10482.
"""
# Arrange
env_vars = {"PATH": r"C:\foo;C:\bar"}
executable = "script"
which_mock = mocker.patch(
"poetry.utils.env.shutil.which",
return_value=rf"C:\foo\{executable}.cmd",
)
popen_mock = mocker.patch("poetry.utils.env.subprocess.Popen")
# NOTE: adjust the fake-env class/constructor to match the rest of this test module.
fake_env = FakeEnv(command=[executable]) # type: ignore[name-defined]
# Act
fake_env.execute(executable, env=env_vars) # type: ignore[attr-defined]
# Assert: `shutil.which` must be called with the bare executable and an
# env-aware PATH so regressions in name/`path` handling are caught.
which_mock.assert_called_once_with(executable, path=env_vars["PATH"])
# Existing assertions about `Popen` using the full path should remain;
# this ensures the `shutil.which` result is still honored.
popen_mock.assert_called_once()
|
||
| """ | ||
| from poetry.utils.env.base_env import Env | ||
|
|
||
| resolved = r"C:\Users\u\proj\.venv\Scripts\myscript.cmd" | ||
| env_vars = {"PATH": r"C:\foo;C:\bar"} | ||
| which_mock = mocker.patch("shutil.which", return_value=resolved) | ||
| popen_mock = mocker.MagicMock() | ||
| popen_mock.communicate.return_value = (b"", b"") | ||
| popen_mock.returncode = 0 | ||
| popen_cls = mocker.patch( | ||
| "poetry.utils.env.base_env.subprocess.Popen", return_value=popen_mock | ||
| ) | ||
|
|
||
| fake = _FakeWindowsSelf(command=["myscript"]) | ||
| rc = Env.execute(fake, "myscript", "arg1", env=env_vars) # type: ignore[arg-type] | ||
|
|
||
| assert rc == 0 | ||
| # Pin down the ``shutil.which`` contract: bare executable + env-aware | ||
| # PATH. Regressions that drop the ``path=`` kwarg or pass the wrong | ||
| # name would silently re-introduce the cmd.exe PATH-length failure | ||
| # this PR is fixing. | ||
| which_mock.assert_called_once_with("myscript", path=env_vars["PATH"]) | ||
| call_command = popen_cls.call_args.args[0] | ||
| assert call_command[0] == resolved | ||
| assert call_command[1] == "arg1" | ||
|
|
||
|
|
||
| def test_execute_falls_back_to_bare_name_when_unresolved( | ||
| mocker: MockerFixture, | ||
| ) -> None: | ||
| """If ``shutil.which`` cannot resolve the executable, ``Env.execute`` | ||
| must still call ``subprocess.Popen`` so the shell's own resolution | ||
| path can take over (preserving existing behavior for cases where the | ||
| binary lives outside the venv but is reachable via ``PATH``).""" | ||
| from poetry.utils.env.base_env import Env | ||
|
|
||
| mocker.patch("shutil.which", return_value=None) | ||
| popen_mock = mocker.MagicMock() | ||
| popen_mock.communicate.return_value = (b"", b"") | ||
| popen_mock.returncode = 0 | ||
| popen_cls = mocker.patch( | ||
| "poetry.utils.env.base_env.subprocess.Popen", return_value=popen_mock | ||
| ) | ||
|
|
||
| fake = _FakeWindowsSelf(command=["unknown"]) | ||
| rc = Env.execute(fake, "unknown") # type: ignore[arg-type] | ||
|
|
||
| assert rc == 0 | ||
| call_command = popen_cls.call_args.args[0] | ||
| assert call_command[0] == "unknown" | ||
|
|
||
|
|
||
| def test_marker_env_is_equal_for_all_envs(tmp_path: Path, manager: EnvManager) -> None: | ||
| venv_path = tmp_path / "Virtual Env" | ||
| manager.build_venv(venv_path) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
security (python.lang.security.audit.dangerous-subprocess-use-tainted-env-args): Detected subprocess function 'Popen' with user controlled data. A malicious actor could leverage this to perform command injection. You may consider using 'shlex.quote()'.
Source: opengrep