From 7c82485a07a09724c5b545e4dc12b4b04912489d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 10 Nov 2013 16:18:54 +0100 Subject: [PATCH] commands: Start adding commands util helper. This class is intended to be used to contruct a tree of sub-commands. In this intial version it mainly focuses on exposing `add_argument` to allow for nested parsers. Next step is to start adding better `--help` that shows the entire tree and hooking it up to select what code path to really run. --- mopidy/utils/command.py | 54 ++++++++++++++++++ tests/utils/command_test.py | 110 ++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 mopidy/utils/command.py create mode 100644 tests/utils/command_test.py diff --git a/mopidy/utils/command.py b/mopidy/utils/command.py new file mode 100644 index 00000000..37ac29c0 --- /dev/null +++ b/mopidy/utils/command.py @@ -0,0 +1,54 @@ +import argparse +import collections + + +class CommandError(Exception): + pass + + +class Command(object): + def __init__(self, name): + self.name = name + self._children = collections.OrderedDict() + self._arguments = [] + + def _build_parser(self): + parser = argparse.ArgumentParser(add_help=False) + + for args, kwargs in self._arguments: + parser.add_argument(*args, **kwargs) + + if self._children: + parser.add_argument('_args', nargs=argparse.REMAINDER) + else: + parser.set_defaults(_args=[]) + + return parser + + def add_child(self, command): + self._children[command.name] = command + + def add_argument(self, *args, **kwargs): + self._arguments.append((args, kwargs)) + + def parse(self, args, namespace=None): + if not namespace: + namespace = argparse.Namespace() + + parser = self._build_parser() + result, unknown = parser.parse_known_args(args, namespace) + + if unknown: + raise CommandError('Unknown command options.') + + args = result._args + delattr(result, '_args') + + if not args: + result.command = self + return result + + if args[0] not in self._children: + raise CommandError('Invalid sub-command provided.') + + return self._children[args[0]].parse(args[1:], result) diff --git a/tests/utils/command_test.py b/tests/utils/command_test.py new file mode 100644 index 00000000..17d6718c --- /dev/null +++ b/tests/utils/command_test.py @@ -0,0 +1,110 @@ +from __future__ import unicode_literals + +import argparse +import mock +import unittest + +from mopidy.utils import command + + +class CommandParsingTest(unittest.TestCase): + def test_command_parsing_returns_namespace(self): + cmd = command.Command(None) + self.assertIsInstance(cmd.parse([]), argparse.Namespace) + + def test_command_parsing_does_not_contain_args(self): + cmd = command.Command(None) + result = cmd.parse([]) + self.assertFalse(hasattr(result, '_args')) + + def test_sub_command_delegation(self): + mock_cmd = mock.Mock(spec=command.Command) + mock_cmd.name = 'foo' + + cmd = command.Command(None) + cmd.add_child(mock_cmd) + + cmd.parse(['foo']) + mock_cmd.parse.assert_called_with([], mock.ANY) + + def test_unknown_options_raises_error(self): + cmd = command.Command(None) + with self.assertRaises(command.CommandError): + cmd.parse(['--foobar']) + + def test_invalid_sub_command_raises_error(self): + cmd = command.Command(None) + with self.assertRaises(command.CommandError): + cmd.parse(['foo']) + + def test_command_arguments(self): + cmd = command.Command(None) + 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('foo') + child.add_argument('--baz') + + cmd = command.Command(None) + cmd.add_argument('--bar') + cmd.add_child(child) + + result = cmd.parse(['--bar', 'baz', 'foo']) + self.assertEqual(result.bar, 'baz') + self.assertEqual(result.baz, None) + + def test_multiple_sub_commands(self): + mock_foo_cmd = mock.Mock(spec=command.Command) + mock_foo_cmd.name = 'foo' + + mock_bar_cmd = mock.Mock(spec=command.Command) + mock_bar_cmd.name = 'bar' + + mock_baz_cmd = mock.Mock(spec=command.Command) + mock_baz_cmd.name = 'baz' + + cmd = command.Command(None) + cmd.add_child(mock_foo_cmd) + cmd.add_child(mock_bar_cmd) + cmd.add_child(mock_baz_cmd) + + cmd.parse(['bar']) + mock_bar_cmd.parse.assert_called_with([], mock.ANY) + + cmd.parse(['baz']) + mock_baz_cmd.parse.assert_called_with([], mock.ANY) + + def test_subcommand_may_have_positional(self): + child = command.Command('foo') + child.add_argument('bar') + + cmd = command.Command(None) + cmd.add_child(child) + + result = cmd.parse(['foo', 'baz']) + self.assertEqual(result.bar, 'baz') + + def test_subcommand_may_have_remainder(self): + child = command.Command('foo') + child.add_argument('bar', nargs=argparse.REMAINDER) + + cmd = command.Command(None) + cmd.add_child(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('foo') + + cmd = command.Command(None) + cmd.add_child(child) + + result = cmd.parse(['foo']) + self.assertEqual(result.command, child) + + result = cmd.parse([]) + self.assertEqual(result.command, cmd)