Skip to content

Commit

Permalink
fix(run): simplify and fix run arg parsing
Browse files Browse the repository at this point in the history
Resolves: #10051
  • Loading branch information
abn committed Jan 17, 2025
1 parent 81f2935 commit aeb98fb
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 117 deletions.
104 changes: 70 additions & 34 deletions src/poetry/console/application.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 -- <subcommand>". 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(
Expand Down
Empty file removed src/poetry/console/io/__init__.py
Empty file.
Empty file.
83 changes: 0 additions & 83 deletions src/poetry/console/io/inputs/run_argv_input.py

This file was deleted.

38 changes: 38 additions & 0 deletions tests/console/commands/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit aeb98fb

Please sign in to comment.