From 7273f418c240ce5c2b7c8c312357fb8845262731 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Sun, 31 Mar 2019 17:54:35 -0400 Subject: [PATCH 01/16] fix: RunCommand calling scripts with incorrect executable path Fixes GH-965 Calling `sys.argv` should run the same program as the currently running program. To make calling Poetry scripts through RunCommand match this behavior, we must set `sys.argv[0]` to be the full path of the executable. This change makes the behavior of calling a script through `poetry run` the same as calling a script directly from the .venv/bin. --- src/poetry/console/commands/run.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index 6496b45af24..6df6ab1b7de 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -45,6 +45,13 @@ def _module(self) -> Module: return module def run_script(self, script: str | dict[str, str], args: str) -> int: + # Calling `sys.argv` should run the same program as the currently + # running program. To make calling Poetry scripts through RunCommand + # match this behavior, we must set `sys.argv[0]` to be the full path of + # the executable. + full_path_args = args.copy() + full_path_args[0] = self.env._bin(full_path_args[0]) + if isinstance(script, dict): script = script["callable"] @@ -57,7 +64,7 @@ def run_script(self, script: str | dict[str, str], args: str) -> int: cmd += [ "import sys; " "from importlib import import_module; " - f"sys.argv = {args!r}; {src_in_sys_path}" + f"sys.argv = {full_path_args!r}; {src_in_sys_path}" f"import_module('{module}').{callable_}()" ] From 9173580503a6ce44f75b525d90fd9bcc96b461aa Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Sun, 31 Mar 2019 18:57:08 -0400 Subject: [PATCH 02/16] Python 2 compatibility --- src/poetry/console/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index 6df6ab1b7de..a8f377a9689 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -49,7 +49,7 @@ def run_script(self, script: str | dict[str, str], args: str) -> int: # running program. To make calling Poetry scripts through RunCommand # match this behavior, we must set `sys.argv[0]` to be the full path of # the executable. - full_path_args = args.copy() + full_path_args = list(args) full_path_args[0] = self.env._bin(full_path_args[0]) if isinstance(script, dict): From 6209264d6ee2a7ea26409318bf7b54df29f224b5 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Sun, 31 Mar 2019 19:35:44 -0400 Subject: [PATCH 03/16] Add basic test There are probably cleaner ways to test the CLI, so let me know. --- tests/console/commands/test_run.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 6465c7f55ef..588b2867a18 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -105,3 +105,31 @@ def test_run_console_scripts_of_editable_dependencies_on_windows( # We prove that the CMD script executed successfully by verifying the exit code # matches what we wrote in the script assert tester.execute("quix") == 123 + + +def test_run_script_sets_argv(app): + """ + If RunCommand calls a script defined in pyproject.toml, sys.argv[0] should + be set to the full path of the script. + """ + + def mock_foo(bin): + return "/full/path/to/" + bin + + def mock_run(*args, **kwargs): + return dict(args=args, kwargs=kwargs) + + command = app.find("run") + command._env = Env.get() + command._env._bin = mock_foo + command._env.run = mock_run + res = command.run_script("cli:cli", ["foogit", "status"]) + expected = dict( + args=( + "python", + "-c", + "\"import sys; from importlib import import_module; sys.argv = ['/full/path/to/foogit', 'status']; import_module('cli').cli()\"", + ), + kwargs={"call": True, "shell": True}, + ) + assert res == expected \ No newline at end of file From 399ed6d1fcb05f781ffb85809760976e680212a2 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Sun, 31 Mar 2019 19:37:24 -0400 Subject: [PATCH 04/16] doc --- tests/console/commands/test_run.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 588b2867a18..d454650baa4 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -121,7 +121,9 @@ def mock_run(*args, **kwargs): command = app.find("run") command._env = Env.get() + # fake the existence of our script command._env._bin = mock_foo + # we don't want to run anything command._env.run = mock_run res = command.run_script("cli:cli", ["foogit", "status"]) expected = dict( From 1a915a936056e5a576e5ac96ed14f12438e4badb Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Fri, 7 Oct 2022 21:09:30 +0200 Subject: [PATCH 05/16] Rewrite test to run project script --- tests/console/commands/test_run.py | 51 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index d454650baa4..40c70adc4b9 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -1,9 +1,11 @@ from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING import pytest +from poetry.factory import Factory from poetry.utils._compat import WINDOWS @@ -107,31 +109,32 @@ def test_run_console_scripts_of_editable_dependencies_on_windows( assert tester.execute("quix") == 123 -def test_run_script_sets_argv(app): +@pytest.mark.parametrize( + "installed_script", [False, True], ids=["not installed", "installed"] +) +def test_run_project_script( + installed_script: bool, + mocker: MockerFixture, + tmp_venv: VirtualEnv, + command_tester_factory: CommandTesterFactory, +): """ - If RunCommand calls a script defined in pyproject.toml, sys.argv[0] should - be set to the full path of the script. + If RunCommand calls an installed script defined in pyproject.toml, sys.argv[0] + must be set to the full path of the script. """ + if installed_script: + cli_script = tmp_venv._bin_dir / "foo" + cli_script.touch() + else: + cli_script = "foo" - def mock_foo(bin): - return "/full/path/to/" + bin - - def mock_run(*args, **kwargs): - return dict(args=args, kwargs=kwargs) - - command = app.find("run") - command._env = Env.get() - # fake the existence of our script - command._env._bin = mock_foo - # we don't want to run anything - command._env.run = mock_run - res = command.run_script("cli:cli", ["foogit", "status"]) - expected = dict( - args=( - "python", - "-c", - "\"import sys; from importlib import import_module; sys.argv = ['/full/path/to/foogit', 'status']; import_module('cli').cli()\"", - ), - kwargs={"call": True, "shell": True}, + poetry = Factory().create_poetry( + Path(__file__).parent.parent.parent / "fixtures" / "simple_project" ) - assert res == expected \ No newline at end of file + env_execute_mock = mocker.patch("poetry.utils.env.Env.execute", return_value=0) + tester = command_tester_factory("run", poetry, environment=tmp_venv) + + tester.execute("foo status") + + expected_sys_argv = f"sys.argv = ['{cli_script}', 'status']" + assert expected_sys_argv in env_execute_mock.call_args_list[0][0][2] From 121e42fcf9f7642f3c259a99bfa472247854c444 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Fri, 7 Oct 2022 22:33:43 +0200 Subject: [PATCH 06/16] simplify code --- src/poetry/console/commands/run.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index a8f377a9689..7d72531b867 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -49,8 +49,7 @@ def run_script(self, script: str | dict[str, str], args: str) -> int: # running program. To make calling Poetry scripts through RunCommand # match this behavior, we must set `sys.argv[0]` to be the full path of # the executable. - full_path_args = list(args) - full_path_args[0] = self.env._bin(full_path_args[0]) + args = [self.env._bin(args[0]), *args[1:]] if isinstance(script, dict): script = script["callable"] @@ -64,7 +63,7 @@ def run_script(self, script: str | dict[str, str], args: str) -> int: cmd += [ "import sys; " "from importlib import import_module; " - f"sys.argv = {full_path_args!r}; {src_in_sys_path}" + f"sys.argv = {args!r}; {src_in_sys_path}" f"import_module('{module}').{callable_}()" ] From 85174c75459015572af06922760d9fd5c0c6a36d Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Fri, 7 Oct 2022 22:34:30 +0200 Subject: [PATCH 07/16] fix type on RunCommand.run_script --- src/poetry/console/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index 7d72531b867..00f7a290fee 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -44,7 +44,7 @@ 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: # Calling `sys.argv` should run the same program as the currently # running program. To make calling Poetry scripts through RunCommand # match this behavior, we must set `sys.argv[0]` to be the full path of From ee4a2a73b3c0ed3ca7ca7a9de6b5aa4b713d5894 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Fri, 7 Oct 2022 23:18:48 +0200 Subject: [PATCH 08/16] fix test for windows platform --- tests/console/commands/test_run.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 40c70adc4b9..04f0854c047 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -122,11 +122,15 @@ def test_run_project_script( If RunCommand calls an installed script defined in pyproject.toml, sys.argv[0] must be set to the full path of the script. """ + cli_script = "foo" + if installed_script: - cli_script = tmp_venv._bin_dir / "foo" + if WINDOWS: + cli_script += ".exe" + cli_script = tmp_venv._bin_dir / cli_script cli_script.touch() - else: - cli_script = "foo" + if WINDOWS: + cli_script = str(cli_script).replace("\\", "\\\\") poetry = Factory().create_poetry( Path(__file__).parent.parent.parent / "fixtures" / "simple_project" From fd4cee74634f391b0818cbab7841b8486018462c Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sat, 8 Oct 2022 09:16:23 +0200 Subject: [PATCH 09/16] replace comments for a nice docstring --- src/poetry/console/commands/run.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index 00f7a290fee..9d8ee3553c8 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -45,10 +45,16 @@ def _module(self) -> Module: return module def run_script(self, script: str | dict[str, str], args: list[str]) -> int: - # Calling `sys.argv` should run the same program as the currently - # running program. To make calling Poetry scripts through RunCommand - # match this behavior, we must set `sys.argv[0]` to be the full path of - # the executable. + """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']``. + """ args = [self.env._bin(args[0]), *args[1:]] if isinstance(script, dict): From c925e39ff40cf6467903d9fa75022e99194ab2cf Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sat, 8 Oct 2022 09:30:31 +0200 Subject: [PATCH 10/16] rename Env.{_bin => get_bin_path} --- src/poetry/console/commands/run.py | 2 +- src/poetry/utils/env.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index 9d8ee3553c8..ffdd7cc823b 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -55,7 +55,7 @@ def run_script(self, script: str | dict[str, str], args: list[str]) -> int: 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']``. """ - args = [self.env._bin(args[0]), *args[1:]] + args = [self.env.get_bin_path(args[0]), *args[1:]] if isinstance(script, dict): script = script["callable"] diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 4c60cf9e46c..c9cce6d557f 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -1249,7 +1249,7 @@ def python(self) -> str: """ Path to current python executable """ - return self._bin(self._executable) + return self.get_bin_path(self._executable) @property def marker_env(self) -> dict[str, Any]: @@ -1317,7 +1317,7 @@ def pip(self) -> str: """ # we do not use as_posix() here due to issues with windows pathlib2 # implementation - path = self._bin(self._pip_executable) + path = self.get_bin_path(self._pip_executable) if not Path(path).exists(): return str(self.pip_embedded) return path @@ -1427,7 +1427,7 @@ def get_marker_env(self) -> dict[str, Any]: raise NotImplementedError() def get_pip_command(self, embedded: bool = False) -> list[str]: - if embedded or not Path(self._bin(self._pip_executable)).exists(): + if embedded or not Path(self.get_bin_path(self._pip_executable)).exists(): return [self.python, self.pip_embedded] # run as module so that pip can update itself on Windows return [self.python, "-m", "pip"] @@ -1457,7 +1457,7 @@ def get_command_from_bin(self, bin: str) -> list[str]: # embedded pip when pip is not available in the environment return self.get_pip_command() - return [self._bin(bin)] + return [self.get_bin_path(bin)] def run(self, bin: str, *args: str, **kwargs: Any) -> str | int: cmd = self.get_command_from_bin(bin) + list(args) @@ -1539,7 +1539,7 @@ def script_dirs(self) -> list[Path]: self._script_dirs.append(self.userbase / self._script_dirs[0].name) return self._script_dirs - def _bin(self, bin: str) -> str: + def get_bin_path(self, bin: str) -> str: """ Return path to the given executable. """ @@ -1898,7 +1898,7 @@ def execute(self, bin: str, *args: str, **kwargs: Any) -> int: return super().execute(bin, *args, **kwargs) return 0 - def _bin(self, bin: str) -> str: + def get_bin_path(self, bin: str) -> str: return bin From 4c919b89c30d2675e62f1fdc0114fc9f9eb3f673 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Wed, 11 Jan 2023 09:31:52 +0100 Subject: [PATCH 11/16] rename test method --- tests/console/commands/test_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 11ffaa9c924..f5beb809bc3 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -153,7 +153,7 @@ def test_run_script_exit_code( @pytest.mark.parametrize( "installed_script", [False, True], ids=["not installed", "installed"] ) -def test_run_project_script( +def test_run_project_script_sys_argv0( installed_script: bool, mocker: MockerFixture, tmp_venv: VirtualEnv, From c761cfea0654ca9684fd274e671b67cfea9a9d8d Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sun, 15 Jan 2023 19:31:21 +0100 Subject: [PATCH 12/16] improve test readability --- tests/console/commands/test_run.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index f5beb809bc3..08e9ff24ed5 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -181,5 +181,9 @@ def test_run_project_script_sys_argv0( tester.execute("foo status") + # `poetry run` dispatches to `Env.execute("python", "-c", )` + # we get the arg `code` to verify if it was built with the proper `sys.argv` + code: str = env_execute_mock.call_args[0][2] + expected_sys_argv = f"sys.argv = ['{cli_script}', 'status']" - assert expected_sys_argv in env_execute_mock.call_args_list[0][0][2] + assert expected_sys_argv in code From c1d579f8ce6868b1aa85a789bd5c3f2e9f0707fe Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sun, 15 Jan 2023 23:00:17 +0100 Subject: [PATCH 13/16] rewrite test assertion In order to avoid argument extraction --- tests/console/commands/test_run.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 08e9ff24ed5..800949eb08b 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -176,14 +176,20 @@ def test_run_project_script_sys_argv0( poetry = Factory().create_poetry( Path(__file__).parent.parent.parent / "fixtures" / "simple_project" ) - env_execute_mock = mocker.patch("poetry.utils.env.Env.execute", return_value=0) + env_execute_mock = mocker.patch( + "poetry.utils.env.VirtualEnv.execute", return_value=0 + ) tester = command_tester_factory("run", poetry, environment=tmp_venv) tester.execute("foo status") - # `poetry run` dispatches to `Env.execute("python", "-c", )` - # we get the arg `code` to verify if it was built with the proper `sys.argv` - code: str = env_execute_mock.call_args[0][2] - - expected_sys_argv = f"sys.argv = ['{cli_script}', 'status']" - assert expected_sys_argv in code + env_execute_mock.assert_called_once_with( + "python", + "-c", + ( + "import sys; " + "from importlib import import_module; " + f"sys.argv = ['{cli_script}', 'status']; " + "sys.exit(import_module('foo').bar())" + ), + ) From e4de1a4d75f6233bdb48467acbaf3f96c6640663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sat, 21 Jan 2023 14:09:52 +0100 Subject: [PATCH 14/16] use script_dirs instead of bin_path, make test more realistic --- src/poetry/console/commands/run.py | 6 +- src/poetry/utils/env.py | 12 ++-- tests/console/commands/test_run.py | 60 ++++++++----------- tests/fixtures/scripts/pyproject.toml | 1 + tests/fixtures/scripts/scripts/check_argv0.py | 23 +++++++ 5 files changed, 60 insertions(+), 42 deletions(-) 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 1dcbabebf69..a50aeac0491 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -55,7 +55,11 @@ def run_script(self, script: str | dict[str, str], args: list[str]) -> int: 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']``. """ - args = [self.env.get_bin_path(args[0]), *args[1:]] + for script_dir in self.env.script_dirs: + script_path = script_dir / args[0] + if script_path.exists(): + args[0] = str(script_path) + break if isinstance(script, dict): script = script["callable"] diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 5ef9870ec5b..614ca092f21 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -1281,7 +1281,7 @@ def python(self) -> str: """ Path to current python executable """ - return self.get_bin_path(self._executable) + return self._bin(self._executable) @property def marker_env(self) -> dict[str, Any]: @@ -1349,7 +1349,7 @@ def pip(self) -> str: """ # we do not use as_posix() here due to issues with windows pathlib2 # implementation - path = self.get_bin_path(self._pip_executable) + path = self._bin(self._pip_executable) if not Path(path).exists(): return str(self.pip_embedded) return path @@ -1459,7 +1459,7 @@ def get_marker_env(self) -> dict[str, Any]: raise NotImplementedError() def get_pip_command(self, embedded: bool = False) -> list[str]: - if embedded or not Path(self.get_bin_path(self._pip_executable)).exists(): + if embedded or not Path(self._bin(self._pip_executable)).exists(): return [self.python, self.pip_embedded] # run as module so that pip can update itself on Windows return [self.python, "-m", "pip"] @@ -1489,7 +1489,7 @@ def get_command_from_bin(self, bin: str) -> list[str]: # embedded pip when pip is not available in the environment return self.get_pip_command() - return [self.get_bin_path(bin)] + return [self._bin(bin)] def run(self, bin: str, *args: str, **kwargs: Any) -> str | int: cmd = self.get_command_from_bin(bin) + list(args) @@ -1571,7 +1571,7 @@ def script_dirs(self) -> list[Path]: self._script_dirs.append(self.userbase / self._script_dirs[0].name) return self._script_dirs - def get_bin_path(self, bin: str) -> str: + def _bin(self, bin: str) -> str: """ Return path to the given executable. """ @@ -1930,7 +1930,7 @@ def execute(self, bin: str, *args: str, **kwargs: Any) -> int: return super().execute(bin, *args, **kwargs) return 0 - def get_bin_path(self, bin: str) -> str: + def _bin(self, bin: str) -> str: return bin diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 800949eb08b..12a4081cf1e 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -2,12 +2,10 @@ import subprocess -from pathlib import Path from typing import TYPE_CHECKING import pytest -from poetry.factory import Factory from poetry.utils._compat import WINDOWS @@ -153,43 +151,35 @@ def test_run_script_exit_code( @pytest.mark.parametrize( "installed_script", [False, True], ids=["not installed", "installed"] ) -def test_run_project_script_sys_argv0( +def test_run_script_sys_argv0( installed_script: bool, - mocker: MockerFixture, - tmp_venv: VirtualEnv, + 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. + If RunCommand calls an installed script defined in pyproject.toml, + sys.argv[0] must be set to the full path of the script. """ - cli_script = "foo" - - if installed_script: - if WINDOWS: - cli_script += ".exe" - cli_script = tmp_venv._bin_dir / cli_script - cli_script.touch() - if WINDOWS: - cli_script = str(cli_script).replace("\\", "\\\\") - - poetry = Factory().create_poetry( - Path(__file__).parent.parent.parent / "fixtures" / "simple_project" + 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), ) - env_execute_mock = mocker.patch( - "poetry.utils.env.VirtualEnv.execute", return_value=0 + + install_tester = command_tester_factory( + "install", + poetry=poetry_with_scripts, + environment=tmp_venv, ) - tester = command_tester_factory("run", poetry, environment=tmp_venv) - - tester.execute("foo status") - - env_execute_mock.assert_called_once_with( - "python", - "-c", - ( - "import sys; " - "from importlib import import_module; " - f"sys.argv = ['{cli_script}', 'status']; " - "sys.exit(import_module('foo').bar())" - ), + 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()) From 2bdc6703e6adf7c5d2427f99ab9180de80b8cb23 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sat, 21 Jan 2023 20:51:45 +0100 Subject: [PATCH 15/16] add script extension ".cmd" when on windows --- src/poetry/console/commands/run.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index a50aeac0491..afd38773ddd 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: @@ -57,6 +58,8 @@ def run_script(self, script: str | dict[str, str], args: list[str]) -> int: """ 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[0] = str(script_path) break From a75819707f860aa4b37e50d2849bebd52218546a Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sun, 22 Jan 2023 16:44:36 +0100 Subject: [PATCH 16/16] do not change args list --- src/poetry/console/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index afd38773ddd..63af286d3b1 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -61,7 +61,7 @@ def run_script(self, script: str | dict[str, str], args: list[str]) -> int: if WINDOWS: script_path = script_path.with_suffix(".cmd") if script_path.exists(): - args[0] = str(script_path) + args = [str(script_path), *args[1:]] break if isinstance(script, dict):