From 33242c22906ec84c7f526537bfd9a98415192d13 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Mon, 23 Jan 2023 06:32:49 +0100 Subject: [PATCH] Fix incorrect sys.argv[0] path when calling project scripts (#6737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christopher Dignam Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com> --- src/poetry/console/commands/run.py | 21 ++++++++++- tests/console/commands/test_run.py | 37 +++++++++++++++++++ tests/fixtures/scripts/pyproject.toml | 1 + tests/fixtures/scripts/scripts/check_argv0.py | 23 ++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/scripts/scripts/check_argv0.py diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index 50e1ae118f6..63af286d3b1 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -5,6 +5,7 @@ from cleo.helpers import argument from poetry.console.commands.env_command import EnvCommand +from poetry.utils._compat import WINDOWS if TYPE_CHECKING: @@ -44,7 +45,25 @@ def _module(self) -> Module: return module - def run_script(self, script: str | dict[str, str], args: str) -> int: + def run_script(self, script: str | dict[str, str], args: list[str]) -> int: + """Runs an entry point script defined in the section ``[tool.poetry.scripts]``. + + When a script exists in the venv bin folder, i.e. after ``poetry install``, + then ``sys.argv[0]`` must be set to the full path of the executable, so + ``poetry run foo`` and ``poetry shell``, ``foo`` have the same ``sys.argv[0]`` + that points to the full path. + + Otherwise (when an entry point script does not exist), ``sys.argv[0]`` is the + script name only, i.e. ``poetry run foo`` has ``sys.argv == ['foo']``. + """ + for script_dir in self.env.script_dirs: + script_path = script_dir / args[0] + if WINDOWS: + script_path = script_path.with_suffix(".cmd") + if script_path.exists(): + args = [str(script_path), *args[1:]] + break + if isinstance(script, dict): script = script["callable"] diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 60bc94e74a7..12a4081cf1e 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -146,3 +146,40 @@ def test_run_script_exit_code( ) assert tester.execute("exit-code") == 42 assert tester.execute("return-code") == 42 + + +@pytest.mark.parametrize( + "installed_script", [False, True], ids=["not installed", "installed"] +) +def test_run_script_sys_argv0( + installed_script: bool, + poetry_with_scripts: Poetry, + command_tester_factory: CommandTesterFactory, + tmp_venv: VirtualEnv, + mocker: MockerFixture, +) -> None: + """ + If RunCommand calls an installed script defined in pyproject.toml, + sys.argv[0] must be set to the full path of the script. + """ + mocker.patch("poetry.utils.env.EnvManager.get", return_value=tmp_venv) + mocker.patch( + "os.execvpe", + lambda file, args, env: subprocess.call([file] + args[1:], env=env), + ) + + install_tester = command_tester_factory( + "install", + poetry=poetry_with_scripts, + environment=tmp_venv, + ) + assert install_tester.execute() == 0 + if not installed_script: + for path in tmp_venv.script_dirs[0].glob("check-argv0*"): + path.unlink() + + tester = command_tester_factory( + "run", poetry=poetry_with_scripts, environment=tmp_venv + ) + argv1 = "absolute" if installed_script else "relative" + assert tester.execute(f"check-argv0 {argv1}") == 0 diff --git a/tests/fixtures/scripts/pyproject.toml b/tests/fixtures/scripts/pyproject.toml index a53f36b930e..06880366cc9 100644 --- a/tests/fixtures/scripts/pyproject.toml +++ b/tests/fixtures/scripts/pyproject.toml @@ -9,6 +9,7 @@ readme = "README.md" python = "^3.7" [tool.poetry.scripts] +check-argv0 = "scripts.check_argv0:main" exit-code = "scripts.exit_code:main" return-code = "scripts.return_code:main" diff --git a/tests/fixtures/scripts/scripts/check_argv0.py b/tests/fixtures/scripts/scripts/check_argv0.py new file mode 100644 index 00000000000..e1dbc79d343 --- /dev/null +++ b/tests/fixtures/scripts/scripts/check_argv0.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import sys + +from pathlib import Path + + +def main() -> int: + path = Path(sys.argv[0]) + if sys.argv[1] == "absolute": + if not path.is_absolute(): + raise RuntimeError(f"sys.argv[0] is not an absolute path: {path}") + if not path.exists(): + raise RuntimeError(f"sys.argv[0] does not exist: {path}") + else: + if path.is_absolute(): + raise RuntimeError(f"sys.argv[0] is an absolute path: {path}") + + return 0 + + +if __name__ == "__main__": + raise sys.exit(main())