Skip to content

Commit aeb98fb

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

File tree

5 files changed

+108
-117
lines changed

5 files changed

+108
-117
lines changed

src/poetry/console/application.py

Lines changed: 70 additions & 34 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
@@ -15,6 +13,7 @@
1513
from cleo.events.event_dispatcher import EventDispatcher
1614
from cleo.exceptions import CleoError
1715
from cleo.formatters.style import Style
16+
from cleo.io.inputs.argv_input import ArgvInput
1817

1918
from poetry.__version__ import __version__
2019
from poetry.console.command_loader import CommandLoader
@@ -28,7 +27,6 @@
2827
from typing import Any
2928

3029
from cleo.events.event import Event
31-
from cleo.io.inputs.argv_input import ArgvInput
3230
from cleo.io.inputs.definition import Definition
3331
from cleo.io.inputs.input import Input
3432
from cleo.io.io import IO
@@ -277,40 +275,78 @@ def _configure_custom_application_options(self, io: IO) -> None:
277275
is_directory=True,
278276
)
279277

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)
278+
def _configure_run_command(self, io: IO) -> None:
279+
"""
280+
Configures the input for the "run" command to properly handle cases where the user
281+
executes commands such as "poetry run -- <subcommand>". This involves reorganizing
282+
input tokens to ensure correct parsing and execution of the run command.
283+
"""
284+
command_name = io.input.first_argument
285+
if command_name == "run":
286+
original_input = cast(ArgvInput, io.input)
287+
tokens: list[str] = original_input._tokens
288+
289+
if "--" in tokens:
290+
# this means the user has done the right thing and used "poetry run -- echo hello"
291+
# in this case there is not much we need to do, we can skip the rest
292+
return
293+
294+
# find the correct command index, in some cases this might not be first occurrence
295+
# eg: poetry -C run run echo
296+
command_index = tokens.index(command_name)
297+
298+
while command_index < (len(tokens) - 1):
299+
try:
300+
# try parsing the tokens so far
301+
_ = ArgvInput(
302+
[self._name or "", *tokens[: command_index + 1]],
303+
definition=self.definition,
304+
)
305+
break
306+
except CleoError:
307+
# parsing failed, try finding the next "run" token
308+
command_index += tokens[command_index + 1 :].index(command_name) + 1
309+
else:
310+
# looks like we reached the end of the road, let clea deal with it
311+
return
312+
313+
# fetch tokens after the "run" command
314+
tokens_without_command = tokens[command_index + 1 :]
315+
316+
# we create a new input for parsing the subcommand pretending
317+
# it is poetry command
318+
without_command = ArgvInput(
319+
[self._name or "", *tokens_without_command], None
320+
)
321+
322+
# the first argument here is the subcommand
323+
subcommand = without_command.first_argument
324+
subcommand_index = (
325+
tokens.index(subcommand) if subcommand else command_index + 1
326+
)
311327

328+
# recreate the original input reordering in the following order
329+
# - all tokens before "run" command
330+
# - all tokens after "run" command but before the subcommand
331+
# - the "run" command token
332+
# - the "--" token to normalise the form
333+
# - all remaining tokens starting with the subcommand
334+
run_input = ArgvInput(
335+
[
336+
self._name or "",
337+
*tokens[:command_index],
338+
*tokens[command_index + 1 : subcommand_index],
339+
command_name,
340+
"--",
341+
*tokens[subcommand_index:],
342+
]
343+
)
344+
345+
# reset the input to our constructed form
312346
io.set_input(run_input)
313347

348+
def _configure_io(self, io: IO) -> None:
349+
self._configure_run_command(io)
314350
super()._configure_io(io)
315351

316352
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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,44 @@ 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_passes_args_after_run_before_command_name_conflict(
62+
app_tester: ApplicationTester, env: MockEnv
63+
) -> None:
64+
app_tester.execute("-vP run run python -V", decorated=True)
65+
assert env.executed == [["python", "-V"]]
66+
67+
68+
def test_run_keeps_options_passed_before_command_args_combined_short_opts(
69+
app_tester: ApplicationTester, env: MockEnv
70+
) -> None:
71+
app_tester.execute("run -VP. --no-ansi python", decorated=True)
72+
73+
assert not app_tester.io.is_decorated()
74+
assert app_tester.io.fetch_output() == app_tester.io.remove_format(
75+
app_tester.application.long_version + "\n"
76+
)
77+
assert env.executed == []
78+
79+
80+
def test_run_keeps_options_passed_before_command_args(
81+
app_tester: ApplicationTester, env: MockEnv
82+
) -> None:
83+
app_tester.execute("run -V --no-ansi python", decorated=True)
84+
85+
assert not app_tester.io.is_decorated()
86+
assert app_tester.io.fetch_output() == app_tester.io.remove_format(
87+
app_tester.application.long_version + "\n"
88+
)
89+
assert env.executed == []
90+
91+
5492
def test_run_keeps_options_passed_before_command(
5593
app_tester: ApplicationTester, env: MockEnv
5694
) -> None:

0 commit comments

Comments
 (0)