diff --git a/examples/number_partition.py b/examples/number_partition.py index e10aaf3..32f6688 100644 --- a/examples/number_partition.py +++ b/examples/number_partition.py @@ -36,17 +36,23 @@ @dataclass(frozen=True) class Success: + """Solution of a partition problem.""" + # type would be better as tuple[set[int], set[int]] but # NumberPartition.interpret returns list[list[int]]. partition: list[list[int]] - def verify(self) -> bool: + def is_valid(self) -> bool: + """Evaluate whether the stored partition is valid. + + A partition is valid if both sets have the same sum. + """ a, b = self.partition return sum(a) == sum(b) class Infeasible: - pass + """Marker for unsolvable partition problems.""" def solve_partition_problem(num_set: set[int]) -> Union[Success, Infeasible]: @@ -87,7 +93,7 @@ def solve_partition_problem(num_set: set[int]) -> Union[Success, Infeasible]: num_set = {1, 3, 4} result = solve_partition_problem(num_set) assert isinstance(result, Success) # noqa: S101 - assert result.verify() # noqa: S101 + assert result.is_valid() # noqa: S101 print(f"Partition for {num_set}:", result.partition) num_set = {1, 2} diff --git a/examples/quickstart-estimator.py b/examples/quickstart-estimator.py index 5a290f4..f0c1370 100644 --- a/examples/quickstart-estimator.py +++ b/examples/quickstart-estimator.py @@ -18,8 +18,13 @@ the ground state energy of a Hamiltonian. """ +from collections.abc import Sequence + +from qiskit import QuantumCircuit from qiskit.circuit.library import TwoLocal +from qiskit.primitives import BaseEstimator from qiskit.quantum_info import SparsePauliOp +from qiskit.quantum_info.operators.base_operator import BaseOperator from scipy.optimize import minimize from qiskit_aqt_provider import AQTProvider @@ -48,16 +53,21 @@ # Define the VQE Ansatz, initial point, and cost function ansatz = TwoLocal(num_qubits=2, rotation_blocks="ry", entanglement_blocks="cz") -initial_point = initial_point = [0] * 8 +initial_point = [0] * 8 -def cost_function(params, ansatz, hamiltonian, estimator): +def cost_function( + params: Sequence[float], + ansatz: QuantumCircuit, + hamiltonian: BaseOperator, + estimator: BaseEstimator, +) -> float: """Cost function for the VQE. Return the estimated expectation value of the Hamiltonian on the state prepared by the Ansatz circuit. """ - return estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0] + return float(estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]) # Run the VQE using the SciPy minimizer routine diff --git a/pyproject.toml b/pyproject.toml index f2c56ad..0bfba22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,63 +119,67 @@ extend-exclude = [ ] lint.select = [ - "ARG", # flake8-unused-arguments - "BLE", # flake8-blind-except - "C4", # flake8-comprehensions - "C90", # mccabe - "COM", # flake8-commas - "D", # pydocstyle - "E", # pycodestyle errors - "ERA", # eradicate - "F", # pyflakes - "I", # isort - "ICN", # flake8-import-conventions - "ISC", # flake8-implicit-str-concat - "NPY", # numpy - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PT", # flake8-pytest-style - "PTH", # flake8-use-pathlib - "PYI", # flake8-pyi - "RET", # flake8-return - "RSE", # flake8-raise - "RUF", # ruff specials - "S", # flake8-bandit - "SIM", # flake8-simplify - "T10", # flake8-debugger - "T20", # flake8-print - "TID", # flake8-tidy-imports - "UP", # pyupgrade - "W", # pycodestyle warnings + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # mccabe + "COM", # flake8-commas + "D", # pydocstyle + "E", # pycodestyle errors + "ERA", # eradicate + "EXE", # flake8-executable + "F", # pyflakes + "FLY", # flynt + "I", # isort + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "NPY", # numpy + "PERF", # perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # ruff specials + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "T10", # flake8-debugger + "T20", # flake8-print + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 ] lint.ignore = [ - "COM812", - "COM819", + "ANN101", # missing-type-self (deprecated) + "ANN102", # missing-type-cls (deprecated) + "ANN401", # any-type + "COM812", # missing-trailing-comma + "COM819", # prohibited-trailing-comma "D100", # missing docstring in public module - "D101", # missing docstring in public class - "D102", # missing docstring in public method "D104", # missing docstring in public package "D107", # missing docstring in __init__ - "D206", + "D206", # indent-with-spaces "D211", # no-blank-line-before-class (incompatible with D203) "D213", # multiline-summary-second-line (incompatible with D212) - "D300", - "E111", - "E114", - "E117", - "E501", - "ISC001", - "ISC002", - "Q000", - "Q001", - "Q002", - "Q003", + "D300", # triple-single-quotes + "E111", # indentation-with-invalid-multiple + "E114", # indentation-with-invalid-multiple-comment + "E117", # over-idented + "ISC001", # single-line-implicit-string-concatenation + "ISC002", # multi-line-implicit-string-concatenation + "Q000", # bad-quotes-inline-string + "Q001", # bad-quotes-multiline-string + "Q002", # bad-quotes-docstring + "Q003", # avoidable-escaped-quote "S311", # suspicious-non-cryptographic-random-usage "SIM117", # multiple-with-statements - # - # ignore following rules since they are conflicting with the formatter - # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules - "W191", + "W191", # tab-indentation ] lint.per-file-ignores."examples/*.py" = [ "T201", # allow prints @@ -184,9 +188,10 @@ lint.per-file-ignores."scripts/*.py" = [ "T201", # allow prints ] lint.per-file-ignores."test/**/*.py" = [ - "D205", # allow multiline docstring summaries - "PT011", # allow pytest.raises without match= - "S101", # allow assertions + "D205", # allow multiline docstring summaries + "PLR2004", # magic-value-comparison + "PT011", # allow pytest.raises without match= + "S101", # allow assertions ] lint.pydocstyle.convention = "google" diff --git a/qiskit_aqt_provider/aqt_job.py b/qiskit_aqt_provider/aqt_job.py index 6bb3127..9735359 100644 --- a/qiskit_aqt_provider/aqt_job.py +++ b/qiskit_aqt_provider/aqt_job.py @@ -14,6 +14,7 @@ from collections import Counter, defaultdict from dataclasses import dataclass from pathlib import Path +from types import TracebackType from typing import ( TYPE_CHECKING, Any, @@ -111,7 +112,13 @@ def update(self, n: int = 1) -> None: def __enter__(self) -> Self: return self - def __exit__(*args) -> None: ... + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + /, + ) -> None: ... class AQTJob(JobV1): @@ -153,7 +160,7 @@ def __init__( backend: "AQTResource", circuits: list[QuantumCircuit], options: AQTOptions, - ): + ) -> None: """Initialize an :class:`AQTJob` instance. .. tip:: :class:`AQTJob` instances should not be created directly. Use @@ -389,7 +396,9 @@ def callback( class AQTDirectAccessJob(JobV1): """Handle for quantum circuits jobs running on direct-access AQT backends. - Use :meth:`AQTDirectAccessResource.run ` + Use + :meth:`AQTDirectAccessResource.run + ` to get a handle and evaluate circuits on a direct-access backend. """ @@ -400,7 +409,7 @@ def __init__( backend: "AQTDirectAccessResource", circuits: list[QuantumCircuit], options: AQTOptions, - ): + ) -> None: """Initialize the :class:`AQTDirectAccessJob` instance. Args: diff --git a/qiskit_aqt_provider/aqt_options.py b/qiskit_aqt_provider/aqt_options.py index cabfb19..11a1f13 100644 --- a/qiskit_aqt_provider/aqt_options.py +++ b/qiskit_aqt_provider/aqt_options.py @@ -44,7 +44,8 @@ class AQTOptions(pdt.BaseModel, Mapping[str, Any]): Option overrides can also be applied on a per-job basis, as keyword arguments to :meth:`AQTResource.run ` or - :meth:`AQTDirectAccessResource.run `: + :meth:`AQTDirectAccessResource.run + `: >>> backend.options.shots 50 @@ -132,9 +133,8 @@ def max_shots(cls) -> int: if isinstance(metadata, annotated_types.Lt): # pragma: no cover return int(str(metadata.lt)) - 1 - else: # pragma: no cover - msg = "No upper bound found for 'shots'." - raise ValueError(msg) + + raise ValueError("No upper bound found for 'shots'.") # pragma: no cover class AQTDirectAccessOptions(AQTOptions): diff --git a/qiskit_aqt_provider/aqt_provider.py b/qiskit_aqt_provider/aqt_provider.py index 2e6169a..3209fc9 100644 --- a/qiskit_aqt_provider/aqt_provider.py +++ b/qiskit_aqt_provider/aqt_provider.py @@ -77,7 +77,7 @@ class BackendsTable(Sequence[AQTResource]): in IPython/Jupyter notebooks. """ - def __init__(self, backends: list[AQTResource]): + def __init__(self, backends: list[AQTResource]) -> None: """Initialize the table. Args: @@ -154,7 +154,7 @@ def __init__( *, load_dotenv: bool = True, dotenv_path: Optional[StrPath] = None, - ): + ) -> None: """Initialize the AQT provider. The access token for the AQT cloud can be provided either through the @@ -269,22 +269,23 @@ def backends( ) ) - # add (filtered) remote resources - for _workspace in remote_workspaces.root: - for resource in _workspace.resources: - backends.append( - AQTResource( - self, - resource_id=api_models.ResourceId( - workspace_id=_workspace.id, - resource_id=resource.id, - resource_name=resource.name, - resource_type=resource.type.value, - ), - ) + return BackendsTable( + backends + # add (filtered) remote resources + + [ + AQTResource( + self, + resource_id=api_models.ResourceId( + workspace_id=_workspace.id, + resource_id=resource.id, + resource_name=resource.name, + resource_type=resource.type.value, + ), ) - - return BackendsTable(backends) + for _workspace in remote_workspaces.root + for resource in _workspace.resources + ] + ) def get_backend( self, diff --git a/qiskit_aqt_provider/aqt_resource.py b/qiskit_aqt_provider/aqt_resource.py index b70b201..967cdba 100644 --- a/qiskit_aqt_provider/aqt_resource.py +++ b/qiskit_aqt_provider/aqt_resource.py @@ -85,7 +85,9 @@ def make_transpiler_target(target_cls: type[TargetT], num_qubits: int) -> Target class _ResourceBase(Generic[_OptionsType], Backend): """Common setup for AQT backends.""" - def __init__(self, provider: "AQTProvider", name: str, options_type: type[_OptionsType]): + def __init__( + self, provider: "AQTProvider", name: str, options_type: type[_OptionsType] + ) -> None: """Initialize the Qiskit backend. Args: @@ -206,7 +208,7 @@ def __init__( self, provider: "AQTProvider", resource_id: api_models.ResourceId, - ): + ) -> None: """Initialize the backend. Args: @@ -281,7 +283,9 @@ def result(self, job_id: UUID) -> api_models.JobResponse: class AQTDirectAccessResource(_ResourceBase[AQTDirectAccessOptions]): """Qiskit backend for AQT direct-access quantum computing resources. - Use :meth:`AQTProvider.get_direct_access_backend ` + Use + :meth:`AQTProvider.get_direct_access_backend + ` to retrieve backend instances. """ @@ -310,7 +314,8 @@ def run( """Prepare circuits for execution on this resource. .. warning:: The circuits are only evaluated during - the :meth:`AQTDirectAccessJob.result ` + the :meth:`AQTDirectAccessJob.result + ` call. Args: @@ -416,8 +421,9 @@ class OfflineSimulatorResource(AQTResource): `with_noise_model` is true, a noise model approximating that of AQT hardware backends is used. .. tip:: - The simulator backend is provided by `Qiskit Aer `_. The - Qiskit Aer resource is exposed for detailed detuning as the + The simulator backend is provided by + `Qiskit Aer `_. + The Qiskit Aer resource is exposed for detailed detuning as the ``OfflineSimulatorResource.simulator`` attribute. """ diff --git a/qiskit_aqt_provider/primitives/estimator.py b/qiskit_aqt_provider/primitives/estimator.py index c501dcf..d41559a 100644 --- a/qiskit_aqt_provider/primitives/estimator.py +++ b/qiskit_aqt_provider/primitives/estimator.py @@ -30,7 +30,7 @@ def __init__( options: Optional[dict[str, Any]] = None, abelian_grouping: bool = True, skip_transpilation: bool = False, - ): + ) -> None: """Initialize an ``Estimator`` primitive using an AQT backend. See :class:`AQTSampler ` for diff --git a/qiskit_aqt_provider/primitives/sampler.py b/qiskit_aqt_provider/primitives/sampler.py index ac3c167..8335730 100644 --- a/qiskit_aqt_provider/primitives/sampler.py +++ b/qiskit_aqt_provider/primitives/sampler.py @@ -29,7 +29,7 @@ def __init__( backend: AQTResource, options: Optional[dict[str, Any]] = None, skip_transpilation: bool = False, - ): + ) -> None: """Initialize a ``Sampler`` primitive using an AQT backend. Args: diff --git a/qiskit_aqt_provider/test/resources.py b/qiskit_aqt_provider/test/resources.py index 6a86ebf..febd0bf 100644 --- a/qiskit_aqt_provider/test/resources.py +++ b/qiskit_aqt_provider/test/resources.py @@ -131,7 +131,7 @@ class TestResource(AQTResource): # pylint: disable=too-many-instance-attributes __test__ = False # disable pytest collection - def __init__( + def __init__( # noqa: PLR0913 self, *, min_queued_duration: float = 0.0, diff --git a/scripts/api_models.py b/scripts/api_models.py index 32ce0e4..4969b3d 100755 --- a/scripts/api_models.py +++ b/scripts/api_models.py @@ -66,7 +66,7 @@ def generate_models(schema_path: Path) -> str: print(e.stderr.decode()) print("-------------------------------------------------") print(f"datamodel-codegen failed with error code: {e.returncode}") - exit(1) + raise typer.Exit(code=1) return proc.stdout.decode() @@ -104,6 +104,7 @@ def check( proc = subprocess.run( shlex.split(f"diff -u {filepath} {models_path}"), # noqa: S603 capture_output=True, + check=True, ) if proc.returncode != 0: diff --git a/scripts/extract-changelog.py b/scripts/extract-changelog.py old mode 100644 new mode 100755 index f19f2a4..1d49a01 --- a/scripts/extract-changelog.py +++ b/scripts/extract-changelog.py @@ -11,11 +11,15 @@ from mistletoe import Document, block_token from mistletoe.base_renderer import BaseRenderer +REVISION_HEADER_LEVEL: Final = 2 HEADER_REGEX: Final = re.compile(r"([a-z-]+)\s+(v\d+\.\d+\.\d+)") class Renderer(BaseRenderer): + """Markdown renderer.""" + def render_list_item(self, token: block_token.ListItem) -> str: + """Tweak lists rendering. Use '*' as item marker.""" return f"* {self.render_inner(token)}\n" @@ -25,6 +29,7 @@ def default_changelog_path() -> Path: subprocess.run( shlex.split("git rev-parse --show-toplevel"), # noqa: S603 capture_output=True, + check=True, ) .stdout.strip() .decode("utf-8") @@ -45,7 +50,7 @@ def main( current_version: Optional[str] = None for node in md_ast.children: - if isinstance(node, block_token.Heading) and node.level == 2: + if isinstance(node, block_token.Heading) and node.level == REVISION_HEADER_LEVEL: if (match := HEADER_REGEX.search(node.children[0].content)) is not None: _, revision = match.groups() current_version = revision diff --git a/scripts/package_version.py b/scripts/package_version.py index 88120d7..c2cf771 100755 --- a/scripts/package_version.py +++ b/scripts/package_version.py @@ -29,9 +29,16 @@ @dataclass(frozen=True) class CommonArgs: + """Parsed arguments common to all sub-commands.""" + pyproject_path: Path + """Path to the pyproject file.""" + docs_conf_path: Path + """Path to the Sphinx docs configuration module.""" + verbose: bool + """Verbosity flag.""" def get_args(ctx: typer.Context) -> CommonArgs: diff --git a/scripts/read-target-coverage.py b/scripts/read-target-coverage.py index cba1ca4..c5d205d 100755 --- a/scripts/read-target-coverage.py +++ b/scripts/read-target-coverage.py @@ -19,6 +19,7 @@ def default_pyproject_path() -> Path: subprocess.run( shlex.split("git rev-parse --show-toplevel"), # noqa: S603 capture_output=True, + check=True, ) .stdout.strip() .decode("utf-8") diff --git a/test/test_resource.py b/test/test_resource.py index 1dffb83..d45737b 100644 --- a/test/test_resource.py +++ b/test/test_resource.py @@ -466,7 +466,7 @@ def test_offline_simulator_resource_propagate_memory_option( def test_direct_access_bad_request(httpx_mock: HTTPXMock) -> None: - """Check that direct-access resources raise a httpx.HTTPError if the request is flagged bad by the server.""" + """Check that direct-access resources raise a httpx.HTTPError on bad requests.""" backend = DummyDirectAccessResource("token") httpx_mock.add_response(status_code=httpx.codes.BAD_REQUEST)