Repository: dwavesystems/dwave-cloud-client
Pull request #130 (2018-04-19 22:51:51)

Simplify interface of 'dwave' command
    $ dwave --help
    Usage: dwave [OPTIONS] COMMAND [ARGS]...

    D-Wave cloud tool.

    Options:
    --version  Show the version and exit.
    --help     Show this message and exit.

    Commands:
    config  Create, update or inspect cloud client...
    ping    Ping the QPU by submitting a single-qubit...

.

    $ dwave config --help
    Usage: dwave config [OPTIONS] COMMAND [ARGS]...

    Create, update or inspect cloud client configuration file(s).

    Options:
    --help  Show this message and exit.

    Commands:
    create   Create and/or update cloud client...
    inspect  Inspect existing config/profile.
    ls       List config files detected (and/or paths...

.

    $ dwave config create --help
    Usage: dwave config create [OPTIONS]

    Create and/or update cloud client configuration file.

    Options:
    -c, --config-file PATH  Config file path
    -p, --profile TEXT      Connection profile name (config section name)
    --help                  Show this message and exit.

.

    $ dwave config inspect --help
    Usage: dwave config inspect [OPTIONS]

    Inspect existing config/profile.

    Options:
    -c, --config-file PATH  Config file path
    -p, --profile TEXT      Connection profile name (config section name)
    --help                  Show this message and exit.

.

    $ dwave config ls --help
    Usage: dwave config ls [OPTIONS]

    List config files detected (and/or paths examined).

    Options:
    --system               List paths of system-wide config files
    --user                 List paths of user-local config files
    --local                List paths of local config files
    -m, --include-missing  List all examined paths, not only existing
    --help                 Show this message and exit.

.

    $ dwave ping --help
    Usage: dwave ping [OPTIONS]

    Ping the QPU by submitting a single-qubit problem.

    Options:
    -c, --config-file PATH  Config file path
    -p, --profile TEXT      Connection profile name (config section name)
    --help                  Show this message and exit.



Patch:
diff --git a/dwave/cloud/cli.py b/dwave/cloud/cli.py
index b4b37d9..0fbec46 100644
--- a/dwave/cloud/cli.py
+++ b/dwave/cloud/cli.py
@@ -1,11 +1,10 @@
 import os
-from functools import wraps
 
 import click
 from timeit import default_timer as timer
 
 from dwave.cloud import Client
-from dwave.cloud.utils import readline_input
+from dwave.cloud.utils import readline_input, click_info_switch
 from dwave.cloud.package_info import __title__, __version__
 from dwave.cloud.exceptions import (
     SolverAuthenticationError, InvalidAPIResponseError, UnsupportedSolverError,
@@ -16,61 +15,45 @@ from dwave.cloud.config import (
     get_configfile_paths)
 
 
-def click_info_switch(f):
-    """Decorator to create Click eager info switch option, as described in docs:
-    http://click.pocoo.org/6/options/#callbacks-and-eager-options.
-
-    Takes a no-argument function and abstracts the boilerplate required by
-    Click (value checking, exit on done).
-
-    Example:
-
-        @click.option('--my-option', is_flag=True, callback=my_option,
-                    expose_value=False, is_eager=True)
-        def test():
-            pass
-
-        @click_info_switch
-        def my_option()
-            click.echo('some info related to my switch')
-    """
+@click.group()
+@click.version_option(prog_name=__title__, version=__version__)
+def cli():
+    """D-Wave cloud tool."""
 
-    @wraps(f)
-    def wrapped(ctx, param, value):
-        if not value or ctx.resilient_parsing:
-            return
-        f()
-        ctx.exit()
-    return wrapped
 
+@cli.group()
+def config():
+    """Create, update or inspect cloud client configuration file(s)."""
 
-@click_info_switch
-def list_config_files():
-    for path in get_configfile_paths():
-        click.echo(path)
 
-@click_info_switch
-def list_system_config():
-    for path in get_configfile_paths(user=False, local=False, only_existing=False):
-        click.echo(path)
+@config.command()
+@click.option('--system', is_flag=True,
+              help='List paths of system-wide config files')
+@click.option('--user', is_flag=True,
+              help='List paths of user-local config files')
+@click.option('--local', is_flag=True,
+              help='List paths of local config files')
+@click.option('--include-missing', '-m', is_flag=True,
+              help='List all examined paths, not only existing')
+def ls(system, user, local, include_missing):
+    """List config files detected (and/or paths examined)."""
 
-@click_info_switch
-def list_user_config():
-    for path in get_configfile_paths(system=False, local=False, only_existing=False):
-        click.echo(path)
+    # default action is to list *all* auto-detected files
+    if not (system or user or local):
+        system = user = local = True
 
-@click_info_switch
-def list_local_config():
-    for path in get_configfile_paths(system=False, user=False, only_existing=False):
+    for path in get_configfile_paths(system=system, user=user, local=local,
+                                     only_existing=not include_missing):
         click.echo(path)
 
 
-def inspect_config(ctx, param, value):
-    if not value or ctx.resilient_parsing:
-        return
-
-    config_file = ctx.params.get('config_file')
-    profile = ctx.params.get('profile')
+@config.command()
+@click.option('--config-file', '-c', default=None,
+              type=click.Path(exists=True, dir_okay=False), help='Config file path')
+@click.option('--profile', '-p', default=None,
+              help='Connection profile name (config section name)')
+def inspect(config_file, profile):
+    """Inspect existing config/profile."""
 
     try:
         section = load_profile_from_files(
@@ -85,36 +68,13 @@ def inspect_config(ctx, param, value):
     except (ValueError, ConfigFileReadError, ConfigFileParseError) as e:
         click.echo(e)
 
-    ctx.exit()
-
 
-@click.group()
-@click.version_option(prog_name=__title__, version=__version__)
-def cli():
-    """D-Wave cloud tool."""
-
-
-@cli.command()
-@click.option('--config-file', '-c', default=None, is_eager=True,
-              type=click.Path(exists=False, dir_okay=False),
-              help='Config file path')
-@click.option('--profile', '-p', default=None, is_eager=True,
+@config.command()
+@click.option('--config-file', '-c', default=None,
+              type=click.Path(exists=False, dir_okay=False), help='Config file path')
+@click.option('--profile', '-p', default=None,
               help='Connection profile name (config section name)')
-@click.option('--inspect', is_flag=True, expose_value=False, callback=inspect_config,
-              help='Only inspect existing config/profile (no update)')
-@click.option('--list-config-files', is_flag=True, callback=list_config_files,
-              expose_value=False, is_eager=True,
-              help='List paths of all config files detected on this system')
-@click.option('--list-system-paths', is_flag=True, callback=list_system_config,
-              expose_value=False, is_eager=True,
-              help='List paths of system-wide config files examined')
-@click.option('--list-user-paths', is_flag=True, callback=list_user_config,
-              expose_value=False, is_eager=True,
-              help='List paths of user-local config files examined')
-@click.option('--list-local-paths', is_flag=True, callback=list_local_config,
-              expose_value=False, is_eager=True,
-              help='List paths of local config files examined')
-def configure(config_file, profile):
+def create(config_file, profile):
     """Create and/or update cloud client configuration file."""
 
     # determine the config file path
@@ -190,9 +150,10 @@ def configure(config_file, profile):
 
 
 @cli.command()
-@click.option('--config-file', '-c', default=None, help='Config file path',
-              type=click.Path(exists=True, dir_okay=False))
-@click.option('--profile', '-p', default=None, help='Connection profile name')
+@click.option('--config-file', '-c', default=None,
+              type=click.Path(exists=True, dir_okay=False), help='Config file path')
+@click.option('--profile', '-p', default=None,
+              help='Connection profile name (config section name)')
 def ping(config_file, profile):
     """Ping the QPU by submitting a single-qubit problem."""
 
@@ -235,7 +196,7 @@ def ping(config_file, profile):
     t2 = timer()
 
     click.echo("\nWall clock time:")
-    click.echo(" * Solver definition fetch web request: {:.3f} ms".format((t1-t0)*1000.0))
+    click.echo(" * Solver definition fetch: {:.3f} ms".format((t1-t0)*1000.0))
     click.echo(" * Problem submit and results fetch: {:.3f} ms".format((t2-t1)*1000.0))
     click.echo(" * Total: {:.3f} ms".format((t2-t0)*1000.0))
     click.echo("\nQPU timing:")
diff --git a/dwave/cloud/utils.py b/dwave/cloud/utils.py
index 9789b42..cb0f3f2 100644
--- a/dwave/cloud/utils.py
+++ b/dwave/cloud/utils.py
@@ -1,5 +1,7 @@
 from __future__ import division, absolute_import
 
+from functools import wraps
+
 import six
 import readline
 
@@ -63,3 +65,31 @@ def readline_input(prompt, prefill=''):
         return six.moves.input(prompt)
     finally:
         readline.set_startup_hook()
+
+
+def click_info_switch(f):
+    """Decorator to create eager Click info switch option, as described in:
+    http://click.pocoo.org/6/options/#callbacks-and-eager-options.
+
+    Takes a no-argument function and abstracts the boilerplate required by
+    Click (value checking, exit on done).
+
+    Example:
+
+        @click.option('--my-option', is_flag=True, callback=my_option,
+                    expose_value=False, is_eager=True)
+        def test():
+            pass
+
+        @click_info_switch
+        def my_option()
+            click.echo('some info related to my switch')
+    """
+
+    @wraps(f)
+    def wrapped(ctx, param, value):
+        if not value or ctx.resilient_parsing:
+            return
+        f()
+        ctx.exit()
+    return wrapped


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 0ada056..3ffc3eb 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -19,7 +19,7 @@ def touch(path):
 
 class TestCli(unittest.TestCase):
 
-    def test_configure(self):
+    def test_config_create(self):
         config_file = 'dwave.conf'
         profile = 'profile'
         values = 'endpoint token client solver proxy'.split()
@@ -30,7 +30,7 @@ class TestCli(unittest.TestCase):
             touch(config_file)
             with mock.patch("six.moves.input", side_effect=values, create=True):
                 result = runner.invoke(cli, [
-                    'configure', '--config-file', config_file, '--profile', profile
+                    'config', 'create', '--config-file', config_file, '--profile', profile
                 ], input='\n'.join(values))
                 self.assertEqual(result.exit_code, 0)
 
@@ -40,7 +40,7 @@ class TestCli(unittest.TestCase):
                 for val in values:
                     self.assertEqual(config.get(val), val)
 
-    def test_configure_list_config(self):
+    def test_config_ls(self):
         runner = CliRunner()
         with runner.isolated_filesystem():
             touch('dwave.conf')
@@ -49,30 +49,65 @@ class TestCli(unittest.TestCase):
                 with mock.patch('dwave.cloud.config.homebase.user_config_dir',
                                 lambda **kw: '/user'):
                     with mock.patch('os.path.exists', lambda *x: True):
-                        # test --list-config-files
+                        # test listing of all auto-detected config files
                         result = runner.invoke(cli, [
-                            'configure', '--list-config-files'
+                            'config', 'ls'
                         ])
                         self.assertEqual(result.output.strip(), '\n'.join([
                             '/system1/dwave.conf', '/system2/dwave.conf',
                             '/user/dwave.conf', './dwave.conf']))
 
-                        # test --list-system-paths
+                        # test --system
                         result = runner.invoke(cli, [
-                            'configure', '--list-system-paths'
+                            'config', 'ls', '--system'
                         ])
                         self.assertEqual(result.output.strip(), '\n'.join([
                             '/system1/dwave.conf', '/system2/dwave.conf']))
 
-                        # test --list-user-paths
+                        # test --user
                         result = runner.invoke(cli, [
-                            'configure', '--list-user-paths'
+                            'config', 'ls', '--user'
                         ])
                         self.assertEqual(result.output.strip(), '/user/dwave.conf')
 
-                        # test --list-local-paths
+                        # test --local
                         result = runner.invoke(cli, [
-                            'configure', '--list-local-paths'
+                            'config', 'ls', '--local'
+                        ])
+                        self.assertEqual(result.output.strip(), './dwave.conf')
+
+                    # test --include-missing (none of the examined paths exist)
+                    with mock.patch('os.path.exists', lambda *x: False):
+                        # test listing of all examined paths
+                        result = runner.invoke(cli, [
+                            'config', 'ls', '--include-missing'
+                        ])
+                        self.assertEqual(result.output.strip(), '\n'.join([
+                            '/system1/dwave.conf', '/system2/dwave.conf',
+                            '/user/dwave.conf', './dwave.conf']))
+
+                        # test none exists
+                        result = runner.invoke(cli, [
+                            'config', 'ls', '--system', '--user', '--local'
+                        ])
+                        self.assertEqual(result.output.strip(), '')
+
+                        # test --system
+                        result = runner.invoke(cli, [
+                            'config', 'ls', '--system', '--include-missing'
+                        ])
+                        self.assertEqual(result.output.strip(), '\n'.join([
+                            '/system1/dwave.conf', '/system2/dwave.conf']))
+
+                        # test --user
+                        result = runner.invoke(cli, [
+                            'config', 'ls', '--user', '--include-missing'
+                        ])
+                        self.assertEqual(result.output.strip(), '/user/dwave.conf')
+
+                        # test --local
+                        result = runner.invoke(cli, [
+                            'config', 'ls', '--local', '--include-missing'
                         ])
                         self.assertEqual(result.output.strip(), './dwave.conf')
 
@@ -93,28 +128,19 @@ class TestCli(unittest.TestCase):
             with mock.patch('dwave.cloud.config.get_configfile_paths',
                             lambda **kw: [config_file]):
                 result = runner.invoke(cli, [
-                    'configure', '--inspect'
+                    'config', 'inspect'
                 ])
                 self.assertIn('endpoint = 2', result.output)
 
             # test explicit config
             result = runner.invoke(cli, [
-                'configure', '--inspect', '--config-file', config_file
+                'config', 'inspect', '--config-file', config_file
             ])
             self.assertIn('endpoint = 2', result.output)
 
             # test explicit profile
             result = runner.invoke(cli, [
-                'configure', '--inspect', '--config-file', config_file,
-                '--profile', 'b'
-            ])
-            self.assertIn('endpoint = 1', result.output)
-            self.assertIn('token = 3', result.output)
-
-            # test eagerness of config-file ane profile
-            result = runner.invoke(cli, [
-                'configure', '--config-file', config_file,
-                '--profile', 'b', '--inspect'
+                'config', 'inspect', '--config-file', config_file, '--profile', 'b'
             ])
             self.assertIn('endpoint = 1', result.output)
             self.assertIn('token = 3', result.output)