Skip to content

Commit 7dcc5d5

Browse files
committed
fix(run): simplify and fix run arg parsing
Resolves: #10051
1 parent 81f2935 commit 7dcc5d5

File tree

5 files changed

+84
-118
lines changed

5 files changed

+84
-118
lines changed

src/poetry/console/application.py

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4-
import re
54

6-
from contextlib import suppress
75
from importlib import import_module
86
from pathlib import Path
97
from typing import TYPE_CHECKING
@@ -13,8 +11,8 @@
1311
from cleo.events.console_command_event import ConsoleCommandEvent
1412
from cleo.events.console_events import COMMAND
1513
from cleo.events.event_dispatcher import EventDispatcher
16-
from cleo.exceptions import CleoError
1714
from cleo.formatters.style import Style
15+
from cleo.io.inputs.argv_input import ArgvInput
1816

1917
from poetry.__version__ import __version__
2018
from poetry.console.command_loader import CommandLoader
@@ -28,7 +26,6 @@
2826
from typing import Any
2927

3028
from cleo.events.event import Event
31-
from cleo.io.inputs.argv_input import ArgvInput
3229
from cleo.io.inputs.definition import Definition
3330
from cleo.io.inputs.input import Input
3431
from cleo.io.io import IO
@@ -277,40 +274,61 @@ def _configure_custom_application_options(self, io: IO) -> None:
277274
is_directory=True,
278275
)
279276

280-
def _configure_io(self, io: IO) -> None:
281-
# We need to check if the command being run
282-
# is the "run" command.
283-
definition = self.definition
284-
with suppress(CleoError):
285-
io.input.bind(definition)
286-
287-
name = io.input.first_argument
288-
if name == "run":
289-
from poetry.console.io.inputs.run_argv_input import RunArgvInput
290-
291-
input = cast("ArgvInput", io.input)
292-
run_input = RunArgvInput([self._name or "", *input._tokens])
293-
# For the run command reset the definition
294-
# with only the set options (i.e. the options given before the command)
295-
for option_name, value in input.options.items():
296-
if value:
297-
option = definition.option(option_name)
298-
run_input.add_parameter_option("--" + option.name)
299-
if option.shortcut:
300-
shortcuts = re.split(r"\|-?", option.shortcut.lstrip("-"))
301-
shortcuts = [s for s in shortcuts if s]
302-
for shortcut in shortcuts:
303-
run_input.add_parameter_option("-" + shortcut.lstrip("-"))
304-
305-
with suppress(CleoError):
306-
run_input.bind(definition)
307-
308-
for option_name, value in input.options.items():
309-
if value:
310-
run_input.set_option(option_name, value)
277+
def _configure_run_command(self, io: IO) -> None:
278+
"""
279+
Configures the input for the "run" command to properly handle cases where the user
280+
executes commands such as "poetry run -- <subcommand>". This involves reorganizing
281+
input tokens to ensure correct parsing and execution of the run command.
282+
"""
283+
command_name = io.input.first_argument
284+
if command_name == "run":
285+
original_input = cast(ArgvInput, io.input)
286+
tokens: list[str] = original_input._tokens
287+
288+
if "--" in tokens:
289+
# this means the user has done the right thing and used "poetry run -- echo hello"
290+
# in this case there is not much we need to do, we can skip the rest
291+
return
292+
293+
command_index = tokens.index(command_name)
294+
295+
# fetch tokens after the "run" command
296+
tokens_without_command = tokens[command_index + 1 :]
297+
298+
# we create a new input for parsing the subcommand pretending
299+
# it is poetry command
300+
without_command = ArgvInput(
301+
[self._name or "", *tokens_without_command], None
302+
)
303+
304+
# the first argument here is the subcommand
305+
subcommand = without_command.first_argument
306+
subcommand_index = (
307+
tokens.index(subcommand) if subcommand else command_index + 1
308+
)
311309

310+
# recreate the original input reordering in the following order
311+
# - all tokens before "run" command
312+
# - all tokens after "run" command but before the subcommand
313+
# - the "run" command token
314+
# - the "--" token to normalise the form
315+
# - all remaining tokens starting with the subcommand
316+
run_input = ArgvInput(
317+
[
318+
self._name or "",
319+
*tokens[:command_index],
320+
*tokens[command_index + 1 : subcommand_index],
321+
command_name,
322+
"--",
323+
*tokens[subcommand_index:],
324+
]
325+
)
326+
327+
# reset the input to our constructed form
312328
io.set_input(run_input)
313329

330+
def _configure_io(self, io: IO) -> None:
331+
self._configure_run_command(io)
314332
super()._configure_io(io)
315333

316334
def register_command_loggers(

src/poetry/console/io/__init__.py

Whitespace-only changes.

src/poetry/console/io/inputs/__init__.py

Whitespace-only changes.

src/poetry/console/io/inputs/run_argv_input.py

Lines changed: 0 additions & 83 deletions
This file was deleted.

tests/console/commands/test_run.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,37 @@ def test_run_passes_all_args(app_tester: ApplicationTester, env: MockEnv) -> Non
5151
assert env.executed == [["python", "-V"]]
5252

5353

54+
def test_run_passes_args_after_run_before_command_(
55+
app_tester: ApplicationTester, env: MockEnv
56+
) -> None:
57+
app_tester.execute("run -P. python -V", decorated=True)
58+
assert env.executed == [["python", "-V"]]
59+
60+
61+
def test_run_keeps_options_passed_before_command_args_combined_short_opts(
62+
app_tester: ApplicationTester, env: MockEnv
63+
) -> None:
64+
app_tester.execute("run -VP. --no-ansi python", decorated=True)
65+
66+
assert not app_tester.io.is_decorated()
67+
assert app_tester.io.fetch_output() == app_tester.io.remove_format(
68+
app_tester.application.long_version + "\n"
69+
)
70+
assert env.executed == []
71+
72+
73+
def test_run_keeps_options_passed_before_command_args(
74+
app_tester: ApplicationTester, env: MockEnv
75+
) -> None:
76+
app_tester.execute("run -V --no-ansi python", decorated=True)
77+
78+
assert not app_tester.io.is_decorated()
79+
assert app_tester.io.fetch_output() == app_tester.io.remove_format(
80+
app_tester.application.long_version + "\n"
81+
)
82+
assert env.executed == []
83+
84+
5485
def test_run_keeps_options_passed_before_command(
5586
app_tester: ApplicationTester, env: MockEnv
5687
) -> None:

0 commit comments

Comments
 (0)