diff --git a/compose/cli/command.py b/compose/cli/command.py index 443b89c611..1a9bc3dcbf 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import contextlib import logging import os import re @@ -16,7 +17,6 @@ from .. import config from ..project import Project from ..service import ConfigError from .docker_client import docker_client -from .docopt_command import DocoptCommand from .utils import call_silently from .utils import is_mac from .utils import is_ubuntu @@ -24,40 +24,32 @@ from .utils import is_ubuntu log = logging.getLogger(__name__) -class Command(DocoptCommand): - base_dir = '.' - - def dispatch(self, *args, **kwargs): - try: - super(Command, self).dispatch(*args, **kwargs) - except SSLError as e: - raise errors.UserError('SSL error: %s' % e) - except ConnectionError: - if call_silently(['which', 'docker']) != 0: - if is_mac(): - raise errors.DockerNotFoundMac() - elif is_ubuntu(): - raise errors.DockerNotFoundUbuntu() - else: - raise errors.DockerNotFoundGeneric() - elif call_silently(['which', 'boot2docker']) == 0: - raise errors.ConnectionErrorDockerMachine() +@contextlib.contextmanager +def friendly_error_message(): + try: + yield + except SSLError as e: + raise errors.UserError('SSL error: %s' % e) + except ConnectionError: + if call_silently(['which', 'docker']) != 0: + if is_mac(): + raise errors.DockerNotFoundMac() + elif is_ubuntu(): + raise errors.DockerNotFoundUbuntu() else: - raise errors.ConnectionErrorGeneric(self.get_client().base_url) + raise errors.DockerNotFoundGeneric() + elif call_silently(['which', 'docker-machine']) == 0: + raise errors.ConnectionErrorDockerMachine() + else: + raise errors.ConnectionErrorGeneric(get_client().base_url) - def perform_command(self, options, handler, command_options): - if options['COMMAND'] in ('help', 'version'): - # Skip looking up the compose file. - handler(None, command_options) - return - project = get_project( - self.base_dir, - get_config_path(options.get('--file')), - project_name=options.get('--project-name'), - verbose=options.get('--verbose')) - - handler(project, command_options) +def project_from_options(base_dir, options): + return get_project( + base_dir, + get_config_path(options.get('--file')), + project_name=options.get('--project-name'), + verbose=options.get('--verbose')) def get_config_path(file_option): diff --git a/compose/cli/docopt_command.py b/compose/cli/docopt_command.py index 27f4b2bd7f..e3f4aa9e5b 100644 --- a/compose/cli/docopt_command.py +++ b/compose/cli/docopt_command.py @@ -25,9 +25,6 @@ class DocoptCommand(object): def dispatch(self, argv, global_options): self.perform_command(*self.parse(argv, global_options)) - def perform_command(self, options, handler, command_options): - handler(command_options) - def parse(self, argv, global_options): options = docopt_full_help(getdoc(self), argv, **self.docopt_options()) command = options['COMMAND'] diff --git a/compose/cli/main.py b/compose/cli/main.py index 60e60b795d..0f0a69cad6 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -23,7 +23,9 @@ from ..project import NoSuchService from ..service import BuildError from ..service import ConvergenceStrategy from ..service import NeedsBuildError -from .command import Command +from .command import friendly_error_message +from .command import project_from_options +from .docopt_command import DocoptCommand from .docopt_command import NoSuchCommand from .errors import UserError from .formatter import Formatter @@ -89,6 +91,15 @@ def setup_logging(): logging.getLogger("requests").propagate = False +def setup_console_handler(verbose): + if verbose: + console_handler.setFormatter(logging.Formatter('%(name)s.%(funcName)s: %(message)s')) + console_handler.setLevel(logging.DEBUG) + else: + console_handler.setFormatter(logging.Formatter()) + console_handler.setLevel(logging.INFO) + + # stolen from docopt master def parse_doc_section(name, source): pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)', @@ -96,7 +107,7 @@ def parse_doc_section(name, source): return [s.strip() for s in pattern.findall(source)] -class TopLevelCommand(Command): +class TopLevelCommand(DocoptCommand): """Define and run multi-container applications with Docker. Usage: @@ -130,20 +141,24 @@ class TopLevelCommand(Command): version Show the Docker-Compose version information """ + base_dir = '.' + def docopt_options(self): options = super(TopLevelCommand, self).docopt_options() options['version'] = get_version_info('compose') return options - def perform_command(self, options, *args, **kwargs): - if options.get('--verbose'): - console_handler.setFormatter(logging.Formatter('%(name)s.%(funcName)s: %(message)s')) - console_handler.setLevel(logging.DEBUG) - else: - console_handler.setFormatter(logging.Formatter()) - console_handler.setLevel(logging.INFO) + def perform_command(self, options, handler, command_options): + setup_console_handler(options.get('--verbose')) - return super(TopLevelCommand, self).perform_command(options, *args, **kwargs) + if options['COMMAND'] in ('help', 'version'): + # Skip looking up the compose file. + handler(None, command_options) + return + + project = project_from_options(self.base_dir, options) + with friendly_error_message(): + handler(project, command_options) def build(self, project, options): """ diff --git a/tests/unit/cli/command_test.py b/tests/unit/cli/command_test.py new file mode 100644 index 0000000000..0d4324e355 --- /dev/null +++ b/tests/unit/cli/command_test.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import + +import pytest +from requests.exceptions import ConnectionError + +from compose.cli import errors +from compose.cli.command import friendly_error_message +from tests import mock +from tests import unittest + + +class FriendlyErrorMessageTestCase(unittest.TestCase): + + def test_dispatch_generic_connection_error(self): + with pytest.raises(errors.ConnectionErrorGeneric): + with mock.patch( + 'compose.cli.command.call_silently', + autospec=True, + side_effect=[0, 1] + ): + with friendly_error_message(): + raise ConnectionError()