Преглед изворни кода

Resolves #369, add verbose output on --verbose flag

Signed-off-by: Daniel Nephin <[email protected]>
Daniel Nephin пре 11 година
родитељ
комит
df7c2cc43f

+ 57 - 50
fig/cli/command.py

@@ -12,9 +12,10 @@ from ..packages import six
 from ..project import Project
 from ..service import ConfigError
 from .docopt_command import DocoptCommand
-from .formatter import Formatter
-from .utils import cached_property, docker_url, call_silently, is_mac, is_ubuntu
+from .utils import docker_url, call_silently, is_mac, is_ubuntu
+from . import verbose_proxy
 from . import errors
+from .. import __version__
 
 log = logging.getLogger(__name__)
 
@@ -22,10 +23,6 @@ log = logging.getLogger(__name__)
 class Command(DocoptCommand):
     base_dir = '.'
 
-    def __init__(self):
-        self._yaml_path = os.environ.get('FIG_FILE', None)
-        self.explicit_project_name = None
-
     def dispatch(self, *args, **kwargs):
         try:
             super(Command, self).dispatch(*args, **kwargs)
@@ -40,60 +37,70 @@ class Command(DocoptCommand):
             elif call_silently(['which', 'docker-osx']) == 0:
                 raise errors.ConnectionErrorDockerOSX()
             else:
-                raise errors.ConnectionErrorGeneric(self.client.base_url)
-
-    def perform_command(self, options, *args, **kwargs):
-        if options['--file'] is not None:
-            self.yaml_path = os.path.join(self.base_dir, options['--file'])
-        if options['--project-name'] is not None:
-            self.explicit_project_name = options['--project-name']
-        return super(Command, self).perform_command(options, *args, **kwargs)
-
-    @cached_property
-    def client(self):
-        return Client(docker_url())
-
-    @cached_property
-    def project(self):
+                raise errors.ConnectionErrorGeneric(self.get_client().base_url)
+
+    def perform_command(self, options, handler, command_options):
+        explicit_config_path = options.get('--file') or os.environ.get('FIG_FILE')
+        project = self.get_project(
+            self.get_config_path(explicit_config_path),
+            project_name=options.get('--project-name'),
+            verbose=options.get('--verbose'))
+
+        handler(project, command_options)
+
+    def get_client(self, verbose=False):
+        client = Client(docker_url())
+        if verbose:
+            version_info = six.iteritems(client.version())
+            log.info("Fig version %s", __version__)
+            log.info("Docker base_url: %s", client.base_url)
+            log.info("Docker version: %s",
+                     ", ".join("%s=%s" % item for item in version_info))
+            return verbose_proxy.VerboseProxy('docker', client)
+        return client
+
+    def get_config(self, config_path):
         try:
-            config = yaml.safe_load(open(self.yaml_path))
+            with open(config_path, 'r') as fh:
+                return yaml.safe_load(fh)
         except IOError as e:
             if e.errno == errno.ENOENT:
                 raise errors.FigFileNotFound(os.path.basename(e.filename))
             raise errors.UserError(six.text_type(e))
 
+    def get_project(self, config_path, project_name=None, verbose=False):
         try:
-            return Project.from_config(self.project_name, config, self.client)
+            return Project.from_config(
+                self.get_project_name(config_path, project_name),
+                self.get_config(config_path),
+                self.get_client(verbose=verbose))
         except ConfigError as e:
             raise errors.UserError(six.text_type(e))
 
-    @cached_property
-    def project_name(self):
-        project = os.path.basename(os.path.dirname(os.path.abspath(self.yaml_path)))
-        if self.explicit_project_name is not None:
-            project = self.explicit_project_name
-        project = re.sub(r'[^a-zA-Z0-9]', '', project)
-        if not project:
-            project = 'default'
-        return project
-
-    @cached_property
-    def formatter(self):
-        return Formatter()
-
-    @cached_property
-    def yaml_path(self):
-        if self._yaml_path is not None:
-            return self._yaml_path
-        elif os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
-
-            log.warning("Fig just read the file 'fig.yaml' on startup, rather than 'fig.yml'")
-            log.warning("Please be aware that fig.yml the expected extension in most cases, and using .yaml can cause compatibility issues in future")
+    def get_project_name(self, config_path, project_name=None):
+        def normalize_name(name):
+            return re.sub(r'[^a-zA-Z0-9]', '', name)
+
+        if project_name is not None:
+            return normalize_name(project_name)
+
+        project = os.path.basename(os.path.dirname(os.path.abspath(config_path)))
+        if project:
+            return normalize_name(project)
+
+        return 'default'
+
+    def get_config_path(self, file_path=None):
+        if file_path:
+            return os.path.join(self.base_dir, file_path)
+
+        if os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
+            log.warning("Fig just read the file 'fig.yaml' on startup, rather "
+                        "than 'fig.yml'")
+            log.warning("Please be aware that fig.yml the expected extension "
+                        "in most cases, and using .yaml can cause compatibility "
+                        "issues in future")
 
             return os.path.join(self.base_dir, 'fig.yaml')
-        else:
-            return os.path.join(self.base_dir, 'fig.yml')
 
-    @yaml_path.setter
-    def yaml_path(self, value):
-        self._yaml_path = value
+        return os.path.join(self.base_dir, 'fig.yml')

+ 2 - 2
fig/cli/docopt_command.py

@@ -23,7 +23,7 @@ class DocoptCommand(object):
     def dispatch(self, argv, global_options):
         self.perform_command(*self.parse(argv, global_options))
 
-    def perform_command(self, options, command, handler, command_options):
+    def perform_command(self, options, handler, command_options):
         handler(command_options)
 
     def parse(self, argv, global_options):
@@ -43,7 +43,7 @@ class DocoptCommand(object):
             raise NoSuchCommand(command, self)
 
         command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
-        return (options, command, handler, command_options)
+        return options, handler, command_options
 
 
 class NoSuchCommand(Exception):

+ 35 - 32
fig/cli/main.py

@@ -98,7 +98,7 @@ class TopLevelCommand(Command):
         options['version'] = "fig %s" % __version__
         return options
 
-    def build(self, options):
+    def build(self, project, options):
         """
         Build or rebuild services.
 
@@ -112,9 +112,9 @@ class TopLevelCommand(Command):
             --no-cache  Do not use cache when building the image.
         """
         no_cache = bool(options.get('--no-cache', False))
-        self.project.build(service_names=options['SERVICE'], no_cache=no_cache)
+        project.build(service_names=options['SERVICE'], no_cache=no_cache)
 
-    def help(self, options):
+    def help(self, project, options):
         """
         Get help on a command.
 
@@ -125,15 +125,15 @@ class TopLevelCommand(Command):
             raise NoSuchCommand(command, self)
         raise SystemExit(getdoc(getattr(self, command)))
 
-    def kill(self, options):
+    def kill(self, project, options):
         """
         Force stop service containers.
 
         Usage: kill [SERVICE...]
         """
-        self.project.kill(service_names=options['SERVICE'])
+        project.kill(service_names=options['SERVICE'])
 
-    def logs(self, options):
+    def logs(self, project, options):
         """
         View output from containers.
 
@@ -142,14 +142,13 @@ class TopLevelCommand(Command):
         Options:
             --no-color  Produce monochrome output.
         """
-        containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
+        containers = project.containers(service_names=options['SERVICE'], stopped=True)
 
         monochrome = options['--no-color']
-
         print("Attaching to", list_containers(containers))
         LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()
 
-    def ps(self, options):
+    def ps(self, project, options):
         """
         List containers.
 
@@ -158,7 +157,7 @@ class TopLevelCommand(Command):
         Options:
             -q    Only display IDs
         """
-        containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=True)
+        containers = project.containers(service_names=options['SERVICE'], stopped=True) + project.containers(service_names=options['SERVICE'], one_off=True)
 
         if options['-q']:
             for container in containers:
@@ -183,7 +182,7 @@ class TopLevelCommand(Command):
                 ])
             print(Formatter().table(headers, rows))
 
-    def rm(self, options):
+    def rm(self, project, options):
         """
         Remove stopped service containers.
 
@@ -193,21 +192,21 @@ class TopLevelCommand(Command):
             --force   Don't ask to confirm removal
             -v        Remove volumes associated with containers
         """
-        all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
+        all_containers = project.containers(service_names=options['SERVICE'], stopped=True)
         stopped_containers = [c for c in all_containers if not c.is_running]
 
         if len(stopped_containers) > 0:
             print("Going to remove", list_containers(stopped_containers))
             if options.get('--force') \
                     or yesno("Are you sure? [yN] ", default=False):
-                self.project.remove_stopped(
+                project.remove_stopped(
                     service_names=options['SERVICE'],
                     v=options.get('-v', False)
                 )
         else:
             print("No stopped containers")
 
-    def run(self, options):
+    def run(self, project, options):
         """
         Run a one-off command on a service.
 
@@ -229,14 +228,13 @@ class TopLevelCommand(Command):
             --rm       Remove container after run. Ignored in detached mode.
             --no-deps  Don't start linked services.
         """
-
-        service = self.project.get_service(options['SERVICE'])
+        service = project.get_service(options['SERVICE'])
 
         if not options['--no-deps']:
             deps = service.get_linked_names()
 
             if len(deps) > 0:
-                self.project.up(
+                project.up(
                     service_names=deps,
                     start_links=True,
                     recreate=False,
@@ -262,14 +260,14 @@ class TopLevelCommand(Command):
             print(container.name)
         else:
             service.start_container(container, ports=None, one_off=True)
-            dockerpty.start(self.client, container.id)
+            dockerpty.start(project.client, container.id)
             exit_code = container.wait()
             if options['--rm']:
                 log.info("Removing %s..." % container.name)
-                self.client.remove_container(container.id)
+                project.client.remove_container(container.id)
             sys.exit(exit_code)
 
-    def scale(self, options):
+    def scale(self, project, options):
         """
         Set number of containers to run for a service.
 
@@ -290,19 +288,24 @@ class TopLevelCommand(Command):
                 raise UserError('Number of containers for service "%s" is not a '
                                 'number' % service_name)
             try:
-                self.project.get_service(service_name).scale(num)
+                project.get_service(service_name).scale(num)
             except CannotBeScaledError:
-                raise UserError('Service "%s" cannot be scaled because it specifies a port on the host. If multiple containers for this service were created, the port would clash.\n\nRemove the ":" from the port definition in fig.yml so Docker can choose a random port for each container.' % service_name)
-
-    def start(self, options):
+                raise UserError(
+                    'Service "%s" cannot be scaled because it specifies a port '
+                    'on the host. If multiple containers for this service were '
+                    'created, the port would clash.\n\nRemove the ":" from the '
+                    'port definition in fig.yml so Docker can choose a random '
+                    'port for each container.' % service_name)
+
+    def start(self, project, options):
         """
         Start existing containers.
 
         Usage: start [SERVICE...]
         """
-        self.project.start(service_names=options['SERVICE'])
+        project.start(service_names=options['SERVICE'])
 
-    def stop(self, options):
+    def stop(self, project, options):
         """
         Stop running containers without removing them.
 
@@ -310,9 +313,9 @@ class TopLevelCommand(Command):
 
         Usage: stop [SERVICE...]
         """
-        self.project.stop(service_names=options['SERVICE'])
+        project.stop(service_names=options['SERVICE'])
 
-    def up(self, options):
+    def up(self, project, options):
         """
         Build, (re)create, start and attach to containers for a service.
 
@@ -343,13 +346,13 @@ class TopLevelCommand(Command):
         recreate = not options['--no-recreate']
         service_names = options['SERVICE']
 
-        self.project.up(
+        project.up(
             service_names=service_names,
             start_links=start_links,
             recreate=recreate
         )
 
-        to_attach = [c for s in self.project.get_services(service_names) for c in s.containers()]
+        to_attach = [c for s in project.get_services(service_names) for c in s.containers()]
 
         if not detached:
             print("Attaching to", list_containers(to_attach))
@@ -359,12 +362,12 @@ class TopLevelCommand(Command):
                 log_printer.run()
             finally:
                 def handler(signal, frame):
-                    self.project.kill(service_names=service_names)
+                    project.kill(service_names=service_names)
                     sys.exit(0)
                 signal.signal(signal.SIGINT, handler)
 
                 print("Gracefully stopping... (press Ctrl+C again to force)")
-                self.project.stop(service_names=service_names)
+                project.stop(service_names=service_names)
 
 
 def list_containers(containers):

+ 0 - 19
fig/cli/utils.py

@@ -7,25 +7,6 @@ import subprocess
 import platform
 
 
-def cached_property(f):
-    """
-    returns a cached property that is calculated by function f
-    http://code.activestate.com/recipes/576563-cached-property/
-    """
-    def get(self):
-        try:
-            return self._property_cache[f]
-        except AttributeError:
-            self._property_cache = {}
-            x = self._property_cache[f] = f(self)
-            return x
-        except KeyError:
-            x = self._property_cache[f] = f(self)
-            return x
-
-    return property(get)
-
-
 def yesno(prompt, default=None):
     """
     Prompt the user for a yes or no.

+ 58 - 0
fig/cli/verbose_proxy.py

@@ -0,0 +1,58 @@
+
+import functools
+from itertools import chain
+import logging
+import pprint
+
+from fig.packages import six
+
+
+def format_call(args, kwargs):
+    args = (repr(a) for a in args)
+    kwargs = ("{0!s}={1!r}".format(*item) for item in six.iteritems(kwargs))
+    return "({0})".format(", ".join(chain(args, kwargs)))
+
+
+def format_return(result, max_lines):
+    if isinstance(result, (list, tuple, set)):
+        return "({0} with {1} items)".format(type(result).__name__, len(result))
+
+    if result:
+        lines = pprint.pformat(result).split('\n')
+        extra = '\n...' if len(lines) > max_lines else ''
+        return '\n'.join(lines[:max_lines]) + extra
+
+    return result
+
+
+class VerboseProxy(object):
+    """Proxy all function calls to another class and log method name, arguments
+    and return values for each call.
+    """
+
+    def __init__(self, obj_name, obj, log_name=None, max_lines=10):
+        self.obj_name = obj_name
+        self.obj = obj
+        self.max_lines = max_lines
+        self.log = logging.getLogger(log_name or __name__)
+
+    def __getattr__(self, name):
+        attr = getattr(self.obj, name)
+
+        if not six.callable(attr):
+            return attr
+
+        return functools.partial(self.proxy_callable, name)
+
+    def proxy_callable(self, call_name, *args, **kwargs):
+        self.log.info("%s %s <- %s",
+                      self.obj_name,
+                      call_name,
+                      format_call(args, kwargs))
+
+        result = getattr(self.obj, call_name)(*args, **kwargs)
+        self.log.info("%s %s -> %s",
+                      self.obj_name,
+                      call_name,
+                      format_return(result, self.max_lines))
+        return result

+ 33 - 30
tests/integration/cli_test.py

@@ -5,6 +5,7 @@ from fig.cli.main import TopLevelCommand
 from fig.packages.six import StringIO
 import sys
 
+
 class CLITestCase(DockerClientTestCase):
     def setUp(self):
         super(CLITestCase, self).setUp()
@@ -15,12 +16,16 @@ class CLITestCase(DockerClientTestCase):
 
     def tearDown(self):
         sys.exit = self.old_sys_exit
-        self.command.project.kill()
-        self.command.project.remove_stopped()
+        self.project.kill()
+        self.project.remove_stopped()
+
+    @property
+    def project(self):
+        return self.command.get_project(self.command.get_config_path())
 
     @patch('sys.stdout', new_callable=StringIO)
     def test_ps(self, mock_stdout):
-        self.command.project.get_service('simple').create_container()
+        self.project.get_service('simple').create_container()
         self.command.dispatch(['ps'], None)
         self.assertIn('simplefigfile_simple_1', mock_stdout.getvalue())
 
@@ -64,17 +69,17 @@ class CLITestCase(DockerClientTestCase):
 
     def test_up(self):
         self.command.dispatch(['up', '-d'], None)
-        service = self.command.project.get_service('simple')
-        another = self.command.project.get_service('another')
+        service = self.project.get_service('simple')
+        another = self.project.get_service('another')
         self.assertEqual(len(service.containers()), 1)
         self.assertEqual(len(another.containers()), 1)
 
     def test_up_with_links(self):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['up', '-d', 'web'], None)
-        web = self.command.project.get_service('web')
-        db = self.command.project.get_service('db')
-        console = self.command.project.get_service('console')
+        web = self.project.get_service('web')
+        db = self.project.get_service('db')
+        console = self.project.get_service('console')
         self.assertEqual(len(web.containers()), 1)
         self.assertEqual(len(db.containers()), 1)
         self.assertEqual(len(console.containers()), 0)
@@ -82,16 +87,16 @@ class CLITestCase(DockerClientTestCase):
     def test_up_with_no_deps(self):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['up', '-d', '--no-deps', 'web'], None)
-        web = self.command.project.get_service('web')
-        db = self.command.project.get_service('db')
-        console = self.command.project.get_service('console')
+        web = self.project.get_service('web')
+        db = self.project.get_service('db')
+        console = self.project.get_service('console')
         self.assertEqual(len(web.containers()), 1)
         self.assertEqual(len(db.containers()), 0)
         self.assertEqual(len(console.containers()), 0)
 
     def test_up_with_recreate(self):
         self.command.dispatch(['up', '-d'], None)
-        service = self.command.project.get_service('simple')
+        service = self.project.get_service('simple')
         self.assertEqual(len(service.containers()), 1)
 
         old_ids = [c.id for c in service.containers()]
@@ -105,7 +110,7 @@ class CLITestCase(DockerClientTestCase):
 
     def test_up_with_keep_old(self):
         self.command.dispatch(['up', '-d'], None)
-        service = self.command.project.get_service('simple')
+        service = self.project.get_service('simple')
         self.assertEqual(len(service.containers()), 1)
 
         old_ids = [c.id for c in service.containers()]
@@ -117,19 +122,18 @@ class CLITestCase(DockerClientTestCase):
 
         self.assertEqual(old_ids, new_ids)
 
-
     @patch('dockerpty.start')
     def test_run_service_without_links(self, mock_stdout):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['run', 'console', '/bin/true'], None)
-        self.assertEqual(len(self.command.project.containers()), 0)
+        self.assertEqual(len(self.project.containers()), 0)
 
     @patch('dockerpty.start')
     def test_run_service_with_links(self, __):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['run', 'web', '/bin/true'], None)
-        db = self.command.project.get_service('db')
-        console = self.command.project.get_service('console')
+        db = self.project.get_service('db')
+        console = self.project.get_service('console')
         self.assertEqual(len(db.containers()), 1)
         self.assertEqual(len(console.containers()), 0)
 
@@ -137,14 +141,14 @@ class CLITestCase(DockerClientTestCase):
     def test_run_with_no_deps(self, __):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
-        db = self.command.project.get_service('db')
+        db = self.project.get_service('db')
         self.assertEqual(len(db.containers()), 0)
 
     @patch('dockerpty.start')
     def test_run_does_not_recreate_linked_containers(self, __):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['up', '-d', 'db'], None)
-        db = self.command.project.get_service('db')
+        db = self.project.get_service('db')
         self.assertEqual(len(db.containers()), 1)
 
         old_ids = [c.id for c in db.containers()]
@@ -161,11 +165,11 @@ class CLITestCase(DockerClientTestCase):
         self.command.base_dir = 'tests/fixtures/commands-figfile'
         self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test')
 
-        for c in self.command.project.containers(stopped=True, one_off=True):
+        for c in self.project.containers(stopped=True, one_off=True):
             c.remove()
 
         self.command.dispatch(['run', 'implicit'], None)
-        service = self.command.project.get_service('implicit')
+        service = self.project.get_service('implicit')
         containers = service.containers(stopped=True, one_off=True)
         self.assertEqual(
             [c.human_readable_command for c in containers],
@@ -173,7 +177,7 @@ class CLITestCase(DockerClientTestCase):
         )
 
         self.command.dispatch(['run', 'explicit'], None)
-        service = self.command.project.get_service('explicit')
+        service = self.project.get_service('explicit')
         containers = service.containers(stopped=True, one_off=True)
         self.assertEqual(
             [c.human_readable_command for c in containers],
@@ -181,7 +185,7 @@ class CLITestCase(DockerClientTestCase):
         )
 
     def test_rm(self):
-        service = self.command.project.get_service('simple')
+        service = self.project.get_service('simple')
         service.create_container()
         service.kill()
         self.assertEqual(len(service.containers(stopped=True)), 1)
@@ -189,24 +193,23 @@ class CLITestCase(DockerClientTestCase):
         self.assertEqual(len(service.containers(stopped=True)), 0)
 
     def test_scale(self):
-        project = self.command.project
+        project = self.project
 
-        self.command.scale({'SERVICE=NUM': ['simple=1']})
+        self.command.scale(project, {'SERVICE=NUM': ['simple=1']})
         self.assertEqual(len(project.get_service('simple').containers()), 1)
 
-        self.command.scale({'SERVICE=NUM': ['simple=3', 'another=2']})
+        self.command.scale(project, {'SERVICE=NUM': ['simple=3', 'another=2']})
         self.assertEqual(len(project.get_service('simple').containers()), 3)
         self.assertEqual(len(project.get_service('another').containers()), 2)
 
-        self.command.scale({'SERVICE=NUM': ['simple=1', 'another=1']})
+        self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
         self.assertEqual(len(project.get_service('simple').containers()), 1)
         self.assertEqual(len(project.get_service('another').containers()), 1)
 
-        self.command.scale({'SERVICE=NUM': ['simple=1', 'another=1']})
+        self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
         self.assertEqual(len(project.get_service('simple').containers()), 1)
         self.assertEqual(len(project.get_service('another').containers()), 1)
 
-        self.command.scale({'SERVICE=NUM': ['simple=0', 'another=0']})
+        self.command.scale(project, {'SERVICE=NUM': ['simple=0', 'another=0']})
         self.assertEqual(len(project.get_service('simple').containers()), 0)
         self.assertEqual(len(project.get_service('another').containers()), 0)
-

+ 0 - 0
tests/unit/cli/__init__.py


+ 30 - 0
tests/unit/cli/verbose_proxy_test.py

@@ -0,0 +1,30 @@
+from __future__ import unicode_literals
+from __future__ import absolute_import
+from tests import unittest
+
+from fig.cli import verbose_proxy
+
+
+class VerboseProxy(unittest.TestCase):
+
+    def test_format_call(self):
+        expected = "(u'arg1', True, key=u'value')"
+        actual = verbose_proxy.format_call(
+            ("arg1", True),
+            {'key': 'value'})
+
+        self.assertEqual(expected, actual)
+
+    def test_format_return_sequence(self):
+        expected = "(list with 10 items)"
+        actual = verbose_proxy.format_return(list(range(10)), 2)
+        self.assertEqual(expected, actual)
+
+    def test_format_return(self):
+        expected = "{u'Id': u'ok'}"
+        actual = verbose_proxy.format_return({'Id': 'ok'}, 2)
+        self.assertEqual(expected, actual)
+
+    def test_format_return_no_result(self):
+        actual = verbose_proxy.format_return(None, 2)
+        self.assertEqual(None, actual)

+ 20 - 5
tests/unit/cli_test.py

@@ -4,6 +4,8 @@ import logging
 import os
 from .. import unittest
 
+import mock
+
 from fig.cli import main
 from fig.cli.main import TopLevelCommand
 from fig.packages.six import StringIO
@@ -16,24 +18,37 @@ class CLITestCase(unittest.TestCase):
         try:
             os.chdir('tests/fixtures/simple-figfile')
             command = TopLevelCommand()
-            self.assertEquals('simplefigfile', command.project_name)
+            project_name = command.get_project_name(command.get_config_path())
+            self.assertEquals('simplefigfile', project_name)
         finally:
             os.chdir(cwd)
 
     def test_project_name_with_explicit_base_dir(self):
         command = TopLevelCommand()
         command.base_dir = 'tests/fixtures/simple-figfile'
-        self.assertEquals('simplefigfile', command.project_name)
+        project_name = command.get_project_name(command.get_config_path())
+        self.assertEquals('simplefigfile', project_name)
 
     def test_project_name_with_explicit_project_name(self):
         command = TopLevelCommand()
-        command.explicit_project_name = 'explicit-project-name'
-        self.assertEquals('explicitprojectname', command.project_name)
+        name = 'explicit-project-name'
+        project_name = command.get_project_name(None, project_name=name)
+        self.assertEquals('explicitprojectname', project_name)
 
     def test_yaml_filename_check(self):
         command = TopLevelCommand()
         command.base_dir = 'tests/fixtures/longer-filename-figfile'
-        self.assertTrue(command.project.get_service('definedinyamlnotyml'))
+        with mock.patch('fig.cli.command.log', autospec=True) as mock_log:
+            self.assertTrue(command.get_config_path())
+        self.assertEqual(mock_log.warning.call_count, 2)
+
+    def test_get_project(self):
+        command = TopLevelCommand()
+        command.base_dir = 'tests/fixtures/longer-filename-figfile'
+        project = command.get_project(command.get_config_path())
+        self.assertEqual(project.name, 'longerfilenamefigfile')
+        self.assertTrue(project.client)
+        self.assertTrue(project.services)
 
     def test_help(self):
         command = TopLevelCommand()