commands: Add help_formatter() with tests.

This commit is contained in:
Thomas Adamcik 2013-11-12 21:52:23 +01:00
parent 88bc046605
commit 6bddcb7875
2 changed files with 285 additions and 19 deletions

View File

@ -1,5 +1,6 @@
import argparse
import collections
import os
import sys
@ -24,11 +25,8 @@ class Command(object):
for args, kwargs in self._arguments:
actions.append(parser.add_argument(*args, **kwargs))
if self._children:
parser.add_argument('_args', nargs=argparse.REMAINDER)
else:
parser.set_defaults(_args=[])
parser.add_argument('_args', nargs=argparse.REMAINDER,
help=argparse.SUPPRESS)
return parser, actions
def add_child(self, name, command):
@ -39,10 +37,54 @@ class Command(object):
def format_usage(self, prog=None):
actions = self._build()[1]
formatter = argparse.HelpFormatter(prog or sys.argv[0])
prog = prog or os.path.basename(sys.argv[0])
formatter = argparse.HelpFormatter(prog)
formatter.add_usage(None, actions, [])
return formatter.format_help()
def format_help(self, prog=None):
actions = self._build()[1]
prog = prog or os.path.basename(sys.argv[0])
formatter = argparse.HelpFormatter(prog)
formatter.add_usage(None, actions, [])
if self.__doc__:
formatter.add_text(self.__doc__)
if actions:
formatter.add_text('OPTIONS:')
formatter.start_section(None)
formatter.add_arguments(actions)
formatter.end_section()
subhelp = []
for name, child in self._children.items():
child._subhelp(name, subhelp)
if subhelp:
formatter.add_text('COMMANDS:')
subhelp.insert(0, '')
return formatter.format_help() + '\n'.join(subhelp)
def _subhelp(self, name, result):
actions = self._build()[1]
if self.__doc__ or actions:
formatter = argparse.HelpFormatter(name)
formatter.add_usage(None, actions, [], '')
formatter.start_section(None)
formatter.add_text(self.__doc__)
formatter.start_section(None)
formatter.add_arguments(actions)
formatter.end_section()
formatter.end_section()
result.append(formatter.format_help())
for childname, child in self._children.items():
child._subhelp(' '.join((name, childname)), result)
def parse(self, args, namespace=None):
if not namespace:
namespace = argparse.Namespace()

View File

@ -103,39 +103,72 @@ class CommandParsingTest(unittest.TestCase):
result = cmd.parse([])
self.assertEqual(result.command, cmd)
def test_invalid_type(self):
cmd = command.Command()
cmd.add_argument('--bar', type=int)
with self.assertRaises(command.CommandError) as cm:
cmd.parse(['--bar', b'zero'])
self.assertEqual(cm.exception.message,
"argument --bar: invalid int value: 'zero'")
def test_missing_required(self):
cmd = command.Command()
cmd.add_argument('--bar', required=True)
with self.assertRaises(command.CommandError) as cm:
cmd.parse([])
self.assertEqual(cm.exception.message, 'argument --bar is required')
def test_missing_positionals(self):
cmd = command.Command()
cmd.add_argument('foo')
cmd.add_argument('bar')
with self.assertRaises(command.CommandError):
with self.assertRaises(command.CommandError) as cm:
cmd.parse([])
self.assertEqual(cm.exception.message, 'too few arguments')
def test_missing_positionals_subcommand(self):
child = command.Command()
child.add_argument('baz')
cmd = command.Command()
cmd.add_child('bar', child)
with self.assertRaises(command.CommandError) as cm:
cmd.parse(['bar'])
self.assertEqual(cm.exception.message, 'too few arguments')
class UsageTest(unittest.TestCase):
@mock.patch('sys.argv')
def test_basic_usage(self, argv_mock):
argv_mock.__getitem__.return_value = 'foo'
def test_prog_name_default_and_override(self, argv_mock):
argv_mock.__getitem__.return_value = '/usr/bin/foo'
cmd = command.Command()
self.assertEqual('usage: foo', cmd.format_usage().strip())
self.assertEqual('usage: baz', cmd.format_usage('baz').strip())
def test_basic_usage(self):
cmd = command.Command()
self.assertEqual('usage: foo', cmd.format_usage('foo').strip())
cmd.add_argument('-h', '--help', action='store_true')
self.assertEqual('usage: foo [-h]', cmd.format_usage().strip())
self.assertEqual('usage: foo [-h]', cmd.format_usage('foo').strip())
cmd.add_argument('bar')
self.assertEqual('usage: foo [-h] bar', cmd.format_usage().strip())
@mock.patch('sys.argv')
def test_nested_usage(self, argv_mock):
argv_mock.__getitem__.return_value = 'foo'
self.assertEqual('usage: foo [-h] bar',
cmd.format_usage('foo').strip())
def test_nested_usage(self):
child = command.Command()
cmd = command.Command()
cmd.add_child('bar', child)
self.assertEqual('usage: foo', cmd.format_usage().strip())
self.assertEqual('usage: foo', cmd.format_usage('foo').strip())
self.assertEqual('usage: foo bar', cmd.format_usage('foo bar').strip())
cmd.add_argument('-h', '--help', action='store_true')
@ -145,3 +178,194 @@ class UsageTest(unittest.TestCase):
child.add_argument('-h', '--help', action='store_true')
self.assertEqual('usage: foo bar [-h]',
child.format_usage('foo bar').strip())
class HelpTest(unittest.TestCase):
@mock.patch('sys.argv')
def test_prog_name_default_and_override(self, argv_mock):
argv_mock.__getitem__.return_value = '/usr/bin/foo'
cmd = command.Command()
self.assertEqual('usage: foo', cmd.format_help().strip())
self.assertEqual('usage: bar', cmd.format_help('bar').strip())
def test_command_without_documenation_or_options(self):
cmd = command.Command()
self.assertEqual('usage: bar', cmd.format_help('bar').strip())
def test_command_with_option(self):
cmd = command.Command()
cmd.add_argument('-h', '--help', action='store_true',
help='show this message')
expected = ('usage: foo [-h]\n\n'
'OPTIONS:\n\n'
' -h, --help show this message')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_command_with_option_and_positional(self):
cmd = command.Command()
cmd.add_argument('-h', '--help', action='store_true',
help='show this message')
cmd.add_argument('bar', help='some help text')
expected = ('usage: foo [-h] bar\n\n'
'OPTIONS:\n\n'
' -h, --help show this message\n'
' bar some help text')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_command_with_documentation(self):
cmd = command.Command()
cmd.__doc__ = 'some text about everything this command does.'
expected = ('usage: foo\n\n'
'some text about everything this command does.')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_command_with_documentation_and_option(self):
cmd = command.Command()
cmd.__doc__ = 'some text about everything this command does.'
cmd.add_argument('-h', '--help', action='store_true',
help='show this message')
expected = ('usage: foo [-h]\n\n'
'some text about everything this command does.\n\n'
'OPTIONS:\n\n'
' -h, --help show this message')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_subcommand_without_documentation_or_options(self):
child = command.Command()
cmd = command.Command()
cmd.add_child('bar', child)
self.assertEqual('usage: foo', cmd.format_help('foo').strip())
def test_subcommand_with_documentation_shown(self):
child = command.Command()
child.__doc__ = 'some text about everything this command does.'
cmd = command.Command()
cmd.add_child('bar', child)
expected = ('usage: foo\n\n'
'COMMANDS:\n\n'
'bar\n\n'
' some text about everything this command does.')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_subcommand_with_options_shown(self):
child = command.Command()
child.add_argument('-h', '--help', action='store_true',
help='show this message')
cmd = command.Command()
cmd.add_child('bar', child)
expected = ('usage: foo\n\n'
'COMMANDS:\n\n'
'bar [-h]\n\n'
' -h, --help show this message')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_subcommand_with_positional_shown(self):
child = command.Command()
child.add_argument('baz', help='the great and wonderful')
cmd = command.Command()
cmd.add_child('bar', child)
expected = ('usage: foo\n\n'
'COMMANDS:\n\n'
'bar baz\n\n'
' baz the great and wonderful')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_subcommand_with_options_and_documentation(self):
child = command.Command()
child.__doc__ = ' some text about everything this command does.'
child.add_argument('-h', '--help', action='store_true',
help='show this message')
cmd = command.Command()
cmd.add_child('bar', child)
expected = ('usage: foo\n\n'
'COMMANDS:\n\n'
'bar [-h]\n\n'
' some text about everything this command does.\n\n'
' -h, --help show this message')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_nested_subcommands_with_options(self):
subchild = command.Command()
subchild.add_argument('--test', help='the great and wonderful')
child = command.Command()
child.add_child('baz', subchild)
child.add_argument('-h', '--help', action='store_true',
help='show this message')
cmd = command.Command()
cmd.add_child('bar', child)
expected = ('usage: foo\n\n'
'COMMANDS:\n\n'
'bar [-h]\n\n'
' -h, --help show this message\n\n'
'bar baz [--test TEST]\n\n'
' --test TEST the great and wonderful')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_nested_subcommands_skipped_intermediate(self):
subchild = command.Command()
subchild.add_argument('--test', help='the great and wonderful')
child = command.Command()
child.add_child('baz', subchild)
cmd = command.Command()
cmd.add_child('bar', child)
expected = ('usage: foo\n\n'
'COMMANDS:\n\n'
'bar baz [--test TEST]\n\n'
' --test TEST the great and wonderful')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_command_with_option_and_subcommand_with_option(self):
child = command.Command()
child.add_argument('--test', help='the great and wonderful')
cmd = command.Command()
cmd.add_argument('-h', '--help', action='store_true',
help='show this message')
cmd.add_child('bar', child)
expected = ('usage: foo [-h]\n\n'
'OPTIONS:\n\n'
' -h, --help show this message\n\n'
'COMMANDS:\n\n'
'bar [--test TEST]\n\n'
' --test TEST the great and wonderful')
self.assertEqual(expected, cmd.format_help('foo').strip())
def test_command_with_options_doc_and_subcommand_with_option_and_doc(self):
child = command.Command()
child.__doc__ = 'some text about this sub-command.'
child.add_argument('--test', help='the great and wonderful')
cmd = command.Command()
cmd.__doc__ = 'some text about everything this command does.'
cmd.add_argument('-h', '--help', action='store_true',
help='show this message')
cmd.add_child('bar', child)
expected = ('usage: foo [-h]\n\n'
'some text about everything this command does.\n\n'
'OPTIONS:\n\n'
' -h, --help show this message\n\n'
'COMMANDS:\n\n'
'bar [--test TEST]\n\n'
' some text about this sub-command.\n\n'
' --test TEST the great and wonderful')
self.assertEqual(expected, cmd.format_help('foo').strip())