Skip to content

Commit 9087265

Browse files
jyu00yeralin
authored andcommitted
Autocomplete of backend names (Qiskit#303)
* backend namespace * add timeout * add tests * changelog * docstring update * move provider backend to separate file
1 parent c23f32d commit 9087265

File tree

6 files changed

+125
-10
lines changed

6 files changed

+125
-10
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ The format is based on [Keep a Changelog].
1616

1717
## [UNRELEASED]
1818

19+
### Added
20+
21+
- Added support for autocompleting backend names. A user can now use
22+
`AccountProvider.provider_backends.<tab>` to see a list of backend names (\#303).
23+
1924
### Changed
2025

2126
- The `IBMQBackend.run()` function now accepts an optional `job_name` parameter.

qiskit/providers/ibmq/accountprovider.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from .circuits import CircuitsManager
2929
from .ibmqbackend import IBMQBackend, IBMQSimulator
3030
from .credentials import Credentials
31+
from .providerbackends import ProviderBackends
3132

3233

3334
logger = logging.getLogger(__name__)
@@ -39,6 +40,11 @@ class AccountProvider(BaseProvider):
3940
def __init__(self, credentials: Credentials, access_token: str) -> None:
4041
"""Return a new AccountProvider.
4142
43+
The ``provider_backends`` attribute can be used to autocomplete
44+
backend names, by pressing ``tab`` after
45+
``AccountProvider.provider_backends.``. Note that this feature may
46+
not be available if an error occurs during backend discovery.
47+
4248
Args:
4349
credentials (Credentials): IBM Q Experience credentials.
4450
access_token (str): access token for IBM Q Experience.
@@ -58,10 +64,13 @@ def __init__(self, credentials: Credentials, access_token: str) -> None:
5864
# access.
5965
self._backends = None
6066

67+
self.provider_backends = ProviderBackends(self)
68+
6169
def backends(
6270
self,
6371
name: Optional[str] = None,
6472
filters: Optional[Callable[[List[IBMQBackend]], bool]] = None,
73+
timeout: Optional[float] = None,
6574
**kwargs: Dict
6675
) -> List[IBMQBackend]:
6776
"""Return all backends accessible via this provider, subject to optional filtering.
@@ -71,6 +80,7 @@ def backends(
7180
filters (callable): more complex filters, such as lambda functions
7281
e.g. AccountProvider.backends(
7382
filters=lambda b: b.configuration['n_qubits'] > 5)
83+
timeout (float or None): number of seconds to wait for backend discovery.
7484
kwargs: simple filters specifying a true/false criteria in the
7585
backend configuration or backend status or provider credentials
7686
e.g. AccountProvider.backends(n_qubits=5, operational=True)
@@ -80,7 +90,7 @@ def backends(
8090
"""
8191
# pylint: disable=arguments-differ
8292
if self._backends is None:
83-
self._backends = self._discover_remote_backends()
93+
self._backends = self._discover_remote_backends(timeout=timeout)
8494

8595
backends = self._backends.values()
8696

@@ -94,15 +104,18 @@ def backends(
94104

95105
return filter_backends(backends, filters=filters, **kwargs)
96106

97-
def _discover_remote_backends(self) -> Dict[str, IBMQBackend]:
107+
def _discover_remote_backends(self, timeout: Optional[float] = None) -> Dict[str, IBMQBackend]:
98108
"""Return the remote backends available.
99109
110+
Args:
111+
timeout (float or None): number of seconds to wait for the discovery.
112+
100113
Returns:
101114
dict[str:IBMQBackend]: a dict of the remote backend instances,
102115
keyed by backend name.
103116
"""
104117
ret = OrderedDict()
105-
configs_list = self._api.available_backends()
118+
configs_list = self._api.available_backends(timeout=timeout)
106119
for raw_config in configs_list:
107120
# Make sure the raw_config is of proper type
108121
if not isinstance(raw_config, dict):

qiskit/providers/ibmq/api_v2/clients/account.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,16 @@ def __init__(
5858

5959
# Backend-related public functions.
6060

61-
def list_backends(self) -> List[Dict[str, Any]]:
61+
def list_backends(self, timeout: Optional[float] = None) -> List[Dict[str, Any]]:
6262
"""Return the list of backends.
6363
64+
Args:
65+
timeout (float or None): number of seconds to wait for the request.
66+
6467
Returns:
6568
list[dict]: a list of backends.
6669
"""
67-
return self.client_api.backends()
70+
return self.client_api.backends(timeout=timeout)
6871

6972
def backend_status(self, backend_name: str) -> Dict[str, Any]:
7073
"""Return the status of a backend.
@@ -367,9 +370,9 @@ def backend_defaults(self, backend):
367370
# pylint: disable=missing-docstring
368371
return self.backend_pulse_defaults(backend)
369372

370-
def available_backends(self):
373+
def available_backends(self, timeout=None):
371374
# pylint: disable=missing-docstring
372-
return self.list_backends()
375+
return self.list_backends(timeout=timeout)
373376

374377
def get_job(self, id_job, exclude_fields=None, include_fields=None):
375378
# pylint: disable=missing-docstring

qiskit/providers/ibmq/api_v2/rest/root.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,17 @@ def job(self, job_id: str) -> Job:
5757
"""
5858
return Job(self.session, job_id)
5959

60-
def backends(self) -> List[Dict[str, Any]]:
61-
"""Return the list of backends."""
60+
def backends(self, timeout: Optional[float] = None) -> List[Dict[str, Any]]:
61+
"""Return the list of backends.
62+
63+
Args:
64+
timeout (float or None): number of seconds to wait for the request.
65+
66+
Returns:
67+
list[dict]: json response.
68+
"""
6269
url = self.get_url('backends')
63-
return self.session.get(url).json()
70+
return self.session.get(url, timeout=timeout).json()
6471

6572
def hubs(self) -> List[Dict[str, Any]]:
6673
"""Return the list of hubs available to the user."""
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This code is part of Qiskit.
4+
#
5+
# (C) Copyright IBM 2019.
6+
#
7+
# This code is licensed under the Apache License, Version 2.0. You may
8+
# obtain a copy of this license in the LICENSE.txt file in the root directory
9+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
10+
#
11+
# Any modifications or derivative works of this code must retain this
12+
# copyright notice, and modified files need to carry a notice indicating
13+
# that they have been altered from the originals.
14+
15+
"""Backend namespace for an IBM Quantum Experience account provider."""
16+
17+
import re
18+
import keyword
19+
from types import SimpleNamespace
20+
21+
from .api_v2.exceptions import RequestsApiError
22+
23+
24+
class ProviderBackends(SimpleNamespace):
25+
"""Backend namespace for an IBM Quantum Experience account provider."""
26+
27+
def __init__(self, provider):
28+
"""Creates a new ProviderBackends instance.
29+
30+
Args:
31+
provider (AccountProvider): IBM Q Experience account provider
32+
"""
33+
self._provider = provider
34+
self._initialized = False
35+
super().__init__()
36+
37+
def _discover_backends(self):
38+
"""Discovers the remote backends if not already known."""
39+
if not self._initialized:
40+
try:
41+
# Python identifiers can only contain alphanumeric characters
42+
# and underscores and cannot start with a digit.
43+
pattern = re.compile(r"\W|^(?=\d)", re.ASCII)
44+
for backend in self._provider.backends(timeout=3):
45+
backend_name = backend.name()
46+
47+
# Make it a valid identifier
48+
if not backend_name.isidentifier():
49+
backend_name = re.sub(pattern, '_', backend_name)
50+
51+
# Append _ if is keyword or duplicate
52+
while keyword.iskeyword(backend_name) or backend_name in self.__dict__:
53+
backend_name += '_'
54+
55+
setattr(self, backend_name, backend)
56+
self._initialized = True
57+
except RequestsApiError:
58+
# Ignore any networking errors since this is a convenience
59+
# feature meant for interactive sessions.
60+
pass
61+
62+
def __dir__(self):
63+
self._discover_backends()
64+
return super().__dir__()
65+
66+
def __getattr__(self, item):
67+
self._discover_backends()
68+
return super().__getattribute__(item)

test/ibmq/test_ibmq_provider.py

+19
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
"""Tests for all IBMQ backends."""
1717
from datetime import datetime
18+
from unittest.mock import patch
19+
from requests.exceptions import Timeout
20+
1821
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
1922
from qiskit.providers.ibmq import IBMQProvider
2023
from qiskit.providers.exceptions import QiskitBackendNotFoundError
@@ -25,6 +28,7 @@
2528
from qiskit.test import slow_test, providers
2629
from qiskit.compiler import assemble, transpile
2730
from qiskit.providers.models.backendproperties import BackendProperties
31+
from qiskit.providers.ibmq.api_v2.session import Session
2832

2933
from ..decorators import (requires_qe_access,
3034
requires_classic_api,
@@ -190,3 +194,18 @@ def test_remote_backend_properties_filter_date(self):
190194
self.assertLessEqual(last_update_date, datetime_filter)
191195
else:
192196
self.assertEqual(properties, None)
197+
198+
def test_provider_backends(self):
199+
"""Test provider_backends have correct attributes."""
200+
provider_backends = {back for back in dir(self.provider.provider_backends)
201+
if not back.startswith('_')}
202+
backends = {back.name() for back in self.provider.backends()}
203+
self.assertEqual(provider_backends, backends)
204+
205+
def test_provider_backends_timeout(self):
206+
"""Test provider_backends timeout."""
207+
with patch.object(Session, "request", side_effect=Timeout):
208+
provider_backends = {
209+
back for back in dir(self.provider.provider_backends)
210+
if not back.startswith('_')}
211+
self.assertEqual(len(provider_backends), 0)

0 commit comments

Comments
 (0)