From 2769039d2b71028430867627c98906a157c942db Mon Sep 17 00:00:00 2001 From: ptr25 Date: Mon, 28 Sep 2026 19:01:22 +0530 Subject: [PATCH 1/9] Validate that control and target qubits are disjoint Raises ValueError when a command is initialized or when control qubits are added that overlap with target qubits, or when target qubits are not distinct. This prevents obscure IndexError exceptions during optimization passes (fixes #343). --- projectq/meta/_control_test.py | 16 +++++++++++++--- projectq/ops/_command.py | 26 ++++++++++++++++++++++++++ projectq/ops/_command_test.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/projectq/meta/_control_test.py b/projectq/meta/_control_test.py index 73810b95..e7d53260 100755 --- a/projectq/meta/_control_test.py +++ b/projectq/meta/_control_test.py @@ -61,9 +61,9 @@ def test_canonical_representation(): def test_has_negative_control(): qubit0 = WeakQubitRef(None, 0) - qubit1 = WeakQubitRef(None, 0) - qubit2 = WeakQubitRef(None, 0) - qubit3 = WeakQubitRef(None, 0) + qubit1 = WeakQubitRef(None, 1) + qubit2 = WeakQubitRef(None, 2) + qubit3 = WeakQubitRef(None, 3) assert not _control.has_negative_control(Command(None, H, ([qubit0],))) assert not _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1])) assert not _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1], control_state=CtrlAll.One)) @@ -175,3 +175,13 @@ def test_control_state_contradiction(): with _control.Control(eng, qureg[0], '1'): H | qubit eng.flush() + + +def test_control_overlapping_target_raises(): + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) + q0 = eng.allocate_qubit() + with pytest.raises(ValueError, match="Control and target qubits must be disjoint"): + with _control.Control(eng, q0): + H | q0 + eng.flush() diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py index 481efcc1..5fcd59ce 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -126,6 +126,8 @@ def qubits(self): def qubits(self, qubits): """Set the qubits stored in a Command object.""" self._qubits = self._order_qubits(qubits) + if hasattr(self, '_control_qubits'): + self._check_disjoint_qubits() def __deepcopy__(self, memo): """Deepcopy implementation. Engine should stay a reference.""" @@ -230,6 +232,7 @@ def control_qubits(self, qubits): """ self._control_qubits = [WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits] self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) + self._check_disjoint_qubits() @property def control_state(self): @@ -251,6 +254,20 @@ def control_state(self, state): self._control_state = canonical_ctrl_state(state, len(self._control_qubits)) + def _check_disjoint_qubits(self): + """Check that control qubits and target qubits are disjoint, and target qubits are distinct.""" + if hasattr(self, '_qubits') and self._qubits: + target_ids = [q.id for qreg in self._qubits for q in qreg] + if len(target_ids) != len(set(target_ids)): + raise ValueError('Target qubits for a gate must be distinct.') + if hasattr(self, '_control_qubits') and self._control_qubits: + ctrl_ids = {q.id for q in self._control_qubits} + overlap = ctrl_ids.intersection(target_ids) + if overlap: + raise ValueError( + f'Control and target qubits must be disjoint. Overlapping qubit ID(s): {sorted(overlap)}' + ) + def add_control_qubits(self, qubits, state=CtrlAll.One): """ Add (additional) control qubits to this command object. @@ -271,6 +288,13 @@ def add_control_qubits(self, qubits, state=CtrlAll.One): if not isinstance(qubits, list): raise ValueError('Control qubits must be a list of qubits!') + + new_ctrl_ids = {q.id for q in qubits} + target_ids = {q.id for qreg in self._qubits for q in qreg} + overlap = new_ctrl_ids.intersection(target_ids) + if overlap: + raise ValueError(f'Control and target qubits must be disjoint. Overlapping qubit ID(s): {sorted(overlap)}') + self._control_qubits.extend([WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits]) self._control_state += canonical_ctrl_state(state, len(qubits)) @@ -286,6 +310,8 @@ def add_control_qubits(self, qubits, state=CtrlAll.One): f'Control qubits {list(qubits)} cannot have conflicting control states: {states}' ) + self._check_disjoint_qubits() + @property def all_qubits(self): """ diff --git a/projectq/ops/_command_test.py b/projectq/ops/_command_test.py index 19db3644..1cbf2c24 100755 --- a/projectq/ops/_command_test.py +++ b/projectq/ops/_command_test.py @@ -311,3 +311,31 @@ def test_command_to_string(main_engine): else: assert cmd.to_string(symbols=False) == "CRx(1.5707963268) | ( Qureg[1], Qureg[0] )" assert cmd2.to_string(symbols=False) == "Rx(1.5707963268) | Qureg[0]" + + +def test_command_disjoint_control_target(main_engine): + qubit0 = Qubit(main_engine, 0) + qubit1 = Qubit(main_engine, 1) + gate = BasicGate() + + # Overlapping control and target at initialization + with pytest.raises(ValueError, match="Control and target qubits must be disjoint"): + _command.Command(main_engine, gate, ([qubit0],), controls=[qubit0]) + + # Adding an overlapping control qubit + cmd = _command.Command(main_engine, gate, ([qubit0],)) + with pytest.raises(ValueError, match="Control and target qubits must be disjoint"): + cmd.add_control_qubits([qubit0]) + + # Valid disjoint qubits succeed + cmd.add_control_qubits([qubit1]) + assert len(cmd.control_qubits) == 1 + + +def test_command_distinct_target_qubits(main_engine): + qubit0 = Qubit(main_engine, 0) + gate = BasicGate() + + # Duplicate target qubits for a gate + with pytest.raises(ValueError, match="Target qubits for a gate must be distinct"): + _command.Command(main_engine, gate, ([qubit0], [qubit0])) From 9069565c68a065cebeb570ff0533e74f00f6715a Mon Sep 17 00:00:00 2001 From: ptr25 Date: Mon, 28 Sep 2026 19:11:57 +0530 Subject: [PATCH 2/9] Update CHANGELOG and fix compiler dry_run check for modern setuptools - Add CHANGELOG.md entry for #343 and #485 - Handle missing compiler.dry_run attribute in setup.py under setuptools >= 81.0.0 - Use dict.fromkeys in _plot.py to satisfy flake8 comprehension rule --- CHANGELOG.md | 2 ++ projectq/backends/_circuits/_plot.py | 2 +- setup.py | 11 ++++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e58e1202..6dd80666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed some typos (thanks to @eltociear, @Darkdragon84) - Fixed support for Python 3.12 +- Fixed obscure IndexError in optimizer when control and target qubits overlap or are not distinct (#343) +- Fixed installation failure with setuptools >= 81.0.0 due to compiler dry_run attribute (#485) ### Removed diff --git a/projectq/backends/_circuits/_plot.py b/projectq/backends/_circuits/_plot.py index b450309f..c86363b4 100644 --- a/projectq/backends/_circuits/_plot.py +++ b/projectq/backends/_circuits/_plot.py @@ -98,7 +98,7 @@ def to_draw(qubit_lines, qubit_labels=None, drawing_order=None, **kwargs): wires (roughly in inches) """ if qubit_labels is None: - qubit_labels = {qubit_id: r'$|0\rangle$' for qubit_id in qubit_lines} + qubit_labels = dict.fromkeys(qubit_lines, r'$|0\rangle$') else: if list(qubit_labels) != list(qubit_lines): raise RuntimeError('Qubit IDs in qubit_labels do not match qubit IDs in qubit_lines!') diff --git a/setup.py b/setup.py index 24a647bd..1bc10c79 100755 --- a/setup.py +++ b/setup.py @@ -429,8 +429,11 @@ def _get_compilation_commands(self, ext): def _configure_compiler(self): # Force dry_run = False to allow for compiler feature testing - dry_run_old = self.compiler.dry_run - self.compiler.dry_run = False + dry_run_old = getattr(self.compiler, 'dry_run', getattr(self, 'dry_run', False)) + if hasattr(self.compiler, 'dry_run'): + self.compiler.dry_run = False + dry_run_cmd_old = getattr(self, 'dry_run', False) + self.dry_run = False if ( int(os.environ.get('PROJECTQ_CLEANUP_COMPILER_FLAGS', 0)) @@ -472,7 +475,9 @@ def _configure_compiler(self): if compiler_type == 'unix' and compiler_test(self.compiler, '-fvisibility=hidden'): self.opts.append('-fvisibility=hidden') - self.compiler.dry_run = dry_run_old + if hasattr(self.compiler, 'dry_run'): + self.compiler.dry_run = dry_run_old + self.dry_run = dry_run_cmd_old status_msgs('Finished configuring compiler!') def _configure_openmp(self): From 4a9e89317285eb8bc1eec3e417312a130abe11ea Mon Sep 17 00:00:00 2001 From: ptr25 Date: Mon, 28 Sep 2026 19:19:31 +0530 Subject: [PATCH 3/9] Fix NumPy 2.0 deprecations in tests and refine control disjointness check - Fix np.complex_ and np.array copy=False for NumPy 2.0 compatibility in tests - Keep command disjointness check focused strictly on control vs target qubits --- projectq/backends/_sim/_simulator_test.py | 4 ++-- projectq/ops/_command.py | 21 +++++++++----------- projectq/ops/_command_test.py | 9 --------- projectq/setups/decompositions/rz2rx_test.py | 4 ++-- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/projectq/backends/_sim/_simulator_test.py b/projectq/backends/_sim/_simulator_test.py index 1b4b6b7b..0eaa6a23 100755 --- a/projectq/backends/_sim/_simulator_test.py +++ b/projectq/backends/_sim/_simulator_test.py @@ -559,8 +559,8 @@ def build_matrix(list_single_matrices): res_matrix += build_matrix(matrix) * c res_matrix *= -1j * time_to_evolve - init_wavefunction = numpy.array(init_wavefunction, copy=False) - final_wavefunction = numpy.array(final_wavefunction, copy=False) + init_wavefunction = numpy.asarray(init_wavefunction) + final_wavefunction = numpy.asarray(final_wavefunction) res = scipy.sparse.linalg.expm_multiply(res_matrix, init_wavefunction) half = int(len(final_wavefunction) / 2) diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py index 5fcd59ce..3eb69219 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -255,18 +255,15 @@ def control_state(self, state): self._control_state = canonical_ctrl_state(state, len(self._control_qubits)) def _check_disjoint_qubits(self): - """Check that control qubits and target qubits are disjoint, and target qubits are distinct.""" - if hasattr(self, '_qubits') and self._qubits: - target_ids = [q.id for qreg in self._qubits for q in qreg] - if len(target_ids) != len(set(target_ids)): - raise ValueError('Target qubits for a gate must be distinct.') - if hasattr(self, '_control_qubits') and self._control_qubits: - ctrl_ids = {q.id for q in self._control_qubits} - overlap = ctrl_ids.intersection(target_ids) - if overlap: - raise ValueError( - f'Control and target qubits must be disjoint. Overlapping qubit ID(s): {sorted(overlap)}' - ) + """Check that control qubits and target qubits are disjoint.""" + if hasattr(self, '_qubits') and self._qubits and hasattr(self, '_control_qubits') and self._control_qubits: + ctrl_ids = {q.id for q in self._control_qubits} + target_ids = {q.id for qreg in self._qubits for q in qreg} + overlap = ctrl_ids.intersection(target_ids) + if overlap: + raise ValueError( + f'Control and target qubits must be disjoint. Overlapping qubit ID(s): {sorted(overlap)}' + ) def add_control_qubits(self, qubits, state=CtrlAll.One): """ diff --git a/projectq/ops/_command_test.py b/projectq/ops/_command_test.py index 1cbf2c24..08348f03 100755 --- a/projectq/ops/_command_test.py +++ b/projectq/ops/_command_test.py @@ -330,12 +330,3 @@ def test_command_disjoint_control_target(main_engine): # Valid disjoint qubits succeed cmd.add_control_qubits([qubit1]) assert len(cmd.control_qubits) == 1 - - -def test_command_distinct_target_qubits(main_engine): - qubit0 = Qubit(main_engine, 0) - gate = BasicGate() - - # Duplicate target qubits for a gate - with pytest.raises(ValueError, match="Target qubits for a gate must be distinct"): - _command.Command(main_engine, gate, ([qubit0], [qubit0])) diff --git a/projectq/setups/decompositions/rz2rx_test.py b/projectq/setups/decompositions/rz2rx_test.py index 7418c5a1..41322647 100644 --- a/projectq/setups/decompositions/rz2rx_test.py +++ b/projectq/setups/decompositions/rz2rx_test.py @@ -107,8 +107,8 @@ def test_decomposition(angle): # Create empty vectors for the wave vectors for the correct and # test qubits - correct_vector = np.zeros((2, 1), dtype=np.complex_) - test_vector = np.zeros((2, 1), dtype=np.complex_) + correct_vector = np.zeros((2, 1), dtype=complex) + test_vector = np.zeros((2, 1), dtype=complex) i = 0 for fstate in ['0', '1']: From f059f75af676aa27bd39acb6e7320d26348a75f3 Mon Sep 17 00:00:00 2001 From: ptr25 Date: Mon, 28 Sep 2026 19:32:09 +0530 Subject: [PATCH 4/9] Fix Python 3.8 compatibility with azure-quantum dependency --- projectq/backends/_azure/__init__.py | 2 +- projectq/backends/_azure/_azure_quantum.py | 2 +- projectq/backends/_azure/_azure_quantum_client_test.py | 2 +- projectq/backends/_azure/_azure_quantum_test.py | 2 +- projectq/backends/_azure/_utils_test.py | 2 +- pyproject.toml | 3 ++- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/projectq/backends/_azure/__init__.py b/projectq/backends/_azure/__init__.py index 23d2cb3c..b56cff51 100644 --- a/projectq/backends/_azure/__init__.py +++ b/projectq/backends/_azure/__init__.py @@ -16,7 +16,7 @@ try: from ._azure_quantum import AzureQuantumBackend -except ImportError: # pragma: no cover +except (ImportError, TypeError): # pragma: no cover class AzureQuantumBackend: """Dummy class.""" diff --git a/projectq/backends/_azure/_azure_quantum.py b/projectq/backends/_azure/_azure_quantum.py index 682caa69..f9e17fa4 100644 --- a/projectq/backends/_azure/_azure_quantum.py +++ b/projectq/backends/_azure/_azure_quantum.py @@ -39,7 +39,7 @@ from azure.quantum import Workspace from azure.quantum.target import IonQ, Quantinuum, Target from azure.quantum.target.target_factory import TargetFactory -except ImportError: # pragma: no cover +except (ImportError, TypeError): # pragma: no cover raise ImportError( # pylint: disable=raise-missing-from "Missing optional 'azure-quantum' dependencies. To install run: pip install projectq[azure-quantum]" ) diff --git a/projectq/backends/_azure/_azure_quantum_client_test.py b/projectq/backends/_azure/_azure_quantum_client_test.py index 5ce1a6d8..bb69a317 100644 --- a/projectq/backends/_azure/_azure_quantum_client_test.py +++ b/projectq/backends/_azure/_azure_quantum_client_test.py @@ -25,7 +25,7 @@ import azure.quantum # noqa: F401 from projectq.backends._azure._azure_quantum_client import retrieve, send -except ImportError: +except (ImportError, TypeError): _has_azure_quantum = False has_azure_quantum = pytest.mark.skipif(not _has_azure_quantum, reason="azure quantum package is not installed") diff --git a/projectq/backends/_azure/_azure_quantum_test.py b/projectq/backends/_azure/_azure_quantum_test.py index f6d2193d..1f1df1df 100644 --- a/projectq/backends/_azure/_azure_quantum_test.py +++ b/projectq/backends/_azure/_azure_quantum_test.py @@ -30,7 +30,7 @@ import projectq.backends._azure._azure_quantum from projectq.backends import AzureQuantumBackend from projectq.backends._azure._exceptions import AzureQuantumTargetNotFoundError -except ImportError: +except (ImportError, TypeError): _has_azure_quantum = False has_azure_quantum = pytest.mark.skipif(not _has_azure_quantum, reason="azure quantum package is not installed") diff --git a/projectq/backends/_azure/_utils_test.py b/projectq/backends/_azure/_utils_test.py index 8fdf70b4..312fce38 100644 --- a/projectq/backends/_azure/_utils_test.py +++ b/projectq/backends/_azure/_utils_test.py @@ -64,7 +64,7 @@ to_json, to_qasm, ) -except ImportError: +except (ImportError, TypeError): _has_azure_quantum = False has_azure_quantum = pytest.mark.skipif(not _has_azure_quantum, reason="azure quantum package is not installed") diff --git a/pyproject.toml b/pyproject.toml index 6e4686e2..4b38f7c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,8 @@ dependencies = [ [project.optional-dependencies] azure-quantum = [ - 'azure-quantum' + 'azure-quantum <= 3.5.0; python_version <= "3.8"', + 'azure-quantum; python_version > "3.8"' ] braket = [ From 8ff49a4951b0373f0546717ff1ccb4353a641714 Mon Sep 17 00:00:00 2001 From: ptr25 <62867142+ptr25@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:42:15 +0530 Subject: [PATCH 5/9] Preserve dependency marker quotes in fallback requirements parser --- projectq/_setup_test.py | 44 +++++++++++++++++++++++++++++++++++++++++ setup.py | 3 ++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 projectq/_setup_test.py diff --git a/projectq/_setup_test.py b/projectq/_setup_test.py new file mode 100644 index 00000000..70dfc908 --- /dev/null +++ b/projectq/_setup_test.py @@ -0,0 +1,44 @@ +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed 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. +"""Regression tests for source-tree packaging commands.""" + +import subprocess +import sys +from pathlib import Path + +import pytest + + +def test_requirements_fallback_preserves_marker_quotes(tmp_path): + """Requirement generation must work without an optional TOML parser.""" + setup_path = Path(__file__).resolve().parents[1] / 'setup.py' + if not setup_path.is_file(): + pytest.skip('Requires a source checkout') + script = """ +import runpy +import sys +import setuptools + +sys.modules['tomllib'] = None +sys.modules['toml'] = None +sys.argv = [sys.argv[1], 'gen_reqfile', '--include-extras=azure-quantum'] +runpy.run_path(sys.argv[0], run_name='__main__') +""" + result = subprocess.run( + [sys.executable, '-c', script, str(setup_path)], cwd=tmp_path, capture_output=True, text=True, check=False + ) + assert result.returncode == 0, result.stdout + result.stderr + requirements = (tmp_path / 'requirements.txt').read_text().splitlines() + assert 'azure-quantum <= 3.5.0; python_version <= "3.8"' in requirements + assert 'azure-quantum; python_version > "3.8"' in requirements diff --git a/setup.py b/setup.py index 1bc10c79..ac79a272 100755 --- a/setup.py +++ b/setup.py @@ -116,7 +116,8 @@ def _parse_list(lines): continue if line.startswith("]"): return (name, elements, idx + 1) - elements.append(line.rstrip(',').strip("'").strip('"')) + # Remove only the TOML string delimiters, preserving quotes in dependency markers. + elements.append(line.rstrip(',')[1:-1]) raise RuntimeError(f'Failed to locate closing "]" for {name}') From d39b9b681b51299ec1f994e816140ff1311aed74 Mon Sep 17 00:00:00 2001 From: ptr25 <62867142+ptr25@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:42:15 +0530 Subject: [PATCH 6/9] Repair CI dependency builds and legacy runner setup --- .github/scripts/ci-centos.sh | 40 ++++++++++++ .github/workflows/ci.yml | 123 +++++++++++------------------------ .github/workflows/format.yml | 7 ++ 3 files changed, 85 insertions(+), 85 deletions(-) create mode 100644 .github/scripts/ci-centos.sh diff --git a/.github/scripts/ci-centos.sh b/.github/scripts/ci-centos.sh new file mode 100644 index 00000000..06f9dc45 --- /dev/null +++ b/.github/scripts/ci-centos.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Run inside the CentOS container; checkout and other Node actions run on the host. +set -euo pipefail + +if [[ "$CENTOS_VERSION" == 7 ]]; then + vault_version=7.9.2009 +else + vault_version=8.5.2111 +fi + +# Both CentOS releases have moved from the live mirrors to the vault. +sed -i \ + -e 's|^mirrorlist=|#mirrorlist=|' \ + -e "s|^#baseurl=http://mirror.centos.org/centos/\$releasever|baseurl=https://vault.centos.org/$vault_version|" \ + -e 's|^#baseurl=http://mirror.centos.org/$contentdir/$releasever|baseurl=https://vault.centos.org/'"$vault_version"'|' \ + /etc/yum.repos.d/CentOS-*.repo +yum update -y + +if [[ "$CENTOS_VERSION" == 7 ]]; then + yum install -y centos-release-scl-rh + sed -i \ + -e 's|^mirrorlist=|#mirrorlist=|' \ + -e 's|^#baseurl=http://mirror.centos.org/centos/7|baseurl=https://vault.centos.org/7.9.2009|' \ + /etc/yum.repos.d/CentOS-SCLo-scl-rh.repo + yum install -y https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm + yum install -y --enablerepo=centos-sclo-rh rh-python38-python-pip rh-python38-python-devel + export PATH="/opt/rh/rh-python38/root/usr/bin:$PATH" + export LD_LIBRARY_PATH="/opt/rh/rh-python38/root/usr/lib64:${LD_LIBRARY_PATH:-}" +else + yum install -y python38-devel +fi + +yum install -y git gcc-c++ make +git config --global --add safe.directory /src +python3 -m pip install -U pip setuptools wheel +python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket +python3 -m pip install -r requirements.txt --prefer-binary +python3 -m pip install -ve '.[azure-quantum,braket,test]' +echo 'backend: Agg' > matplotlibrc +python3 -m pytest -p no:warnings diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb242ba8..5a456b3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [ubuntu-latest, windows-latest, macos-latest] + runs-on: [ubuntu-latest, windows-latest, macos-15-intel] python: - 3.8 - 3.9 @@ -41,6 +41,10 @@ jobs: git fetch --prune --unshallow git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Install Python runtime library (macOS) + if: runner.os == 'macOS' + run: brew install gettext + - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v4 with: @@ -65,9 +69,17 @@ jobs: run: | python setup.py gen_reqfile --include-extras=test,azure-quantum,braket - - name: Prepare env + - name: Install build tools run: | python -m pip install -U pip setuptools wheel pybind11 + + # RevKit's legacy build imports pybind11 without declaring it as a build dependency. + - name: Build RevKit with preinstalled pybind11 (Unix) + if: runner.os != 'Windows' + run: python -m pip install --no-build-isolation 'revkit==3.0a2.dev2' + + - name: Install dependencies + run: | cat requirements.txt python -m pip install -r requirements.txt --prefer-binary python -m pip install coveralls @@ -123,9 +135,16 @@ jobs: PROJECTQ_CLEANUP_COMPILER_FLAGS: ${{ (matrix.clang <= 10) && 1 || 0 }} name: "Python 3 • Clang ${{ matrix.clang }} • x64" - container: "silkeh/clang:${{ matrix.clang }}" + container: "silkeh/clang:${{ matrix.clang == 11 && '11-bullseye' || matrix.clang }}" steps: + # Use HTTPS and the security archive origin to avoid stale HTTP mirror indexes. + - name: Refresh Debian repository URLs + run: | + find /etc/apt -name '*.list' -exec sed -i \ + -e 's|http://deb.debian.org/debian-security|https://security.debian.org/debian-security|g' \ + -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + + - name: Install Git run: | apt-get update && apt-get install -y git --no-install-recommends @@ -182,6 +201,13 @@ jobs: container: "gcc:${{ matrix.gcc }}" steps: + # Use HTTPS and the security archive origin to avoid stale HTTP mirror indexes. + - name: Refresh Debian repository URLs + run: | + find /etc/apt -name '*.list' -exec sed -i \ + -e 's|http://deb.debian.org/debian-security|https://security.debian.org/debian-security|g' \ + -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + + - uses: actions/checkout@v3 # Work-around for https://github.com/actions/runner-images/issues/6775 @@ -221,99 +247,26 @@ jobs: ./venv/bin/python3 -m pytest -p no:warnings - # Testing on CentOS (manylinux uses a centos base, and this is an easy way - # to get GCC 4.8, which is the manylinux1 compiler). + # Keep JavaScript actions on the host: Node 24 cannot run on CentOS 7's glibc. + # Build and test in the original containers to retain the older compiler coverage. centos: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - centos: - - 7 # GCC 4.8 - - 8 - include: - - centos: 7 - python_pkg: rh-python38-python-pip rh-python38-python-devel - enable_repo: --enablerepo=centos-sclo-rh - - centos: 8 - python_pkg: python38-devel - - + centos: [7, 8] name: "Python 3 • CentOS ${{ matrix.centos }} • x64" - container: "centos:${{ matrix.centos }}" - steps: - - name: Enable cache for yum - run: echo 'keepcache=1' >> /etc/yum.conf - - - name: Setup yum cache - uses: actions/cache@v3 - with: - path: | - /var/cache/yum/ - /var/cache/dnf/ - key: ${{ runner.os }}-centos${{ matrix.centos }}-yum-${{ secrets.yum_cache }} - - - name: Fix repository URLs (CentOS 8 only) - if: matrix.centos == 8 - run: | - sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* - sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* - - - name: Update YUM/DNF - run: yum update -y - - - name: Enable extra repositories && modify PATH (CentOS 7) - if: matrix.centos == 7 - run: | - yum install -y centos-release-scl-rh - yum -y install https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm - echo '/opt/rh/rh-python38/root/usr/bin' >> $GITHUB_PATH - - - name: Add Python 3 and other dependencies - run: yum install -y ${{ matrix.enable_repo }} ${{ matrix.python_pkg }} gcc-c++ make - - - name: Install Git > 2.18 - run: | - yum install -y git - git config --global --add safe.directory /__w/ProjectQ/ProjectQ - - # Work-around for https://github.com/actions/runner-images/issues/6775 - - name: Change Owner of Container Working Directory - run: chown root:root . - - uses: actions/checkout@v3 - - - name: Get history and tags for SCM versioning to work - if: ${{ !env.ACT }} - run: | - git fetch --prune --unshallow - git fetch --depth=1 origin +refs/tags/*:refs/tags/* - - - name: Create pip cache dir - run: mkdir -p ~/.cache/pip - - - name: Cache wheels - uses: actions/cache@v3 with: - path: ~/.cache/pip - key: ${{ runner.os }}-centos${{ matrix.centos }}-pip-${{ hashFiles('**/setup.cfg', '**/pyproject.toml') }} - restore-keys: ${{ runner.os }}-centos-pip- + fetch-depth: 0 - - name: Install dependencies + - name: Build and test in CentOS run: | - python3 -m pip install -U pip setuptools wheel - python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket - cat requirements.txt - python3 -m pip install -r requirements.txt --prefer-binary - - - name: Build and install package - run: python3 -m pip install -ve .[azure-quantum,braket,test] - - - name: Pytest - run: | - echo 'backend: Agg' > matplotlibrc - python3 -m pytest -p no:warnings + docker run --rm \ + -v "$GITHUB_WORKSPACE:/src" -w /src \ + -e CENTOS_VERSION=${{ matrix.centos }} \ + centos:${{ matrix.centos }} bash .github/scripts/ci-centos.sh documentation: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 18a5a10c..a65b8644 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -27,6 +27,13 @@ jobs: CXX: clang++ steps: + # Use HTTPS and the security archive origin to avoid stale HTTP mirror indexes. + - name: Refresh Debian repository URLs + run: | + find /etc/apt -name '*.list' -exec sed -i \ + -e 's|http://deb.debian.org/debian-security|https://security.debian.org/debian-security|g' \ + -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + + - uses: actions/checkout@v3 - name: Prepare env From 4c473bfccb7af946fd422eabaa372b66d7c387a4 Mon Sep 17 00:00:00 2001 From: ptr25 <62867142+ptr25@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:45:41 +0530 Subject: [PATCH 7/9] Pin archived Debian packages and GCC 4.8 build dependencies --- .github/scripts/ci-centos.sh | 8 +++++++- .github/workflows/ci.yml | 8 ++++---- .github/workflows/format.yml | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/scripts/ci-centos.sh b/.github/scripts/ci-centos.sh index 06f9dc45..acde1a8b 100644 --- a/.github/scripts/ci-centos.sh +++ b/.github/scripts/ci-centos.sh @@ -35,6 +35,12 @@ git config --global --add safe.directory /src python3 -m pip install -U pip setuptools wheel python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket python3 -m pip install -r requirements.txt --prefer-binary -python3 -m pip install -ve '.[azure-quantum,braket,test]' +if [[ "$CENTOS_VERSION" == 7 ]]; then + # Retain GCC 4.8 coverage with the last compatible pybind11 series. + python3 -m pip install 'pybind11<2.11' 'setuptools_scm[toml]>6' + python3 -m pip install --no-build-isolation -ve '.[azure-quantum,braket,test]' +else + python3 -m pip install -ve '.[azure-quantum,braket,test]' +fi echo 'backend: Agg' > matplotlibrc python3 -m pytest -p no:warnings diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a456b3d..5c55828b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,11 +138,11 @@ jobs: container: "silkeh/clang:${{ matrix.clang == 11 && '11-bullseye' || matrix.clang }}" steps: - # Use HTTPS and the security archive origin to avoid stale HTTP mirror indexes. + # Bullseye security packages have left the live mirrors; keep this legacy toolchain reproducible. - name: Refresh Debian repository URLs run: | find /etc/apt -name '*.list' -exec sed -i \ - -e 's|http://deb.debian.org/debian-security|https://security.debian.org/debian-security|g' \ + -e 's|^deb http://deb.debian.org/debian-security bullseye-security|deb [check-valid-until=no] https://snapshot.debian.org/archive/debian-security/20260801T000000Z bullseye-security|' \ -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + - name: Install Git @@ -201,11 +201,11 @@ jobs: container: "gcc:${{ matrix.gcc }}" steps: - # Use HTTPS and the security archive origin to avoid stale HTTP mirror indexes. + # Bullseye security packages have left the live mirrors; keep this legacy toolchain reproducible. - name: Refresh Debian repository URLs run: | find /etc/apt -name '*.list' -exec sed -i \ - -e 's|http://deb.debian.org/debian-security|https://security.debian.org/debian-security|g' \ + -e 's|^deb http://deb.debian.org/debian-security bullseye-security|deb [check-valid-until=no] https://snapshot.debian.org/archive/debian-security/20260801T000000Z bullseye-security|' \ -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + - uses: actions/checkout@v3 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index a65b8644..50f13fe1 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -27,11 +27,11 @@ jobs: CXX: clang++ steps: - # Use HTTPS and the security archive origin to avoid stale HTTP mirror indexes. + # Bullseye security packages have left the live mirrors; keep this legacy toolchain reproducible. - name: Refresh Debian repository URLs run: | find /etc/apt -name '*.list' -exec sed -i \ - -e 's|http://deb.debian.org/debian-security|https://security.debian.org/debian-security|g' \ + -e 's|^deb http://deb.debian.org/debian-security bullseye-security|deb [check-valid-until=no] https://snapshot.debian.org/archive/debian-security/20260801T000000Z bullseye-security|' \ -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + - uses: actions/checkout@v3 From a75d8210aacedde4290d7d1f5ba8141694e4f6e0 Mon Sep 17 00:00:00 2001 From: ptr25 <62867142+ptr25@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:48:35 +0530 Subject: [PATCH 8/9] Handle clang-tidy dry runs without removed setuptools spawn argument --- projectq/_setup_test.py | 29 +++++++++++++++++++++++++++++ setup.py | 3 ++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/projectq/_setup_test.py b/projectq/_setup_test.py index 70dfc908..5212d1a1 100644 --- a/projectq/_setup_test.py +++ b/projectq/_setup_test.py @@ -13,9 +13,12 @@ # limitations under the License. """Regression tests for source-tree packaging commands.""" +import runpy import subprocess import sys from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch import pytest @@ -42,3 +45,29 @@ def test_requirements_fallback_preserves_marker_quotes(tmp_path): requirements = (tmp_path / 'requirements.txt').read_text().splitlines() assert 'azure-quantum <= 3.5.0; python_version <= "3.8"' in requirements assert 'azure-quantum; python_version > "3.8"' in requirements + + +@pytest.mark.parametrize('dry_run', [False, True]) +def test_clang_tidy_spawn_compatibility(dry_run): + """Clang-Tidy respects dry runs without relying on removed spawn arguments.""" + from setuptools import Distribution + + setup_path = Path(__file__).resolve().parents[1] / 'setup.py' + if not setup_path.is_file(): + pytest.skip('Requires a source checkout') + with patch('setuptools.setup'): + namespace = runpy.run_path(str(setup_path)) + command = namespace['ClangTidy'](Distribution()) + command.distribution.ext_modules = [] + command.dry_run = dry_run + command.warning_as_errors = True + build = SimpleNamespace(run=lambda: None) + calls = [] + + def spawn(args): + calls.append(args) + + with patch.object(command, 'get_finalized_command', return_value=build): + with patch.dict(command.run.__globals__, spawn=spawn): + command.run() + assert calls == ([] if dry_run else [['clang-tidy', '--warnings-as-errors=*']]) diff --git a/setup.py b/setup.py index ac79a272..957c2de5 100755 --- a/setup.py +++ b/setup.py @@ -686,7 +686,8 @@ def run(self): command.append('--warnings-as-errors=*') for ext in self.distribution.ext_modules: command.extend(os.path.abspath(p) for p in ext.sources) - spawn(command, dry_run=self.dry_run) + if not self.dry_run: + spawn(command) # ------------------------------------------------------------------------------ From 18a3e2387c3a927e07b9331cca0159c33776dd66 Mon Sep 17 00:00:00 2001 From: ptr25 <62867142+ptr25@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:06:10 +0530 Subject: [PATCH 9/9] Fix pre-commit YAML lint and pin legacy hooks to Python 3.11 --- .github/workflows/ci.yml | 8 ++++++-- .github/workflows/format.yml | 4 +++- .pre-commit-config.yaml | 4 ++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c55828b..15ceb0ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,8 +141,10 @@ jobs: # Bullseye security packages have left the live mirrors; keep this legacy toolchain reproducible. - name: Refresh Debian repository URLs run: | + security_snapshot=https://snapshot.debian.org/archive/debian-security/20260801T000000Z + security_source="deb [check-valid-until=no] $security_snapshot bullseye-security" find /etc/apt -name '*.list' -exec sed -i \ - -e 's|^deb http://deb.debian.org/debian-security bullseye-security|deb [check-valid-until=no] https://snapshot.debian.org/archive/debian-security/20260801T000000Z bullseye-security|' \ + -e "s|^deb http://deb.debian.org/debian-security bullseye-security|$security_source|" \ -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + - name: Install Git @@ -204,8 +206,10 @@ jobs: # Bullseye security packages have left the live mirrors; keep this legacy toolchain reproducible. - name: Refresh Debian repository URLs run: | + security_snapshot=https://snapshot.debian.org/archive/debian-security/20260801T000000Z + security_source="deb [check-valid-until=no] $security_snapshot bullseye-security" find /etc/apt -name '*.list' -exec sed -i \ - -e 's|^deb http://deb.debian.org/debian-security bullseye-security|deb [check-valid-until=no] https://snapshot.debian.org/archive/debian-security/20260801T000000Z bullseye-security|' \ + -e "s|^deb http://deb.debian.org/debian-security bullseye-security|$security_source|" \ -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + - uses: actions/checkout@v3 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 50f13fe1..556edaa9 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -30,8 +30,10 @@ jobs: # Bullseye security packages have left the live mirrors; keep this legacy toolchain reproducible. - name: Refresh Debian repository URLs run: | + security_snapshot=https://snapshot.debian.org/archive/debian-security/20260801T000000Z + security_source="deb [check-valid-until=no] $security_snapshot bullseye-security" find /etc/apt -name '*.list' -exec sed -i \ - -e 's|^deb http://deb.debian.org/debian-security bullseye-security|deb [check-valid-until=no] https://snapshot.debian.org/archive/debian-security/20260801T000000Z bullseye-security|' \ + -e "s|^deb http://deb.debian.org/debian-security bullseye-security|$security_source|" \ -e 's|http://deb.debian.org/debian|https://deb.debian.org/debian|g' {} + - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf59ca9c..de1fc785 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,6 +76,8 @@ repos: rev: v3.15.2 hooks: - id: pyupgrade + # This pinned version predates Python 3.14 tokenizer changes. + language_version: python3.11 args: [--py37-plus, --keep-mock] - repo: https://github.com/PyCQA/isort @@ -116,6 +118,8 @@ repos: rev: 'v3.0.0a5' hooks: - id: pylint + # This pinned version predates Python 3.14 AST changes. + language_version: python3.11 args: ['--score=n', '--disable=no-member'] additional_dependencies: [pybind11>=2.6, numpy, requests, matplotlib, networkx]