Skip to content

Commit a1cdb6e

Browse files
committed
fix(app): improve global opt and run cmd parsing
This change fixes an issue introduced in #10021 that led to options provided in the form `-<shortcut><value>` to be incorrectly handled. Further, it also simplifies and fixes run command processing. Resolves: #10051
1 parent ab5afb9 commit a1cdb6e

File tree

6 files changed

+284
-140
lines changed

6 files changed

+284
-140
lines changed

src/poetry/console/application.py

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

3+
import argparse
34
import logging
4-
import re
55

66
from contextlib import suppress
77
from importlib import import_module
@@ -16,6 +16,7 @@
1616
from cleo.exceptions import CleoCommandNotFoundError
1717
from cleo.exceptions import CleoError
1818
from cleo.formatters.style import Style
19+
from cleo.io.inputs.argv_input import ArgvInput
1920

2021
from poetry.__version__ import __version__
2122
from poetry.console.command_loader import CommandLoader
@@ -26,10 +27,8 @@
2627

2728
if TYPE_CHECKING:
2829
from collections.abc import Callable
29-
from typing import Any
3030

3131
from cleo.events.event import Event
32-
from cleo.io.inputs.argv_input import ArgvInput
3332
from cleo.io.inputs.definition import Definition
3433
from cleo.io.inputs.input import Input
3534
from cleo.io.io import IO
@@ -243,7 +242,7 @@ def _run(self, io: IO) -> int:
243242
# to ensure the users are not exposed to a stack trace for providing invalid values to
244243
# the options --directory or --project, configuring the options here allow cleo to trap and
245244
# display the error cleanly unless the user uses verbose or debug
246-
self._configure_custom_application_options(io)
245+
self._configure_global_options(io)
247246

248247
self._load_plugins(io)
249248

@@ -265,40 +264,29 @@ def _run(self, io: IO) -> int:
265264

266265
return exit_code
267266

268-
def _option_get_value(self, io: IO, name: str, default: Any) -> Any:
269-
option = self.definition.option(name)
267+
def _configure_global_options(self, io: IO) -> None:
268+
"""
269+
Configures global options for the application by setting up the relevant
270+
directories, disabling plugins or cache, and managing the working and
271+
project directories. This method ensures that all directories are valid
272+
paths and handles the resolution of the project directory relative to the
273+
working directory if necessary.
270274
271-
if option is None:
272-
return default
275+
:param io: The IO instance whose input and options are being read.
276+
:return: Nothing.
277+
"""
278+
self._sort_global_options(io)
273279

274-
values = [f"--{option.name}"]
275-
276-
if option.shortcut:
277-
values.append(f"-{option.shortcut}")
278-
279-
if not io.input.has_parameter_option(values):
280-
return default
281-
282-
if option.is_flag():
283-
return True
284-
285-
return io.input.parameter_option(values=values, default=default)
286-
287-
def _configure_custom_application_options(self, io: IO) -> None:
288-
self._disable_plugins = self._option_get_value(
289-
io, "no-plugins", self._disable_plugins
290-
)
291-
self._disable_cache = self._option_get_value(
292-
io, "no-cache", self._disable_cache
293-
)
280+
self._disable_plugins = io.input.option("no-plugins")
281+
self._disable_cache = io.input.option("no-cache")
294282

295283
# we use ensure_path for the directories to make sure these are valid paths
296284
# this will raise an exception if the path is invalid
297285
self._working_directory = ensure_path(
298-
self._option_get_value(io, "directory", Path.cwd()), is_directory=True
286+
io.input.option("directory") or Path.cwd(), is_directory=True
299287
)
300288

301-
self._project_directory = self._option_get_value(io, "project", None)
289+
self._project_directory = io.input.option("project")
302290
if self._project_directory is not None:
303291
self._project_directory = Path(self._project_directory)
304292
self._project_directory = ensure_path(
@@ -310,40 +298,151 @@ def _configure_custom_application_options(self, io: IO) -> None:
310298
is_directory=True,
311299
)
312300

313-
def _configure_io(self, io: IO) -> None:
314-
# We need to check if the command being run
315-
# is the "run" command.
316-
definition = self.definition
301+
def _sort_global_options(self, io: IO) -> None:
302+
"""
303+
Sorts global options of the provided IO instance according to the
304+
definition of the available options, reordering and parsing arguments
305+
to ensure consistency in input handling.
306+
307+
The function interprets the options and their corresponding values
308+
using an argument parser, constructs a sorted list of tokens, and
309+
recreates the input with the rearranged sequence while maintaining
310+
compatibility with the initially provided input stream.
311+
312+
If using in conjunction with `_configure_run_command`, it is recommended that
313+
it be called first in order to correctly handling cases like
314+
`poetry run -V python -V`.
315+
316+
:param io: The IO instance whose input and options are being processed
317+
and reordered.
318+
:return: Nothing.
319+
"""
320+
original_input = cast(ArgvInput, io.input)
321+
tokens: list[str] = original_input._tokens
322+
323+
parser = argparse.ArgumentParser(add_help=False)
324+
325+
for option in self.definition.options:
326+
parser.add_argument(
327+
f"--{option.name}",
328+
*([f"-{option.shortcut}"] if option.shortcut else []),
329+
action="store_true" if option.is_flag() else "store",
330+
)
331+
332+
args, remaining_args = parser.parse_known_args(tokens)
333+
334+
tokens = []
335+
for option in self.definition.options:
336+
key = option.name.replace("-", "_")
337+
value = getattr(args, key, None)
338+
339+
if value is not None:
340+
if value: # is truthy
341+
tokens.append(f"--{option.name}")
342+
343+
if option.accepts_value():
344+
tokens.append(str(value))
345+
346+
sorted_input = ArgvInput([self._name or "", *tokens, *remaining_args])
347+
sorted_input.set_stream(original_input.stream)
348+
349+
with suppress(CleoError):
350+
sorted_input.bind(self.definition)
351+
352+
io.set_input(sorted_input)
353+
354+
def _configure_run_command(self, io: IO) -> None:
355+
"""
356+
Configures the input for the "run" command to properly handle cases where the user
357+
executes commands such as "poetry run -- <subcommand>". This involves reorganizing
358+
input tokens to ensure correct parsing and execution of the run command.
359+
"""
317360
with suppress(CleoError):
318-
io.input.bind(definition)
319-
320-
name = io.input.first_argument
321-
if name == "run":
322-
from poetry.console.io.inputs.run_argv_input import RunArgvInput
323-
324-
input = cast("ArgvInput", io.input)
325-
run_input = RunArgvInput([self._name or "", *input._tokens])
326-
# For the run command reset the definition
327-
# with only the set options (i.e. the options given before the command)
328-
for option_name, value in input.options.items():
329-
if value:
330-
option = definition.option(option_name)
331-
run_input.add_parameter_option("--" + option.name)
332-
if option.shortcut:
333-
shortcuts = re.split(r"\|-?", option.shortcut.lstrip("-"))
334-
shortcuts = [s for s in shortcuts if s]
335-
for shortcut in shortcuts:
336-
run_input.add_parameter_option("-" + shortcut.lstrip("-"))
361+
io.input.bind(self.definition)
362+
363+
command_name = io.input.first_argument
364+
365+
if command_name == "run":
366+
original_input = cast(ArgvInput, io.input)
367+
tokens: list[str] = original_input._tokens
368+
369+
if "--" in tokens:
370+
# this means the user has done the right thing and used "poetry run -- echo hello"
371+
# in this case there is not much we need to do, we can skip the rest
372+
return
373+
374+
# find the correct command index, in some cases this might not be first occurrence
375+
# eg: poetry -C run run echo
376+
command_index = tokens.index(command_name)
377+
378+
while command_index < (len(tokens) - 1):
379+
try:
380+
# try parsing the tokens so far
381+
_ = ArgvInput(
382+
[self._name or "", *tokens[: command_index + 1]],
383+
definition=self.definition,
384+
)
385+
break
386+
except CleoError:
387+
# parsing failed, try finding the next "run" token
388+
try:
389+
command_index += (
390+
tokens[command_index + 1 :].index(command_name) + 1
391+
)
392+
except ValueError:
393+
command_index = len(tokens)
394+
else:
395+
# looks like we reached the end of the road, let clea deal with it
396+
return
397+
398+
# fetch tokens after the "run" command
399+
tokens_without_command = tokens[command_index + 1 :]
400+
401+
# we create a new input for parsing the subcommand pretending
402+
# it is poetry command
403+
without_command = ArgvInput(
404+
[self._name or "", *tokens_without_command], None
405+
)
337406

338407
with suppress(CleoError):
339-
run_input.bind(definition)
408+
# we want to bind the definition here so that cleo knows what should be
409+
# parsed, and how
410+
without_command.bind(self.definition)
411+
412+
# the first argument here is the subcommand
413+
subcommand = without_command.first_argument
414+
subcommand_index = (
415+
(tokens_without_command.index(subcommand) if subcommand else 0)
416+
+ command_index
417+
+ 1
418+
)
419+
420+
# recreate the original input reordering in the following order
421+
# - all tokens before "run" command
422+
# - all tokens after "run" command but before the subcommand
423+
# - the "run" command token
424+
# - the "--" token to normalise the form
425+
# - all remaining tokens starting with the subcommand
426+
run_input = ArgvInput(
427+
[
428+
self._name or "",
429+
*tokens[:command_index],
430+
*tokens[command_index + 1 : subcommand_index],
431+
command_name,
432+
"--",
433+
*tokens[subcommand_index:],
434+
]
435+
)
436+
run_input.set_stream(original_input.stream)
340437

341-
for option_name, value in input.options.items():
342-
if value:
343-
run_input.set_option(option_name, value)
438+
with suppress(CleoError):
439+
run_input.bind(self.definition)
344440

441+
# reset the input to our constructed form
345442
io.set_input(run_input)
346443

444+
def _configure_io(self, io: IO) -> None:
445+
self._configure_run_command(io)
347446
super()._configure_io(io)
348447

349448
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.

0 commit comments

Comments
 (0)