1
1
from __future__ import annotations
2
2
3
+ import argparse
3
4
import logging
4
- import re
5
5
6
6
from contextlib import suppress
7
7
from importlib import import_module
16
16
from cleo .exceptions import CleoCommandNotFoundError
17
17
from cleo .exceptions import CleoError
18
18
from cleo .formatters .style import Style
19
+ from cleo .io .inputs .argv_input import ArgvInput
19
20
20
21
from poetry .__version__ import __version__
21
22
from poetry .console .command_loader import CommandLoader
26
27
27
28
if TYPE_CHECKING :
28
29
from collections .abc import Callable
29
- from typing import Any
30
30
31
31
from cleo .events .event import Event
32
- from cleo .io .inputs .argv_input import ArgvInput
33
32
from cleo .io .inputs .definition import Definition
34
33
from cleo .io .inputs .input import Input
35
34
from cleo .io .io import IO
@@ -243,7 +242,7 @@ def _run(self, io: IO) -> int:
243
242
# to ensure the users are not exposed to a stack trace for providing invalid values to
244
243
# the options --directory or --project, configuring the options here allow cleo to trap and
245
244
# display the error cleanly unless the user uses verbose or debug
246
- self ._configure_custom_application_options (io )
245
+ self ._configure_global_options (io )
247
246
248
247
self ._load_plugins (io )
249
248
@@ -265,40 +264,29 @@ def _run(self, io: IO) -> int:
265
264
266
265
return exit_code
267
266
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.
270
274
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 )
273
279
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" )
294
282
295
283
# we use ensure_path for the directories to make sure these are valid paths
296
284
# this will raise an exception if the path is invalid
297
285
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
299
287
)
300
288
301
- self ._project_directory = self . _option_get_value ( io , "project" , None )
289
+ self ._project_directory = io . input . option ( "project" )
302
290
if self ._project_directory is not None :
303
291
self ._project_directory = Path (self ._project_directory )
304
292
self ._project_directory = ensure_path (
@@ -310,40 +298,151 @@ def _configure_custom_application_options(self, io: IO) -> None:
310
298
is_directory = True ,
311
299
)
312
300
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
+ """
317
360
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
+ )
337
406
338
407
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 )
340
437
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 )
344
440
441
+ # reset the input to our constructed form
345
442
io .set_input (run_input )
346
443
444
+ def _configure_io (self , io : IO ) -> None :
445
+ self ._configure_run_command (io )
347
446
super ()._configure_io (io )
348
447
349
448
def register_command_loggers (
0 commit comments