Repository: dwavesystems/dwave-cloud-client

Issue:
Add option(s) to list system/user/local config lookup paths in CLI tool
Something like:
```
$ dwave --list-system
/usr/share/dwave/dwave.conf
/usr/local/share/dwave/dwave.conf

$ dwave --list-user
/home/user/.config/dwave/dwave.conf

$ dwave --list-local
./dwave.conf
```

Comments:

Patch:
diff --git a/dwave/cloud/cli.py b/dwave/cloud/cli.py
index 0e5a503..da8a816 100644
--- a/dwave/cloud/cli.py
+++ b/dwave/cloud/cli.py
@@ -1,18 +1,72 @@
 from __future__ import print_function
 
+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.package_info import __title__, __version__
 from dwave.cloud.exceptions import (
     SolverAuthenticationError, InvalidAPIResponseError, UnsupportedSolverError)
 from dwave.cloud.config import (
     load_config_from_files, get_default_config,
-    get_configfile_path, get_default_configfile_path)
+    get_configfile_path, get_default_configfile_path,
+    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')
+    """
+
+    @wraps(f)
+    def wrapped(ctx, param, value):
+        if not value or ctx.resilient_parsing:
+            return
+        f()
+        ctx.exit()
+    return wrapped
+
+
+@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)
+
+@click_info_switch
+def list_user_config():
+    for path in get_configfile_paths(system=False, local=False, only_existing=False):
+        click.echo(path)
+
+@click_info_switch
+def list_local_config():
+    for path in get_configfile_paths(system=False, user=False, only_existing=False):
+        click.echo(path)
 
 
 @click.group()
+@click.version_option(prog_name=__title__, version=__version__)
 def cli():
     """D-Wave cloud tool."""
 
@@ -22,6 +76,18 @@ def cli():
               type=click.Path(exists=True, dir_okay=False))
 @click.option('--profile', default=None,
               help='Connection profile name (config section name)')
+@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):
     """Create and/or update cloud client configuration file."""
 
diff --git a/dwave/cloud/config.py b/dwave/cloud/config.py
index e84711f..e61d1b1 100644
--- a/dwave/cloud/config.py
+++ b/dwave/cloud/config.py
@@ -13,38 +13,61 @@ CONF_AUTHOR = "dwavesystem"
 CONF_FILENAME = "dwave.conf"
 
 
-def detect_existing_configfile_paths():
-    """Returns the list of existing config files found on disk.
+def get_configfile_paths(system=True, user=True, local=True, only_existing=True):
+    """Returns a list of (existing) config files found on disk.
 
     Candidates examined depend on the OS, but for Linux possible list is:
     ``dwave.conf`` in CWD, user-local ``.config/dwave/``, system-wide
     ``/etc/dwave/``. For details, see :func:`load_config_from_file`.
+
+    Args:
+        system (boolean, default=True):
+            Search for system-wide config files.
+
+        user (boolean, default=True):
+            Search for user-local config files.
+
+        local (boolean, default=True):
+            Search for local config files (in CWD).
+
+        only_existing (boolean, default=True):
+            Return only paths for files that exist on disk.
+
+    Returns:
+        list[str]:
+            A list of config file paths.
     """
 
+    candidates = []
+
     # system-wide has the lowest priority, `/etc/dwave/dwave.conf`
-    candidates = homebase.site_config_dir_list(
-        app_author=CONF_AUTHOR, app_name=CONF_APP,
-        use_virtualenv=False, create=False)
+    if system:
+        candidates.extend(homebase.site_config_dir_list(
+            app_author=CONF_AUTHOR, app_name=CONF_APP,
+            use_virtualenv=False, create=False))
 
     # user-local will override it, `~/.config/dwave/dwave.conf`
-    candidates.append(homebase.user_config_dir(
-        app_author=CONF_AUTHOR, app_name=CONF_APP, roaming=False,
-        use_virtualenv=False, create=False))
+    if user:
+        candidates.append(homebase.user_config_dir(
+            app_author=CONF_AUTHOR, app_name=CONF_APP, roaming=False,
+            use_virtualenv=False, create=False))
 
     # highest priority (overrides all): `./dwave.conf`
-    candidates.append(".")
+    if local:
+        candidates.append(".")
 
     paths = [os.path.join(base, CONF_FILENAME) for base in candidates]
-    existing_paths = [path for path in paths if os.path.exists(path)]
+    if only_existing:
+        paths = list(filter(os.path.exists, paths))
 
-    return existing_paths
+    return paths
 
 
 def get_configfile_path():
     """Returns the highest-priority existing config file from a list
-    of possible candidates returned by `detect_existing_configfile_paths()`, and
+    of possible candidates returned by `get_configfile_paths()`, and
     ``None`` if no candidate config file exists."""
-    paths = detect_existing_configfile_paths()
+    paths = get_configfile_paths()
     return paths[-1] if paths else None
 
 
@@ -123,7 +146,7 @@ def load_config_from_files(filenames=None):
             Config file parse failed.
     """
     if filenames is None:
-        filenames = detect_existing_configfile_paths()
+        filenames = get_configfile_paths()
 
     config = configparser.ConfigParser(default_section="defaults")
     for filename in filenames:


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index d48105b..266ee25 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -40,6 +40,43 @@ class TestCli(unittest.TestCase):
                 for val in values:
                     self.assertEqual(config.get(val), val)
 
+    def test_configure_list_config(self):
+        runner = CliRunner()
+        with runner.isolated_filesystem():
+            touch('dwave.conf')
+            with mock.patch('dwave.cloud.config.homebase.site_config_dir_list',
+                            lambda **kw: ['/system1', '/system2']):
+                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
+                        result = runner.invoke(cli, [
+                            'configure', '--list-config-files'
+                        ])
+                        self.assertEqual(result.output.strip(), '\n'.join([
+                            '/system1/dwave.conf', '/system2/dwave.conf',
+                            '/user/dwave.conf', './dwave.conf']))
+
+                        # test --list-system-paths
+                        result = runner.invoke(cli, [
+                            'configure', '--list-system-paths'
+                        ])
+                        self.assertEqual(result.output.strip(), '\n'.join([
+                            '/system1/dwave.conf', '/system2/dwave.conf']))
+
+                        # test --list-user-paths
+                        result = runner.invoke(cli, [
+                            'configure', '--list-user-paths'
+                        ])
+                        self.assertEqual(result.output.strip(), '/user/dwave.conf')
+
+                        # test --list-local-paths
+                        result = runner.invoke(cli, [
+                            'configure', '--list-local-paths'
+                        ])
+                        self.assertEqual(result.output.strip(), './dwave.conf')
+
+
     def test_ping(self):
         config_file = 'dwave.conf'
         profile = 'profile'
diff --git a/tests/test_config.py b/tests/test_config.py
index 84ef393..11d26ff 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -9,7 +9,7 @@ from functools import partial
 from dwave.cloud.exceptions import ConfigFileParseError, ConfigFileReadError
 from dwave.cloud.testing import mock, iterable_mock_open
 from dwave.cloud.config import (
-    detect_existing_configfile_paths, load_config_from_files, load_config)
+    get_configfile_paths, load_config_from_files, load_config)
 
 
 class TestConfig(unittest.TestCase):
@@ -70,7 +70,7 @@ class TestConfig(unittest.TestCase):
     def test_no_config_detected(self):
         """When no config file detected, `load_config_from_files` should return
         empty config."""
-        with mock.patch("dwave.cloud.config.detect_existing_configfile_paths", lambda: []):
+        with mock.patch("dwave.cloud.config.get_configfile_paths", lambda: []):
             self.assertFalse(load_config_from_files().sections())
 
     def test_invalid_filename_given(self):
@@ -79,7 +79,7 @@ class TestConfig(unittest.TestCase):
     def test_config_file_detection_cwd(self):
         configpath = "./dwave.conf"
         with mock.patch("os.path.exists", lambda path: path == configpath):
-            self.assertEqual(detect_existing_configfile_paths(), [configpath])
+            self.assertEqual(get_configfile_paths(), [configpath])
 
     def test_config_file_detection_user(self):
         if sys.platform == 'win32':
@@ -91,7 +91,7 @@ class TestConfig(unittest.TestCase):
             configpath = os.path.expanduser("~/.config/dwave/dwave.conf")
 
         with mock.patch("os.path.exists", lambda path: path == configpath):
-            self.assertEqual(detect_existing_configfile_paths(), [configpath])
+            self.assertEqual(get_configfile_paths(), [configpath])
 
     def test_config_file_detection_system(self):
         if sys.platform == 'win32':
@@ -103,11 +103,11 @@ class TestConfig(unittest.TestCase):
             configpath = "/etc/xdg/dwave/dwave.conf"
 
         with mock.patch("os.path.exists", lambda path: path == configpath):
-            self.assertEqual(detect_existing_configfile_paths(), [configpath])
+            self.assertEqual(get_configfile_paths(), [configpath])
 
     def test_config_file_detection_nonexisting(self):
         with mock.patch("os.path.exists", lambda path: False):
-            self.assertEqual(detect_existing_configfile_paths(), [])
+            self.assertEqual(get_configfile_paths(), [])
 
 
     def _assert_config_valid(self, config):
@@ -281,7 +281,7 @@ class TestConfig(unittest.TestCase):
             endpoint = beta
         """
 
-        with mock.patch("dwave.cloud.config.detect_existing_configfile_paths",
+        with mock.patch("dwave.cloud.config.get_configfile_paths",
                         lambda: ['config_system', 'config_user']):
 
             # test per-key override
diff --git a/tests/test_mock_solver_loading.py b/tests/test_mock_solver_loading.py
index d1ee10c..21adf08 100644
--- a/tests/test_mock_solver_loading.py
+++ b/tests/test_mock_solver_loading.py
@@ -210,7 +210,7 @@ solver = alpha-solver
 
 
 # patch the new config loading mechanism, to test only legacy config loading
-@mock.patch("dwave.cloud.config.detect_existing_configfile_paths", lambda: [])
+@mock.patch("dwave.cloud.config.get_configfile_paths", lambda: [])
 class MockLegacyConfiguration(unittest.TestCase):
     """Ensure that the precedence of configuration sources is followed."""
 
@@ -265,7 +265,7 @@ class MockLegacyConfiguration(unittest.TestCase):
     def test_only_file_key(self):
         """If give a name from the config file the proper URL should be loaded."""
         with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True):
-            with mock.patch("dwave.cloud.config.detect_existing_configfile_paths", lambda *x: ['file']):
+            with mock.patch("dwave.cloud.config.get_configfile_paths", lambda *x: ['file']):
                 with Client.from_config(profile='alpha') as client:
                     client.session.get = GetEvent.handle
                     try: