diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45de549c238e..60433022114e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ The format is based on `Keep a Changelog`_. Added ----- +- Add ``unregister()`` for removing previously registered providers (#584). Changed ------- diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 09aa39b0d2b2..fb995f1aae25 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -32,8 +32,11 @@ from ._quantumjob import QuantumJob from ._quantumprogram import QuantumProgram from ._result import Result -from .wrapper._wrapper import (available_backends, execute, register, get_backend, compile, - load_qasm_string, load_qasm_file) + +from .wrapper._wrapper import ( + available_backends, local_backends, remote_backends, + get_backend, compile, execute, register, unregister, + registered_providers, load_qasm_string, load_qasm_file) # Import the wrapper, to make it available when doing "import qiskit". from . import wrapper diff --git a/qiskit/_quantumprogram.py b/qiskit/_quantumprogram.py index 089f67b7579b..c4e91147e4a6 100644 --- a/qiskit/_quantumprogram.py +++ b/qiskit/_quantumprogram.py @@ -666,7 +666,7 @@ def set_api(self, token, url, hub=None, group=None, project=None, # TODO: the setting of self._api and self.__api_config is left for # backwards-compatibility. # pylint: disable=no-member - self.__api = qiskit.wrapper._wrapper._DEFAULT_PROVIDER.providers[-1]._api + self.__api = qiskit.wrapper._wrapper._DEFAULT_PROVIDER.providers['ibmq']._api config_dict = { 'url': url, } diff --git a/qiskit/wrapper/__init__.py b/qiskit/wrapper/__init__.py index ce8a66f91ce5..f26d285b2b7c 100644 --- a/qiskit/wrapper/__init__.py +++ b/qiskit/wrapper/__init__.py @@ -16,5 +16,5 @@ """ from ._wrapper import (available_backends, local_backends, remote_backends, - get_backend, compile, execute, register, - load_qasm_string, load_qasm_file) + get_backend, compile, execute, register, unregister, + registered_providers, load_qasm_string, load_qasm_file) diff --git a/qiskit/wrapper/_wrapper.py b/qiskit/wrapper/_wrapper.py index 540495d65418..07cb882a934b 100644 --- a/qiskit/wrapper/_wrapper.py +++ b/qiskit/wrapper/_wrapper.py @@ -8,8 +8,7 @@ """Helper module for simplified QISKit usage.""" from qiskit import transpiler -from qiskit import QISKitError -from qiskit.backends.ibmq.ibmqprovider import IBMQProvider + from qiskit.wrapper.defaultqiskitprovider import DefaultQISKitProvider from ._circuittoolkit import circuit_from_qasm_file, circuit_from_qasm_string @@ -21,7 +20,7 @@ def register(token, url='https://quantumexperience.ng.bluemix.net/api', hub=None, group=None, project=None, proxies=None, verify=True, - provider_name='ibmq'): + provider_name=None): """ Authenticate against an online backend provider. @@ -36,17 +35,34 @@ def register(token, url='https://quantumexperience.ng.bluemix.net/api', proxies (dict): Proxy configuration for the API, as a dict with 'urls' and credential keys. verify (bool): If False, ignores SSL certificates errors. - provider_name (str): the unique name for the online backend - provider (for example, 'ibmq' for the IBM Quantum Experience). + provider_name (str): the user-provided name for the registered + provider. Raises: QISKitError: if the provider name is not recognized. """ - if provider_name == 'ibmq': - provider = IBMQProvider(token, url, - hub, group, project, proxies, verify) - _DEFAULT_PROVIDER.add_provider(provider) - else: - raise QISKitError('provider name %s is not recognized' % provider_name) + # Convert the credentials to a dict. + credentials = { + 'token': token, 'url': url, 'hub': hub, 'group': group, + 'project': project, 'proxies': proxies, 'verify': verify + } + _DEFAULT_PROVIDER.add_ibmq_provider(credentials, provider_name) + + +def unregister(provider_name): + """ + Removes a provider of list of registered providers. + + Args: + provider_name (str): The unique name for the online provider. + Raises: + QISKitError: if the provider name is not valid. + """ + _DEFAULT_PROVIDER.remove_provider(provider_name) + + +def registered_providers(): + """Return the names of the currently registered providers.""" + return list(_DEFAULT_PROVIDER.providers.keys()) # Functions for inspecting and retrieving backends. diff --git a/qiskit/wrapper/defaultqiskitprovider.py b/qiskit/wrapper/defaultqiskitprovider.py index 3d16aabde229..af797d60a66d 100644 --- a/qiskit/wrapper/defaultqiskitprovider.py +++ b/qiskit/wrapper/defaultqiskitprovider.py @@ -7,9 +7,12 @@ """Meta-provider that aggregates several providers.""" import logging - +from collections import OrderedDict from itertools import combinations + +from qiskit import QISKitError from qiskit.backends.baseprovider import BaseProvider +from qiskit.backends.ibmq import IBMQProvider from qiskit.backends.local.localprovider import LocalProvider logger = logging.getLogger(__name__) @@ -22,12 +25,12 @@ class DefaultQISKitProvider(BaseProvider): def __init__(self): super().__init__() - # List of providers. - self.providers = [LocalProvider()] + # Dict of providers. + self.providers = OrderedDict({'local': LocalProvider()}) def get_backend(self, name): name = self.resolve_backend_name(name) - for provider in self.providers: + for provider in self.providers.values(): try: return provider.get_backend(name) except KeyError: @@ -45,7 +48,7 @@ def available_backends(self, filters=None): """ # pylint: disable=arguments-differ backends = [] - for provider in self.providers: + for provider in self.providers.values(): backends.extend(provider.available_backends(filters)) return backends @@ -60,7 +63,7 @@ def aliased_backend_names(self): ValueError: if a backend is mapped to multiple aliases """ aliases = {} - for provider in self.providers: + for provider in self.providers.values(): aliases = {**aliases, **provider.aliased_backend_names()} for pair in combinations(aliases.values(), r=2): if not set.isdisjoint(set(pair[0]), set(pair[1])): @@ -76,19 +79,95 @@ def deprecated_backend_names(self): dict[str: list[str]]: aggregated alias dictionary """ deprecates = {} - for provider in self.providers: + for provider in self.providers.values(): deprecates = {**deprecates, **provider.deprecated_backend_names()} return deprecates - def add_provider(self, provider): + def add_provider(self, provider, provider_name): """ Add a new provider to the list of known providers. + Note: + If some backend in the new provider has a name in use by an + already registered provider, the backend will not be available, + and the name of the backend will still refer to that previously + registered. + Args: provider (BaseProvider): Provider instance. + provider_name (str): User-provided name for the provider. + + Returns: + BaseProvider: the provider instance. + + Raises: + QISKitError: if a provider with the same name is already in the + list. + """ + if provider_name in self.providers.keys(): + raise QISKitError( + 'A provider with name "%s" is already registered.' + % provider_name) + + # Check for backend name clashes, emitting a warning. + current_backends = {str(backend) for backend in self.available_backends()} + added_backends = {str(backend) for backend in provider.available_backends()} + common_backends = added_backends.intersection(current_backends) + if common_backends: + logger.warning( + 'The backend names "%s" (provided by "%s") are already in use. ' + 'Consider using unregister() for avoiding name conflicts.', + list(common_backends), provider_name) + + self.providers[provider_name] = provider + + return provider + + def add_ibmq_provider(self, credentials_dict, provider_name=None): + """ + Add a new IBMQProvider to the list of known providers. + + Args: + credentials_dict (dict): dictionary of credentials for a provider. + provider_name (str): User-provided name for the provider. A name + will automatically be assigned if possible. + Raises: + QISKitError: if a provider with the same name is already in the + list; or if a provider name could not be assigned. + Returns: + IBMQProvider: the new IBMQProvider instance. + """ + # Automatically assign a name if not specified. + if not provider_name: + if 'quantumexperience' in credentials_dict['url']: + provider_name = 'ibmq' + elif 'q-console' in credentials_dict['url']: + provider_name = 'qnet' + else: + raise QISKitError( + 'Cannot parse provider name from credentials.') + + ibmq_provider = IBMQProvider(**credentials_dict) + + return self.add_provider(ibmq_provider, provider_name) + + def remove_provider(self, provider_name): + """ + Remove a provider from the list of known providers. + + Args: + provider_name (str): name of the provider to be removed. + + Raises: + QISKitError: if the provider name is not valid. """ - self.providers.append(provider) + if provider_name == 'local': + raise QISKitError("Cannot unregister 'local' provider.") + try: + self.providers.pop(provider_name) + except KeyError: + raise QISKitError("'%s' provider is not registered.") def resolve_backend_name(self, name): """Resolve backend name from a possible short alias or a deprecated name. diff --git a/test/python/common.py b/test/python/common.py index 0d68e5006de9..9a501c4555ac 100644 --- a/test/python/common.py +++ b/test/python/common.py @@ -15,6 +15,7 @@ import unittest from unittest.util import safe_repr from qiskit import __path__ as qiskit_path +from qiskit.wrapper.defaultqiskitprovider import DefaultQISKitProvider class Path(Enum): @@ -57,6 +58,12 @@ def setUpClass(cls): logging.INFO) cls.log.setLevel(level) + def tearDown(self): + # Reset the default provider, as in practice it acts as a singleton + # due to importing the wrapper from qiskit. + from qiskit.wrapper import _wrapper + _wrapper._DEFAULT_PROVIDER = DefaultQISKitProvider() + @staticmethod def _get_resource_path(filename, path=Path.TEST): """ Get the absolute path to a resource. diff --git a/test/python/test_backends.py b/test/python/test_backends.py index 8a1154f23fb5..04413d36504d 100644 --- a/test/python/test_backends.py +++ b/test/python/test_backends.py @@ -10,14 +10,12 @@ """Backends Test.""" import json -import unittest import jsonschema -import qiskit.wrapper from qiskit.backends.ibmq import IBMQProvider from qiskit.wrapper.defaultqiskitprovider import DefaultQISKitProvider -from .common import requires_qe_access, QiskitTestCase, Path +from .common import Path, QiskitTestCase, requires_qe_access def remove_backends_from_list(backends): @@ -220,32 +218,3 @@ def test_remote_backend_parameters(self, QE_TOKEN, QE_URL, 'last_update_date', 'qubits', 'backend'))) - - @requires_qe_access - def test_wrapper_register_ok(self, QE_TOKEN, QE_URL, - hub=None, group=None, project=None): - """Test wrapper.register().""" - qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, provider_name='ibmq') - backends = qiskit.wrapper.available_backends() - backends = remove_backends_from_list(backends) - self.log.info(backends) - self.assertTrue(len(backends) > 0) - - @requires_qe_access - def test_wrapper_available_backends_with_filter(self, QE_TOKEN, QE_URL, - hub=None, group=None, project=None): - """Test wrapper.available_backends(filter=...).""" - qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, provider_name='ibmq') - backends = qiskit.wrapper.available_backends({'local': False, 'simulator': True}) - self.log.info(backends) - self.assertTrue(len(backends) > 0) - - def test_wrapper_local_backends(self): - """Test wrapper.local_backends(filter=...).""" - local_backends = qiskit.wrapper.local_backends() - self.log.info(local_backends) - self.assertTrue(len(local_backends) > 0) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/test/python/test_quantumprogram.py b/test/python/test_quantumprogram.py index 7aff48948ac8..e8ca5ecba614 100644 --- a/test/python/test_quantumprogram.py +++ b/test/python/test_quantumprogram.py @@ -41,8 +41,6 @@ def setUp(self): "size": 3}] }] } - self.qp_program_finished = False - self.qp_program_exception = Exception() ############################################################### # Tests to initiate an build a quantum program @@ -1599,7 +1597,8 @@ def test_timeout(self): # TODO: use the backend directly when the deprecation is completed. from ._mockutils import DummyProvider import qiskit.wrapper - qiskit.wrapper._wrapper._DEFAULT_PROVIDER.add_provider(DummyProvider()) + qiskit.wrapper._wrapper._DEFAULT_PROVIDER.add_provider(DummyProvider(), + 'dummy') q_program = QuantumProgram(specs=self.QPS_SPECS) qr = q_program.get_quantum_register("q_name") diff --git a/test/python/test_wrapper.py b/test/python/test_wrapper.py new file mode 100644 index 000000000000..53b1258a8107 --- /dev/null +++ b/test/python/test_wrapper.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +# pylint: disable=invalid-name + +"""Tests for the wrapper functionality.""" + +import logging +import unittest + +import qiskit.wrapper +from qiskit.wrapper import registered_providers +from qiskit import QISKitError +from .common import QiskitTestCase, requires_qe_access +from .test_backends import remove_backends_from_list + + +class TestWrapper(QiskitTestCase): + """Wrapper test case.""" + @requires_qe_access + def test_wrapper_register_ok(self, QE_TOKEN, QE_URL, hub, group, project): + """Test wrapper.register().""" + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, + provider_name='ibmq') + backends = qiskit.wrapper.available_backends() + backends = remove_backends_from_list(backends) + self.log.info(backends) + self.assertTrue(len(backends) > 0) + + @requires_qe_access + def test_backends_with_filter(self, QE_TOKEN, QE_URL, hub, group, project): + """Test wrapper.available_backends(filter=...).""" + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, + provider_name='ibmq') + backends = qiskit.wrapper.available_backends({'local': False, + 'simulator': True}) + self.log.info(backends) + self.assertTrue(len(backends) > 0) + + def test_local_backends(self): + """Test wrapper.local_backends(filter=...).""" + local_backends = qiskit.wrapper.local_backends() + self.log.info(local_backends) + self.assertTrue(len(local_backends) > 0) + + @requires_qe_access + def test_register_twice(self, QE_TOKEN, QE_URL, hub, group, project): + """Test double registration of the same credentials.""" + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project) + initial_providers = registered_providers() + with self.assertRaises(QISKitError): + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project) + self.assertCountEqual(initial_providers, registered_providers()) + + @requires_qe_access + def test_register_twice_with_different_names(self, QE_TOKEN, QE_URL, + hub, group, project): + """Test double registration of same credentials but different names.""" + initial_providers = registered_providers() + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, + provider_name='provider1') + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, + provider_name='provider2') + self.assertCountEqual(initial_providers + ['provider1', 'provider2'], + registered_providers()) + + def test_register_unknown_name(self): + """Test registering a provider with not explicit name.""" + initial_providers = registered_providers() + with self.assertRaises(QISKitError): + qiskit.wrapper.register('FAKE_TOKEN', 'http://unknown') + self.assertEqual(initial_providers, registered_providers()) + + @requires_qe_access + def test_unregister(self, QE_TOKEN, QE_URL, hub, group, project): + """Test unregistering.""" + initial_providers = registered_providers() + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, + provider_name='provider1') + self.assertCountEqual(initial_providers + ['provider1'], + registered_providers()) + qiskit.wrapper.unregister('provider1') + self.assertEqual(initial_providers, registered_providers()) + + def test_unregister_non_existent(self): + """Test unregistering a non existent provider.""" + initial_providers = registered_providers() + with self.assertRaises(QISKitError): + qiskit.wrapper.unregister('provider1') + self.assertEqual(initial_providers, registered_providers()) + + @requires_qe_access + def test_register_backend_name_conflicts(self, QE_TOKEN, QE_URL, + hub, group, project): + """Test backend name conflicts when registering.""" + + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, + provider_name='provider1') + initial_providers = registered_providers() + initial_backends = qiskit.wrapper.available_backends() + ibmqx4_backend = qiskit.wrapper.get_backend('ibmqx4') + with self.assertLogs(level=logging.WARNING) as logs: + qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project, + provider_name='provider2') + + # Check that one, and only one warning has been issued. + self.assertEqual(len(logs.records), 1) + # Check that the provider has been registered. + self.assertCountEqual(initial_providers + ['provider2'], + registered_providers()) + # Check that no new backends have been added. + self.assertCountEqual(initial_backends, + qiskit.wrapper.available_backends()) + + # Check the name of the backend still refers to the previous one. + self.assertEqual(ibmqx4_backend, qiskit.wrapper.get_backend('ibmqx4')) + + +if __name__ == '__main__': + unittest.main(verbosity=2)