From 6bddcb7875abcb5afc3545bab0a2d06ac98fe6c4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Nov 2013 21:52:23 +0100 Subject: [PATCH] commands: Add help_formatter() with tests. --- mopidy/utils/command.py | 54 +++++++- tests/utils/command_test.py | 250 ++++++++++++++++++++++++++++++++++-- 2 files changed, 285 insertions(+), 19 deletions(-) diff --git a/mopidy/utils/command.py b/mopidy/utils/command.py index 74a327e3..158d6e2e 100644 --- a/mopidy/utils/command.py +++ b/mopidy/utils/command.py @@ -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() diff --git a/tests/utils/command_test.py b/tests/utils/command_test.py index 344e2b90..cada744b 100644 --- a/tests/utils/command_test.py +++ b/tests/utils/command_test.py @@ -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())