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.
This commit is contained in:
Thomas Adamcik 2013-11-10 16:18:54 +01:00
parent 851cf65328
commit 7c82485a07
2 changed files with 164 additions and 0 deletions

54
mopidy/utils/command.py Normal file
View File

@ -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)

110
tests/utils/command_test.py Normal file
View File

@ -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)