Repository: dwavesystems/dwave-cloud-client

Issue:
Fix legacy config load
- make it more robust (fix things like #19)
- follow the same logic the new config has:
  - load from file, override with environment, override with arguments,
  - instead of: if minimal set of required variables is present in environment (or via arguments), use them and skip the file load
- unfortunately, the format has to stay the same
- return the first *valid* profile from file, don't fail if the first is invalid

Discussion:

Patch:
diff --git a/dwave/cloud/client.py b/dwave/cloud/client.py
index a0390e0..62d9e91 100644
--- a/dwave/cloud/client.py
+++ b/dwave/cloud/client.py
@@ -152,31 +152,19 @@ class Client(object):
 
         # try loading configuration from a preferred new config subsystem
         # (`./dwave.conf`, `~/.config/dwave/dwave.conf`, etc)
-        try:
-            config = load_config(
-                config_file=config_file, profile=profile, client=client,
-                endpoint=endpoint, token=token, solver=solver, proxy=proxy)
-        except ValueError:
-            config = dict(
-                endpoint=endpoint, token=token, solver=solver, proxy=proxy,
-                client=client)
+        config = load_config(
+            config_file=config_file, profile=profile, client=client,
+            endpoint=endpoint, token=token, solver=solver, proxy=proxy)
 
-        # and fallback to the legacy `.dwrc`
+        # fallback to legacy `.dwrc` if key variables missing
         if legacy_config_fallback and (
-                config.get('token') is None or config.get('endpoint') is None):
-            try:
-                _endpoint, _token, _proxy, _solver = legacy_load_config(
-                    key=profile,
-                    endpoint=endpoint, token=token, solver=solver, proxy=proxy)
-                config = dict(
-                    endpoint=_endpoint, token=_token, solver=_solver, proxy=_proxy,
-                    client=client)
-            except (ValueError, IOError):
-                pass
+                not config.get('token') or not config.get('endpoint')):
+            config = legacy_load_config(
+                profile=profile, client=client,
+                endpoint=endpoint, token=token, solver=solver, proxy=proxy)
 
         # manual override of other (client-custom) arguments
-        for var, val in kwargs.items():
-            config[var] = val
+        config.update(kwargs)
 
         from dwave.cloud import qpu, sw
         _clients = {'qpu': qpu.Client, 'sw': sw.Client}
diff --git a/dwave/cloud/config.py b/dwave/cloud/config.py
index 2a1883c..893d731 100644
--- a/dwave/cloud/config.py
+++ b/dwave/cloud/config.py
@@ -1,9 +1,13 @@
 import os
 import configparser
+from collections import OrderedDict
+
 import homebase
 
+from dwave.cloud.utils import uniform_get
 from dwave.cloud.exceptions import ConfigFileReadError, ConfigFileParseError
 
+
 CONF_APP = "dwave"
 CONF_AUTHOR = "dwavesystem"
 CONF_FILENAME = "dwave.conf"
@@ -356,25 +360,28 @@ def load_config(config_file=None, profile=None, client=None,
     return section
 
 
-def legacy_load_config(key=None, endpoint=None, token=None, solver=None, proxy=None):
+def legacy_load_config(profile=None, endpoint=None, token=None, solver=None,
+                       proxy=None, **kwargs):
     """Load the configured URLs and token for the SAPI server.
 
     .. warning:: Included only for backward compatibility, please use
         :func:`load_config` instead, or the client factory
         :meth:`~dwave.cloud.client.Client.from_config`.
 
-    First, this method tries to read from environment variables.
-    If these are not set, it tries to load a configuration file from ``~/.dwrc``.
+    This method tries to load a configuration file from ``~/.dwrc``, select a
+    specified `profile` (or first if not specified), and then override
+    individual keys with the values read from environment variables, and finally
+    with values given explicitly through function arguments.
 
     The environment variables searched are:
 
      - ``DW_INTERNAL__HTTPLINK``
      - ``DW_INTERNAL__TOKEN``
-     - ``DW_INTERNAL__HTTPPROXY`` (optional)
-     - ``DW_INTERNAL__SOLVER`` (optional)
+     - ``DW_INTERNAL__HTTPPROXY``
+     - ``DW_INTERNAL__SOLVER``
 
     The configuration file format is a modified CSV where the first comma is
-    replaced with a bar character ``|``. Each line encodes a single connection.
+    replaced with a bar character ``|``. Each line encodes a single profile.
 
     The columns are::
 
@@ -383,7 +390,7 @@ def legacy_load_config(key=None, endpoint=None, token=None, solver=None, proxy=N
     Everything after the ``authentication_token`` is optional.
 
     When there are multiple connections in a file, the first one is taken to be
-    the default. Any commas in the urls are percent encoded.
+    the default. Any commas in the urls must be percent encoded.
 
     Example:
 
@@ -406,7 +413,7 @@ def legacy_load_config(key=None, endpoint=None, token=None, solver=None, proxy=N
         # Will try to connect with the url `https://two.com` and the token `new-token`.
 
     Args:
-        key (str):
+        profile (str):
             The profile name in the legacy config file.
 
         endpoint (str, default=None):
@@ -426,46 +433,56 @@ def legacy_load_config(key=None, endpoint=None, token=None, solver=None, proxy=N
             connect directly to the API (unless you use a system-level proxy).
 
     Returns:
-        A tuple of SAPI info, as (url, token, proxy, default_solver_name)
+        A dictionary with keys: endpoint, token, solver, proxy.
     """
-    # Try to load environment variables
-    url = endpoint or os.environ.get('DW_INTERNAL__HTTPLINK')
-    token = token or os.environ.get('DW_INTERNAL__TOKEN')
-    proxy = proxy or os.environ.get('DW_INTERNAL__HTTPPROXY')
-    solver = solver or os.environ.get('DW_INTERNAL__SOLVER')
-
-    if url is not None and token is not None:
-        return url, token, proxy, solver
-
-    # Load the configuration file
-    user_path = os.path.expanduser('~')
-    file_path = os.path.join(user_path, '.dwrc')
-
-    # Parse the config file
-    try:
-        with open(file_path, 'r') as handle:
-            lines = handle.readlines()
-    except (IOError, OSError):
-        # Make sure python 2 and 3 raise the same error
-        raise IOError("Could not load configuration from {}".format(file_path))
-
-    # Clean whitespace and empty lines
-    lines = [line.strip() for line in lines]
-    lines = [line for line in lines if line != '']
-
-    # Go through the connections and select entry matching the key
-    for line in lines:
+
+    def _parse_config(fp, filename):
+        fields = ('endpoint', 'token', 'proxy', 'solver')
+        config = OrderedDict()
+        for line in fp:
+            # strip whitespace, skip blank and comment lines
+            line = line.strip()
+            if not line or line.startswith('#'):
+                continue
+            # parse each record, store in dict with label as key
+            try:
+                label, data = line.split('|', 1)
+                values = [v.strip() or None for v in data.split(',')]
+                config[label] = dict(zip(fields, values))
+            except:
+                raise ConfigFileParseError(
+                    "Failed to parse {!r}, line {!r}".format(filename, line))
+        return config
+
+    def _read_config(filename):
         try:
-            label, data = line.split('|', 1)
-            data = {index: value for index, value in enumerate(data.split(','))}
-
-            if label == key or data[0] == key or key is None:
-                return (endpoint or data[0] or None,
-                        token or data[1] or None,
-                        proxy or data.get(2),
-                        solver or data.get(3))
-        except:
-            pass  # Just ignore any malformed lines
-            # TODO issue a warning
-
-    raise ValueError("No configuration for the client could be discovered.")
+            with open(filename, 'r') as f:
+                return _parse_config(f, filename)
+        except (IOError, OSError):
+            raise ConfigFileReadError("Failed to read {!r}".format(filename))
+
+    config = {}
+    filename = os.path.expanduser('~/.dwrc')
+    if os.path.exists(filename):
+        config = _read_config(filename)
+
+    # load profile if specified, or first one in file
+    if profile:
+        try:
+            section = config[profile]
+        except KeyError:
+            raise ValueError("Config profile {!r} not found".format(profile))
+    else:
+        try:
+            _, section = next(iter(config.items()))
+        except StopIteration:
+            section = {}
+
+    # override config variables (if any) with environment and then with arguments
+    section['endpoint'] = endpoint or os.getenv("DW_INTERNAL__HTTPLINK", section.get('endpoint'))
+    section['token'] = token or os.getenv("DW_INTERNAL__TOKEN", section.get('token'))
+    section['proxy'] = proxy or os.getenv("DW_INTERNAL__HTTPPROXY", section.get('proxy'))
+    section['solver'] = solver or os.getenv("DW_INTERNAL__SOLVER", section.get('solver'))
+    section.update(kwargs)
+
+    return section


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 8012a42..0323ad1 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -12,16 +12,16 @@ try:
 except ImportError:
     import mock
 
-from dwave.cloud.config import legacy_load_config
+from dwave.cloud.config import load_config
 from dwave.cloud.qpu import Client
 from dwave.cloud.exceptions import SolverAuthenticationError
 import dwave.cloud
 
 
 try:
-    config_url, config_token, _, config_solver = legacy_load_config()
-    if None in [config_url, config_token, config_solver]:
-        raise ValueError()
+    config = load_config()
+    if not config['endpoint'] or not config['token'] or not config['solver']:
+        raise ValueError
     skip_live = False
 except:
     skip_live = True
@@ -34,20 +34,20 @@ class ConnectivityTests(unittest.TestCase):
     def test_bad_url(self):
         """Connect with a bad URL."""
         with self.assertRaises(IOError):
-            with Client("not-a-url", config_token) as client:
+            with Client("not-a-url", config['token']) as client:
                 client.get_solvers()
 
     @unittest.skipIf(skip_live, "No live server available.")
     def test_bad_token(self):
         """Connect with a bad token."""
         with self.assertRaises(SolverAuthenticationError):
-            with Client(config_url, 'not-a-token') as client:
+            with Client(config['endpoint'], 'not-a-token') as client:
                 client.get_solvers()
 
     @unittest.skipIf(skip_live, "No live server available.")
     def test_good_connection(self):
         """Connect with a valid URL and token."""
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             self.assertTrue(len(client.get_solvers()) > 0)
 
 
@@ -57,28 +57,28 @@ class SolverLoading(unittest.TestCase):
     @unittest.skipIf(skip_live, "No live server available.")
     def test_list_all_solvers(self):
         """List all the solvers."""
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             self.assertTrue(len(client.get_solvers()) > 0)
 
     @unittest.skipIf(skip_live, "No live server available.")
     def test_load_all_solvers(self):
         """List and retrieve all the solvers."""
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             for name in client.get_solvers():
                 self.assertEqual(client.get_solver(name).id, name)
 
     @unittest.skipIf(skip_live, "No live server available.")
     def test_load_bad_solvers(self):
         """Try to load a nonexistent solver."""
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             with self.assertRaises(KeyError):
                 client.get_solver("not-a-solver")
 
     @unittest.skipIf(skip_live, "No live server available.")
     def test_load_any_solver(self):
         """Load a single solver without calling get_solvers (which caches data)."""
-        with Client(config_url, config_token) as client:
-            self.assertEqual(client.get_solver(config_solver).id, config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            self.assertEqual(client.get_solver(config['solver']).id, config['solver'])
 
 
 class ClientFactory(unittest.TestCase):
@@ -110,7 +110,7 @@ class ClientFactory(unittest.TestCase):
                     endpoint='endpoint', token='token', custom='new-custom')
 
     def test_legacy_config_load_fallback(self):
-        conf = 'endpoint token proxy solver'.split()
+        conf = {k: k for k in 'endpoint token proxy solver'.split()}
         with mock.patch("dwave.cloud.client.load_config", return_value={}):
             with mock.patch("dwave.cloud.client.legacy_load_config", lambda **kwargs: conf):
                 # test fallback works (legacy config is loaded)
diff --git a/tests/test_mock_solver_loading.py b/tests/test_mock_solver_loading.py
index 2e8dc38..4baba26 100644
--- a/tests/test_mock_solver_loading.py
+++ b/tests/test_mock_solver_loading.py
@@ -12,8 +12,9 @@ except ImportError:
     import mock
 
 from dwave.cloud.qpu import Client, Solver
-from dwave.cloud.exceptions import InvalidAPIResponseError, ConfigFileReadError
-from dwave.cloud.config import legacy_load_config
+from dwave.cloud.exceptions import (
+    InvalidAPIResponseError, ConfigFileReadError, ConfigFileParseError)
+from dwave.cloud.config import legacy_load_config, load_config
 
 from .test_config import iterable_mock_open, configparser_open_namespace
 
@@ -197,11 +198,22 @@ class GetEvent(Exception):
         raise GetEvent(path)
 
 
-config_body = """
+legacy_config_body = """
 prod|file-prod-url,file-prod-token
 alpha|file-alpha-url,file-alpha-token,,alpha-solver
 """
 
+config_body = """
+[prod]
+endpoint = file-prod-url
+token = file-prod-token
+
+[alpha]
+endpoint = file-alpha-url
+token = file-alpha-token
+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: [])
@@ -234,7 +246,7 @@ class MockConfiguration(unittest.TestCase):
 
     def test_explicit_with_file(self):
         """With arguments and a config file, the config file should be ignored."""
-        with mock.patch("dwave.cloud.config.open", mock.mock_open(read_data=config_body), create=True):
+        with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True):
             with Client.from_config(endpoint='arg-url', token='arg-token') as client:
                 client.session.get = GetEvent.handle
                 try:
@@ -246,8 +258,8 @@ class MockConfiguration(unittest.TestCase):
 
     def test_only_file(self):
         """With no arguments or environment variables, the default connection from the config file should be used."""
-        with mock.patch("dwave.cloud.config.open", mock.mock_open(read_data=config_body), create=True):
-            with Client.from_config() as client:
+        with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True):
+            with Client.from_config('config_file') as client:
                 client.session.get = GetEvent.handle
                 try:
                     client.get_solver('arg-solver')
@@ -258,10 +270,8 @@ class MockConfiguration(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", mock.mock_open(read_data=config_body), create=True):
-            with mock.patch(configparser_open_namespace, iterable_mock_open(config_body), create=True):
-                # this will try parsing legacy format as new, fail,
-                # then try parsing it as legacy config
+        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 Client.from_config(profile='alpha') as client:
                     client.session.get = GetEvent.handle
                     try:
@@ -273,9 +283,9 @@ class MockConfiguration(unittest.TestCase):
 
     def test_env_with_file_set(self):
         """With environment variables and a config file, the config file should be ignored."""
-        with mock.patch("dwave.cloud.config.open", mock.mock_open(read_data=config_body), create=True):
+        with mock.patch("dwave.cloud.config.open", iterable_mock_open(legacy_config_body), create=True):
             with mock.patch.dict(os.environ, {'DW_INTERNAL__HTTPLINK': 'env-url', 'DW_INTERNAL__TOKEN': 'env-token'}):
-                with Client.from_config() as client:
+                with Client.from_config(False) as client:
                     client.session.get = GetEvent.handle
                     try:
                         client.get_solver('arg-solver')
@@ -297,13 +307,8 @@ class MockConfiguration(unittest.TestCase):
                 self.fail()
 
     def test_file_read_error(self):
-        """On config file read error, we should fail with `IOError`."""
+        """On config file read error, we should fail with `ConfigFileReadError`,
+        but only if .dwrc actually exists on disk."""
         with mock.patch("dwave.cloud.config.open", side_effect=OSError, create=True):
-            self.assertRaises(IOError, legacy_load_config)
-
-    def test_file_format_error(self):
-        """Config parsing error should be suppressed."""
-        with mock.patch("dwave.cloud.config.open", mock.mock_open(read_data="|\na|b,c"), create=True):
-            self.assertEqual(legacy_load_config(key='a'), ('b', 'c', None, None))
-        with mock.patch("dwave.cloud.config.open", mock.mock_open(read_data="|"), create=True):
-            self.assertRaises(ValueError, legacy_load_config)
+            with mock.patch("os.path.exists", lambda fn: True):
+                self.assertRaises(ConfigFileReadError, legacy_load_config)
diff --git a/tests/test_solver.py b/tests/test_solver.py
index d993a2b..f6e74a0 100644
--- a/tests/test_solver.py
+++ b/tests/test_solver.py
@@ -12,16 +12,16 @@ import random
 import numpy
 
 from dwave.cloud.utils import evaluate_ising
-from dwave.cloud.config import legacy_load_config
+from dwave.cloud.config import load_config
 from dwave.cloud.qpu import Client
 from dwave.cloud.exceptions import CanceledFutureError
 import dwave.cloud.computation
 
 
 try:
-    config_url, config_token, _, config_solver = legacy_load_config()
-    if None in [config_url, config_token, config_solver]:
-        raise ValueError()
+    config = load_config()
+    if not config['endpoint'] or not config['token'] or not config['solver']:
+        raise ValueError
     skip_live = False
 except:
     skip_live = True
@@ -33,22 +33,22 @@ class PropertyLoading(unittest.TestCase):
     @unittest.skipIf(skip_live, "No live server available.")
     def test_load_properties(self):
         """Ensure that the propreties are populated."""
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
             self.assertTrue(len(solver.properties) > 0)
 
     @unittest.skipIf(skip_live, "No live server available.")
     def test_load_parameters(self):
         """Make sure the parameters are populated."""
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
             self.assertTrue(len(solver.parameters) > 0)
 
     @unittest.skipIf(skip_live, "No live server available.")
     def test_submit_invalid_parameter(self):
         """Ensure that the parameters are populated."""
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
             assert 'not_a_parameter' not in solver.parameters
             with self.assertRaises(KeyError):
                 solver.sample_ising({}, {}, not_a_parameter=True)
@@ -56,8 +56,8 @@ class PropertyLoading(unittest.TestCase):
     @unittest.skipIf(skip_live, "No live server available.")
     def test_read_connectivity(self):
         """Ensure that the edge set is populated."""
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
             self.assertTrue(len(solver.edges) > 0)
 
 
@@ -80,8 +80,8 @@ class Submission(_QueryTest):
 
     @unittest.skipIf(skip_live, "No live server available.")
     def test_result_structure(self):
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
             computation = solver.sample_ising({}, {})
             result = computation.result()
             self.assertIn('samples', result)
@@ -93,8 +93,8 @@ class Submission(_QueryTest):
     def test_submit_extra_qubit(self):
         """Submit a defective problem with an unsupported variable."""
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             # Build a linear problem
             linear = [0] * (max(solver.nodes) + 1)
@@ -113,8 +113,8 @@ class Submission(_QueryTest):
     def test_submit_linear_problem(self):
         """Submit a problem with all the linear terms populated."""
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             # Build a linear problem
             linear = [0] * (max(solver.nodes) + 1)
@@ -129,8 +129,8 @@ class Submission(_QueryTest):
     def test_submit_full_problem(self):
         """Submit a problem with all supported coefficients set."""
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             # Build a linear problem
             linear = [0] * (max(solver.nodes) + 1)
@@ -147,8 +147,8 @@ class Submission(_QueryTest):
     def test_submit_dict_problem(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             # Build a problem
             linear = {index: random.choice([-1, 1]) for index in solver.nodes}
@@ -161,8 +161,8 @@ class Submission(_QueryTest):
     def test_submit_partial_problem(self):
         """Submit a problem with only some of the terms set."""
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             # Build a linear problem
             linear = [0] * (max(solver.nodes) + 1)
@@ -185,8 +185,8 @@ class Submission(_QueryTest):
     def test_submit_batch(self):
         """Submit batch of problems."""
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             result_list = []
             for _ in range(100):
@@ -214,8 +214,8 @@ class Submission(_QueryTest):
     def test_cancel_batch(self):
         """Submit batch of problems, then cancel them."""
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             # Build a linear problem
             linear = [0] * (max(solver.nodes) + 1)
@@ -248,8 +248,8 @@ class Submission(_QueryTest):
     def test_wait_many(self):
         """Submit a batch of problems then use `wait_multiple` to wait on all of them."""
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             # Build a linear problem
             linear = [0] * (max(solver.nodes) + 1)
@@ -283,8 +283,8 @@ class Submission(_QueryTest):
         all of them."""
 
         # Connect
-        with Client(config_url, config_token) as client:
-            solver = client.get_solver(config_solver)
+        with Client(config['endpoint'], config['token']) as client:
+            solver = client.get_solver(config['solver'])
 
             # Build a problem
             linear = [0] * (max(solver.nodes) + 1)
@@ -325,9 +325,9 @@ class DecodingMethod(_QueryTest):
     def test_request_matrix_with_no_numpy(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             dwave.cloud.computation._numpy = False
-            solver = client.get_solver(config_solver)
+            solver = client.get_solver(config['solver'])
             solver.return_matrix = True
 
             # Build a problem
@@ -342,9 +342,9 @@ class DecodingMethod(_QueryTest):
     def test_request_matrix_with_numpy(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             assert dwave.cloud.computation._numpy
-            solver = client.get_solver(config_solver)
+            solver = client.get_solver(config['solver'])
             solver.return_matrix = True
 
             # Build a problem
@@ -361,9 +361,9 @@ class DecodingMethod(_QueryTest):
     def test_request_list_with_no_numpy(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             dwave.cloud.computation._numpy = False
-            solver = client.get_solver(config_solver)
+            solver = client.get_solver(config['solver'])
             solver.return_matrix = False
 
             # Build a problem
@@ -377,9 +377,9 @@ class DecodingMethod(_QueryTest):
     def test_request_list_with_numpy(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             assert dwave.cloud.computation._numpy
-            solver = client.get_solver(config_solver)
+            solver = client.get_solver(config['solver'])
             solver.return_matrix = False
 
             # Build a problem
@@ -393,9 +393,9 @@ class DecodingMethod(_QueryTest):
     def test_request_raw_matrix_with_no_numpy(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             dwave.cloud.computation._numpy = False
-            solver = client.get_solver(config_solver)
+            solver = client.get_solver(config['solver'])
             solver.return_matrix = True
 
             # Build a problem
@@ -410,9 +410,9 @@ class DecodingMethod(_QueryTest):
     def test_request_raw_matrix_with_numpy(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             assert dwave.cloud.computation._numpy
-            solver = client.get_solver(config_solver)
+            solver = client.get_solver(config['solver'])
             solver.return_matrix = True
 
             # Build a problem
@@ -429,9 +429,9 @@ class DecodingMethod(_QueryTest):
     def test_request_raw_list_with_no_numpy(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             dwave.cloud.computation._numpy = False
-            solver = client.get_solver(config_solver)
+            solver = client.get_solver(config['solver'])
             solver.return_matrix = False
 
             # Build a problem
@@ -445,9 +445,9 @@ class DecodingMethod(_QueryTest):
     def test_request_raw_list_with_numpy(self):
         """Submit a problem using a dict for the linear terms."""
         # Connect
-        with Client(config_url, config_token) as client:
+        with Client(config['endpoint'], config['token']) as client:
             assert dwave.cloud.computation._numpy
-            solver = client.get_solver(config_solver)
+            solver = client.get_solver(config['solver'])
             solver.return_matrix = False
 
             # Build a problem