Skip to content

Commit 473cb08

Browse files
authored
Merge pull request #573 from python-cmd2/double_dash
Double dash
2 parents f38e100 + 8bed244 commit 473cb08

File tree

9 files changed

+123
-61
lines changed

9 files changed

+123
-61
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
the argparse object. Also, single-character tokens that happen to be a
1515
prefix char are not treated as flags by argparse and AutoCompleter now
1616
matches that behavior.
17+
* Fixed bug where AutoCompleter was not distinguishing between a negative number and a flag
18+
* Fixed bug where AutoCompleter did not handle -- the same way argparse does (all args after -- are non-options)
1719
* Enhancements
1820
* Added ``exit_code`` attribute of ``cmd2.Cmd`` class
1921
* Enables applications to return a non-zero exit code when exiting from ``cmdloop``

cmd2/argparse_completer.py

100755100644
Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ def register_custom_actions(parser: argparse.ArgumentParser) -> None:
209209
parser.register('action', 'append', _AppendRangeAction)
210210

211211

212-
def token_resembles_flag(token: str, parser: argparse.ArgumentParser) -> bool:
213-
"""Determine if a token looks like a flag. Based on argparse._parse_optional()."""
212+
def is_potential_flag(token: str, parser: argparse.ArgumentParser) -> bool:
213+
"""Determine if a token looks like a potential flag. Based on argparse._parse_optional()."""
214214
# if it's an empty string, it was meant to be a positional
215215
if not token:
216216
return False
@@ -340,6 +340,10 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
340340
# Skip any flags or flag parameter tokens
341341
next_pos_arg_index = 0
342342

343+
# This gets set to True when flags will no longer be processed as argparse flags
344+
# That can happen when -- is used or an argument with nargs=argparse.REMAINDER is used
345+
skip_remaining_flags = False
346+
343347
pos_arg = AutoCompleter._ArgumentState()
344348
pos_action = None
345349

@@ -363,7 +367,7 @@ def consume_flag_argument() -> None:
363367
"""Consuming token as a flag argument"""
364368
# we're consuming flag arguments
365369
# if the token does not look like a new flag, then count towards flag arguments
366-
if not token_resembles_flag(token, self._parser) and flag_action is not None:
370+
if not is_potential_flag(token, self._parser) and flag_action is not None:
367371
flag_arg.count += 1
368372

369373
# does this complete a option item for the flag
@@ -432,8 +436,10 @@ def process_action_nargs(action: argparse.Action, arg_state: AutoCompleter._Argu
432436

433437
for idx, token in enumerate(tokens):
434438
is_last_token = idx >= len(tokens) - 1
439+
435440
# Only start at the start token index
436441
if idx >= self._token_start_index:
442+
437443
# If a remainder action is found, force all future tokens to go to that
438444
if remainder['arg'] is not None:
439445
if remainder['action'] == pos_action:
@@ -442,28 +448,38 @@ def process_action_nargs(action: argparse.Action, arg_state: AutoCompleter._Argu
442448
elif remainder['action'] == flag_action:
443449
consume_flag_argument()
444450
continue
451+
445452
current_is_positional = False
446453
# Are we consuming flag arguments?
447454
if not flag_arg.needed:
448-
# Special case when each of the following is true:
449-
# - We're not in the middle of consuming flag arguments
450-
# - The current positional argument count has hit the max count
451-
# - The next positional argument is a REMAINDER argument
452-
# Argparse will now treat all future tokens as arguments to the positional including tokens that
453-
# look like flags so the completer should skip any flag related processing once this happens
454-
skip_flag = False
455-
if (pos_action is not None) and pos_arg.count >= pos_arg.max and \
456-
next_pos_arg_index < len(self._positional_actions) and \
457-
self._positional_actions[next_pos_arg_index].nargs == argparse.REMAINDER:
458-
skip_flag = True
455+
456+
if not skip_remaining_flags:
457+
# Special case when each of the following is true:
458+
# - We're not in the middle of consuming flag arguments
459+
# - The current positional argument count has hit the max count
460+
# - The next positional argument is a REMAINDER argument
461+
# Argparse will now treat all future tokens as arguments to the positional including tokens that
462+
# look like flags so the completer should skip any flag related processing once this happens
463+
if (pos_action is not None) and pos_arg.count >= pos_arg.max and \
464+
next_pos_arg_index < len(self._positional_actions) and \
465+
self._positional_actions[next_pos_arg_index].nargs == argparse.REMAINDER:
466+
skip_remaining_flags = True
459467

460468
# At this point we're no longer consuming flag arguments. Is the current argument a potential flag?
461-
if token_resembles_flag(token, self._parser) and not skip_flag:
469+
if is_potential_flag(token, self._parser) and not skip_remaining_flags:
462470
# reset some tracking values
463471
flag_arg.reset()
464472
# don't reset positional tracking because flags can be interspersed anywhere between positionals
465473
flag_action = None
466474

475+
if token == '--':
476+
if is_last_token:
477+
# Exit loop and see if -- can be completed into a flag
478+
break
479+
else:
480+
# In argparse, all args after -- are non-flags
481+
skip_remaining_flags = True
482+
467483
# does the token fully match a known flag?
468484
if token in self._flag_to_action:
469485
flag_action = self._flag_to_action[token]
@@ -524,22 +540,25 @@ def process_action_nargs(action: argparse.Action, arg_state: AutoCompleter._Argu
524540
else:
525541
consume_flag_argument()
526542

543+
if remainder['arg'] is not None:
544+
skip_remaining_flags = True
545+
527546
# don't reset this if we're on the last token - this allows completion to occur on the current token
528-
if not is_last_token and flag_arg.min is not None:
547+
elif not is_last_token and flag_arg.min is not None:
529548
flag_arg.needed = flag_arg.count < flag_arg.min
530549

531550
# Here we're done parsing all of the prior arguments. We know what the next argument is.
532551

552+
completion_results = []
553+
533554
# if we don't have a flag to populate with arguments and the last token starts with
534555
# a flag prefix then we'll complete the list of flag options
535-
completion_results = []
536556
if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars and \
537-
remainder['arg'] is None:
557+
not skip_remaining_flags:
538558
return AutoCompleter.basic_complete(text, line, begidx, endidx,
539559
[flag for flag in self._flags if flag not in matched_flags])
540560
# we're not at a positional argument, see if we're in a flag argument
541561
elif not current_is_positional:
542-
# current_items = []
543562
if flag_action is not None:
544563
consumed = consumed_arg_values[flag_action.dest]\
545564
if flag_action.dest in consumed_arg_values else []

cmd2/cmd2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3563,8 +3563,8 @@ def set_window_title(self, title: str) -> None: # pragma: no cover
35633563
except AttributeError:
35643564
# Debugging in Pycharm has issues with setting terminal title
35653565
pass
3566-
3567-
self.terminal_lock.release()
3566+
finally:
3567+
self.terminal_lock.release()
35683568

35693569
else:
35703570
raise RuntimeError("another thread holds terminal_lock")

cmd2/pyscript_bridge.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import sys
1313
from typing import List, Callable, Optional
1414

15-
from .argparse_completer import _RangeAction, token_resembles_flag
15+
from .argparse_completer import _RangeAction, is_potential_flag
1616
from .utils import namedtuple_with_defaults, StdSim, quote_string_if_needed
1717

1818
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
@@ -222,10 +222,12 @@ def process_argument(action, value):
222222
if action.option_strings:
223223
cmd_str[0] += '{} '.format(action.option_strings[0])
224224

225+
is_remainder_arg = action.dest == self._remainder_arg
226+
225227
if isinstance(value, List) or isinstance(value, tuple):
226228
for item in value:
227229
item = str(item).strip()
228-
if token_resembles_flag(item, self._parser):
230+
if not is_remainder_arg and is_potential_flag(item, self._parser):
229231
raise ValueError('{} appears to be a flag and should be supplied as a keyword argument '
230232
'to the function.'.format(item))
231233
item = quote_string_if_needed(item)
@@ -240,7 +242,7 @@ def process_argument(action, value):
240242

241243
else:
242244
value = str(value).strip()
243-
if token_resembles_flag(value, self._parser):
245+
if not is_remainder_arg and is_potential_flag(value, self._parser):
244246
raise ValueError('{} appears to be a flag and should be supplied as a keyword argument '
245247
'to the function.'.format(value))
246248
value = quote_string_if_needed(value)

examples/tab_autocompletion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def do_hybrid_suggest(self, args):
163163

164164
# This variant demonstrates the AutoCompleter working with the orginial argparse.
165165
# Base argparse is unable to specify narg ranges. Autocompleter will keep expecting additional arguments
166-
# for the -d/--duration flag until you specify a new flaw or end the list it with '--'
166+
# for the -d/--duration flag until you specify a new flag or end processing of flags with '--'
167167

168168
suggest_parser_orig = argparse.ArgumentParser()
169169

tests/test_acargparse.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Released under MIT license, see LICENSE file
66
"""
77
import pytest
8-
from cmd2.argparse_completer import ACArgumentParser, token_resembles_flag
8+
from cmd2.argparse_completer import ACArgumentParser, is_potential_flag
99

1010

1111
def test_acarg_narg_empty_tuple():
@@ -53,16 +53,16 @@ def test_acarg_narg_tuple_zero_to_one():
5353
parser.add_argument('tuple', nargs=(0, 1))
5454

5555

56-
def test_token_resembles_flag():
56+
def test_is_potential_flag():
5757
parser = ACArgumentParser()
5858

5959
# Not valid flags
60-
assert not token_resembles_flag('', parser)
61-
assert not token_resembles_flag('non-flag', parser)
62-
assert not token_resembles_flag('-', parser)
63-
assert not token_resembles_flag('--has space', parser)
64-
assert not token_resembles_flag('-2', parser)
60+
assert not is_potential_flag('', parser)
61+
assert not is_potential_flag('non-flag', parser)
62+
assert not is_potential_flag('-', parser)
63+
assert not is_potential_flag('--has space', parser)
64+
assert not is_potential_flag('-2', parser)
6565

6666
# Valid flags
67-
assert token_resembles_flag('-flag', parser)
68-
assert token_resembles_flag('--flag', parser)
67+
assert is_potential_flag('-flag', parser)
68+
assert is_potential_flag('--flag', parser)

tests/test_autocompletion.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,66 @@ def test_autcomp_custom_func_list_and_dict_arg(cmd2_app):
279279
cmd2_app.completion_matches == ['S01E02', 'S01E03', 'S02E01', 'S02E03']
280280

281281

282+
def test_argparse_remainder_flag_completion(cmd2_app):
283+
import cmd2
284+
import argparse
285+
286+
# Test flag completion as first arg of positional with nargs=argparse.REMAINDER
287+
text = '--h'
288+
line = 'help command {}'.format(text)
289+
endidx = len(line)
290+
begidx = endidx - len(text)
291+
292+
# --h should not complete into --help because we are in the argparse.REMAINDER section
293+
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
294+
295+
# Test flag completion within an already started positional with nargs=argparse.REMAINDER
296+
text = '--h'
297+
line = 'help command subcommand {}'.format(text)
298+
endidx = len(line)
299+
begidx = endidx - len(text)
300+
301+
# --h should not complete into --help because we are in the argparse.REMAINDER section
302+
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
303+
304+
# Test a flag with nargs=argparse.REMAINDER
305+
parser = argparse.ArgumentParser()
306+
parser.add_argument('-f', nargs=argparse.REMAINDER)
307+
308+
# Overwrite eof's parser for this test
309+
cmd2.Cmd.do_eof.argparser = parser
310+
311+
text = '--h'
312+
line = 'eof -f {}'.format(text)
313+
endidx = len(line)
314+
begidx = endidx - len(text)
315+
316+
# --h should not complete into --help because we are in the argparse.REMAINDER section
317+
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
318+
319+
320+
def test_completion_after_double_dash(cmd2_app):
321+
"""
322+
Test completion after --, which argparse says (all args after -- are non-options)
323+
All of these tests occur outside of an argparse.REMAINDER section since those tests
324+
are handled in test_argparse_remainder_flag_completion
325+
"""
326+
327+
# Test -- as the last token
328+
text = '--'
329+
line = 'help {}'.format(text)
330+
endidx = len(line)
331+
begidx = endidx - len(text)
332+
333+
# Since -- is the last token, then it should show flag choices
334+
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
335+
assert first_match is not None and '--help' in cmd2_app.completion_matches
336+
337+
# Test -- to end all flag completion
338+
text = '--'
339+
line = 'help -- {}'.format(text)
340+
endidx = len(line)
341+
begidx = endidx - len(text)
342+
343+
# Since -- appeared before the -- being completed, nothing should be completed
344+
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None

tests/test_completion.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -716,31 +716,6 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app):
716716
os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \
717717
cmd2_app.display_matches == expected_display
718718

719-
def test_argparse_remainder_completion(cmd2_app):
720-
# First test a positional with nargs=argparse.REMAINDER
721-
text = '--h'
722-
line = 'help command subcommand {}'.format(text)
723-
endidx = len(line)
724-
begidx = endidx - len(text)
725-
726-
# --h should not complete into --help because we are in the argparse.REMAINDER sections
727-
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
728-
729-
# Now test a flag with nargs=argparse.REMAINDER
730-
parser = argparse.ArgumentParser()
731-
parser.add_argument('-f', nargs=argparse.REMAINDER)
732-
733-
# Overwrite eof's parser for this test
734-
cmd2.Cmd.do_eof.argparser = parser
735-
736-
text = '--h'
737-
line = 'eof -f {}'.format(text)
738-
endidx = len(line)
739-
begidx = endidx - len(text)
740-
741-
# --h should not complete into --help because we are in the argparse.REMAINDER sections
742-
assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
743-
744719
@pytest.fixture
745720
def sc_app():
746721
c = SubcommandsExample()

tests/test_pyscript.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,10 @@ def test_pyscript_custom_name(ps_echo, request):
238238

239239
def test_pyscript_argparse_checks(ps_app, capsys):
240240
# Test command that has nargs.REMAINDER and make sure all tokens are accepted
241-
run_cmd(ps_app, 'py app.alias.create("my_alias", "alias_command", "command_arg1", "command_arg2")')
241+
# Include a flag in the REMAINDER section to show that they are processed as literals in that section
242+
run_cmd(ps_app, 'py app.alias.create("my_alias", "alias_command", "command_arg1", "-h")')
242243
out = run_cmd(ps_app, 'alias list my_alias')
243-
assert out == normalize('alias create my_alias alias_command command_arg1 command_arg2')
244+
assert out == normalize('alias create my_alias alias_command command_arg1 -h')
244245

245246
# Specify flag outside of keyword argument
246247
run_cmd(ps_app, 'py app.help("-h")')

0 commit comments

Comments
 (0)