Skip to content

Commit de471a8

Browse files
committed
Some minor tweaks to AutoCompleter handling a collection of index-based function arguments.
Added example for fully custom completion functions mixed with argparse/AutoCompleter handling - Also demonstrates the ability to pass in a list, tuple, or dict of parameters to append to the custom completion function. Added new test cases exercising the custom completion function calls. Added AutoCompleter and rl_utils to the coverage report.
1 parent 94da51a commit de471a8

5 files changed

Lines changed: 191 additions & 5 deletions

File tree

AutoCompleter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ def _complete_for_arg(self, action: argparse.Action,
429429
list_args = None
430430
kw_args = None
431431
for index in range(1, len(arg_choices)):
432-
if isinstance(arg_choices[index], list):
432+
if isinstance(arg_choices[index], list) or isinstance(arg_choices[index], tuple):
433433
list_args = arg_choices[index]
434434
elif isinstance(arg_choices[index], dict):
435435
kw_args = arg_choices[index]

examples/tab_autocompletion.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
import argparse
66
import AutoCompleter
7+
import itertools
78
from typing import List
89

910
import cmd2
@@ -20,6 +21,7 @@ def __init__(self):
2021

2122
# For mocking a data source for the example commands
2223
ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']
24+
show_ratings = ['TV-Y', 'TV-Y7', 'TV-G', 'TV-PG', 'TV-14', 'TV-MA']
2325
static_list_directors = ['J. J. Abrams', 'Irvin Kershner', 'George Lucas', 'Richard Marquand',
2426
'Rian Johnson', 'Gareth Edwards']
2527
actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew',
@@ -66,6 +68,24 @@ def __init__(self):
6668
},
6769

6870
}
71+
USER_SHOW_LIBRARY = {'SW_REB': ['S01E01', 'S02E02']}
72+
SHOW_DATABASE_IDS = ['SW_CW', 'SW_TCW', 'SW_REB']
73+
SHOW_DATABASE = {'SW_CW': {'title': 'Star Wars: Clone Wars',
74+
'rating': 'TV-Y7',
75+
'seasons': {1: ['S01E01', 'S01E02', 'S01E03'],
76+
2: ['S02E01', 'S02E02', 'S02E03']}
77+
},
78+
'SW_TCW': {'title': 'Star Wars: The Clone Wars',
79+
'rating': 'TV-PG',
80+
'seasons': {1: ['S01E01', 'S01E02', 'S01E03'],
81+
2: ['S02E01', 'S02E02', 'S02E03']}
82+
},
83+
'SW_REB': {'title': 'Star Wars: Rebels',
84+
'rating': 'TV-Y7',
85+
'seasons': {1: ['S01E01', 'S01E02', 'S01E03'],
86+
2: ['S02E01', 'S02E02', 'S02E03']}
87+
},
88+
}
6989

7090
# This demonstrates a number of customizations of the AutoCompleter version of ArgumentParser
7191
# - The help output will separately group required vs optional flags
@@ -179,6 +199,19 @@ def _do_media_shows(self, args) -> None:
179199
if not args.command:
180200
self.do_help('media shows')
181201

202+
elif args.command == 'list':
203+
for show_id in TabCompleteExample.SHOW_DATABASE:
204+
show = TabCompleteExample.SHOW_DATABASE[show_id]
205+
print('{}\n-----------------------------\n{} ID: {}'
206+
.format(show['title'], show['rating'], show_id))
207+
for season in show['seasons']:
208+
ep_list = show['seasons'][season]
209+
print(' Season {}:\n {}'
210+
.format(season,
211+
'\n '.join(ep_list)))
212+
print()
213+
214+
182215
media_parser = AutoCompleter.ACArgumentParser(prog='media')
183216

184217
media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type')
@@ -207,6 +240,10 @@ def _do_media_shows(self, args) -> None:
207240
shows_parser = media_types_subparsers.add_parser('shows')
208241
shows_parser.set_defaults(func=_do_media_shows)
209242

243+
shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command')
244+
245+
shows_list_parser = shows_commands_subparsers.add_parser('list')
246+
210247
@with_category(CAT_AUTOCOMPLETE)
211248
@with_argparser(media_parser)
212249
def do_media(self, args):
@@ -257,6 +294,10 @@ def _query_movie_database(self):
257294
def _query_movie_user_library(self):
258295
return TabCompleteExample.USER_MOVIE_LIBRARY
259296

297+
def _filter_library(self, text, line, begidx, endidx, full, exclude=[]):
298+
candidates = list(set(full).difference(set(exclude)))
299+
return [entry for entry in candidates if entry.startswith(text)]
300+
260301
library_parser = AutoCompleter.ACArgumentParser(prog='library')
261302

262303
library_subcommands = library_parser.add_subparsers(title='Media Types', dest='type')
@@ -276,6 +317,32 @@ def _query_movie_user_library(self):
276317
library_show_parser = library_subcommands.add_parser('show')
277318
library_show_parser.set_defaults(func=_do_library_show)
278319

320+
library_show_subcommands = library_show_parser.add_subparsers(title='Command', dest='command')
321+
322+
library_show_add_parser = library_show_subcommands.add_parser('add')
323+
library_show_add_parser.add_argument('show_id', help='Show IDs to add')
324+
library_show_add_parser.add_argument('episode_id', nargs='*', help='Show IDs to add')
325+
326+
library_show_rmv_parser = library_show_subcommands.add_parser('remove')
327+
328+
# Demonstrates a custom completion function that does more with the command line than is
329+
# allowed by the standard completion functions
330+
def _filter_episodes(self, text, line, begidx, endidx, show_db, user_lib):
331+
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
332+
show_id = tokens[3]
333+
if show_id:
334+
if show_id in show_db:
335+
show = show_db[show_id]
336+
all_episodes = itertools.chain(*(show['seasons'].values()))
337+
338+
if show_id in user_lib:
339+
user_eps = user_lib[show_id]
340+
else:
341+
user_eps = []
342+
343+
return self._filter_library(text, line, begidx, endidx, all_episodes, user_eps)
344+
return []
345+
279346
@with_category(CAT_AUTOCOMPLETE)
280347
@with_argparser(library_parser)
281348
def do_library(self, args):
@@ -300,21 +367,42 @@ def complete_library(self, text, line, begidx, endidx):
300367
movie_add_choices = {'movie_id': self._query_movie_database}
301368
movie_remove_choices = {'movie_id': self._query_movie_user_library}
302369

370+
# This demonstrates the ability to mix custom completion functions with argparse completion.
371+
# By specifying a tuple for a completer, AutoCompleter expects a custom completion function
372+
# with optional index-based as well as keyword based arguments. This is an alternative to using
373+
# a partial function.
374+
375+
show_add_choices = {'show_id': (self._filter_library, # This is a custom completion function
376+
# This tuple represents index-based args to append to the function call
377+
(list(TabCompleteExample.SHOW_DATABASE.keys()),)
378+
),
379+
'episode_id': (self._filter_episodes, # this is a custom completion function
380+
# this list represents index-based args to append to the function call
381+
[TabCompleteExample.SHOW_DATABASE],
382+
# this dict contains keyword-based args to append to the function call
383+
{'user_lib': TabCompleteExample.USER_SHOW_LIBRARY})}
384+
show_remove_choices = {}
385+
303386
# The library movie sub-parser group 'command' has 2 sub-parsers:
304387
# 'add' and 'remove'
305388
library_movie_command_params = \
306389
{'add': (movie_add_choices, None),
307390
'remove': (movie_remove_choices, None)}
308391

392+
library_show_command_params = \
393+
{'add': (show_add_choices, None),
394+
'remove': (show_remove_choices, None)}
395+
309396
# The 'library movie' command has a sub-parser group called 'command'
310397
library_movie_subcommand_groups = {'command': library_movie_command_params}
398+
library_show_subcommand_groups = {'command': library_show_command_params}
311399

312400
# Mapping of a specific sub-parser of the 'type' group to a tuple. Each
313401
# tuple has 2 values corresponding what's passed to the constructor
314402
# parameters (arg_choices,subcmd_args_lookup) of the nested
315403
# instance of AutoCompleter
316404
library_type_params = {'movie': (None, library_movie_subcommand_groups),
317-
'show': (None, None)}
405+
'show': (None, library_show_subcommand_groups)}
318406

319407
# maps the a subcommand group to a dictionary mapping a specific
320408
# sub-command to a tuple of (arg_choices, subcmd_args_lookup)

tests/test_acargparse.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
Unit/functional testing for readline tab-completion functions in the cmd2.py module.
3+
4+
These are primarily tests related to readline completer functions which handle tab-completion of cmd2/cmd commands,
5+
file system paths, and shell commands.
6+
7+
Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com>
8+
Released under MIT license, see LICENSE file
9+
"""
10+
import argparse
11+
import os
12+
import sys
13+
14+
import cmd2
15+
from unittest import mock
16+
import pytest
17+
from AutoCompleter import ACArgumentParser
18+
19+
# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
20+
try:
21+
import gnureadline as readline
22+
except ImportError:
23+
# Try to import readline, but allow failure for convenience in Windows unit testing
24+
# Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
25+
try:
26+
# noinspection PyUnresolvedReferences
27+
import readline
28+
except ImportError:
29+
pass
30+
31+
32+
def test_acarg_narg_empty_tuple():
33+
with pytest.raises(ValueError) as excinfo:
34+
parser = ACArgumentParser(prog='test')
35+
parser.add_argument('invalid_tuple', nargs=())
36+
assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
37+
38+
39+
def test_acarg_narg_single_tuple():
40+
with pytest.raises(ValueError) as excinfo:
41+
parser = ACArgumentParser(prog='test')
42+
parser.add_argument('invalid_tuple', nargs=(1,))
43+
assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
44+
45+
46+
def test_acarg_narg_tuple_triple():
47+
with pytest.raises(ValueError) as excinfo:
48+
parser = ACArgumentParser(prog='test')
49+
parser.add_argument('invalid_tuple', nargs=(1, 2, 3))
50+
assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
51+
52+
53+
def test_acarg_narg_tuple_order():
54+
with pytest.raises(ValueError) as excinfo:
55+
parser = ACArgumentParser(prog='test')
56+
parser.add_argument('invalid_tuple', nargs=(2, 1))
57+
assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value)
58+
59+
60+
def test_acarg_narg_tuple_negative():
61+
with pytest.raises(ValueError) as excinfo:
62+
parser = ACArgumentParser(prog='test')
63+
parser.add_argument('invalid_tuple', nargs=(-1, 1))
64+
assert 'Negative numbers are invalid for nargs range' in str(excinfo.value)
65+
66+
67+
def test_acarg_narg_tuple_zero_base():
68+
parser = ACArgumentParser(prog='test')
69+
parser.add_argument('tuple', nargs=(0, 3))
70+
71+
72+
def test_acarg_narg_tuple_zero_to_one():
73+
parser = ACArgumentParser(prog='test')
74+
parser.add_argument('tuple', nargs=(0, 1))
75+
76+

tests/test_autocompletion.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,28 @@ def test_autcomp_pos_after_flag(cmd2_app):
298298
cmd2_app.completion_matches == ['John Boyega" ']
299299

300300

301+
def test_autcomp_custom_func_list_arg(cmd2_app):
302+
text = 'SW_'
303+
line = 'library show add {}'.format(text)
304+
endidx = len(line)
305+
begidx = endidx - len(text)
306+
307+
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
308+
assert first_match is not None and \
309+
cmd2_app.completion_matches == ['SW_CW', 'SW_REB', 'SW_TCW']
310+
311+
312+
def test_autcomp_custom_func_list_and_dict_arg(cmd2_app):
313+
text = ''
314+
line = 'library show add SW_REB {}'.format(text)
315+
endidx = len(line)
316+
begidx = endidx - len(text)
317+
318+
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
319+
assert first_match is not None and \
320+
cmd2_app.completion_matches == ['S01E02', 'S01E03', 'S02E01', 'S02E03']
321+
322+
301323

302324

303325

tox.ini

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ deps =
2020
pytest-xdist
2121
wcwidth
2222
commands =
23-
py.test {posargs: -n 2} --cov=cmd2 --cov-report=term-missing --forked
23+
py.test {posargs: -n 2} --cov=cmd2 --cov=AutoCompleter --cov=rl_utils --cov-report=term-missing --forked
2424
codecov
2525

2626
[testenv:py35]
@@ -55,7 +55,7 @@ deps =
5555
pytest-xdist
5656
wcwidth
5757
commands =
58-
py.test {posargs: -n 2} --cov=cmd2 --cov-report=term-missing --forked
58+
py.test {posargs: -n 2} --cov=cmd2 --cov=AutoCompleter --cov=rl_utils --cov-report=term-missing --forked
5959
codecov
6060

6161
[testenv:py36-win]
@@ -68,7 +68,7 @@ deps =
6868
pytest-cov
6969
pytest-xdist
7070
commands =
71-
py.test {posargs: -n 2} --cov=cmd2 --cov-report=term-missing
71+
py.test {posargs: -n 2} --cov=cmd2 --cov=AutoCompleter --cov=rl_utils --cov-report=term-missing
7272
codecov
7373

7474
[testenv:py37]

0 commit comments

Comments
 (0)