10
10
import inspect
11
11
import numbers
12
12
import shutil
13
+ from collections import deque
13
14
from typing import Dict , List , Optional , Union
14
15
15
16
from . import cmd2
16
17
from . import utils
17
- from .ansi import ansi_safe_wcswidth , style_error
18
+ from .ansi import ansi_aware_write , ansi_safe_wcswidth , style_error
18
19
from .argparse_custom import ATTR_CHOICES_CALLABLE , INFINITY , generate_range_error
19
20
from .argparse_custom import ATTR_SUPPRESS_TAB_HINT , ATTR_DESCRIPTIVE_COMPLETION_HEADER , ATTR_NARGS_RANGE
20
21
from .argparse_custom import ChoicesCallable , CompletionError , CompletionItem
@@ -116,13 +117,13 @@ def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
116
117
parent_tokens = dict ()
117
118
self ._parent_tokens = parent_tokens
118
119
119
- self ._flags = [] # all flags in this command
120
- self ._flag_to_action = {} # maps flags to the argparse action object
121
- self ._positional_actions = [] # actions for positional arguments (by position index)
122
- self ._subcommand_action = None # this will be set if self._parser has subcommands
120
+ self ._flags = [] # all flags in this command
121
+ self ._flag_to_action = {} # maps flags to the argparse action object
122
+ self ._positional_actions = [] # actions for positional arguments (by position index)
123
+ self ._subcommand_action = None # this will be set if self._parser has subcommands
123
124
124
125
# Start digging through the argparse structures.
125
- # _actions is the top level container of parameter definitions
126
+ # _actions is the top level container of parameter definitions
126
127
for action in self ._parser ._actions :
127
128
# if the parameter is flag based, it will have option_strings
128
129
if action .option_strings :
@@ -143,9 +144,8 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
143
144
if not tokens :
144
145
return []
145
146
146
- # Count which positional argument index we're at now. Loop through all tokens on the command line so far
147
- # Skip any flags or flag parameter tokens
148
- next_pos_arg_index = 0
147
+ # Positionals args that are left to parse
148
+ remaining_positionals = deque (self ._positional_actions )
149
149
150
150
# This gets set to True when flags will no longer be processed as argparse flags
151
151
# That can happen when -- is used or an argument with nargs=argparse.REMAINDER is used
@@ -163,12 +163,58 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
163
163
# Keeps track of arguments we've seen and any tokens they consumed
164
164
consumed_arg_values = dict () # dict(arg_name -> List[tokens])
165
165
166
+ # Completed mutually exclusive groups
167
+ completed_mutex_groups = dict () # dict(argparse._MutuallyExclusiveGroup -> Action which completed group)
168
+
166
169
def consume_argument (arg_state : AutoCompleter ._ArgumentState ) -> None :
167
170
"""Consuming token as an argument"""
168
171
arg_state .count += 1
169
172
consumed_arg_values .setdefault (arg_state .action .dest , [])
170
173
consumed_arg_values [arg_state .action .dest ].append (token )
171
174
175
+ def update_mutex_groups (arg_action : argparse .Action ) -> bool :
176
+ """
177
+ Check if an argument belongs to a mutually exclusive group and either mark that group
178
+ as complete or print an error if the group has already been completed
179
+ :param arg_action: the action of the argument
180
+ :return: False if the group has already been completed and there is a conflict, otherwise True
181
+ """
182
+ # Check if this action is in a mutually exclusive group
183
+ for group in self ._parser ._mutually_exclusive_groups :
184
+ if arg_action in group ._group_actions :
185
+
186
+ # Check if the group this action belongs to has already been completed
187
+ if group in completed_mutex_groups :
188
+
189
+ # If this is the action that completed the group, then there is no error
190
+ # since it's allowed to appear on the command line more than once.
191
+ completer_action = completed_mutex_groups [group ]
192
+ if arg_action == completer_action :
193
+ return True
194
+
195
+ error = style_error ("\n Error: argument {}: not allowed with argument {}\n " .
196
+ format (argparse ._get_action_name (arg_action ),
197
+ argparse ._get_action_name (completer_action )))
198
+ self ._print_message (error )
199
+ return False
200
+
201
+ # Mark that this action completed the group
202
+ completed_mutex_groups [group ] = arg_action
203
+
204
+ # Don't tab complete any of the other args in the group
205
+ for group_action in group ._group_actions :
206
+ if group_action == arg_action :
207
+ continue
208
+ elif group_action in self ._flag_to_action .values ():
209
+ matched_flags .extend (group_action .option_strings )
210
+ elif group_action in remaining_positionals :
211
+ remaining_positionals .remove (group_action )
212
+
213
+ # Arg can only be in one group, so we are done
214
+ break
215
+
216
+ return True
217
+
172
218
#############################################################################################
173
219
# Parse all but the last token
174
220
#############################################################################################
@@ -222,14 +268,17 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
222
268
action = self ._flag_to_action [candidates_flags [0 ]]
223
269
224
270
if action is not None :
271
+ if not update_mutex_groups (action ):
272
+ return []
273
+
225
274
if isinstance (action , (argparse ._AppendAction ,
226
275
argparse ._AppendConstAction ,
227
276
argparse ._CountAction )):
228
277
# Flags with action set to append, append_const, and count can be reused
229
278
# Therefore don't erase any tokens already consumed for this flag
230
279
consumed_arg_values .setdefault (action .dest , [])
231
280
else :
232
- # This flag is not resusable , so mark that we've seen it
281
+ # This flag is not reusable , so mark that we've seen it
233
282
matched_flags .extend (action .option_strings )
234
283
235
284
# It's possible we already have consumed values for this flag if it was used
@@ -255,12 +304,9 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
255
304
else :
256
305
# If we aren't current tracking a positional, then get the next positional arg to handle this token
257
306
if pos_arg_state is None :
258
- pos_index = next_pos_arg_index
259
- next_pos_arg_index += 1
260
-
261
- # Make sure we are still have positional arguments to fill
262
- if pos_index < len (self ._positional_actions ):
263
- action = self ._positional_actions [pos_index ]
307
+ # Make sure we are still have positional arguments to parse
308
+ if remaining_positionals :
309
+ action = remaining_positionals .popleft ()
264
310
265
311
# Are we at a subcommand? If so, forward to the matching completer
266
312
if action == self ._subcommand_action :
@@ -285,6 +331,10 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
285
331
286
332
# Check if we have a positional to consume this token
287
333
if pos_arg_state is not None :
334
+ # No need to check for an error since we remove a completed group's positional from
335
+ # remaining_positionals which means this action can't belong to a completed mutex group
336
+ update_mutex_groups (pos_arg_state .action )
337
+
288
338
consume_argument (pos_arg_state )
289
339
290
340
# No more flags are allowed if this is a REMAINDER argument
@@ -295,10 +345,9 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
295
345
elif pos_arg_state .count >= pos_arg_state .max :
296
346
pos_arg_state = None
297
347
298
- # Check if this a case in which we've finished all positionals before one that has nargs
299
- # set to argparse.REMAINDER. At this point argparse allows no more flags to be processed.
300
- if next_pos_arg_index < len (self ._positional_actions ) and \
301
- self ._positional_actions [next_pos_arg_index ].nargs == argparse .REMAINDER :
348
+ # Check if the next positional has nargs set to argparse.REMAINDER.
349
+ # At this point argparse allows no more flags to be processed.
350
+ if remaining_positionals and remaining_positionals [0 ].nargs == argparse .REMAINDER :
302
351
skip_remaining_flags = True
303
352
304
353
#############################################################################################
@@ -338,12 +387,11 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
338
387
return []
339
388
340
389
# Otherwise check if we have a positional to complete
341
- elif pos_arg_state is not None or next_pos_arg_index < len ( self . _positional_actions ) :
390
+ elif pos_arg_state is not None or remaining_positionals :
342
391
343
392
# If we aren't current tracking a positional, then get the next positional arg to handle this token
344
393
if pos_arg_state is None :
345
- pos_index = next_pos_arg_index
346
- action = self ._positional_actions [pos_index ]
394
+ action = remaining_positionals .popleft ()
347
395
pos_arg_state = AutoCompleter ._ArgumentState (action )
348
396
349
397
try :
@@ -532,23 +580,11 @@ def _complete_for_arg(self, arg_action: argparse.Action,
532
580
533
581
return self ._format_completions (arg_action , results )
534
582
535
- @staticmethod
536
- def _format_message_prefix (arg_action : argparse .Action ) -> str :
537
- """Format the arg prefix text that appears before messages printed to the user"""
538
- # Check if this is a flag
539
- if arg_action .option_strings :
540
- flags = ', ' .join (arg_action .option_strings )
541
- param = ' ' + str (arg_action .dest ).upper ()
542
- return '{}{}' .format (flags , param )
543
-
544
- # Otherwise this is a positional
545
- else :
546
- return '{}' .format (str (arg_action .dest ).upper ())
547
-
548
583
@staticmethod
549
584
def _print_message (msg : str ) -> None :
550
585
"""Print a message instead of tab completions and redraw the prompt and input line"""
551
- print (msg )
586
+ import sys
587
+ ansi_aware_write (sys .stdout , msg + '\n ' )
552
588
rl_force_redisplay ()
553
589
554
590
def _print_arg_hint (self , arg_action : argparse .Action ) -> None :
@@ -558,47 +594,34 @@ def _print_arg_hint(self, arg_action: argparse.Action) -> None:
558
594
"""
559
595
# Check if hinting is disabled
560
596
suppress_hint = getattr (arg_action , ATTR_SUPPRESS_TAB_HINT , False )
561
- if suppress_hint or arg_action .help == argparse .SUPPRESS or arg_action . dest == argparse . SUPPRESS :
597
+ if suppress_hint or arg_action .help == argparse .SUPPRESS :
562
598
return
563
599
564
- prefix = self ._format_message_prefix (arg_action )
565
- prefix = ' {0: <{width}} ' .format (prefix , width = 20 )
566
- pref_len = len (prefix )
567
-
568
- help_text = '' if arg_action .help is None else arg_action .help
569
- help_lines = help_text .splitlines ()
570
-
571
- if len (help_lines ) == 1 :
572
- self ._print_message ('\n Hint:\n {}{}\n ' .format (prefix , help_lines [0 ]))
573
- else :
574
- out_str = '\n {}' .format (prefix )
575
- out_str += '\n {0: <{width}}' .format ('' , width = pref_len ).join (help_lines )
576
- self ._print_message ('\n Hint:' + out_str + '\n ' )
600
+ # Use the parser's help formatter to print just this action's help text
601
+ formatter = self ._parser ._get_formatter ()
602
+ formatter .start_section ("Hint" )
603
+ formatter .add_argument (arg_action )
604
+ formatter .end_section ()
605
+ out_str = formatter .format_help ()
606
+ self ._print_message ('\n ' + out_str )
577
607
578
608
def _print_unfinished_flag_error (self , flag_arg_state : _ArgumentState ) -> None :
579
609
"""
580
610
Print an error during tab completion when the user has not finished the current flag
581
611
:param flag_arg_state: information about the unfinished flag action
582
612
"""
583
- prefix = self ._format_message_prefix (flag_arg_state .action )
584
-
585
- out_str = "\n Error:\n "
586
- out_str += ' {0: <{width}} ' .format (prefix , width = 20 )
587
- out_str += generate_range_error (flag_arg_state .min , flag_arg_state .max )
588
-
589
- out_str += ' ({} entered)' .format (flag_arg_state .count )
590
- self ._print_message (style_error ('{}\n ' .format (out_str )))
613
+ error = "\n Error: argument {}: {} ({} entered)\n " .\
614
+ format (argparse ._get_action_name (flag_arg_state .action ),
615
+ generate_range_error (flag_arg_state .min , flag_arg_state .max ),
616
+ flag_arg_state .count )
617
+ self ._print_message (style_error ('{}' .format (error )))
591
618
592
619
def _print_completion_error (self , arg_action : argparse .Action , completion_error : CompletionError ) -> None :
593
620
"""
594
621
Print a CompletionError to the user
595
622
:param arg_action: action being tab completed
596
623
:param completion_error: error that occurred
597
624
"""
598
- prefix = self ._format_message_prefix (arg_action )
599
-
600
- out_str = "\n Error:\n "
601
- out_str += ' {0: <{width}} ' .format (prefix , width = 20 )
602
- out_str += str (completion_error )
603
-
604
- self ._print_message (style_error ('{}\n ' .format (out_str )))
625
+ error = ("\n Error tab completing {}:\n "
626
+ " {}\n " .format (argparse ._get_action_name (arg_action ), str (completion_error )))
627
+ self ._print_message (style_error ('{}' .format (error )))
0 commit comments