diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 751cf4db1e..a0b3a5ec80 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -50,5 +50,8 @@ jobs: pip install poetry poetry install --extras dev + - name: Run type checking + run: poetry run mypy qumat/ + - name: Run tests run: poetry run pytest testing/ -v diff --git a/pyproject.toml b/pyproject.toml index 36ae5ee4b8..61e240c70d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,10 +12,11 @@ dependencies = [ "cirq>=1.5.0,<1.6.0", "amazon-braket-sdk>=1.102.6,<2.0", "sympy>=1.14.0,<2.0", + "flask (>=3.1.2,<4.0.0)", ] [project.optional-dependencies] -dev = ["pytest>=8.1.1", "ruff>=0.13.1", "pre-commit>=3.0.0"] +dev = ["pytest>=8.1.1", "ruff>=0.13.1", "pre-commit>=3.0.0", "mypy>=1.0.0"] [tool.pytest.ini_options] testpaths = ["testing"] @@ -23,6 +24,29 @@ python_files = "test_*.py" python_functions = "test_*" addopts = ["-v", "--tb=short"] +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +disallow_incomplete_defs = false +check_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +strict_equality = true + +# Ignore missing type stubs for third-party quantum libraries +[[tool.mypy.overrides]] +module = [ + "qiskit.*", + "qiskit_aer.*", + "cirq.*", + "braket.*", + "sympy.*" +] +ignore_missing_imports = true + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/qumat/amazon_braket_backend.py b/qumat/amazon_braket_backend.py index 0121c70be6..3b0fe69e3e 100644 --- a/qumat/amazon_braket_backend.py +++ b/qumat/amazon_braket_backend.py @@ -14,12 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from typing import Any, cast + +import numpy as np from braket.aws import AwsDevice -from braket.devices import LocalSimulator from braket.circuits import Circuit, FreeParameter +from braket.devices import LocalSimulator -def initialize_backend(backend_config): +def initialize_backend(backend_config: dict[str, Any]) -> LocalSimulator | AwsDevice: backend_options = backend_config["backend_options"] simulator_type = backend_options.get("simulator_type", "default") if simulator_type == "local": @@ -33,7 +36,7 @@ def initialize_backend(backend_config): return AwsDevice("arn:aws:braket:::device/quantum-simulator/amazon/sv1") -def create_empty_circuit(num_qubits: int | None = None): +def create_empty_circuit(num_qubits: int | None = None) -> Circuit: circuit = Circuit() if num_qubits is not None: for i in range(num_qubits): @@ -41,47 +44,55 @@ def create_empty_circuit(num_qubits: int | None = None): return circuit -def apply_not_gate(circuit, qubit_index): +def apply_not_gate(circuit: Circuit, qubit_index: int) -> None: circuit.x(qubit_index) -def apply_hadamard_gate(circuit, qubit_index): +def apply_hadamard_gate(circuit: Circuit, qubit_index: int) -> None: circuit.h(qubit_index) -def apply_cnot_gate(circuit, control_qubit_index, target_qubit_index): +def apply_cnot_gate(circuit: Circuit, control_qubit_index: int, target_qubit_index: int) -> None: circuit.cnot(control_qubit_index, target_qubit_index) def apply_toffoli_gate( - circuit, control_qubit_index1, control_qubit_index2, target_qubit_index -): + circuit: Circuit, + control_qubit_index1: int, + control_qubit_index2: int, + target_qubit_index: int, +) -> None: circuit.ccnot(control_qubit_index1, control_qubit_index2, target_qubit_index) -def apply_swap_gate(circuit, qubit_index1, qubit_index2): +def apply_swap_gate(circuit: Circuit, qubit_index1: int, qubit_index2: int) -> None: circuit.swap(qubit_index1, qubit_index2) def apply_cswap_gate( - circuit, control_qubit_index, target_qubit_index1, target_qubit_index2 -): + circuit: Circuit, + control_qubit_index: int, + target_qubit_index1: int, + target_qubit_index2: int, +) -> None: circuit.cswap(control_qubit_index, target_qubit_index1, target_qubit_index2) -def apply_pauli_x_gate(circuit, qubit_index): +def apply_pauli_x_gate(circuit: Circuit, qubit_index: int) -> None: circuit.x(qubit_index) -def apply_pauli_y_gate(circuit, qubit_index): +def apply_pauli_y_gate(circuit: Circuit, qubit_index: int) -> None: circuit.y(qubit_index) -def apply_pauli_z_gate(circuit, qubit_index): +def apply_pauli_z_gate(circuit: Circuit, qubit_index: int) -> None: circuit.z(qubit_index) -def execute_circuit(circuit, backend, backend_config): +def execute_circuit( + circuit: Circuit, backend: LocalSimulator | AwsDevice, backend_config: dict[str, Any] +) -> dict[str, int]: shots = backend_config["backend_options"].get("shots", 1) parameter_values = backend_config.get("parameter_values", {}) if parameter_values and circuit.parameters: @@ -95,26 +106,28 @@ def execute_circuit(circuit, backend, backend_config): else: task = backend.run(circuit, shots=shots) result = task.result() - return result.measurement_counts + return cast(dict[str, int], result.measurement_counts) # placeholder method for use in the testing suite -def get_final_state_vector(circuit, backend, backend_config): +def get_final_state_vector( + circuit: Circuit, backend: LocalSimulator | AwsDevice, backend_config: dict[str, Any] +) -> np.ndarray: circuit.state_vector() result = backend.run(circuit, shots=0).result() state_vector = result.values[0] - return state_vector + return cast(np.ndarray, state_vector) -def draw_circuit(circuit): +def draw_circuit(circuit: Circuit) -> None: # Unfortunately, Amazon Braket does not have direct support for drawing circuits in the same way # as Qiskit and Cirq. You would typically visualize Amazon Braket circuits using external tools. # For simplicity, we'll print the circuit object which gives some textual representation. print(circuit) -def apply_rx_gate(circuit, qubit_index, angle): +def apply_rx_gate(circuit: Circuit, qubit_index: int, angle: float | str) -> None: if isinstance(angle, (int, float)): circuit.rx(qubit_index, angle) else: @@ -122,7 +135,7 @@ def apply_rx_gate(circuit, qubit_index, angle): circuit.rx(qubit_index, param) -def apply_ry_gate(circuit, qubit_index, angle): +def apply_ry_gate(circuit: Circuit, qubit_index: int, angle: float | str) -> None: if isinstance(angle, (int, float)): circuit.ry(qubit_index, angle) else: @@ -130,7 +143,7 @@ def apply_ry_gate(circuit, qubit_index, angle): circuit.ry(qubit_index, param) -def apply_rz_gate(circuit, qubit_index, angle): +def apply_rz_gate(circuit: Circuit, qubit_index: int, angle: float | str) -> None: if isinstance(angle, (int, float)): circuit.rz(qubit_index, angle) else: @@ -138,14 +151,18 @@ def apply_rz_gate(circuit, qubit_index, angle): circuit.rz(qubit_index, param) -def apply_u_gate(circuit, qubit_index, theta, phi, lambd): +def apply_u_gate( + circuit: Circuit, qubit_index: int, theta: float, phi: float, lambd: float +) -> None: # U(θ, φ, λ) = Rz(φ) · Ry(θ) · Rz(λ) circuit.rz(qubit_index, lambd) circuit.ry(qubit_index, theta) circuit.rz(qubit_index, phi) -def calculate_prob_zero(results, ancilla_qubit, num_qubits): +def calculate_prob_zero( + results: dict[str, int] | list[dict[str, int]], ancilla_qubit: int, num_qubits: int +) -> float: """ Calculate the probability of measuring the ancilla qubit in |0> state. diff --git a/qumat/cirq_backend.py b/qumat/cirq_backend.py index 674d9d4d44..ffa6326110 100644 --- a/qumat/cirq_backend.py +++ b/qumat/cirq_backend.py @@ -14,11 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from typing import Any + import cirq +import numpy as np import sympy -def initialize_backend(backend_config): +def initialize_backend(backend_config: dict[str, Any]) -> cirq.Simulator: # Assuming 'simulator_type' specifies the type of simulator in Cirq simulator_type = backend_config.get("backend_options", {}).get( "simulator_type", "default" @@ -31,7 +34,7 @@ def initialize_backend(backend_config): return cirq.Simulator() -def create_empty_circuit(num_qubits: int | None = None): +def create_empty_circuit(num_qubits: int | None = None) -> cirq.Circuit: circuit = cirq.Circuit() if num_qubits is not None: qubits = [cirq.LineQubit(i) for i in range(num_qubits)] @@ -40,62 +43,72 @@ def create_empty_circuit(num_qubits: int | None = None): return circuit -def apply_not_gate(circuit, qubit_index): +def apply_not_gate(circuit: cirq.Circuit, qubit_index: int) -> None: qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.X(qubit)) -def apply_hadamard_gate(circuit, qubit_index): +def apply_hadamard_gate(circuit: cirq.Circuit, qubit_index: int) -> None: qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.H(qubit)) -def apply_cnot_gate(circuit, control_qubit_index, target_qubit_index): +def apply_cnot_gate( + circuit: cirq.Circuit, control_qubit_index: int, target_qubit_index: int +) -> None: control_qubit = cirq.LineQubit(control_qubit_index) target_qubit = cirq.LineQubit(target_qubit_index) circuit.append(cirq.CNOT(control_qubit, target_qubit)) def apply_toffoli_gate( - circuit, control_qubit_index1, control_qubit_index2, target_qubit_index -): + circuit: cirq.Circuit, + control_qubit_index1: int, + control_qubit_index2: int, + target_qubit_index: int, +) -> None: control_qubit1 = cirq.LineQubit(control_qubit_index1) control_qubit2 = cirq.LineQubit(control_qubit_index2) target_qubit = cirq.LineQubit(target_qubit_index) circuit.append(cirq.CCX(control_qubit1, control_qubit2, target_qubit)) -def apply_swap_gate(circuit, qubit_index1, qubit_index2): +def apply_swap_gate(circuit: cirq.Circuit, qubit_index1: int, qubit_index2: int) -> None: qubit1 = cirq.LineQubit(qubit_index1) qubit2 = cirq.LineQubit(qubit_index2) circuit.append(cirq.SWAP(qubit1, qubit2)) def apply_cswap_gate( - circuit, control_qubit_index, target_qubit_index1, target_qubit_index2 -): + circuit: cirq.Circuit, + control_qubit_index: int, + target_qubit_index1: int, + target_qubit_index2: int, +) -> None: control_qubit = cirq.LineQubit(control_qubit_index) target_qubit1 = cirq.LineQubit(target_qubit_index1) target_qubit2 = cirq.LineQubit(target_qubit_index2) circuit.append(cirq.CSWAP(control_qubit, target_qubit1, target_qubit2)) -def apply_pauli_x_gate(circuit, qubit_index): +def apply_pauli_x_gate(circuit: cirq.Circuit, qubit_index: int) -> None: qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.X(qubit)) -def apply_pauli_y_gate(circuit, qubit_index): +def apply_pauli_y_gate(circuit: cirq.Circuit, qubit_index: int) -> None: qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.Y(qubit)) -def apply_pauli_z_gate(circuit, qubit_index): +def apply_pauli_z_gate(circuit: cirq.Circuit, qubit_index: int) -> None: qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.Z(qubit)) -def execute_circuit(circuit, backend, backend_config): +def execute_circuit( + circuit: cirq.Circuit, backend: cirq.Simulator, backend_config: dict[str, Any] +) -> list[dict[int, int]]: # handle 0-qubit circuits before adding measurements if not circuit.all_qubits(): shots = backend_config["backend_options"].get("shots", 1) @@ -122,42 +135,48 @@ def execute_circuit(circuit, backend, backend_config): return [result.histogram(key="result")] -def draw_circuit(circuit): +def draw_circuit(circuit: cirq.Circuit) -> None: print(circuit) -def apply_rx_gate(circuit, qubit_index, angle): +def apply_rx_gate(circuit: cirq.Circuit, qubit_index: int, angle: float | str) -> None: param = sympy.Symbol(angle) if isinstance(angle, str) else angle qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.rx(param).on(qubit)) -def apply_ry_gate(circuit, qubit_index, angle): +def apply_ry_gate(circuit: cirq.Circuit, qubit_index: int, angle: float | str) -> None: param = sympy.Symbol(angle) if isinstance(angle, str) else angle qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.ry(param).on(qubit)) -def apply_rz_gate(circuit, qubit_index, angle): +def apply_rz_gate(circuit: cirq.Circuit, qubit_index: int, angle: float | str) -> None: param = sympy.Symbol(angle) if isinstance(angle, str) else angle qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.rz(param).on(qubit)) -def apply_u_gate(circuit, qubit_index, theta, phi, lambd): +def apply_u_gate( + circuit: cirq.Circuit, qubit_index: int, theta: float, phi: float, lambd: float +) -> None: qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.rz(lambd).on(qubit)) circuit.append(cirq.ry(theta).on(qubit)) circuit.append(cirq.rz(phi).on(qubit)) -def get_final_state_vector(circuit, backend, backend_config): +def get_final_state_vector( + circuit: cirq.Circuit, backend: cirq.Simulator, backend_config: dict[str, Any] +) -> np.ndarray: simulator = cirq.Simulator() result = simulator.simulate(circuit) return result.final_state_vector -def calculate_prob_zero(results, ancilla_qubit, num_qubits): +def calculate_prob_zero( + results: list[dict[int, int]] | dict[int, int], ancilla_qubit: int, num_qubits: int +) -> float: """ Calculate the probability of measuring the ancilla qubit in |0> state. diff --git a/qumat/qiskit_backend.py b/qumat/qiskit_backend.py index e7156eada6..375e524508 100644 --- a/qumat/qiskit_backend.py +++ b/qumat/qiskit_backend.py @@ -14,11 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from typing import Any, cast + +import numpy as np import qiskit from qiskit_aer import Aer, AerSimulator -def initialize_backend(backend_config): +def initialize_backend(backend_config: dict[str, Any]) -> Any: backend_options = backend_config["backend_options"] simulator_type = backend_options["simulator_type"] shots = backend_options["shots"] @@ -27,65 +30,77 @@ def initialize_backend(backend_config): return backend -def create_empty_circuit(num_qubits: int | None = None): +def create_empty_circuit(num_qubits: int | None = None) -> qiskit.QuantumCircuit: if num_qubits is not None: return qiskit.QuantumCircuit(num_qubits) else: return qiskit.QuantumCircuit() -def apply_not_gate(circuit, qubit_index): +def apply_not_gate(circuit: qiskit.QuantumCircuit, qubit_index: int) -> None: # Apply a NOT gate (X gate) on the specified qubit circuit.x(qubit_index) -def apply_hadamard_gate(circuit, qubit_index): +def apply_hadamard_gate(circuit: qiskit.QuantumCircuit, qubit_index: int) -> None: # Apply a Hadamard gate on the specified qubit circuit.h(qubit_index) -def apply_cnot_gate(circuit, control_qubit_index, target_qubit_index): +def apply_cnot_gate( + circuit: qiskit.QuantumCircuit, control_qubit_index: int, target_qubit_index: int +) -> None: # Apply a CNOT gate (controlled-X gate) with the specified control and # target qubits circuit.cx(control_qubit_index, target_qubit_index) def apply_toffoli_gate( - circuit, control_qubit_index1, control_qubit_index2, target_qubit_index -): + circuit: qiskit.QuantumCircuit, + control_qubit_index1: int, + control_qubit_index2: int, + target_qubit_index: int, +) -> None: # Apply a Toffoli gate (controlled-controlled-X gate) with the # specified control and target qubits circuit.ccx(control_qubit_index1, control_qubit_index2, target_qubit_index) -def apply_swap_gate(circuit, qubit_index1, qubit_index2): +def apply_swap_gate( + circuit: qiskit.QuantumCircuit, qubit_index1: int, qubit_index2: int +) -> None: # Apply a SWAP gate to exchange the states of two qubits circuit.swap(qubit_index1, qubit_index2) def apply_cswap_gate( - circuit, control_qubit_index, target_qubit_index1, target_qubit_index2 -): + circuit: qiskit.QuantumCircuit, + control_qubit_index: int, + target_qubit_index1: int, + target_qubit_index2: int, +) -> None: # Apply a controlled-SWAP (Fredkin) gate with the specified control and target qubits circuit.cswap(control_qubit_index, target_qubit_index1, target_qubit_index2) -def apply_pauli_x_gate(circuit, qubit_index): +def apply_pauli_x_gate(circuit: qiskit.QuantumCircuit, qubit_index: int) -> None: # Apply a Pauli X gate on the specified qubit circuit.x(qubit_index) -def apply_pauli_y_gate(circuit, qubit_index): +def apply_pauli_y_gate(circuit: qiskit.QuantumCircuit, qubit_index: int) -> None: # Apply a Pauli Y gate on the specified qubit circuit.y(qubit_index) -def apply_pauli_z_gate(circuit, qubit_index): +def apply_pauli_z_gate(circuit: qiskit.QuantumCircuit, qubit_index: int) -> None: # Apply a Pauli Z gate on the specified qubit circuit.z(qubit_index) -def execute_circuit(circuit, backend, backend_config): +def execute_circuit( + circuit: qiskit.QuantumCircuit, backend: Any, backend_config: dict[str, Any] +) -> dict[str, int]: # Add measurements if they are not already present # Check if circuit already has measurement operations has_measurements = any( @@ -107,18 +122,20 @@ def execute_circuit(circuit, backend, backend_config): bound_circuit, shots=backend_config["backend_options"]["shots"] ) result = job.result() - return result.get_counts() + return cast(dict[str, int], result.get_counts()) else: transpiled_circuit = qiskit.transpile(circuit, backend) job = backend.run( transpiled_circuit, shots=backend_config["backend_options"]["shots"] ) result = job.result() - return result.get_counts() + return cast(dict[str, int], result.get_counts()) # placeholder method for use in the testing suite -def get_final_state_vector(circuit, backend, backend_config): +def get_final_state_vector( + circuit: qiskit.QuantumCircuit, backend: Any, backend_config: dict[str, Any] +) -> np.ndarray: simulator = AerSimulator(method="statevector") # Add save_statevector instruction @@ -129,35 +146,45 @@ def get_final_state_vector(circuit, backend, backend_config): job = simulator.run(transpiled_circuit) result = job.result() - return result.get_statevector() + return cast(np.ndarray, result.get_statevector()) -def draw_circuit(circuit): +def draw_circuit(circuit: qiskit.QuantumCircuit) -> None: # Use Qiskit's built-in drawing function print(circuit.draw()) -def apply_rx_gate(circuit, qubit_index, angle): +def apply_rx_gate( + circuit: qiskit.QuantumCircuit, qubit_index: int, angle: float | str +) -> None: param = qiskit.circuit.Parameter(angle) if isinstance(angle, str) else angle circuit.rx(param, qubit_index) -def apply_ry_gate(circuit, qubit_index, angle): +def apply_ry_gate( + circuit: qiskit.QuantumCircuit, qubit_index: int, angle: float | str +) -> None: param = qiskit.circuit.Parameter(angle) if isinstance(angle, str) else angle circuit.ry(param, qubit_index) -def apply_rz_gate(circuit, qubit_index, angle): +def apply_rz_gate( + circuit: qiskit.QuantumCircuit, qubit_index: int, angle: float | str +) -> None: param = qiskit.circuit.Parameter(angle) if isinstance(angle, str) else angle circuit.rz(param, qubit_index) -def apply_u_gate(circuit, qubit_index, theta, phi, lambd): +def apply_u_gate( + circuit: qiskit.QuantumCircuit, qubit_index: int, theta: float, phi: float, lambd: float +) -> None: # Apply the U gate directly with specified parameters circuit.u(theta, phi, lambd, qubit_index) -def calculate_prob_zero(results, ancilla_qubit, num_qubits): +def calculate_prob_zero( + results: dict[str, int] | list[dict[str, int]], ancilla_qubit: int, num_qubits: int +) -> float: """ Calculate the probability of measuring the ancilla qubit in |0> state. diff --git a/testing/test_type_checking.py b/testing/test_type_checking.py new file mode 100644 index 0000000000..c62019fa49 --- /dev/null +++ b/testing/test_type_checking.py @@ -0,0 +1,149 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import subprocess +import sys +from pathlib import Path + +import pytest + + +class TestTypeChecking: + """Test class for validating type hints using mypy.""" + + @pytest.fixture + def project_root(self): + """Get the project root directory.""" + return Path(__file__).parent.parent + + @pytest.fixture + def qumat_dir(self, project_root): + """Get the qumat package directory.""" + return project_root / "qumat" + + def run_mypy(self, target_path: Path) -> tuple[int, str, str]: + """Run mypy on the specified path and return results. + + Args: + target_path: Path to the file or directory to type check. + + Returns: + Tuple of (return_code, stdout, stderr) + """ + result = subprocess.run( + [sys.executable, "-m", "mypy", str(target_path)], + capture_output=True, + text=True, + ) + return result.returncode, result.stdout, result.stderr + + def test_qiskit_backend_types(self, qumat_dir): + """Test that qiskit_backend.py passes mypy type checking.""" + qiskit_backend = qumat_dir / "qiskit_backend.py" + assert qiskit_backend.exists(), "qiskit_backend.py not found" + + returncode, stdout, stderr = self.run_mypy(qiskit_backend) + + # Check for success or only notes/warnings (not errors) + assert returncode == 0, ( + f"Type checking failed for qiskit_backend.py:\n" + f"STDOUT:\n{stdout}\n" + f"STDERR:\n{stderr}" + ) + + def test_cirq_backend_types(self, qumat_dir): + """Test that cirq_backend.py passes mypy type checking.""" + cirq_backend = qumat_dir / "cirq_backend.py" + assert cirq_backend.exists(), "cirq_backend.py not found" + + returncode, stdout, stderr = self.run_mypy(cirq_backend) + + assert returncode == 0, ( + f"Type checking failed for cirq_backend.py:\n" + f"STDOUT:\n{stdout}\n" + f"STDERR:\n{stderr}" + ) + + def test_amazon_braket_backend_types(self, qumat_dir): + """Test that amazon_braket_backend.py passes mypy type checking.""" + amazon_braket_backend = qumat_dir / "amazon_braket_backend.py" + assert amazon_braket_backend.exists(), "amazon_braket_backend.py not found" + + returncode, stdout, stderr = self.run_mypy(amazon_braket_backend) + + assert returncode == 0, ( + f"Type checking failed for amazon_braket_backend.py:\n" + f"STDOUT:\n{stdout}\n" + f"STDERR:\n{stderr}" + ) + + def test_all_backend_modules_together(self, qumat_dir): + """Test that all backend modules pass type checking together. + + This helps catch issues with type consistency across modules. + """ + returncode, stdout, stderr = self.run_mypy(qumat_dir) + + assert returncode == 0, ( + f"Type checking failed for qumat package:\n" + f"STDOUT:\n{stdout}\n" + f"STDERR:\n{stderr}" + ) + + def test_union_type_syntax(self, qumat_dir): + """Test that modern union type syntax (int | None) is supported.""" + qiskit_backend = qumat_dir / "qiskit_backend.py" + + # Read the file and check for modern union syntax + content = qiskit_backend.read_text() + + # Verify modern union syntax is present + assert "int | None" in content or "float | str" in content, ( + "Expected to find modern union type syntax (e.g., 'int | None')" + ) + + # Ensure mypy can handle it + returncode, stdout, stderr = self.run_mypy(qiskit_backend) + assert returncode == 0, ( + f"Modern union syntax caused type checking errors:\n" + f"STDOUT:\n{stdout}\n" + f"STDERR:\n{stderr}" + ) + + def test_no_type_ignore_comments(self, qumat_dir): + """Verify that backend modules don't rely on type: ignore comments. + + This ensures type hints are genuinely correct rather than suppressed. + """ + backend_files = [ + "qiskit_backend.py", + "cirq_backend.py", + "amazon_braket_backend.py", + ] + + for filename in backend_files: + filepath = qumat_dir / filename + content = filepath.read_text() + + # Count type: ignore comments + ignore_count = content.count("# type: ignore") + + # We want minimal to no type: ignore comments + assert ignore_count == 0, ( + f"{filename} contains {ignore_count} '# type: ignore' comment(s). " + f"Type hints should be correct without suppression." + )