diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 89f78bf0819..9122f6f26c7 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -1,9 +1,7 @@ from __future__ import annotations import logging -import re -from contextlib import suppress from importlib import import_module from pathlib import Path from typing import TYPE_CHECKING @@ -15,6 +13,7 @@ from cleo.events.event_dispatcher import EventDispatcher from cleo.exceptions import CleoError from cleo.formatters.style import Style +from cleo.io.inputs.argv_input import ArgvInput from poetry.__version__ import __version__ from poetry.console.command_loader import CommandLoader @@ -28,7 +27,6 @@ from typing import Any from cleo.events.event import Event - from cleo.io.inputs.argv_input import ArgvInput from cleo.io.inputs.definition import Definition from cleo.io.inputs.input import Input from cleo.io.io import IO @@ -277,40 +275,78 @@ def _configure_custom_application_options(self, io: IO) -> None: is_directory=True, ) - def _configure_io(self, io: IO) -> None: - # We need to check if the command being run - # is the "run" command. - definition = self.definition - with suppress(CleoError): - io.input.bind(definition) - - name = io.input.first_argument - if name == "run": - from poetry.console.io.inputs.run_argv_input import RunArgvInput - - input = cast("ArgvInput", io.input) - run_input = RunArgvInput([self._name or "", *input._tokens]) - # For the run command reset the definition - # with only the set options (i.e. the options given before the command) - for option_name, value in input.options.items(): - if value: - option = definition.option(option_name) - run_input.add_parameter_option("--" + option.name) - if option.shortcut: - shortcuts = re.split(r"\|-?", option.shortcut.lstrip("-")) - shortcuts = [s for s in shortcuts if s] - for shortcut in shortcuts: - run_input.add_parameter_option("-" + shortcut.lstrip("-")) - - with suppress(CleoError): - run_input.bind(definition) - - for option_name, value in input.options.items(): - if value: - run_input.set_option(option_name, value) + def _configure_run_command(self, io: IO) -> None: + """ + Configures the input for the "run" command to properly handle cases where the user + executes commands such as "poetry run -- ". This involves reorganizing + input tokens to ensure correct parsing and execution of the run command. + """ + command_name = io.input.first_argument + if command_name == "run": + original_input = cast(ArgvInput, io.input) + tokens: list[str] = original_input._tokens + + if "--" in tokens: + # this means the user has done the right thing and used "poetry run -- echo hello" + # in this case there is not much we need to do, we can skip the rest + return + + # find the correct command index, in some cases this might not be first occurrence + # eg: poetry -C run run echo + command_index = tokens.index(command_name) + + while command_index < (len(tokens) - 1): + try: + # try parsing the tokens so far + _ = ArgvInput( + [self._name or "", *tokens[: command_index + 1]], + definition=self.definition, + ) + break + except CleoError: + # parsing failed, try finding the next "run" token + command_index += tokens[command_index + 1 :].index(command_name) + 1 + else: + # looks like we reached the end of the road, let clea deal with it + return + + # fetch tokens after the "run" command + tokens_without_command = tokens[command_index + 1 :] + + # we create a new input for parsing the subcommand pretending + # it is poetry command + without_command = ArgvInput( + [self._name or "", *tokens_without_command], None + ) + + # the first argument here is the subcommand + subcommand = without_command.first_argument + subcommand_index = ( + tokens.index(subcommand) if subcommand else command_index + 1 + ) + # recreate the original input reordering in the following order + # - all tokens before "run" command + # - all tokens after "run" command but before the subcommand + # - the "run" command token + # - the "--" token to normalise the form + # - all remaining tokens starting with the subcommand + run_input = ArgvInput( + [ + self._name or "", + *tokens[:command_index], + *tokens[command_index + 1 : subcommand_index], + command_name, + "--", + *tokens[subcommand_index:], + ] + ) + + # reset the input to our constructed form io.set_input(run_input) + def _configure_io(self, io: IO) -> None: + self._configure_run_command(io) super()._configure_io(io) def register_command_loggers( diff --git a/src/poetry/console/io/__init__.py b/src/poetry/console/io/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/poetry/console/io/inputs/__init__.py b/src/poetry/console/io/inputs/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/poetry/console/io/inputs/run_argv_input.py b/src/poetry/console/io/inputs/run_argv_input.py deleted file mode 100644 index 36735202118..00000000000 --- a/src/poetry/console/io/inputs/run_argv_input.py +++ /dev/null @@ -1,83 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from cleo.io.inputs.argv_input import ArgvInput - - -if TYPE_CHECKING: - from cleo.io.inputs.definition import Definition - - -class RunArgvInput(ArgvInput): - def __init__( - self, - argv: list[str] | None = None, - definition: Definition | None = None, - ) -> None: - super().__init__(argv, definition=definition) - - self._parameter_options: list[str] = [] - - @property - def first_argument(self) -> str | None: - return "run" - - def add_parameter_option(self, name: str) -> None: - self._parameter_options.append(name) - - def has_parameter_option( - self, values: str | list[str], only_params: bool = False - ) -> bool: - if not isinstance(values, list): - values = [values] - - for token in self._tokens: - if only_params and token == "--": - return False - - for value in values: - if value not in self._parameter_options: - continue - - # Options with values: - # For long options, test for '--option=' at beginning - # For short options, test for '-o' at beginning - leading = value + "=" if value.startswith("--") else value - - if token == value or leading != "" and token.startswith(leading): - return True - - return False - - def _parse(self) -> None: - parse_options = True - self._parsed = self._tokens[:] - - try: - token = self._parsed.pop(0) - except IndexError: - token = None - - while token is not None: - if parse_options and token == "": - self._parse_argument(token) - elif parse_options and token == "--": - parse_options = False - elif parse_options and token.find("--") == 0: - if token in self._parameter_options: - self._parse_long_option(token) - else: - self._parse_argument(token) - elif parse_options and token[0] == "-" and token != "-": - if token in self._parameter_options: - self._parse_short_option(token) - else: - self._parse_argument(token) - else: - self._parse_argument(token) - - try: - token = self._parsed.pop(0) - except IndexError: - token = None diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 4f07205db6e..7efb903a237 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -51,6 +51,44 @@ def test_run_passes_all_args(app_tester: ApplicationTester, env: MockEnv) -> Non assert env.executed == [["python", "-V"]] +def test_run_passes_args_after_run_before_command( + app_tester: ApplicationTester, env: MockEnv +) -> None: + app_tester.execute("run -P. python -V", decorated=True) + assert env.executed == [["python", "-V"]] + + +def test_run_passes_args_after_run_before_command_name_conflict( + app_tester: ApplicationTester, env: MockEnv +) -> None: + app_tester.execute("-vP run run python -V", decorated=True) + assert env.executed == [["python", "-V"]] + + +def test_run_keeps_options_passed_before_command_args_combined_short_opts( + app_tester: ApplicationTester, env: MockEnv +) -> None: + app_tester.execute("run -VP. --no-ansi python", decorated=True) + + assert not app_tester.io.is_decorated() + assert app_tester.io.fetch_output() == app_tester.io.remove_format( + app_tester.application.long_version + "\n" + ) + assert env.executed == [] + + +def test_run_keeps_options_passed_before_command_args( + app_tester: ApplicationTester, env: MockEnv +) -> None: + app_tester.execute("run -V --no-ansi python", decorated=True) + + assert not app_tester.io.is_decorated() + assert app_tester.io.fetch_output() == app_tester.io.remove_format( + app_tester.application.long_version + "\n" + ) + assert env.executed == [] + + def test_run_keeps_options_passed_before_command( app_tester: ApplicationTester, env: MockEnv ) -> None: