From 03c301705d750db2750c5d1a7c50b423902b09c3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 16 Nov 2013 03:01:04 +0100 Subject: [PATCH] commands: Move mopidy.utils.command to mopidy.commands - Also adds documenation to Command class. - Moves scan command to commands to match naming. --- mopidy/backends/local/__init__.py | 2 +- .../local/{command.py => commands.py} | 10 +- mopidy/commands.py | 199 +++++++- mopidy/ext.py | 2 +- mopidy/utils/command.py | 154 ------ tests/commands_test.py | 448 +++++++++++++++++ tests/utils/command_test.py | 454 ------------------ 7 files changed, 650 insertions(+), 619 deletions(-) rename mopidy/backends/local/{command.py => commands.py} (94%) delete mode 100644 mopidy/utils/command.py delete mode 100644 tests/utils/command_test.py diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 3d425084..703b2562 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -38,5 +38,5 @@ class Extension(ext.Extension): return [LocalLibraryUpdateProvider] def get_command(self): - from .command import LocalCommand + from .commands import LocalCommand return LocalCommand() diff --git a/mopidy/backends/local/command.py b/mopidy/backends/local/commands.py similarity index 94% rename from mopidy/backends/local/command.py rename to mopidy/backends/local/commands.py index 6b41dda9..601243d0 100644 --- a/mopidy/backends/local/command.py +++ b/mopidy/backends/local/commands.py @@ -4,22 +4,22 @@ import logging import os import time -from mopidy import exceptions +from mopidy import commands, exceptions from mopidy.audio import scan -from mopidy.utils import command, path +from mopidy.utils import path from . import translator -logger = logging.getLogger('mopidy.backends.local.command') +logger = logging.getLogger('mopidy.backends.local.commands') -class LocalCommand(command.Command): +class LocalCommand(commands.Command): def __init__(self): super(LocalCommand, self).__init__() self.add_child('scan', ScanCommand()) -class ScanCommand(command.Command): +class ScanCommand(commands.Command): help = "Scan local media files and populate the local library." def __init__(self): diff --git a/mopidy/commands.py b/mopidy/commands.py index 503733ea..cb239ca0 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -1,13 +1,16 @@ from __future__ import unicode_literals import argparse +import collections import gobject import logging +import os +import sys from mopidy import config as config_lib from mopidy.audio import Audio from mopidy.core import Core -from mopidy.utils import command, deps, process, versioning +from mopidy.utils import deps, process, versioning logger = logging.getLogger('mopidy.commands') @@ -26,7 +29,195 @@ def config_override_type(value): '%s must have the format section/key=value' % value) -class RootCommand(command.Command): +class _ParserError(Exception): + pass + + +class _HelpError(Exception): + pass + + +class _ArgumentParser(argparse.ArgumentParser): + def error(self, message): + raise _ParserError(message) + + +class _HelpAction(argparse.Action): + def __init__(self, option_strings, dest=None, help=None): + super(_HelpAction, self).__init__( + option_strings=option_strings, + dest=dest or argparse.SUPPRESS, + default=argparse.SUPPRESS, + nargs=0, + help=help) + + def __call__(self, parser, namespace, values, option_string=None): + raise _HelpError() + + +class Command(object): + """Command parser and runner for building trees of commands. + + This class provides a wraper around :class:`argparse.ArgumentParser` + for handling this type of command line application in a better way than + argprases own sub-parser handling. + """ + + help = None + #: Help text to display in help output. + + def __init__(self): + self._children = collections.OrderedDict() + self._arguments = [] + self._overrides = {} + + def _build(self): + actions = [] + parser = _ArgumentParser(add_help=False) + parser.register('action', 'help', _HelpAction) + + for args, kwargs in self._arguments: + actions.append(parser.add_argument(*args, **kwargs)) + + parser.add_argument('_args', nargs=argparse.REMAINDER, + help=argparse.SUPPRESS) + return parser, actions + + def add_child(self, name, command): + """Add a child parser to consider using. + + :param name: name to use for the sub-command that is being added. + :type name: string + """ + self._children[name] = command + + def add_argument(self, *args, **kwargs): + """Add am argument to the parser. + + This method takes all the same arguments as the + :class:`argparse.ArgumentParser` version of this method. + """ + self._arguments.append((args, kwargs)) + + def set(self, **kwargs): + """Override a value in the finaly result of parsing.""" + self._overrides.update(kwargs) + + def exit(self, status_code=0, message=None, usage=None): + """Optionally print a message and exit.""" + print '\n\n'.join(m for m in (usage, message) if m) + sys.exit(status_code) + + def format_usage(self, prog=None): + """Format usage for current parser.""" + actions = self._build()[1] + prog = prog or os.path.basename(sys.argv[0]) + return self._usage(actions, prog) + '\n' + + def _usage(self, actions, prog): + formatter = argparse.HelpFormatter(prog) + formatter.add_usage(None, actions, []) + return formatter.format_help().strip() + + def format_help(self, prog=None): + """Format help for current parser and children.""" + actions = self._build()[1] + prog = prog or os.path.basename(sys.argv[0]) + + formatter = argparse.HelpFormatter(prog) + formatter.add_usage(None, actions, []) + + if self.help: + formatter.add_text(self.help) + + 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.help or actions: + formatter = argparse.HelpFormatter(name) + formatter.add_usage(None, actions, [], '') + formatter.start_section(None) + formatter.add_text(self.help) + 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, prog=None): + """Parse command line arguments. + + Will recursively parse commands until a final parser is found or an + error occurs. In the case of errors we will print a message and exit. + Otherwise, any overrides are applied and the current parser stored + in the command attribute of the return value. + + :param args: list of arguments to parse + :type args: list of strings + :param prog: name to use for program + :type prog: string + :rtype: :class:`argparse.Namespace` + """ + prog = prog or os.path.basename(sys.argv[0]) + try: + return self._parse( + args, argparse.Namespace(), self._overrides.copy(), prog) + except _HelpError: + self.exit(0, self.format_help(prog)) + + def _parse(self, args, namespace, overrides, prog): + overrides.update(self._overrides) + parser, actions = self._build() + + try: + result = parser.parse_args(args, namespace) + except _ParserError as e: + self.exit(1, e.message, self._usage(actions, prog)) + + if not result._args: + for attr, value in overrides.items(): + setattr(result, attr, value) + delattr(result, '_args') + result.command = self + return result + + child = result._args.pop(0) + if child not in self._children: + usage = self._usage(actions, prog) + self.exit(1, 'unrecognized command: %s' % child, usage) + + return self._children[child]._parse( + result._args, result, overrides, ' '.join([prog, child])) + + def run(self, *args, **kwargs): + """Run the command. + + Must be implemented by sub-classes that are not simply and intermediate + in the command namespace. + """ + raise NotImplementedError + + +class RootCommand(Command): def __init__(self): super(RootCommand, self).__init__() self.set(base_verbosity_level=0) @@ -135,7 +326,7 @@ class RootCommand(command.Command): process.stop_actors_by_class(Audio) -class ConfigCommand(command.Command): +class ConfigCommand(Command): help = "Show currently active configuration." def __init__(self): @@ -147,7 +338,7 @@ class ConfigCommand(command.Command): return 0 -class DepsCommand(command.Command): +class DepsCommand(Command): help = "Show dependencies and debug information." def __init__(self): diff --git a/mopidy/ext.py b/mopidy/ext.py index 9ebc978d..e0f50c67 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -91,7 +91,7 @@ class Extension(object): """Command to expose to command line users running mopidy. :returns: - Instance of a :class:`~mopidy.utils.command.Command` class. + Instance of a :class:`~mopidy.commands.Command` class. """ pass diff --git a/mopidy/utils/command.py b/mopidy/utils/command.py deleted file mode 100644 index 8994c6fd..00000000 --- a/mopidy/utils/command.py +++ /dev/null @@ -1,154 +0,0 @@ -from __future__ import unicode_literals - -import argparse -import collections -import os -import sys - - -class _ParserError(Exception): - pass - - -class _HelpError(Exception): - pass - - -class _ArgumentParser(argparse.ArgumentParser): - def error(self, message): - raise _ParserError(message) - - -class _HelpAction(argparse.Action): - def __init__(self, option_strings, dest=None, help=None): - super(_HelpAction, self).__init__( - option_strings=option_strings, - dest=dest or argparse.SUPPRESS, - default=argparse.SUPPRESS, - nargs=0, - help=help) - - def __call__(self, parser, namespace, values, option_string=None): - raise _HelpError() - - -class Command(object): - help = None - - def __init__(self): - self._children = collections.OrderedDict() - self._arguments = [] - self._overrides = {} - - def _build(self): - actions = [] - parser = _ArgumentParser(add_help=False) - parser.register('action', 'help', _HelpAction) - - for args, kwargs in self._arguments: - actions.append(parser.add_argument(*args, **kwargs)) - - parser.add_argument('_args', nargs=argparse.REMAINDER, - help=argparse.SUPPRESS) - return parser, actions - - def add_child(self, name, command): - self._children[name] = command - - def add_argument(self, *args, **kwargs): - self._arguments.append((args, kwargs)) - - def set(self, **kwargs): - self._overrides.update(kwargs) - - def exit(self, status_code=0, message=None, usage=None): - print '\n\n'.join(m for m in (usage, message) if m) - sys.exit(status_code) - - def format_usage(self, prog=None): - actions = self._build()[1] - prog = prog or os.path.basename(sys.argv[0]) - return self._usage(actions, prog) + '\n' - - def _usage(self, actions, prog): - formatter = argparse.HelpFormatter(prog) - formatter.add_usage(None, actions, []) - return formatter.format_help().strip() - - 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.help: - formatter.add_text(self.help) - - 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.help or actions: - formatter = argparse.HelpFormatter(name) - formatter.add_usage(None, actions, [], '') - formatter.start_section(None) - formatter.add_text(self.help) - 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, prog=None): - prog = prog or os.path.basename(sys.argv[0]) - try: - return self._parse( - args, argparse.Namespace(), self._overrides.copy(), prog) - except _HelpError: - self.exit(0, self.format_help(prog)) - - def _parse(self, args, namespace, overrides, prog): - overrides.update(self._overrides) - parser, actions = self._build() - - try: - result = parser.parse_args(args, namespace) - except _ParserError as e: - self.exit(1, e.message, self._usage(actions, prog)) - - if not result._args: - for attr, value in overrides.items(): - setattr(result, attr, value) - delattr(result, '_args') - result.command = self - return result - - child = result._args.pop(0) - if child not in self._children: - usage = self._usage(actions, prog) - self.exit(1, 'unrecognized command: %s' % child, usage) - - return self._children[child]._parse( - result._args, result, overrides, ' '.join([prog, child])) - - def run(self, *args, **kwargs): - raise NotImplementedError diff --git a/tests/commands_test.py b/tests/commands_test.py index 35fd0de5..a651a56e 100644 --- a/tests/commands_test.py +++ b/tests/commands_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import argparse +import mock import unittest from mopidy import commands @@ -42,3 +43,450 @@ class ConfigOverrideTypeTest(unittest.TestCase): self.assertRaises( argparse.ArgumentTypeError, commands.config_override_type, b'section') + + +class CommandParsingTest(unittest.TestCase): + def setUp(self): + self.exit_patcher = mock.patch.object(commands.Command, 'exit') + self.exit_mock = self.exit_patcher.start() + self.exit_mock.side_effect = SystemExit + + def tearDown(self): + self.exit_patcher.stop() + + def test_command_parsing_returns_namespace(self): + cmd = commands.Command() + self.assertIsInstance(cmd.parse([]), argparse.Namespace) + + def test_command_parsing_does_not_contain_args(self): + cmd = commands.Command() + result = cmd.parse([]) + self.assertFalse(hasattr(result, '_args')) + + def test_unknown_options_bails(self): + cmd = commands.Command() + with self.assertRaises(SystemExit): + cmd.parse(['--foobar']) + + def test_invalid_sub_command_bails(self): + cmd = commands.Command() + with self.assertRaises(SystemExit): + cmd.parse(['foo']) + + def test_command_arguments(self): + cmd = commands.Command() + cmd.add_argument('--bar') + + result = cmd.parse(['--bar', 'baz']) + self.assertEqual(result.bar, 'baz') + + def test_command_arguments_and_sub_command(self): + child = commands.Command() + child.add_argument('--baz') + + cmd = commands.Command() + cmd.add_argument('--bar') + cmd.add_child('foo', child) + + result = cmd.parse(['--bar', 'baz', 'foo']) + self.assertEqual(result.bar, 'baz') + self.assertEqual(result.baz, None) + + def test_subcommand_may_have_positional(self): + child = commands.Command() + child.add_argument('bar') + + cmd = commands.Command() + cmd.add_child('foo', child) + + result = cmd.parse(['foo', 'baz']) + self.assertEqual(result.bar, 'baz') + + def test_subcommand_may_have_remainder(self): + child = commands.Command() + child.add_argument('bar', nargs=argparse.REMAINDER) + + cmd = commands.Command() + cmd.add_child('foo', child) + + result = cmd.parse(['foo', 'baz', 'bep', 'bop']) + self.assertEqual(result.bar, ['baz', 'bep', 'bop']) + + def test_result_stores_choosen_command(self): + child = commands.Command() + + cmd = commands.Command() + cmd.add_child('foo', child) + + result = cmd.parse(['foo']) + self.assertEqual(result.command, child) + + result = cmd.parse([]) + self.assertEqual(result.command, cmd) + + child2 = commands.Command() + cmd.add_child('bar', child2) + + subchild = commands.Command() + child.add_child('baz', subchild) + + result = cmd.parse(['bar']) + self.assertEqual(result.command, child2) + + result = cmd.parse(['foo', 'baz']) + self.assertEqual(result.command, subchild) + + def test_invalid_type(self): + cmd = commands.Command() + cmd.add_argument('--bar', type=int) + + with self.assertRaises(SystemExit): + cmd.parse(['--bar', b'zero'], prog='foo') + + self.exit_mock.assert_called_once_with( + 1, "argument --bar: invalid int value: 'zero'", + 'usage: foo [--bar BAR]') + + @mock.patch('sys.argv') + def test_command_error_usage_prog(self, argv_mock): + argv_mock.__getitem__.return_value = '/usr/bin/foo' + + cmd = commands.Command() + cmd.add_argument('--bar', required=True) + + with self.assertRaises(SystemExit): + cmd.parse([]) + self.exit_mock.assert_called_once_with( + mock.ANY, mock.ANY, 'usage: foo --bar BAR') + + self.exit_mock.reset_mock() + with self.assertRaises(SystemExit): + cmd.parse([], prog='baz') + + self.exit_mock.assert_called_once_with( + mock.ANY, mock.ANY, 'usage: baz --bar BAR') + + def test_missing_required(self): + cmd = commands.Command() + cmd.add_argument('--bar', required=True) + + with self.assertRaises(SystemExit): + cmd.parse([], prog='foo') + + self.exit_mock.assert_called_once_with( + 1, 'argument --bar is required', 'usage: foo --bar BAR') + + def test_missing_positionals(self): + cmd = commands.Command() + cmd.add_argument('bar') + + with self.assertRaises(SystemExit): + cmd.parse([], prog='foo') + + self.exit_mock.assert_called_once_with( + 1, 'too few arguments', 'usage: foo bar') + + def test_missing_positionals_subcommand(self): + child = commands.Command() + child.add_argument('baz') + + cmd = commands.Command() + cmd.add_child('bar', child) + + with self.assertRaises(SystemExit): + cmd.parse(['bar'], prog='foo') + + self.exit_mock.assert_called_once_with( + 1, 'too few arguments', 'usage: foo bar baz') + + def test_unknown_command(self): + cmd = commands.Command() + + with self.assertRaises(SystemExit): + cmd.parse(['--help'], prog='foo') + + self.exit_mock.assert_called_once_with( + 1, 'unrecognized arguments: --help', 'usage: foo') + + def test_invalid_subcommand(self): + cmd = commands.Command() + cmd.add_child('baz', commands.Command()) + + with self.assertRaises(SystemExit): + cmd.parse(['bar'], prog='foo') + + self.exit_mock.assert_called_once_with( + 1, 'unrecognized command: bar', 'usage: foo') + + def test_set(self): + cmd = commands.Command() + cmd.set(foo='bar') + + result = cmd.parse([]) + self.assertEqual(result.foo, 'bar') + + def test_set_propegate(self): + child = commands.Command() + + cmd = commands.Command() + cmd.set(foo='bar') + cmd.add_child('command', child) + + result = cmd.parse(['command']) + self.assertEqual(result.foo, 'bar') + + def test_innermost_set_wins(self): + child = commands.Command() + child.set(foo='bar', baz=1) + + cmd = commands.Command() + cmd.set(foo='baz', baz=None) + cmd.add_child('command', child) + + result = cmd.parse(['command']) + self.assertEqual(result.foo, 'bar') + self.assertEqual(result.baz, 1) + + def test_help_action_works(self): + cmd = commands.Command() + cmd.add_argument('-h', action='help') + cmd.format_help = mock.Mock() + + with self.assertRaises(SystemExit): + cmd.parse(['-h']) + + cmd.format_help.assert_called_once_with(mock.ANY) + self.exit_mock.assert_called_once_with(0, cmd.format_help.return_value) + + +class UsageTest(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 = commands.Command() + self.assertEqual('usage: foo', cmd.format_usage().strip()) + self.assertEqual('usage: baz', cmd.format_usage('baz').strip()) + + def test_basic_usage(self): + cmd = commands.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('foo').strip()) + + cmd.add_argument('bar') + self.assertEqual('usage: foo [-h] bar', + cmd.format_usage('foo').strip()) + + def test_nested_usage(self): + child = commands.Command() + cmd = commands.Command() + cmd.add_child('bar', child) + + 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') + self.assertEqual('usage: foo bar', + child.format_usage('foo bar').strip()) + + 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 = commands.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 = commands.Command() + self.assertEqual('usage: bar', cmd.format_help('bar').strip()) + + def test_command_with_option(self): + cmd = commands.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 = commands.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 = commands.Command() + cmd.help = '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 = commands.Command() + cmd.help = '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 = commands.Command() + cmd = commands.Command() + cmd.add_child('bar', child) + + self.assertEqual('usage: foo', cmd.format_help('foo').strip()) + + def test_subcommand_with_documentation_shown(self): + child = commands.Command() + child.help = 'some text about everything this command does.' + + cmd = commands.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 = commands.Command() + child.add_argument('-h', '--help', action='store_true', + help='show this message') + + cmd = commands.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 = commands.Command() + child.add_argument('baz', help='the great and wonderful') + + cmd = commands.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 = commands.Command() + child.help = ' some text about everything this command does.' + child.add_argument('-h', '--help', action='store_true', + help='show this message') + + cmd = commands.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 = commands.Command() + subchild.add_argument('--test', help='the great and wonderful') + + child = commands.Command() + child.add_child('baz', subchild) + child.add_argument('-h', '--help', action='store_true', + help='show this message') + + cmd = commands.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 = commands.Command() + subchild.add_argument('--test', help='the great and wonderful') + + child = commands.Command() + child.add_child('baz', subchild) + + cmd = commands.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 = commands.Command() + child.add_argument('--test', help='the great and wonderful') + + cmd = commands.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 = commands.Command() + child.help = 'some text about this sub-command.' + child.add_argument('--test', help='the great and wonderful') + + cmd = commands.Command() + cmd.help = '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()) + + +class RunTest(unittest.TestCase): + def test_default_implmentation_raises_error(self): + with self.assertRaises(NotImplementedError): + commands.Command().run() diff --git a/tests/utils/command_test.py b/tests/utils/command_test.py deleted file mode 100644 index 634f5df2..00000000 --- a/tests/utils/command_test.py +++ /dev/null @@ -1,454 +0,0 @@ -from __future__ import unicode_literals - -import argparse -import mock -import unittest - -from mopidy.utils import command - - -class CommandParsingTest(unittest.TestCase): - def setUp(self): - self.exit_patcher = mock.patch.object(command.Command, 'exit') - self.exit_mock = self.exit_patcher.start() - self.exit_mock.side_effect = SystemExit - - def tearDown(self): - self.exit_patcher.stop() - - def test_command_parsing_returns_namespace(self): - cmd = command.Command() - self.assertIsInstance(cmd.parse([]), argparse.Namespace) - - def test_command_parsing_does_not_contain_args(self): - cmd = command.Command() - result = cmd.parse([]) - self.assertFalse(hasattr(result, '_args')) - - def test_unknown_options_bails(self): - cmd = command.Command() - with self.assertRaises(SystemExit): - cmd.parse(['--foobar']) - - def test_invalid_sub_command_bails(self): - cmd = command.Command() - with self.assertRaises(SystemExit): - cmd.parse(['foo']) - - def test_command_arguments(self): - cmd = command.Command() - cmd.add_argument('--bar') - - result = cmd.parse(['--bar', 'baz']) - self.assertEqual(result.bar, 'baz') - - def test_command_arguments_and_sub_command(self): - child = command.Command() - child.add_argument('--baz') - - cmd = command.Command() - cmd.add_argument('--bar') - cmd.add_child('foo', child) - - result = cmd.parse(['--bar', 'baz', 'foo']) - self.assertEqual(result.bar, 'baz') - self.assertEqual(result.baz, None) - - def test_subcommand_may_have_positional(self): - child = command.Command() - child.add_argument('bar') - - cmd = command.Command() - cmd.add_child('foo', child) - - result = cmd.parse(['foo', 'baz']) - self.assertEqual(result.bar, 'baz') - - def test_subcommand_may_have_remainder(self): - child = command.Command() - child.add_argument('bar', nargs=argparse.REMAINDER) - - cmd = command.Command() - cmd.add_child('foo', child) - - result = cmd.parse(['foo', 'baz', 'bep', 'bop']) - self.assertEqual(result.bar, ['baz', 'bep', 'bop']) - - def test_result_stores_choosen_command(self): - child = command.Command() - - cmd = command.Command() - cmd.add_child('foo', child) - - result = cmd.parse(['foo']) - self.assertEqual(result.command, child) - - result = cmd.parse([]) - self.assertEqual(result.command, cmd) - - child2 = command.Command() - cmd.add_child('bar', child2) - - subchild = command.Command() - child.add_child('baz', subchild) - - result = cmd.parse(['bar']) - self.assertEqual(result.command, child2) - - result = cmd.parse(['foo', 'baz']) - self.assertEqual(result.command, subchild) - - def test_invalid_type(self): - cmd = command.Command() - cmd.add_argument('--bar', type=int) - - with self.assertRaises(SystemExit): - cmd.parse(['--bar', b'zero'], prog='foo') - - self.exit_mock.assert_called_once_with( - 1, "argument --bar: invalid int value: 'zero'", - 'usage: foo [--bar BAR]') - - @mock.patch('sys.argv') - def test_command_error_usage_prog(self, argv_mock): - argv_mock.__getitem__.return_value = '/usr/bin/foo' - - cmd = command.Command() - cmd.add_argument('--bar', required=True) - - with self.assertRaises(SystemExit): - cmd.parse([]) - self.exit_mock.assert_called_once_with( - mock.ANY, mock.ANY, 'usage: foo --bar BAR') - - self.exit_mock.reset_mock() - with self.assertRaises(SystemExit): - cmd.parse([], prog='baz') - - self.exit_mock.assert_called_once_with( - mock.ANY, mock.ANY, 'usage: baz --bar BAR') - - def test_missing_required(self): - cmd = command.Command() - cmd.add_argument('--bar', required=True) - - with self.assertRaises(SystemExit): - cmd.parse([], prog='foo') - - self.exit_mock.assert_called_once_with( - 1, 'argument --bar is required', 'usage: foo --bar BAR') - - def test_missing_positionals(self): - cmd = command.Command() - cmd.add_argument('bar') - - with self.assertRaises(SystemExit): - cmd.parse([], prog='foo') - - self.exit_mock.assert_called_once_with( - 1, 'too few arguments', 'usage: foo bar') - - def test_missing_positionals_subcommand(self): - child = command.Command() - child.add_argument('baz') - - cmd = command.Command() - cmd.add_child('bar', child) - - with self.assertRaises(SystemExit): - cmd.parse(['bar'], prog='foo') - - self.exit_mock.assert_called_once_with( - 1, 'too few arguments', 'usage: foo bar baz') - - def test_unknown_command(self): - cmd = command.Command() - - with self.assertRaises(SystemExit): - cmd.parse(['--help'], prog='foo') - - self.exit_mock.assert_called_once_with( - 1, 'unrecognized arguments: --help', 'usage: foo') - - def test_invalid_subcommand(self): - cmd = command.Command() - cmd.add_child('baz', command.Command()) - - with self.assertRaises(SystemExit): - cmd.parse(['bar'], prog='foo') - - self.exit_mock.assert_called_once_with( - 1, 'unrecognized command: bar', 'usage: foo') - - def test_set(self): - cmd = command.Command() - cmd.set(foo='bar') - - result = cmd.parse([]) - self.assertEqual(result.foo, 'bar') - - def test_set_propegate(self): - child = command.Command() - - cmd = command.Command() - cmd.set(foo='bar') - cmd.add_child('command', child) - - result = cmd.parse(['command']) - self.assertEqual(result.foo, 'bar') - - def test_innermost_set_wins(self): - child = command.Command() - child.set(foo='bar', baz=1) - - cmd = command.Command() - cmd.set(foo='baz', baz=None) - cmd.add_child('command', child) - - result = cmd.parse(['command']) - self.assertEqual(result.foo, 'bar') - self.assertEqual(result.baz, 1) - - def test_help_action_works(self): - cmd = command.Command() - cmd.add_argument('-h', action='help') - cmd.format_help = mock.Mock() - - with self.assertRaises(SystemExit): - cmd.parse(['-h']) - - cmd.format_help.assert_called_once_with(mock.ANY) - self.exit_mock.assert_called_once_with(0, cmd.format_help.return_value) - - -class UsageTest(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_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('foo').strip()) - - cmd.add_argument('bar') - 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('foo').strip()) - self.assertEqual('usage: foo bar', cmd.format_usage('foo bar').strip()) - - cmd.add_argument('-h', '--help', action='store_true') - self.assertEqual('usage: foo bar', - child.format_usage('foo bar').strip()) - - 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.help = '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.help = '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.help = '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.help = ' 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.help = 'some text about this sub-command.' - child.add_argument('--test', help='the great and wonderful') - - cmd = command.Command() - cmd.help = '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()) - - -class RunTest(unittest.TestCase): - def test_default_implmentation_raises_error(self): - with self.assertRaises(NotImplementedError): - command.Command().run()