Skip to content

Commit 8043b56

Browse files
committed
AutoCompleter now handles mutually exclusive groups
1 parent 29b5252 commit 8043b56

File tree

2 files changed

+47
-6
lines changed

2 files changed

+47
-6
lines changed

cmd2/argparse_completer.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
120120
self._flags = [] # all flags in this command
121121
self._flag_to_action = {} # maps flags to the argparse action object
122122
self._positional_actions = [] # actions for positional arguments (by position index)
123-
self._mutually_exclusive_groups = [] # Each item is a list of actions
124123
self._subcommand_action = None # this will be set if self._parser has subcommands
125124

126125
# Start digging through the argparse structures.
@@ -140,10 +139,6 @@ def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
140139
if isinstance(action, argparse._SubParsersAction):
141140
self._subcommand_action = action
142141

143-
# Keep track of what actions are in mutually exclusive groups
144-
for group in self._parser._mutually_exclusive_groups:
145-
self._mutually_exclusive_groups.append(group._group_actions)
146-
147142
def complete_command(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
148143
"""Complete the command using the argparse metadata and provided argument dictionary"""
149144
if not tokens:
@@ -168,12 +163,52 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
168163
# Keeps track of arguments we've seen and any tokens they consumed
169164
consumed_arg_values = dict() # dict(arg_name -> List[tokens])
170165

166+
# Completed mutually exclusive groups
167+
completed_mutex_groups = dict() # dict(argparse._MutuallyExclusiveGroup -> Action which completed group)
168+
171169
def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
172170
"""Consuming token as an argument"""
173171
arg_state.count += 1
174172
consumed_arg_values.setdefault(arg_state.action.dest, [])
175173
consumed_arg_values[arg_state.action.dest].append(token)
176174

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+
group_action = completed_mutex_groups[group]
189+
error = style_error("\nError: argument {}: not allowed with argument {}\n".
190+
format(argparse._get_action_name(arg_action),
191+
argparse._get_action_name(group_action)))
192+
self._print_message(error)
193+
return False
194+
195+
# Mark that this action completed the group
196+
completed_mutex_groups[group] = arg_action
197+
198+
# Don't tab complete any of the other args in the group
199+
for group_action in group._group_actions:
200+
if group_action == arg_action:
201+
continue
202+
elif group_action in self._flag_to_action.values():
203+
matched_flags.extend(group_action.option_strings)
204+
elif group_action in remaining_positionals:
205+
remaining_positionals.remove(group_action)
206+
207+
# Arg can only be in one group, so we are done
208+
break
209+
210+
return True
211+
177212
#############################################################################################
178213
# Parse all but the last token
179214
#############################################################################################
@@ -227,6 +262,9 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
227262
action = self._flag_to_action[candidates_flags[0]]
228263

229264
if action is not None:
265+
if not update_mutex_groups(action):
266+
return []
267+
230268
if isinstance(action, (argparse._AppendAction,
231269
argparse._AppendConstAction,
232270
argparse._CountAction)):
@@ -287,6 +325,9 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
287325

288326
# Check if we have a positional to consume this token
289327
if pos_arg_state is not None:
328+
if not update_mutex_groups(pos_arg_state.action):
329+
return []
330+
290331
consume_argument(pos_arg_state)
291332

292333
# No more flags are allowed if this is a REMAINDER argument

cmd2/cmd2.py

100755100644
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1848,7 +1848,7 @@ def _complete_statement(self, line: str) -> Statement:
18481848
"""Keep accepting lines of input until the command is complete.
18491849
18501850
There is some pretty hacky code here to handle some quirks of
1851-
self.pseudo_raw_input(). It returns a literal 'eof' if the input
1851+
self._pseudo_raw_input(). It returns a literal 'eof' if the input
18521852
pipe runs out. We can't refactor it because we need to retain
18531853
backwards compatibility with the standard library version of cmd.
18541854

0 commit comments

Comments
 (0)