diff --git a/qiskit_ionq/exceptions.py b/qiskit_ionq/exceptions.py index 7fc4c45..eb8cf51 100644 --- a/qiskit_ionq/exceptions.py +++ b/qiskit_ionq/exceptions.py @@ -151,7 +151,7 @@ def from_response(cls, response: requests.Response) -> IonQAPIError: error_type = error_data.get("type") or error_type return cls(message, status_code, headers, body, error_type) - def __init__(self, message, status_code, headers, body, error_type): + def __init__(self, message, status_code, headers, body, error_type): # pylint: disable=too-many-positional-arguments super().__init__(message) self.status_code = status_code self.headers = headers diff --git a/qiskit_ionq/helpers.py b/qiskit_ionq/helpers.py index 4db5120..24b3f41 100644 --- a/qiskit_ionq/helpers.py +++ b/qiskit_ionq/helpers.py @@ -311,7 +311,9 @@ def get_register_sizes_and_labels( return sizes, labels -def compress_to_metadata_string(metadata: dict | list) -> str: # pylint: disable=invalid-name +def compress_to_metadata_string( + metadata: dict | list, +) -> str: # pylint: disable=invalid-name """ Convert a metadata object to a compact string format (dumped, gzipped, base64 encoded) for storing in IonQ API metadata @@ -330,7 +332,9 @@ def compress_to_metadata_string(metadata: dict | list) -> str: # pylint: disabl return encoded.decode() -def decompress_metadata_string(input_string: str) -> dict | list: # pylint: disable=invalid-name +def decompress_metadata_string( + input_string: str, +) -> dict | list: # pylint: disable=invalid-name """ Convert compact string format (dumped, gzipped, base64 encoded) from IonQ API metadata back into a dict or list of dicts relevant to building @@ -541,8 +545,13 @@ def get_n_qubits(backend: str, _fallback=100) -> int: token = creds.get("token") # could use provider.get_calibration_data().get("qubits", 36) try: + target = ( + backend.split("ionq_qpu.")[1] + if backend.startswith("ionq_qpu.") + else backend + ) return requests.get( - url=f"{url}/characterizations/backends/{backend}/current", + url=f"{url}/characterizations/backends/{target}/current", headers={"Authorization": f"apiKey {token}"}, timeout=5, ).json()["qubits"] diff --git a/qiskit_ionq/ionq_client.py b/qiskit_ionq/ionq_client.py index d73a769..d8d8964 100644 --- a/qiskit_ionq/ionq_client.py +++ b/qiskit_ionq/ionq_client.py @@ -55,9 +55,9 @@ class IonQClient: def __init__( self, - token: str | None = None, - url: str | None = None, - custom_headers: dict | None = None, + token: Optional[str] = None, + url: Optional[str] = None, + custom_headers: Optional[dict] = None, ): self._token = token self._custom_headers = custom_headers or {} diff --git a/qiskit_ionq/ionq_job.py b/qiskit_ionq/ionq_job.py index 579eef3..af2c90a 100644 --- a/qiskit_ionq/ionq_job.py +++ b/qiskit_ionq/ionq_job.py @@ -38,7 +38,7 @@ from __future__ import annotations import warnings -from typing import Any, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Union, Optional import numpy as np from qiskit import QuantumCircuit @@ -81,7 +81,7 @@ def get_bitvalue(bitstring, bit): def _build_counts( data, num_qubits, clbits, shots, use_sampler=False, sampler_seed=None -): +): # pylint: disable=too-many-positional-arguments """Map IonQ's ``counts`` onto qiskit's ``counts`` model. .. NOTE:: For simulator jobs, this method builds counts using a randomly @@ -163,11 +163,14 @@ class IonQJob(JobV1): def __init__( self, backend: ionq_backend.IonQBackend, - job_id: str | None, - client: ionq_client.IonQClient | None = None, - circuit: QuantumCircuit | None = None, - passed_args: dict | None = None, - ): + job_id: Optional[str] = None, + client: Optional[ionq_client.IonQClient] = None, + circuit: Optional[QuantumCircuit] = None, + passed_args: Optional[dict] = None, + ): # pylint: disable=too-many-positional-arguments + assert ( + job_id is not None or circuit is not None + ), "Job must have a job_id or circuit" super().__init__(backend, job_id) self._client = client or backend.client self._result = None @@ -214,7 +217,7 @@ def submit(self) -> None: response = self._client.submit_job(job=self) self._job_id = response["id"] - def get_counts(self, circuit: QuantumCircuit | None = None) -> dict: + def get_counts(self, circuit: Optional[QuantumCircuit] = None) -> dict: """Return the counts for the job. .. ATTENTION:: diff --git a/qiskit_ionq/ionq_provider.py b/qiskit_ionq/ionq_provider.py index ff20c8d..0971bb0 100644 --- a/qiskit_ionq/ionq_provider.py +++ b/qiskit_ionq/ionq_provider.py @@ -30,7 +30,7 @@ import logging -from typing import Callable, Literal +from typing import Callable, Literal, Optional from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.providerutils import filter_backends @@ -55,9 +55,9 @@ class IonQProvider: def __init__( self, - token: str | None = None, - url: str | None = None, - custom_headers: dict | None = None, + token: Optional[str] = None, + url: Optional[str] = None, + custom_headers: Optional[dict] = None, ): super().__init__() self.custom_headers = custom_headers @@ -112,7 +112,7 @@ def __init__(self, backends: list[ionq_backend.Backend]): setattr(self, backend.name(), backend) def __call__( - self, name: str | None = None, filters: Callable | None = None, **kwargs + self, name: Optional[str] = None, filters: Optional[Callable] = None, **kwargs ) -> list[ionq_backend.Backend]: """A listing of all backends from this provider. diff --git a/test/helpers/test_helpers.py b/test/helpers/test_helpers.py index 3b4e640..a47d531 100644 --- a/test/helpers/test_helpers.py +++ b/test/helpers/test_helpers.py @@ -27,7 +27,9 @@ """Test the helper functions.""" import re +from unittest.mock import patch, MagicMock from qiskit_ionq.ionq_client import IonQClient +from qiskit_ionq.helpers import get_n_qubits def test_user_agent_header(): @@ -47,3 +49,69 @@ def test_user_agent_header(): # Checks whether there is at-least 3 version strings from qiskit-ionq, qiskit-terra, python. has_all_version_strings = len(re.findall(r"\s*([\d.]+)", generated_user_agent)) >= 3 assert all_user_agent_keywords_avail and has_all_version_strings + + +def test_get_n_qubits_success(): + """Test get_n_qubits returns correct number of qubits and checks correct URL.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.json.return_value = {"qubits": 11} + mock_get.return_value = mock_response + + backend = "ionq_qpu.aria-1" + result = get_n_qubits(backend) + + expected_url = ( + "https://api.ionq.co/v0.3/characterizations/backends/aria-1/current" + ) + + # Create a regular expression to match the Authorization header with an apiKey + expected_headers = {"Authorization": re.compile(r"apiKey\s+\S+")} + + # Check the arguments of the last call to `requests.get` + mock_get.assert_called() + _, kwargs = mock_get.call_args + assert ( + kwargs["url"] == expected_url + ), f"Expected URL {expected_url}, but got {kwargs['url']}" + + # Assert that the headers contain the apiKey in the expected format + assert re.match( + expected_headers["Authorization"], kwargs["headers"]["Authorization"] + ), ( + f"Expected headers to match {expected_headers['Authorization'].pattern}, " + f"but got {kwargs['headers']['Authorization']}" + ) + + assert result == 11, f"Expected 11 qubits, but got {result}" + + +def test_get_n_qubits_fallback(): + """Test get_n_qubits returns fallback number of qubits and checks correct URL on failure.""" + with patch("requests.get", side_effect=Exception("Network error")) as mock_get: + backend = "aria-1" + result = get_n_qubits(backend) + + expected_url = ( + "https://api.ionq.co/v0.3/characterizations/backends/aria-1/current" + ) + + # Create a regular expression to match the Authorization header with an apiKey + expected_headers = {"Authorization": re.compile(r"apiKey\s+\S+")} + + # Check the arguments of the last call to `requests.get` + mock_get.assert_called() + _, kwargs = mock_get.call_args + assert ( + kwargs["url"] == expected_url + ), f"Expected URL {expected_url}, but got {kwargs['url']}" + + # Assert that the headers contain the apiKey in the expected format + assert re.match( + expected_headers["Authorization"], kwargs["headers"]["Authorization"] + ), ( + f"Expected headers to match {expected_headers['Authorization'].pattern}, " + f"but got {kwargs['headers']['Authorization']}" + ) + + assert result == 100, f"Expected fallback of 100 qubits, but got {result}" diff --git a/tox.ini b/tox.ini index 14d6c26..8cc1460 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ setenv = VIRTUAL_ENV={envdir} LANGUAGE=en_US LC_ALL=en_US.utf-8 +commands = pytest [testenv:lint] deps =