Skip to content

Commit 81d5f04

Browse files
committed
Added ap_completer_type arg to Cmd2ArgumentParser.__init__().
Added unit tests for custom ArgparseCompleter
1 parent bf558c5 commit 81d5f04

File tree

4 files changed

+163
-29
lines changed

4 files changed

+163
-29
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* Added `ArgumentParser.get_ap_completer_type()` and `ArgumentParser.set_ap_completer_type()`. These
88
methods allow developers to enable custom tab completion behavior for a given parser by using a custom
99
`ArgparseCompleter`-based class.
10+
* Added `ap_completer_type` keyword arg to `Cmd2ArgumentParser.__init__()` which saves a call
11+
to `set_ap_completer_type()`. This keyword will also work in `add_parser()` when creating subcommands.
1012
* New function `register_argparse_argument_parameter()` allows developers to specify custom
1113
parameters to be passed to the argparse parser's `add_argument()` method. These parameters will
1214
become accessible in the resulting argparse Action object when modifying `ArgparseCompleter` behavior.

cmd2/argparse_completer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
INFINITY,
3131
)
3232

33-
if TYPE_CHECKING:
33+
if TYPE_CHECKING: # pragma: no cover
3434
from .cmd2 import (
3535
Cmd,
3636
)

cmd2/argparse_custom.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,7 +1252,16 @@ def __init__(
12521252
conflict_handler: str = 'error',
12531253
add_help: bool = True,
12541254
allow_abbrev: bool = True,
1255+
*,
1256+
ap_completer_type: Optional[Type['ArgparseCompleter']] = None,
12551257
) -> None:
1258+
"""
1259+
# Custom parameter added by cmd2
1260+
1261+
:param ap_completer_type: optional parameter which specifies a subclass of ArgparseCompleter for custom tab completion
1262+
behavior on this parser. If this is None or not present, then cmd2 will use
1263+
argparse_completer.DEFAULT_AP_COMPLETER when tab completing this parser's arguments
1264+
"""
12561265
super(Cmd2ArgumentParser, self).__init__(
12571266
prog=prog,
12581267
usage=usage,
@@ -1268,6 +1277,8 @@ def __init__(
12681277
allow_abbrev=allow_abbrev,
12691278
)
12701279

1280+
self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined]
1281+
12711282
# noinspection PyProtectedMember
12721283
def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction:
12731284
"""

tests/test_argparse_completer.py

Lines changed: 149 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import numbers
88
from typing import (
99
List,
10+
cast,
1011
)
1112

1213
import pytest
@@ -329,6 +330,7 @@ def do_standalone(self, args: argparse.Namespace) -> None:
329330
@pytest.fixture
330331
def ac_app():
331332
app = ArgparseCompleterTester()
333+
# noinspection PyTypeChecker
332334
app.stdout = StdSim(app.stdout)
333335
return app
334336

@@ -1156,52 +1158,171 @@ def test_complete_standalone(ac_app, flag, completions):
11561158
assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key)
11571159

11581160

1161+
# Custom ArgparseCompleter-based class
11591162
class CustomCompleter(argparse_completer.ArgparseCompleter):
11601163
def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]:
1161-
"""Override so arguments with 'always_complete' set to True will always be completed"""
1162-
for flag in matched_flags:
1164+
"""Override so flags with 'complete_when_ready' set to True will complete only when app is ready"""
1165+
1166+
# Find flags which should not be completed and place them in matched_flags
1167+
for flag in self._flags:
11631168
action = self._flag_to_action[flag]
1164-
if action.get_always_complete() is True:
1165-
matched_flags.remove(flag)
1169+
app: CustomCompleterApp = cast(CustomCompleterApp, self._cmd2_app)
1170+
if action.get_complete_when_ready() is True and not app.is_ready:
1171+
matched_flags.append(flag)
1172+
11661173
return super(CustomCompleter, self)._complete_flags(text, line, begidx, endidx, matched_flags)
11671174

11681175

1169-
argparse_custom.register_argparse_argument_parameter('always_complete', bool)
1176+
# Add a custom argparse action attribute
1177+
argparse_custom.register_argparse_argument_parameter('complete_when_ready', bool)
11701178

11711179

1180+
# App used to test custom ArgparseCompleter types and custom argparse attributes
11721181
class CustomCompleterApp(cmd2.Cmd):
1173-
_parser = Cmd2ArgumentParser(description="Testing manually wrapping")
1174-
_parser.add_argument('--myflag', always_complete=True, nargs=1)
1182+
def __init__(self):
1183+
super().__init__()
1184+
self.is_ready = True
1185+
1186+
# Parser that's used to test setting the app-wide default ArgparseCompleter type
1187+
default_completer_parser = Cmd2ArgumentParser(description="Testing app-wide argparse completer")
1188+
default_completer_parser.add_argument('--myflag', complete_when_ready=True)
1189+
1190+
@with_argparser(default_completer_parser)
1191+
def do_default_completer(self, args: argparse.Namespace) -> None:
1192+
"""Test command"""
1193+
pass
1194+
1195+
# Parser that's used to test setting a custom completer at the parser level
1196+
custom_completer_parser = Cmd2ArgumentParser(
1197+
description="Testing parser-specific argparse completer", ap_completer_type=CustomCompleter
1198+
)
1199+
custom_completer_parser.add_argument('--myflag', complete_when_ready=True)
1200+
1201+
@with_argparser(custom_completer_parser)
1202+
def do_custom_completer(self, args: argparse.Namespace) -> None:
1203+
"""Test command"""
1204+
pass
1205+
1206+
# Test as_subcommand_to decorator with custom completer
1207+
top_parser = Cmd2ArgumentParser(description="Top Command")
1208+
top_subparsers = top_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
1209+
top_subparsers.required = True
1210+
1211+
@with_argparser(top_parser)
1212+
def do_top(self, args: argparse.Namespace) -> None:
1213+
"""Top level command"""
1214+
# Call handler for whatever subcommand was selected
1215+
handler = args.cmd2_handler.get()
1216+
handler(args)
1217+
1218+
# Parser for a subcommand with no custom completer type
1219+
no_custom_completer_parser = Cmd2ArgumentParser(description="No custom completer")
1220+
no_custom_completer_parser.add_argument('--myflag', complete_when_ready=True)
11751221

1176-
@with_argparser(_parser)
1177-
def do_mycommand(self, cmd: 'CustomCompleterApp', args: argparse.Namespace) -> None:
1178-
"""Test command that will be manually wrapped to use argparse"""
1179-
print(args)
1222+
@cmd2.as_subcommand_to('top', 'no_custom', no_custom_completer_parser, help="no custom completer")
1223+
def _subcmd_no_custom(self, args: argparse.Namespace) -> None:
1224+
pass
1225+
1226+
# Parser for a subcommand with a custom completer type
1227+
custom_completer_parser = Cmd2ArgumentParser(description="Custom completer", ap_completer_type=CustomCompleter)
1228+
custom_completer_parser.add_argument('--myflag', complete_when_ready=True)
1229+
1230+
@cmd2.as_subcommand_to('top', 'custom', custom_completer_parser, help="custom completer")
1231+
def _subcmd_custom(self, args: argparse.Namespace) -> None:
1232+
pass
11801233

11811234

11821235
@pytest.fixture
11831236
def custom_completer_app():
1184-
1185-
argparse_completer.set_default_ap_completer_type(CustomCompleter)
11861237
app = CustomCompleterApp()
1187-
app.stdout = StdSim(app.stdout)
1188-
yield app
1189-
argparse_completer.set_default_ap_completer_type(argparse_completer.ArgparseCompleter)
1238+
return app
11901239

11911240

1192-
@pytest.mark.parametrize(
1193-
'command_and_args, text, output_contains, first_match',
1194-
[
1195-
('mycommand', '--my', '', '--myflag '),
1196-
('mycommand --myflag 5', '--my', '', '--myflag '),
1197-
],
1198-
)
1199-
def test_custom_completer_type(custom_completer_app, command_and_args, text, output_contains, first_match, capsys):
1200-
line = '{} {}'.format(command_and_args, text)
1241+
def test_default_custom_completer_type(custom_completer_app: CustomCompleterApp):
1242+
"""Test altering the app-wide default ArgparseCompleter type"""
1243+
try:
1244+
argparse_completer.set_default_ap_completer_type(CustomCompleter)
1245+
1246+
text = '--m'
1247+
line = f'default_completer {text}'
1248+
endidx = len(line)
1249+
begidx = endidx - len(text)
1250+
1251+
# The flag should complete because app is ready
1252+
custom_completer_app.is_ready = True
1253+
assert complete_tester(text, line, begidx, endidx, custom_completer_app) is not None
1254+
assert custom_completer_app.completion_matches == ['--myflag ']
1255+
1256+
# The flag should not complete because app is not ready
1257+
custom_completer_app.is_ready = False
1258+
assert complete_tester(text, line, begidx, endidx, custom_completer_app) is None
1259+
assert not custom_completer_app.completion_matches
1260+
1261+
finally:
1262+
# Restore the default completer
1263+
argparse_completer.set_default_ap_completer_type(argparse_completer.ArgparseCompleter)
1264+
1265+
1266+
def test_custom_completer_type(custom_completer_app: CustomCompleterApp):
1267+
"""Test parser with a specific custom ArgparseCompleter type"""
1268+
text = '--m'
1269+
line = f'custom_completer {text}'
12011270
endidx = len(line)
12021271
begidx = endidx - len(text)
12031272

1204-
assert first_match == complete_tester(text, line, begidx, endidx, custom_completer_app)
1273+
# The flag should complete because app is ready
1274+
custom_completer_app.is_ready = True
1275+
assert complete_tester(text, line, begidx, endidx, custom_completer_app) is not None
1276+
assert custom_completer_app.completion_matches == ['--myflag ']
12051277

1206-
out, err = capsys.readouterr()
1207-
assert output_contains in out
1278+
# The flag should not complete because app is not ready
1279+
custom_completer_app.is_ready = False
1280+
assert complete_tester(text, line, begidx, endidx, custom_completer_app) is None
1281+
assert not custom_completer_app.completion_matches
1282+
1283+
1284+
def test_decorated_subcmd_custom_completer(custom_completer_app: CustomCompleterApp):
1285+
"""Tests custom completer type on a subcommand created with @cmd2.as_subcommand_to"""
1286+
1287+
# First test the subcommand without the custom completer
1288+
text = '--m'
1289+
line = f'top no_custom {text}'
1290+
endidx = len(line)
1291+
begidx = endidx - len(text)
1292+
1293+
# The flag should complete regardless of ready state since this subcommand isn't using the custom completer
1294+
custom_completer_app.is_ready = True
1295+
assert complete_tester(text, line, begidx, endidx, custom_completer_app) is not None
1296+
assert custom_completer_app.completion_matches == ['--myflag ']
1297+
1298+
custom_completer_app.is_ready = False
1299+
assert complete_tester(text, line, begidx, endidx, custom_completer_app) is not None
1300+
assert custom_completer_app.completion_matches == ['--myflag ']
1301+
1302+
# Now test the subcommand with the custom completer
1303+
text = '--m'
1304+
line = f'top custom {text}'
1305+
endidx = len(line)
1306+
begidx = endidx - len(text)
1307+
1308+
# The flag should complete because app is ready
1309+
custom_completer_app.is_ready = True
1310+
assert complete_tester(text, line, begidx, endidx, custom_completer_app) is not None
1311+
assert custom_completer_app.completion_matches == ['--myflag ']
1312+
1313+
# The flag should not complete because app is not ready
1314+
custom_completer_app.is_ready = False
1315+
assert complete_tester(text, line, begidx, endidx, custom_completer_app) is None
1316+
assert not custom_completer_app.completion_matches
1317+
1318+
1319+
def test_add_parser_custom_completer():
1320+
"""Tests setting a custom completer type on a subcommand using add_parser()"""
1321+
parser = Cmd2ArgumentParser()
1322+
subparsers = parser.add_subparsers()
1323+
1324+
no_custom_completer_parser = subparsers.add_parser(name="no_custom_completer")
1325+
assert no_custom_completer_parser.get_ap_completer_type() is None # type: ignore[attr-defined]
1326+
1327+
custom_completer_parser = subparsers.add_parser(name="no_custom_completer", ap_completer_type=CustomCompleter)
1328+
assert custom_completer_parser.get_ap_completer_type() is CustomCompleter # type: ignore[attr-defined]

0 commit comments

Comments
 (0)