Skip to content

Commit 2a053a7

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

File tree

5 files changed

+124
-117
lines changed

5 files changed

+124
-117
lines changed

src/poetry/console/application.py

Lines changed: 77 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,85 @@ 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+
try:
309+
command_index += (
310+
tokens[command_index + 1 :].index(command_name) + 1
311+
)
312+
except ValueError:
313+
command_index = len(tokens)
314+
else:
315+
# looks like we reached the end of the road, let clea deal with it
316+
return
317+
318+
# fetch tokens after the "run" command
319+
tokens_without_command = tokens[command_index + 1 :]
320+
321+
# we create a new input for parsing the subcommand pretending
322+
# it is poetry command
323+
without_command = ArgvInput(
324+
[self._name or "", *tokens_without_command], None
325+
)
326+
327+
# the first argument here is the subcommand
328+
subcommand = without_command.first_argument
329+
subcommand_index = (
330+
(tokens_without_command.index(subcommand) if subcommand else 0)
331+
+ command_index
332+
+ 1
333+
)
311334

335+
# recreate the original input reordering in the following order
336+
# - all tokens before "run" command
337+
# - all tokens after "run" command but before the subcommand
338+
# - the "run" command token
339+
# - the "--" token to normalise the form
340+
# - all remaining tokens starting with the subcommand
341+
run_input = ArgvInput(
342+
[
343+
self._name or "",
344+
*tokens[:command_index],
345+
*tokens[command_index + 1 : subcommand_index],
346+
command_name,
347+
"--",
348+
*tokens[subcommand_index:],
349+
]
350+
)
351+
352+
# reset the input to our constructed form
312353
io.set_input(run_input)
313354

355+
def _configure_io(self, io: IO) -> None:
356+
self._configure_run_command(io)
314357
super()._configure_io(io)
315358

316359
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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,53 @@ def test_run_passes_all_args(app_tester: ApplicationTester, env: MockEnv) -> Non
5151
assert env.executed == [["python", "-V"]]
5252

5353

54+
def test_run_is_not_eager(app_tester: ApplicationTester, env: MockEnv) -> None:
55+
app_tester.execute("--no-ansi -C run -install", decorated=True)
56+
assert (
57+
app_tester.io.fetch_error().strip()
58+
== "Specified path 'run' is not a valid directory."
59+
)
60+
assert env.executed == []
61+
62+
63+
def test_run_passes_args_after_run_before_command(
64+
app_tester: ApplicationTester, env: MockEnv
65+
) -> None:
66+
app_tester.execute("run -P. python -V", decorated=True)
67+
assert env.executed == [["python", "-V"]]
68+
69+
70+
def test_run_passes_args_after_run_before_command_name_conflict(
71+
app_tester: ApplicationTester, env: MockEnv
72+
) -> None:
73+
app_tester.execute("-vP run run python -V", decorated=True)
74+
assert env.executed == [["python", "-V"]]
75+
76+
77+
def test_run_keeps_options_passed_before_command_args_combined_short_opts(
78+
app_tester: ApplicationTester, env: MockEnv
79+
) -> None:
80+
app_tester.execute("run -VP. --no-ansi python", decorated=True)
81+
82+
assert not app_tester.io.is_decorated()
83+
assert app_tester.io.fetch_output() == app_tester.io.remove_format(
84+
app_tester.application.long_version + "\n"
85+
)
86+
assert env.executed == []
87+
88+
89+
def test_run_keeps_options_passed_before_command_args(
90+
app_tester: ApplicationTester, env: MockEnv
91+
) -> None:
92+
app_tester.execute("run -V --no-ansi python", decorated=True)
93+
94+
assert not app_tester.io.is_decorated()
95+
assert app_tester.io.fetch_output() == app_tester.io.remove_format(
96+
app_tester.application.long_version + "\n"
97+
)
98+
assert env.executed == []
99+
100+
54101
def test_run_keeps_options_passed_before_command(
55102
app_tester: ApplicationTester, env: MockEnv
56103
) -> None:

0 commit comments

Comments
 (0)