mirror of
https://github.com/docker/compose.git
synced 2025-07-21 20:54:32 +02:00
Refactor command dispatch to improve unit testing and support better error messages.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
9e242cdc75
commit
53bea8a720
@ -1,7 +1,6 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import sys
|
|
||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
|
|
||||||
from docopt import docopt
|
from docopt import docopt
|
||||||
@ -15,24 +14,21 @@ def docopt_full_help(docstring, *args, **kwargs):
|
|||||||
raise SystemExit(docstring)
|
raise SystemExit(docstring)
|
||||||
|
|
||||||
|
|
||||||
class DocoptCommand(object):
|
class DocoptDispatcher(object):
|
||||||
def docopt_options(self):
|
|
||||||
return {'options_first': True}
|
|
||||||
|
|
||||||
def sys_dispatch(self):
|
def __init__(self, command_class, options):
|
||||||
self.dispatch(sys.argv[1:])
|
self.command_class = command_class
|
||||||
|
self.options = options
|
||||||
def dispatch(self, argv):
|
|
||||||
self.perform_command(*self.parse(argv))
|
|
||||||
|
|
||||||
def parse(self, argv):
|
def parse(self, argv):
|
||||||
options = docopt_full_help(getdoc(self), argv, **self.docopt_options())
|
command_help = getdoc(self.command_class)
|
||||||
|
options = docopt_full_help(command_help, argv, **self.options)
|
||||||
command = options['COMMAND']
|
command = options['COMMAND']
|
||||||
|
|
||||||
if command is None:
|
if command is None:
|
||||||
raise SystemExit(getdoc(self))
|
raise SystemExit(command_help)
|
||||||
|
|
||||||
handler = self.get_handler(command)
|
handler = get_handler(self.command_class, command)
|
||||||
docstring = getdoc(handler)
|
docstring = getdoc(handler)
|
||||||
|
|
||||||
if docstring is None:
|
if docstring is None:
|
||||||
@ -41,17 +37,18 @@ class DocoptCommand(object):
|
|||||||
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
|
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
|
||||||
return options, handler, command_options
|
return options, handler, command_options
|
||||||
|
|
||||||
def get_handler(self, command):
|
|
||||||
|
def get_handler(command_class, command):
|
||||||
command = command.replace('-', '_')
|
command = command.replace('-', '_')
|
||||||
# we certainly want to have "exec" command, since that's what docker client has
|
# we certainly want to have "exec" command, since that's what docker client has
|
||||||
# but in python exec is a keyword
|
# but in python exec is a keyword
|
||||||
if command == "exec":
|
if command == "exec":
|
||||||
command = "exec_command"
|
command = "exec_command"
|
||||||
|
|
||||||
if not hasattr(self, command):
|
if not hasattr(command_class, command):
|
||||||
raise NoSuchCommand(command, self)
|
raise NoSuchCommand(command, command_class)
|
||||||
|
|
||||||
return getattr(self, command)
|
return getattr(command_class, command)
|
||||||
|
|
||||||
|
|
||||||
class NoSuchCommand(Exception):
|
class NoSuchCommand(Exception):
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import print_function
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@ -33,7 +34,8 @@ from ..service import NeedsBuildError
|
|||||||
from .command import friendly_error_message
|
from .command import friendly_error_message
|
||||||
from .command import get_config_path_from_options
|
from .command import get_config_path_from_options
|
||||||
from .command import project_from_options
|
from .command import project_from_options
|
||||||
from .docopt_command import DocoptCommand
|
from .docopt_command import DocoptDispatcher
|
||||||
|
from .docopt_command import get_handler
|
||||||
from .docopt_command import NoSuchCommand
|
from .docopt_command import NoSuchCommand
|
||||||
from .errors import UserError
|
from .errors import UserError
|
||||||
from .formatter import ConsoleWarningFormatter
|
from .formatter import ConsoleWarningFormatter
|
||||||
@ -52,19 +54,16 @@ console_handler = logging.StreamHandler(sys.stderr)
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
command = dispatch()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
command = TopLevelCommand()
|
command()
|
||||||
command.sys_dispatch()
|
|
||||||
except (KeyboardInterrupt, signals.ShutdownException):
|
except (KeyboardInterrupt, signals.ShutdownException):
|
||||||
log.error("Aborting.")
|
log.error("Aborting.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except (UserError, NoSuchService, ConfigurationError) as e:
|
except (UserError, NoSuchService, ConfigurationError) as e:
|
||||||
log.error(e.msg)
|
log.error(e.msg)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except NoSuchCommand as e:
|
|
||||||
commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))
|
|
||||||
log.error("No such command: %s\n\n%s", e.command, commands)
|
|
||||||
sys.exit(1)
|
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
log_api_error(e)
|
log_api_error(e)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -88,6 +87,40 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch():
|
||||||
|
dispatcher = DocoptDispatcher(
|
||||||
|
TopLevelCommand,
|
||||||
|
{'options_first': True, 'version': get_version_info('compose')})
|
||||||
|
|
||||||
|
try:
|
||||||
|
options, handler, command_options = dispatcher.parse(sys.argv[1:])
|
||||||
|
except NoSuchCommand as e:
|
||||||
|
commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))
|
||||||
|
log.error("No such command: %s\n\n%s", e.command, commands)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
setup_console_handler(console_handler, options.get('--verbose'))
|
||||||
|
return functools.partial(perform_command, options, handler, command_options)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_command(options, handler, command_options):
|
||||||
|
if options['COMMAND'] in ('help', 'version'):
|
||||||
|
# Skip looking up the compose file.
|
||||||
|
handler(command_options)
|
||||||
|
return
|
||||||
|
|
||||||
|
if options['COMMAND'] == 'config':
|
||||||
|
command = TopLevelCommand(None)
|
||||||
|
handler(command, options, command_options)
|
||||||
|
return
|
||||||
|
|
||||||
|
project = project_from_options('.', options)
|
||||||
|
command = TopLevelCommand(project)
|
||||||
|
with friendly_error_message():
|
||||||
|
# TODO: use self.project
|
||||||
|
handler(command, project, command_options)
|
||||||
|
|
||||||
|
|
||||||
def log_api_error(e):
|
def log_api_error(e):
|
||||||
if 'client is newer than server' in e.explanation:
|
if 'client is newer than server' in e.explanation:
|
||||||
# we need JSON formatted errors. In the meantime...
|
# we need JSON formatted errors. In the meantime...
|
||||||
@ -134,7 +167,7 @@ def parse_doc_section(name, source):
|
|||||||
return [s.strip() for s in pattern.findall(source)]
|
return [s.strip() for s in pattern.findall(source)]
|
||||||
|
|
||||||
|
|
||||||
class TopLevelCommand(DocoptCommand):
|
class TopLevelCommand(object):
|
||||||
"""Define and run multi-container applications with Docker.
|
"""Define and run multi-container applications with Docker.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@ -173,26 +206,8 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
"""
|
"""
|
||||||
base_dir = '.'
|
base_dir = '.'
|
||||||
|
|
||||||
def docopt_options(self):
|
def __init__(self, project):
|
||||||
options = super(TopLevelCommand, self).docopt_options()
|
self.project = project
|
||||||
options['version'] = get_version_info('compose')
|
|
||||||
return options
|
|
||||||
|
|
||||||
def perform_command(self, options, handler, command_options):
|
|
||||||
setup_console_handler(console_handler, options.get('--verbose'))
|
|
||||||
|
|
||||||
if options['COMMAND'] in ('help', 'version'):
|
|
||||||
# Skip looking up the compose file.
|
|
||||||
handler(None, command_options)
|
|
||||||
return
|
|
||||||
|
|
||||||
if options['COMMAND'] == 'config':
|
|
||||||
handler(options, command_options)
|
|
||||||
return
|
|
||||||
|
|
||||||
project = project_from_options(self.base_dir, options)
|
|
||||||
with friendly_error_message():
|
|
||||||
handler(project, command_options)
|
|
||||||
|
|
||||||
def build(self, project, options):
|
def build(self, project, options):
|
||||||
"""
|
"""
|
||||||
@ -352,13 +367,14 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
exit_code = project.client.exec_inspect(exec_id).get("ExitCode")
|
exit_code = project.client.exec_inspect(exec_id).get("ExitCode")
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
def help(self, project, options):
|
@classmethod
|
||||||
|
def help(cls, options):
|
||||||
"""
|
"""
|
||||||
Get help on a command.
|
Get help on a command.
|
||||||
|
|
||||||
Usage: help COMMAND
|
Usage: help COMMAND
|
||||||
"""
|
"""
|
||||||
handler = self.get_handler(options['COMMAND'])
|
handler = get_handler(cls, options['COMMAND'])
|
||||||
raise SystemExit(getdoc(handler))
|
raise SystemExit(getdoc(handler))
|
||||||
|
|
||||||
def kill(self, project, options):
|
def kill(self, project, options):
|
||||||
@ -739,7 +755,8 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
print("Aborting on container exit...")
|
print("Aborting on container exit...")
|
||||||
project.stop(service_names=service_names, timeout=timeout)
|
project.stop(service_names=service_names, timeout=timeout)
|
||||||
|
|
||||||
def version(self, project, options):
|
@classmethod
|
||||||
|
def version(cls, options):
|
||||||
"""
|
"""
|
||||||
Show version informations
|
Show version informations
|
||||||
|
|
||||||
|
@ -64,26 +64,20 @@ class CLITestCase(unittest.TestCase):
|
|||||||
self.assertTrue(project.client)
|
self.assertTrue(project.client)
|
||||||
self.assertTrue(project.services)
|
self.assertTrue(project.services)
|
||||||
|
|
||||||
def test_help(self):
|
|
||||||
command = TopLevelCommand()
|
|
||||||
with self.assertRaises(SystemExit):
|
|
||||||
command.dispatch(['-h'])
|
|
||||||
|
|
||||||
def test_command_help(self):
|
def test_command_help(self):
|
||||||
with self.assertRaises(SystemExit) as ctx:
|
with pytest.raises(SystemExit) as exc:
|
||||||
TopLevelCommand().dispatch(['help', 'up'])
|
TopLevelCommand.help({'COMMAND': 'up'})
|
||||||
|
|
||||||
self.assertIn('Usage: up', str(ctx.exception))
|
assert 'Usage: up' in exc.exconly()
|
||||||
|
|
||||||
def test_command_help_nonexistent(self):
|
def test_command_help_nonexistent(self):
|
||||||
with self.assertRaises(NoSuchCommand):
|
with pytest.raises(NoSuchCommand):
|
||||||
TopLevelCommand().dispatch(['help', 'nonexistent'])
|
TopLevelCommand.help({'COMMAND': 'nonexistent'})
|
||||||
|
|
||||||
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
|
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
|
||||||
@mock.patch('compose.cli.main.RunOperation', autospec=True)
|
@mock.patch('compose.cli.main.RunOperation', autospec=True)
|
||||||
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
|
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
|
||||||
def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation):
|
def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation):
|
||||||
command = TopLevelCommand()
|
|
||||||
mock_client = mock.create_autospec(docker.Client)
|
mock_client = mock.create_autospec(docker.Client)
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
@ -92,6 +86,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'service': {'image': 'busybox'}
|
'service': {'image': 'busybox'}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
command = TopLevelCommand(project)
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
command.run(project, {
|
command.run(project, {
|
||||||
@ -126,7 +121,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
command = TopLevelCommand()
|
command = TopLevelCommand(project)
|
||||||
command.run(project, {
|
command.run(project, {
|
||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
@ -147,7 +142,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'always'
|
'always'
|
||||||
)
|
)
|
||||||
|
|
||||||
command = TopLevelCommand()
|
command = TopLevelCommand(project)
|
||||||
command.run(project, {
|
command.run(project, {
|
||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
@ -168,7 +163,6 @@ class CLITestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_command_manula_and_service_ports_together(self):
|
def test_command_manula_and_service_ports_together(self):
|
||||||
command = TopLevelCommand()
|
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
client=None,
|
client=None,
|
||||||
@ -176,6 +170,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'service': {'image': 'busybox'},
|
'service': {'image': 'busybox'},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
command = TopLevelCommand(project)
|
||||||
|
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
command.run(project, {
|
command.run(project, {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user