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]