Skip to content

Commit 5550ab7

Browse files
committed
Added unit tests for sub-commands
1 parent 29ef268 commit 5550ab7

File tree

6 files changed

+194
-4
lines changed

6 files changed

+194
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
and [arg_print.py](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py) examples
1515
* Added support for Argpasre sub-commands when using the **with_argument_parser** or **with_argparser_and_unknown_args** decorators
1616
* See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use subcommands
17+
* Tab-completion of sub-command names is automatically supported
1718
* The **__relative_load** command is now hidden from the help menu by default
1819
* This command is not intended to be called from the command line, only from within scripts
1920
* The **set** command now has an additional **-a/--all** option to also display read-only settings

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Main Features
3131
- Settable environment parameters
3232
- Parsing commands with arguments using `argparse`, including support for sub-commands
3333
- Unicode character support (*Python 3 only*)
34-
- Good tab-completion of commands, file system paths, and shell commands
34+
- Good tab-completion of commands, sub-commands, file system paths, and shell commands
3535
- Python 2.7 and 3.4+ support
3636
- Windows, macOS, and Linux support
3737
- Trivial to provide built-in help for all commands

cmd2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ def complete_subcommand(self, text, line, begidx, endidx):
776776
cmd, args, foo = self.parseline(line)
777777
arglist = args.split()
778778

779-
if cmd + ' ' + args == line:
779+
if len(arglist) <= 1 and cmd + ' ' + args == line:
780780
funcname = self._func_named(cmd)
781781
if funcname:
782782
# Check to see if this function was decorated with an argparse ArgumentParser

examples/subcommands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ def __init__(self):
2121
# sub-command functions for the base command
2222
def foo(self, args):
2323
"""foo subcommand of base command"""
24-
print(args.x * args.y)
24+
self.poutput(args.x * args.y)
2525

2626
def bar(self, args):
2727
"""bar sucommand of base command"""
28-
print('((%s))' % args.z)
28+
self.poutput('((%s))' % args.z)
2929

3030
# create the top-level parser for the base command
3131
base_parser = argparse.ArgumentParser(prog='base')

tests/test_argparse.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import cmd2
99
from conftest import run_cmd, StdOut
1010

11+
1112
class ArgparseApp(cmd2.Cmd):
1213
def __init__(self):
1314
self.maxrepeats = 3
@@ -168,3 +169,94 @@ def test_arglist(argparse_app):
168169
def test_arglist_decorator_twice(argparse_app):
169170
out = run_cmd(argparse_app, 'arglisttwice "we should" get these')
170171
assert out[0] == 'we should get these'
172+
173+
174+
class SubcommandApp(cmd2.Cmd):
175+
""" Example cmd2 application where we a base command which has a couple subcommands."""
176+
177+
def __init__(self):
178+
cmd2.Cmd.__init__(self)
179+
180+
# sub-command functions for the base command
181+
def foo(self, args):
182+
"""foo subcommand of base command"""
183+
self.poutput(args.x * args.y)
184+
185+
def bar(self, args):
186+
"""bar sucommand of base command"""
187+
self.poutput('((%s))' % args.z)
188+
189+
# create the top-level parser for the base command
190+
base_parser = argparse.ArgumentParser(prog='base')
191+
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
192+
193+
# create the parser for the "foo" sub-command
194+
parser_foo = base_subparsers.add_parser('foo', help='foo help')
195+
parser_foo.add_argument('-x', type=int, default=1, help='integer')
196+
parser_foo.add_argument('y', type=float, help='float')
197+
parser_foo.set_defaults(func=foo)
198+
199+
# create the parser for the "bar" sub-command
200+
parser_bar = base_subparsers.add_parser('bar', help='bar help')
201+
parser_bar.add_argument('z', help='string')
202+
parser_bar.set_defaults(func=bar)
203+
204+
# Create a list of subcommand names, which is used to enable tab-completion of sub-commands
205+
subcommands = ['foo', 'bar']
206+
207+
@cmd2.with_argparser_and_unknown_args(base_parser, subcommands)
208+
def do_base(self, args, arglist):
209+
"""Base command help"""
210+
try:
211+
# Call whatever sub-command function was selected
212+
args.func(self, args)
213+
except AttributeError:
214+
# No sub-command was provided, so as called
215+
self.do_help('base')
216+
217+
@pytest.fixture
218+
def subcommand_app():
219+
app = SubcommandApp()
220+
app.stdout = StdOut()
221+
return app
222+
223+
224+
def test_subcommand_foo(subcommand_app):
225+
out = run_cmd(subcommand_app, 'base foo -x2 5.0')
226+
assert out == ['10.0']
227+
228+
229+
def test_subcommand_bar(subcommand_app):
230+
out = run_cmd(subcommand_app, 'base bar baz')
231+
assert out == ['((baz))']
232+
233+
def test_subcommand_invalid(subcommand_app, capsys):
234+
run_cmd(subcommand_app, 'base baz')
235+
out, err = capsys.readouterr()
236+
err = err.splitlines()
237+
assert err[0].startswith('usage: base')
238+
assert err[1].startswith("base: error: invalid choice: 'baz'")
239+
240+
def test_subcommand_base_help(subcommand_app, capsys):
241+
run_cmd(subcommand_app, 'help base')
242+
out, err = capsys.readouterr()
243+
out = out.splitlines()
244+
assert out[0].startswith('usage: base')
245+
assert out[1] == ''
246+
assert out[2] == 'Base command help'
247+
248+
def test_subcommand_help(subcommand_app, capsys):
249+
run_cmd(subcommand_app, 'help base foo')
250+
out, err = capsys.readouterr()
251+
out = out.splitlines()
252+
assert out[0].startswith('usage: base foo')
253+
assert out[1] == ''
254+
assert out[2] == 'positional arguments:'
255+
256+
257+
def test_subcommand_invalid_help(subcommand_app, capsys):
258+
run_cmd(subcommand_app, 'help base baz')
259+
out, err = capsys.readouterr()
260+
err = err.splitlines()
261+
assert err[0].startswith('usage: base')
262+
assert err[1].startswith("base: error: invalid choice: 'baz'")

tests/test_completion.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com>
99
Released under MIT license, see LICENSE file
1010
"""
11+
import argparse
1112
import os
1213
import sys
1314

@@ -323,3 +324,99 @@ def test_parseline_expands_shortcuts(cmd2_app):
323324
assert command == 'shell'
324325
assert args == 'cat foobar.txt'
325326
assert line.replace('!', 'shell ') == out_line
327+
328+
329+
class SubcommandsExample(cmd2.Cmd):
330+
""" Example cmd2 application where we a base command which has a couple subcommands."""
331+
332+
def __init__(self):
333+
cmd2.Cmd.__init__(self)
334+
335+
# sub-command functions for the base command
336+
def foo(self, args):
337+
"""foo subcommand of base command"""
338+
self.poutput(args.x * args.y)
339+
340+
def bar(self, args):
341+
"""bar sucommand of base command"""
342+
self.poutput('((%s))' % args.z)
343+
344+
# create the top-level parser for the base command
345+
base_parser = argparse.ArgumentParser(prog='base')
346+
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
347+
348+
# create the parser for the "foo" sub-command
349+
parser_foo = base_subparsers.add_parser('foo', help='foo help')
350+
parser_foo.add_argument('-x', type=int, default=1, help='integer')
351+
parser_foo.add_argument('y', type=float, help='float')
352+
parser_foo.set_defaults(func=foo)
353+
354+
# create the parser for the "bar" sub-command
355+
parser_bar = base_subparsers.add_parser('bar', help='bar help')
356+
parser_bar.add_argument('z', help='string')
357+
parser_bar.set_defaults(func=bar)
358+
359+
# Create a list of subcommand names, which is used to enable tab-completion of sub-commands
360+
subcommands = ['foo', 'bar']
361+
362+
@cmd2.with_argument_parser(base_parser, subcommands)
363+
def do_base(self, args):
364+
"""Base command help"""
365+
try:
366+
# Call whatever sub-command function was selected
367+
args.func(self, args)
368+
except AttributeError:
369+
# No sub-command was provided, so as called
370+
self.do_help('base')
371+
372+
373+
@pytest.fixture
374+
def sc_app():
375+
app = SubcommandsExample()
376+
return app
377+
378+
379+
def test_cmd2_subcommand_completion_single_end(sc_app):
380+
text = 'f'
381+
line = 'base f'
382+
endidx = len(line)
383+
begidx = endidx - len(text)
384+
385+
# It is at end of line, so extra space is present
386+
assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo ']
387+
388+
def test_cmd2_subcommand_completion_single_mid(sc_app):
389+
text = 'f'
390+
line = 'base f'
391+
endidx = len(line) - 1
392+
begidx = endidx - len(text)
393+
394+
# It is at end of line, so extra space is present
395+
assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo']
396+
397+
def test_cmd2_subcommand_completion_multiple(sc_app):
398+
text = ''
399+
line = 'base '
400+
endidx = len(line)
401+
begidx = endidx - len(text)
402+
403+
# It is at end of line, so extra space is present
404+
assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo', 'bar']
405+
406+
def test_cmd2_subcommand_completion_nomatch(sc_app):
407+
text = 'z'
408+
line = 'base z'
409+
endidx = len(line)
410+
begidx = endidx - len(text)
411+
412+
# It is at end of line, so extra space is present
413+
assert sc_app.complete_subcommand(text, line, begidx, endidx) == []
414+
415+
def test_cmd2_subcommand_completion_after_subcommand(sc_app):
416+
text = 'f'
417+
line = 'base foo f'
418+
endidx = len(line)
419+
begidx = endidx - len(text)
420+
421+
# It is at end of line, so extra space is present
422+
assert sc_app.complete_subcommand(text, line, begidx, endidx) == []

0 commit comments

Comments
 (0)