commands: Move mopidy.utils.command to mopidy.commands

- Also adds documenation to Command class.
- Moves scan command to commands to match naming.
This commit is contained in:
Thomas Adamcik 2013-11-16 03:01:04 +01:00
parent 2f6ecd9171
commit 03c301705d
7 changed files with 650 additions and 619 deletions

View File

@ -38,5 +38,5 @@ class Extension(ext.Extension):
return [LocalLibraryUpdateProvider]
def get_command(self):
from .command import LocalCommand
from .commands import LocalCommand
return LocalCommand()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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