diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eaf4f713f07a..5ae4e2aa5805 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,8 @@ The format is based on `Keep a Changelog`_. Added ----- +- Introduced new options for handling credentials (qiskitrc file, environment + variables) and automatic registration. (#547) Changed ------- diff --git a/Qconfig.py.default b/Qconfig.py.default deleted file mode 100644 index 8373c8300bad..000000000000 --- a/Qconfig.py.default +++ /dev/null @@ -1,23 +0,0 @@ -# Before you can use the jobs API, you need to set up an access token. -# Log in to the IBM Q experience. Under "Account", generate a personal -# access token. Replace 'PUT_YOUR_API_TOKEN_HERE' below with the quoted -# token string. Uncomment the APItoken variable, and you will be ready to go. - -#APItoken = 'PUT_YOUR_API_TOKEN_HERE' - -config = { - 'url': 'https://quantumexperience.ng.bluemix.net/api', - - # If you have access to IBM Q features, you also need to fill the "hub", - # "group", and "project" details. Replace "None" on the lines below - # with your details from Quantum Experience, quoting the strings, for - # example: 'hub': 'my_hub' - # You will also need to update the 'url' above, pointing it to your custom - # URL for IBM Q. - 'hub': None, - 'group': None, - 'project': None -} - -if 'APItoken' not in locals(): - raise Exception('Please set up your access token. See Qconfig.py.') diff --git a/README.md b/README.md index c6361da3183f..992bf10ca1aa 100644 --- a/README.md +++ b/README.md @@ -126,43 +126,38 @@ your IBM Q Experience account: #### Configure your API token and QX credentials - 1. Create an _[IBM Q Experience](https://quantumexperience.ng.bluemix.net) > Account_ if you haven't already done so. + 2. Get an API token from the IBM Q Experience website under _My Account > Advanced > API Token_. This API token allows you to execute your programs with the IBM Q Experience backends. See: [Example](doc/example_real_backend.rst). -3. We are going to create a new file called `Qconfig.py` and insert the API token into it. This file must have these contents: - ```python - APItoken = 'MY_API_TOKEN' +3. We are now going to add the necessary credentials to QISKit. Take your token + from step 2, here called `MY_API_TOKEN`, and pass it to the + `store_credentials` function: - config = { - 'url': 'https://quantumexperience.ng.bluemix.net/api', - # The following should only be needed for IBM Q Network users. - 'hub': 'MY_HUB', - 'group': 'MY_GROUP', - 'project': 'MY_PROJECT' - } - ``` + ```python + from qiskit import store_credentials -4. Substitute `MY_API_TOKEN` with your real API Token extracted in step 2. + store_credentials('MY_API_TOKEN') + ``` -5. If you have access to the IBM Q Network features, you also need to setup the - values for your hub, group, and project. You can do so by filling the - `config` variable with the values you can find on your IBM Q account - page. +4. If you have access to the IBM Q Network features, you also need to pass the + values for your url, hub, group, and project found on your IBM Q account + page to `store_credentials`. -Once the `Qconfig.py` file is set up, you have to move it under the same directory/folder where your program/tutorial resides, so it can be imported and be used to authenticate with the `register()` function. For example: +After calling `store_credentials()`, your credentials will be stored into disk. +Once they are stored, Qiskit will automatically load and use them in your program +via: ```python from qiskit import register -import Qconfig -register(Qconfig.APItoken, Qconfig.config["url"], - hub=Qconfig.config["hub"], - group=Qconfig.config["group"], - project=Qconfig.config["project"]) +register() ``` -For more details on this and more information see +For more details on installing Qiskit and for alternative methods for passing +the IBM QX credentials, such as using environment variables, sending them +explicitly and support for the `Qconfig.py` method available in previous +versions, please check [our Qiskit documentation](https://www.qiskit.org/documentation/). diff --git a/doc/install.rst b/doc/install.rst index 0f667a7a0217..c5969ba79156 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -41,19 +41,113 @@ This will install the latest stable release along with all the dependencies. you haven't already done so - Get an API token from the IBM Q experience website under “My Account” > “Personal Access Token” -- The API token needs to be placed in a file called ``Qconfig.py``. For - convenience, we provide a default version of this file that you - can use as a reference: `Qconfig.py.default`_. After downloading that - file, copy it into the folder where you will be invoking the SDK (on - Windows, replace ``cp`` with ``copy``): -.. code:: sh - cp Qconfig.py.default Qconfig.py +3.1 Automatically loading credentials +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since Qiskit 0.6, an automatic method that looks for the credentials in several +places can be used for streamlining the setting up of the IBM Q authentication. +This implies that you can set or store your API credentials once after +installation, and when you want to use them, you can simply run: + +.. code:: python + + from qiskit import register + + register() + +This ``register()`` call (without parameters) performs the automatic loading +of the credentials from several sources, and authenticates against IBM Q, +making the online devices available to your program. Please use one of the +following methods for storing the credentials before calling the automatic +registration: + +3.1.1 Store API credentials locally +""""""""""""""""""""""""""""""""""" + +For most users, storing your API credentials is the most convenient approach. +Your information is stored locally in a configuration file called `qiskitrc`, +and once stored, you can use the credentials without explicitly passing them +to your program. + +To store your information, simply run: + +.. code:: python + + from qiskit import store_credentials + + store_credentials('MY_API_TOKEN') + + +where `MY_API_TOKEN` should be replaced with your token. + +If you are on the IBM Q network, you must also pass `url`, +`hub`, `group`, and `project` arguments to `store_credentials`: + + +.. code:: python + + from qiskit import store_credentials + + store_credentials('MY_API_TOKEN', url='http://...', hub='HUB', + group='GROUP', project='PROJECT') + +3.1.2 Load API credentials from environment variables +""""""""""""""""""""""""""""""""""""""""""""""""""""" -- Open your ``Qconfig.py``, remove the ``#`` from the beginning of the API - token line, and copy/paste your API token into the space between the - quotation marks on that line. Save and close the file. +For more advanced users, it is possible to load API credentials from +environment variables. Specifically, you can set the following environment +variables: + +* `QE_TOKEN`, +* `QE_URL` +* `QE_HUB` +* `QE_GROUP` +* `QE_PROJECT`. + +Note that if they are present in your environment, they will take precedence +over the credentials stored in disk. + +3.1.3 Load API credentials from Qconfig.py +"""""""""""""""""""""""""""""""""""""""""" + +For compatibility with configurations set for Qiskit versions earlier than 0.6, +the credentials can also be stored in a file called ``Qconfig.py`` placed in +the directory where your program is invoked from. For convenience, we provide +a default version of this file you can use as a reference - using your favorite +editor, create a ``Qconfig.py`` file in the folder of your program with the +following contents: + +.. code:: python + + APItoken = 'PUT_YOUR_API_TOKEN_HERE' + + config = { + 'url': 'https://quantumexperience.ng.bluemix.net/api', + + # If you have access to IBM Q features, you also need to fill the "hub", + # "group", and "project" details. Replace "None" on the lines below + # with your details from Quantum Experience, quoting the strings, for + # example: 'hub': 'my_hub' + # You will also need to update the 'url' above, pointing it to your custom + # URL for IBM Q. + 'hub': None, + 'group': None, + 'project': None + } + + if 'APItoken' not in locals(): + raise Exception('Please set up your access token. See Qconfig.py.') + +And customize the following lines: + +* copy/paste your API token into the space between the quotation marks on the + first line (``APItoken = 'PUT_YOUR_API_TOKEN_HERE'``). +* if you have access to the IBM Q features, you also need to setup the + values for your url, hub, group, and project. You can do so by filling the + ``config`` variable with the values you can find on your IBM Q account + page. For example, a valid and fully configured ``Qconfig.py`` file would look like: @@ -65,13 +159,8 @@ For example, a valid and fully configured ``Qconfig.py`` file would look like: 'url': 'https://quantumexperience.ng.bluemix.net/api' } -- If you have access to the IBM Q features, you also need to setup the - values for your hub, group, and project. You can do so by filling the - ``config`` variable with the values you can find on your IBM Q account - page. - -For example, a valid and fully configured ``Qconfig.py`` file for IBM Q -users would look like: +For IBM Q users, a valid and fully configured ``Qconfig.py`` file would look +like: .. code:: python @@ -85,6 +174,26 @@ users would look like: 'project': 'MY_PROJECT' } +Note that if a ``Qconfig.py`` file is present in your directory, it will take +precedence over the environment variables or the credentials stored in disk. + +3.2 Manually loading credentials +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In more complex scenarios or for users that need finer control over multiple +accounts, please note that you can pass the API token and the other parameters +directly to the ``register()`` function, which will ignore the automatic +loading of the credentials and use the arguments directly. For example:: + +.. code:: python + + from qiskit import register + + register('MY_API_TOKEN', url='https://my.url') + +will try to authenticate using ``MY_API_TOKEN`` and the specified URL, +regardless of the configuration stored in the config file, the environment +variables, or the ``Qconfig.py`` file, if any. Install Jupyter-based tutorials =============================== diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 3186649a6255..438f13352739 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -10,6 +10,8 @@ """Main QISKit public functionality.""" +import os + # First, check for required Python and API version from . import _util @@ -35,13 +37,12 @@ from .wrapper._wrapper import ( available_backends, local_backends, remote_backends, get_backend, compile, execute, register, unregister, - registered_providers, load_qasm_string, load_qasm_file, least_busy) + registered_providers, load_qasm_string, load_qasm_file, least_busy, + store_credentials) # Import the wrapper, to make it available when doing "import qiskit". from . import wrapper -import os - ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(ROOT_DIR, "VERSION.txt"), "r") as version_file: __version__ = version_file.read().strip() diff --git a/qiskit/wrapper/__init__.py b/qiskit/wrapper/__init__.py index e647ab157a20..8a6926a49840 100644 --- a/qiskit/wrapper/__init__.py +++ b/qiskit/wrapper/__init__.py @@ -18,4 +18,4 @@ from ._wrapper import (available_backends, local_backends, remote_backends, get_backend, compile, execute, register, unregister, registered_providers, load_qasm_string, load_qasm_file, - least_busy) + least_busy, store_credentials) diff --git a/qiskit/wrapper/_wrapper.py b/qiskit/wrapper/_wrapper.py index df08cf8ae217..78fdcb21268a 100644 --- a/qiskit/wrapper/_wrapper.py +++ b/qiskit/wrapper/_wrapper.py @@ -7,9 +7,11 @@ """Helper module for simplified QISKit usage.""" +import logging import warnings from qiskit import transpiler, QISKitError from qiskit.backends.ibmq import IBMQProvider +from qiskit.wrapper import credentials from qiskit.wrapper.defaultqiskitprovider import DefaultQISKitProvider from ._circuittoolkit import circuit_from_qasm_file, circuit_from_qasm_string @@ -19,11 +21,22 @@ _DEFAULT_PROVIDER = DefaultQISKitProvider() +logger = logging.getLogger(__name__) + + def register(*args, provider_class=IBMQProvider, **kwargs): """ Authenticate against an online backend provider. This is a factory method that returns the provider that gets registered. + Note that if no parameters are passed, this method will try to + automatically discover the credentials for IBMQ in the following places, + in order:: + + 1. in the `Qconfig.py` file in the current working directory. + 2. in the environment variables. + 3. in the `qiskitrc` configuration file. + Args: args (tuple): positional arguments passed to provider class initialization provider_class (BaseProvider): provider class @@ -45,9 +58,18 @@ def register(*args, provider_class=IBMQProvider, **kwargs): BaseProvider: the provider instance that was just registered. Raises: - QISKitError: if the provider could not be registered - (e.g. due to conflict) + QISKitError: if the provider could not be registered (e.g. due to + conflict, or if no credentials were provided.) """ + # Try to autodiscover credentials if not passed. + if not args and not kwargs and provider_class == IBMQProvider: + kwargs = credentials.discover_credentials().get( + credentials.get_account_name(IBMQProvider)) or {} + if not kwargs: + raise QISKitError( + 'No IBMQ credentials found. Please pass them explicitly or ' + 'store them before calling register() with store_credentials()') + try: provider = provider_class(*args, **kwargs) except Exception as ex: @@ -79,8 +101,36 @@ def registered_providers(): return list(_DEFAULT_PROVIDER.providers) -# Functions for inspecting and retrieving backends. +def store_credentials(token, url='https://quantumexperience.ng.bluemix.net/api', + hub=None, group=None, project=None, proxies=None, + verify=True, overwrite=False): + """ + Store credentials for the IBMQ account in the config file. + Args: + token (str): The token used to register on the online backend such + as the quantum experience. + url (str): The url used for online backend such as the quantum + experience. + hub (str): The hub used for online backend. + group (str): The group used for online backend. + project (str): The project used for online backend. + proxies (dict): Proxy configuration for the API, as a dict with + 'urls' and credential keys. + verify (bool): If False, ignores SSL certificates errors. + overwrite (bool): overwrite existing credentials. + + Raises: + QISKitError: if the credentials already exist and overwrite==False. + """ + credentials.store_credentials( + provider_class=IBMQProvider, overwrite=overwrite, + token=token, url=url, hub=hub, group=group, project=project, + proxies=proxies, verify=verify + ) + + +# Functions for inspecting and retrieving backends. def available_backends(filters=None, compact=True): """ diff --git a/qiskit/wrapper/credentials/__init__.py b/qiskit/wrapper/credentials/__init__.py new file mode 100644 index 000000000000..3028bcc673e4 --- /dev/null +++ b/qiskit/wrapper/credentials/__init__.py @@ -0,0 +1,71 @@ +# -*- 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. + +""" +Utilities for working with credentials for the wrapper. +""" +import logging + +from qiskit import QISKitError +from ._configrc import read_credentials_from_qiskitrc, store_credentials +from ._environ import read_credentials_from_environ +from ._qconfig import read_credentials_from_qconfig +from ._utils import get_account_name + + +logger = logging.getLogger(__name__) + + +def discover_credentials(): + """ + Automatically discover credentials for online providers. + + This method looks for credentials in the following locations, in order, + and returning as soon as credentials are found:: + + 1. in the `Qconfig.py` file in the current working directory. + 2. in the environment variables. + 3. in the `qiskitrc` configuration file. + + Returns: + dict: dictionary with the contents of the configuration file, with + the form:: + + {'provider_name': {'token': 'TOKEN', 'url': 'URL', ... }} + """ + # 1. Attempt to read them from the `Qconfig.py` file. + try: + qconfig_credentials = read_credentials_from_qconfig() + if qconfig_credentials: + logger.info('Using credentials from qconfig') + return qconfig_credentials + except QISKitError as ex: + logger.warning( + 'Automatic discovery of qconfig credentials failed: %s', str(ex)) + + # 2. Attempt to read them from the environment variables. + try: + environ_credentials = read_credentials_from_environ() + if environ_credentials: + logger.info('Using credentials from environment variables') + return environ_credentials + except QISKitError as ex: + logger.warning( + 'Automatic discovery of environment credentials failed: %s', + str(ex)) + + # 3. Attempt to read them from the qiskitrc file. + try: + provider_credentials = read_credentials_from_qiskitrc() + if provider_credentials: + logger.info('Using credentials from qiskitrc') + return provider_credentials + except QISKitError as ex: + logger.warning( + 'Automatic discovery of qiskitrc credentials failed: %s', str(ex)) + + return {} diff --git a/qiskit/wrapper/credentials/_configrc.py b/qiskit/wrapper/credentials/_configrc.py new file mode 100644 index 000000000000..4427124f2829 --- /dev/null +++ b/qiskit/wrapper/credentials/_configrc.py @@ -0,0 +1,142 @@ +# -*- 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. + +""" +Utilities for reading and writing credentials from and to configuration files. +""" + +import os +from ast import literal_eval +from configparser import ConfigParser, ParsingError + +from qiskit import QISKitError +from qiskit.backends.ibmq import IBMQProvider +from ._utils import get_account_name + + +DEFAULT_QISKITRC_FILE = os.path.join(os.path.expanduser("~"), + '.qiskit', 'qiskitrc') + + +def read_credentials_from_qiskitrc(filename=None): + """ + Read a configuration file and return a dict with its sections. + + Args: + filename (str): full path to the qiskitrc file. If `None`, the default + location is used (`HOME/.qiskit/qiskitrc`). + + Returns: + dict: dictionary with the contents of the configuration file, with + the form:: + + {'provider_class_name': {'token': 'TOKEN', 'url': 'URL', ... }} + + Raises: + QISKitError: if the file was not parseable. Please note that this + exception is not raised if the file does not exist (instead, an + empty dict is returned). + """ + filename = filename or DEFAULT_QISKITRC_FILE + config_parser = ConfigParser() + try: + config_parser.read(filename) + except ParsingError as ex: + raise QISKitError(str(ex)) + + # Build the credentials dictionary. + credentials_dict = {} + for name in config_parser.sections(): + single_credentials = dict(config_parser.items(name)) + # Individually convert keys to their right types. + # TODO: consider generalizing, moving to json configuration or a more + # robust alternative. + if 'proxies' in single_credentials.keys(): + single_credentials['proxies'] = literal_eval( + single_credentials['proxies']) + if 'verify' in single_credentials.keys(): + single_credentials['verify'] = bool(single_credentials['verify']) + credentials_dict[name] = single_credentials + + return credentials_dict + + +def write_qiskit_rc(credentials, filename=None): + """ + Write credentials to the configuration file. + + Args: + credentials (dict): dictionary with the credentials, with the form:: + {'provider_class_name': {'token': 'TOKEN', 'url': 'URL', ... }} + filename (str): full path to the qiskitrc file. If `None`, the default + location is used (`HOME/.qiskit/qiskitrc`). + """ + filename = filename or DEFAULT_QISKITRC_FILE + # Create the directories and the file if not found. + os.makedirs(os.path.dirname(filename), exist_ok=True) + + # Write the configuration file. + with open(filename, 'w') as config_file: + config_parser = ConfigParser() + config_parser.read_dict(credentials) + config_parser.write(config_file) + + +def store_credentials(provider_class=IBMQProvider, overwrite=False, + filename=None, **kwargs): + """ + Store the credentials for a single provider in the configuration file. + + Args: + provider_class (class): class of the Provider for the credentials. + overwrite (bool): overwrite existing credentials. + filename (str): full path to the qiskitrc file. If `None`, the default + location is used (`HOME/.qiskit/qiskitrc`). + kwargs (dict): keyword arguments passed to provider class + initialization. + + Raises: + QISKitError: If provider already exists and overwrite=False; or if + the account_name could not be assigned. + """ + # Set the name of the Provider from the class. + account_name = get_account_name(provider_class) + # Read the current providers stored in the configuration file. + filename = filename or DEFAULT_QISKITRC_FILE + credentials = read_credentials_from_qiskitrc(filename) + if account_name in credentials.keys() and not overwrite: + raise QISKitError('%s is already present and overwrite=False' + % account_name) + + # Append the provider, trim the empty options and store it in the file. + kwargs = {key: value for key, value in kwargs.items() if value is not None} + credentials[account_name] = {**kwargs} + write_qiskit_rc(credentials, filename) + + +def remove_credentials(provider_class=IBMQProvider, filename=None): + """Remove provider credentials from qiskitrc. + + Args: + provider_class (class): class of the Provider for the credentials. + filename (str): full path to the qiskitrc file. If `None`, the default + location is used (`HOME/.qiskit/qiskitrc`). + + Raises: + QISKitError: If there is no account with that name on the configuration + file. + """ + # Set the name of the Provider from the class. + account_name = get_account_name(provider_class) + credentials = read_credentials_from_qiskitrc(filename) + + try: + credentials.pop(account_name) + except KeyError: + raise QISKitError('The account "%s" does not exist in the ' + 'configuration file') + write_qiskit_rc(credentials, filename) diff --git a/qiskit/wrapper/credentials/_environ.py b/qiskit/wrapper/credentials/_environ.py new file mode 100644 index 000000000000..9f22e32ac093 --- /dev/null +++ b/qiskit/wrapper/credentials/_environ.py @@ -0,0 +1,47 @@ +# -*- 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. + +""" +Utilities for reading credentials from environment variables. +""" + +import os + +from qiskit.backends.ibmq import IBMQProvider +from ._utils import get_account_name + +# Dictionary that maps `ENV_VARIABLE_NAME` to credential parameter. +VARIABLES_MAP = { + 'QE_TOKEN': 'token', + 'QE_URL': 'url', + 'QE_HUB': 'hub', + 'QE_GROUP': 'group', + 'QE_PROJECT': 'project' +} + + +def read_credentials_from_environ(): + """ + Read the environment variables and return its credentials. + + Returns: + dict: dictionary with the credentials, in the form:: + + {'IBMQProvider': {'token': 'TOKEN', 'url': 'URL', ... }} + + """ + # The token is the only required parameter. + if not os.getenv('QE_TOKEN'): + return {} + + # Build the credentials based on environment variables. + credentials = {} + for envar_name, credential_key in VARIABLES_MAP.items(): + if os.getenv(envar_name): + credentials[credential_key] = os.getenv(envar_name) + + return {get_account_name(IBMQProvider): credentials} diff --git a/qiskit/wrapper/credentials/_qconfig.py b/qiskit/wrapper/credentials/_qconfig.py new file mode 100644 index 000000000000..0951529d0a59 --- /dev/null +++ b/qiskit/wrapper/credentials/_qconfig.py @@ -0,0 +1,63 @@ +# -*- 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. + +""" +Utilities for reading credentials from the deprecated `Qconfig.py` file. +""" + +import os +from importlib.util import module_from_spec, spec_from_file_location + +from qiskit import QISKitError +from qiskit.backends.ibmq import IBMQProvider +from ._utils import get_account_name + + +DEFAULT_QCONFIG_FILE = 'Qconfig.py' + + +def read_credentials_from_qconfig(): + """ + Read a `QConfig.py` file and return its credentials. + + Returns: + dict: dictionary with the credentials, in the form:: + + {'token': 'TOKEN', 'url': 'URL', ... } + + Raises: + QISKitError: if the Qconfig.py was not parseable. Please note that this + exception is not raised if the file does not exist (instead, an + empty dict is returned). + """ + if not os.path.isfile(DEFAULT_QCONFIG_FILE): + return {} + else: + # Note this is nested inside the else to prevent some tools marking + # the whole method as deprecated. + pass + # TODO: reintroduce when we decide on deprecatin + # warnings.warn( + # "Using 'Qconfig.py' for storing the credentials will be deprecated in" + # "upcoming versions (>0.6.0). Using .qiskitrc is recommended", + # DeprecationWarning) + + try: + spec = spec_from_file_location('Qconfig', DEFAULT_QCONFIG_FILE) + q_config = module_from_spec(spec) + spec.loader.exec_module(q_config) + + if hasattr(q_config, 'config'): + credentials = q_config.config.copy() + else: + credentials = {} + credentials['token'] = q_config.APItoken + except Exception as ex: + # pylint: disable=broad-except + raise QISKitError('Error loading Qconfig.py: %s' % str(ex)) + + return {get_account_name(IBMQProvider): credentials} diff --git a/qiskit/wrapper/credentials/_utils.py b/qiskit/wrapper/credentials/_utils.py new file mode 100644 index 000000000000..17ad2d84b461 --- /dev/null +++ b/qiskit/wrapper/credentials/_utils.py @@ -0,0 +1,25 @@ +# -*- 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. + +""" +Utilities for credentials. +""" + + +def get_account_name(provider_class): + """ + Return the account name for a particular provider. This name is used by + Qiskit internally and in the configuration file and uniquely identifies + a provider. + + Args: + provider_class (class): class for the account. + + Returns: + str: the account name. + """ + return '{}.{}'.format(provider_class.__module__, provider_class.__name__) diff --git a/test/python/common.py b/test/python/common.py index 9a501c4555ac..cd6b8d746204 100644 --- a/test/python/common.py +++ b/test/python/common.py @@ -15,6 +15,8 @@ import unittest from unittest.util import safe_repr from qiskit import __path__ as qiskit_path +from qiskit.backends.ibmq import IBMQProvider +from qiskit.wrapper.credentials import discover_credentials, get_account_name from qiskit.wrapper.defaultqiskitprovider import DefaultQISKitProvider @@ -227,7 +229,7 @@ def requires_qe_access(func): * determines if the test should be skipped by checking environment variables. * if the test is not skipped, it reads `QE_TOKEN` and `QE_URL` from - `Qconfig.py` or from environment variables. + `Qconfig.py`, environment variables or qiskitrc. * if the test is not skipped, it appends `QE_TOKEN` and `QE_URL` as arguments to the test function. Args: @@ -243,31 +245,25 @@ def _(*args, **kwargs): if SKIP_ONLINE_TESTS: raise unittest.SkipTest('Skipping online tests') - # Try to read the variables from Qconfig. - try: - import Qconfig - QE_TOKEN = Qconfig.APItoken - QE_URL = Qconfig.config['url'] - QE_HUB = Qconfig.config.get('hub') - QE_GROUP = Qconfig.config.get('group') - QE_PROJECT = Qconfig.config.get('project') - except ImportError: - # Try to read them from environment variables (ie. Travis). - QE_TOKEN = os.getenv('QE_TOKEN') - QE_URL = os.getenv('QE_URL') - QE_HUB = os.getenv('QE_HUB') - QE_GROUP = os.getenv('QE_GROUP') - QE_PROJECT = os.getenv('QE_PROJECT') - if not QE_TOKEN or not QE_URL: - raise Exception( - 'Could not locate a valid "Qconfig.py" file nor read the QE ' - 'values from the environment') - - kwargs['QE_TOKEN'] = QE_TOKEN - kwargs['QE_URL'] = QE_URL - kwargs['hub'] = QE_HUB - kwargs['group'] = QE_GROUP - kwargs['project'] = QE_PROJECT + # Cleanup the credentials, as this file is shared by the tests. + from qiskit.wrapper import _wrapper + _wrapper._DEFAULT_PROVIDER = DefaultQISKitProvider() + + # Attempt to read the standard credentials. + account_name = get_account_name(IBMQProvider) + discovered_credentials = discover_credentials() + if account_name in discovered_credentials.keys(): + credentials = discovered_credentials[account_name] + kwargs.update({ + 'QE_TOKEN': credentials.get('token'), + 'QE_URL': credentials.get('url'), + 'hub': credentials.get('hub'), + 'group': credentials.get('group'), + 'project': credentials.get('project'), + }) + else: + raise Exception('Could not locate valid credentials') + return func(*args, **kwargs) return _ diff --git a/test/python/test_registration.py b/test/python/test_registration.py new file mode 100644 index 000000000000..c121111677f9 --- /dev/null +++ b/test/python/test_registration.py @@ -0,0 +1,184 @@ +# -*- 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 + +""" +Test the registration and credentials features of the wrapper. +""" +import os +from contextlib import contextmanager +from tempfile import NamedTemporaryFile +from unittest.mock import patch +from unittest import skipIf + +import qiskit +from qiskit import QISKitError +from qiskit.backends.ibmq import IBMQProvider +from qiskit.wrapper.credentials import (_configrc, _qconfig, + discover_credentials, get_account_name) +from qiskit.wrapper.credentials._environ import VARIABLES_MAP +from .common import QiskitTestCase + + +# TODO: NamedTemporaryFiles do not support name in Windows +@skipIf(os.name == 'nt', 'Test not supported in Windows') +class TestWrapperCredentials(QiskitTestCase): + """Wrapper autoregistration and credentials test case.""" + def setUp(self): + super(TestWrapperCredentials, self).setUp() + self.ibmq_account_name = get_account_name(IBMQProvider) + + def test_autoregister_no_credentials(self): + """Test register() with no credentials available.""" + with no_file('Qconfig.py'), no_file(_configrc.DEFAULT_QISKITRC_FILE), no_envs(): + with self.assertRaises(QISKitError) as cm: + qiskit.wrapper.register() + + self.assertIn('No IBMQ credentials found', str(cm.exception)) + + def test_store_credentials(self): + """Test storing credentials and using them for autoregister.""" + with no_file('Qconfig.py'), no_envs(), custom_qiskitrc(), mock_ibmq_provider(): + qiskit.wrapper.store_credentials('QISKITRC_TOKEN', proxies={'http': 'foo'}) + provider = qiskit.register() + + self.assertEqual(provider._token, 'QISKITRC_TOKEN') + self.assertEqual(provider._proxies, {'http': 'foo'}) + + def test_store_credentials_overwrite(self): + """Test overwritind qiskitrc credentials.""" + with custom_qiskitrc(): + qiskit.wrapper.store_credentials('QISKITRC_TOKEN', hub='HUB') + # Attempt overwriting. + with self.assertRaises(QISKitError) as cm: + qiskit.wrapper.store_credentials('QISKITRC_TOKEN') + self.assertIn('already present', str(cm.exception)) + + with no_file('Qconfig.py'), no_envs(), mock_ibmq_provider(): + # Attempt overwriting. + qiskit.wrapper.store_credentials('QISKITRC_TOKEN_2', + overwrite=True) + provider = qiskit.wrapper.register() + + # Ensure that the credentials are the overwritten ones - note that the + # 'hub' parameter was removed. + self.assertEqual(provider._token, 'QISKITRC_TOKEN_2') + self.assertEqual(provider._hub, None) + + def test_environ_over_qiskitrc(self): + """Test order, without qconfig""" + with custom_qiskitrc(): + # Prepare the credentials: both env and qiskitrc present + qiskit.wrapper.store_credentials('QISKITRC_TOKEN') + with no_file('Qconfig.py'), custom_envs({'QE_TOKEN': 'ENVIRON_TOKEN'}): + credentials = discover_credentials() + + self.assertIn(self.ibmq_account_name, credentials) + self.assertEqual(credentials[self.ibmq_account_name]['token'], 'ENVIRON_TOKEN') + + def test_qconfig_over_all(self): + """Test order, with qconfig""" + with custom_qiskitrc(): + # Prepare the credentials: qconfig, env and qiskitrc present + qiskit.wrapper.store_credentials('QISKITRC_TOKEN') + with custom_qconfig(b"APItoken='QCONFIG_TOKEN'"),\ + custom_envs({'QE_TOKEN': 'ENVIRON_TOKEN'}): + credentials = discover_credentials() + + self.assertIn(self.ibmq_account_name, credentials) + self.assertEqual(credentials[self.ibmq_account_name]['token'], 'QCONFIG_TOKEN') + + +# Context managers + +@contextmanager +def no_file(filename): + """Context manager that disallows access to a file.""" + def side_effect(filename_): + """Return False for the specified file.""" + if filename_ == filename: + return False + return isfile_original(filename_) + + # Store the original `os.path.isfile` function, for mocking. + isfile_original = os.path.isfile + patcher = patch('os.path.isfile', side_effect=side_effect) + patcher.start() + yield + patcher.stop() + + +@contextmanager +def no_envs(): + """Context manager that disables qiskit environment variables.""" + # Remove the original variables from `os.environ`. + modified_environ = {key: value for key, value in os.environ.items() + if key not in VARIABLES_MAP.keys()} + patcher = patch.dict(os.environ, modified_environ) + patcher.start() + yield + patcher.stop() + + +@contextmanager +def custom_qiskitrc(contents=b''): + """Context manager that uses a temporary qiskitrc.""" + # Create a temporary file with the contents. + tmp_file = NamedTemporaryFile() + tmp_file.write(contents) + tmp_file.flush() + + # Temporarily modify the default location of the qiskitrc file. + DEFAULT_QISKITRC_FILE_original = _configrc.DEFAULT_QISKITRC_FILE + _configrc.DEFAULT_QISKITRC_FILE = tmp_file.name + yield + + # Delete the temporary file and restore the default location. + tmp_file.close() + _configrc.DEFAULT_QISKITRC_FILE = DEFAULT_QISKITRC_FILE_original + + +@contextmanager +def custom_qconfig(contents=b''): + """Context manager that uses a temporary qconfig.py.""" + # Create a temporary file with the contents. + tmp_file = NamedTemporaryFile(suffix='.py') + tmp_file.write(contents) + tmp_file.flush() + + # Temporarily modify the default location of the qiskitrc file. + DEFAULT_QCONFIG_FILE_original = _qconfig.DEFAULT_QCONFIG_FILE + _qconfig.DEFAULT_QCONFIG_FILE = tmp_file.name + yield + + # Delete the temporary file and restore the default location. + tmp_file.close() + _qconfig.DEFAULT_QCONFIG_FILE = DEFAULT_QCONFIG_FILE_original + + +@contextmanager +def custom_envs(new_environ): + """Context manager that disables qiskit environment variables.""" + # Remove the original variables from `os.environ`. + modified_environ = {**os.environ, **new_environ} + patcher = patch.dict(os.environ, modified_environ) + patcher.start() + yield + patcher.stop() + + +@contextmanager +def mock_ibmq_provider(): + """Mock the initialization of IBMQProvider, so it does not query the api.""" + patcher = patch.object(IBMQProvider, '_authenticate', return_value=None) + patcher2 = patch.object(IBMQProvider, '_discover_remote_backends', return_value={}) + patcher.start() + patcher2.start() + yield + patcher2.stop() + patcher.stop()