From 8f659f82c883f99b892feab50c5eecea54b9e9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Thu, 9 Apr 2026 10:58:20 +0100 Subject: [PATCH 1/2] chore: Remove `pytket` dependency from `tket-py` --- pyproject.toml | 7 +++---- tket-py/pyproject.toml | 5 ++++- tket-py/tket/passes.py | 11 ++++++++--- uv.lock | 10 ++++++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index abc966140..12ef51f70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,10 @@ dev = [ "pip >=25", # Pyyaml 0.6.0 makes setuptools fail. This ensures we're using a newer version. "pyyaml >=6.0.2", - "hugr>=0.16" -] -conan = [ - "conan >= 2.23.0,<3", + "hugr>=0.16", + "pytket>=2.1.0,<3", ] +conan = ["conan >= 2.23.0,<3"] [tool.uv] prerelease = "explicit" diff --git a/tket-py/pyproject.toml b/tket-py/pyproject.toml index d27df0b9a..d2c13206a 100644 --- a/tket-py/pyproject.toml +++ b/tket-py/pyproject.toml @@ -30,11 +30,14 @@ classifiers = [ dependencies = [ 'hugr ~= 0.16.0', - "pytket>=2.1.0,<3", 'tket_eccs ~= 0.5.1', 'tket_exts >= 0.12.3, <0.13', ] +[project.optional-dependencies] +# The pytket python library is only used for type checking. +pytket = ["pytket>=2.1.0,<3"] + [tool.uv.sources] tket_eccs = { workspace = true } tket_exts = { workspace = true } diff --git a/tket-py/tket/passes.py b/tket-py/tket/passes.py index 54bf5b0b5..f010542df 100644 --- a/tket-py/tket/passes.py +++ b/tket-py/tket/passes.py @@ -3,11 +3,9 @@ from pathlib import Path import json from dataclasses import dataclass +from typing import TYPE_CHECKING from hugr import Hugr -from pytket.passes import ( - BasePass, -) from tket import _state from ._tket import passes as _passes, optimiser as _optimiser @@ -20,6 +18,13 @@ ) from hugr.passes.scope import PassScope, GlobalScope +if TYPE_CHECKING: + try: + # Available via the `pytket` extra, and as a dev dependency for testing. + from pytket.passes import BasePass + except ImportError: + from typing import Any as BasePass # type: ignore + __all__ = ["PytketHugrPass", "PassResult", "NormalizeGuppy", "ModifierResolverPass"] diff --git a/uv.lock b/uv.lock index e0f90fa29..36214ef23 100644 --- a/uv.lock +++ b/uv.lock @@ -30,6 +30,7 @@ dev = [ { name = "pre-commit", specifier = "~=4.3" }, { name = "pytest", specifier = ">=8.3.2,<10" }, { name = "pytest-cov", specifier = "~=7.0" }, + { name = "pytket", specifier = ">=2.1.0,<3" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "ruff", specifier = ">=0.6.2,<0.16" }, ] @@ -3056,11 +3057,15 @@ version = "0.13.0" source = { editable = "tket-py" } dependencies = [ { name = "hugr" }, - { name = "pytket" }, { name = "tket-eccs" }, { name = "tket-exts" }, ] +[package.optional-dependencies] +pytket = [ + { name = "pytket" }, +] + [package.dev-dependencies] docs = [ { name = "furo" }, @@ -3075,10 +3080,11 @@ docs = [ [package.metadata] requires-dist = [ { name = "hugr", specifier = "~=0.16.0" }, - { name = "pytket", specifier = ">=2.1.0,<3" }, + { name = "pytket", marker = "extra == 'pytket'", specifier = ">=2.1.0,<3" }, { name = "tket-eccs", editable = "tket-eccs" }, { name = "tket-exts", editable = "tket-exts" }, ] +provides-extras = ["pytket"] [package.metadata.requires-dev] docs = [ From a3d04e4483ed0f93d0d3b617fd0217501b1836f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Borgna?= Date: Thu, 9 Apr 2026 16:16:44 +0100 Subject: [PATCH 2/2] Remove pytket as a dev-dependency --- .github/workflows/ci.yml | 4 +-- .github/workflows/release-checks.yml | 2 +- .pre-commit-config.yaml | 2 +- justfile | 2 +- pyproject.toml | 1 - qis-compiler/python/tests/generate_hugrs.py | 1 + tket-py/test/test_circuit.py | 28 +++++++++++++-------- tket-py/test/test_optimiser.py | 15 +++++++---- tket-py/test/test_pass.py | 8 +++--- tket-py/test/test_pauli_prop.py | 17 +++++++------ tket-py/test/test_portmatching.py | 7 ++++-- tket-py/test/test_scopes.py | 12 ++++++--- tket-py/tket/_tket/state.pyi | 7 +++++- uv.lock | 1 - 14 files changed, 67 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09302fd3f..8f43b6aeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: run: uv python install ${{ env.PYTHON_LOWEST }} - name: Setup dependencies # avoid building qis-compiler - run: uv sync --package=tket --exact --python ${{ env.PYTHON_LOWEST }} + run: uv sync --package=tket --exact --all-extras --python ${{ env.PYTHON_LOWEST }} - name: Type check with mypy run: uv run mypy . - name: Check formatting with ruff @@ -297,7 +297,7 @@ jobs: run: uv python install ${{ env.PYTHON_LOWEST }} - name: Setup dependencies # avoid building qis-compiler - run: uv sync --package=tket --exact --python ${{ env.PYTHON_LOWEST }} + run: uv sync --package=tket --exact --all-extras --python ${{ env.PYTHON_LOWEST }} - name: Run python tests with coverage instrumentation run: uv run --no-project pytest --cov=./ --cov-report=xml ${{ env.PKGS }} - name: Upload python coverage to codecov.io diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml index cb0959d57..c89466337 100644 --- a/.github/workflows/release-checks.yml +++ b/.github/workflows/release-checks.yml @@ -53,7 +53,7 @@ jobs: RES: ${{ matrix.target.resolution }} run: | UV_RESOLUTION="$RES" uv lock --no-sources -U - UV_RESOLUTION="$RES" uv sync --no-sources --no-install-workspace + UV_RESOLUTION="$RES" uv sync --all-extras --no-sources --no-install-workspace uv pip install --no-sources tket-exts uv pip install --no-sources tket-eccs uv run --no-sync maturin develop diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cddd8545..13d996a91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: - id: mypy-check name: mypy description: Check python code with `mypy`. - entry: uv run mypy . + entry: uv run --all-extras mypy . language: system files: \.py$ pass_filenames: false diff --git a/justfile b/justfile index 151ef196c..5d0baedfc 100644 --- a/justfile +++ b/justfile @@ -18,7 +18,7 @@ _check_default_conan_profile: # setting up the pre-commit hooks. setup: && _check_default_conan_profile _check_nextest_installed uv tool install conan - uv sync + uv sync --all-extras [[ -n "${TKET_JUST_INHIBIT_GIT_HOOKS:-}" ]] || uv run pre-commit install -t pre-commit # Run the pre-commit checks. diff --git a/pyproject.toml b/pyproject.toml index 12ef51f70..a8c2ac2e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ dev = [ # Pyyaml 0.6.0 makes setuptools fail. This ensures we're using a newer version. "pyyaml >=6.0.2", "hugr>=0.16", - "pytket>=2.1.0,<3", ] conan = ["conan >= 2.23.0,<3"] diff --git a/qis-compiler/python/tests/generate_hugrs.py b/qis-compiler/python/tests/generate_hugrs.py index 81b91bcb1..46fdfe65e 100644 --- a/qis-compiler/python/tests/generate_hugrs.py +++ b/qis-compiler/python/tests/generate_hugrs.py @@ -3,6 +3,7 @@ # dependencies = [ # "guppylang ==0.21.9", # "tket", +# "pytket >=2.1.0,<3", # ] # /// diff --git a/tket-py/test/test_circuit.py b/tket-py/test/test_circuit.py index d6c611e2b..6b982f4c0 100644 --- a/tket-py/test/test_circuit.py +++ b/tket-py/test/test_circuit.py @@ -1,6 +1,6 @@ -from dataclasses import dataclass +import pytest -from pytket._tket.circuit import Circuit +from dataclasses import dataclass from tket._state import ( CompilationState, @@ -20,7 +20,11 @@ def __add__(self, other): def test_cost(): - circ = CompilationState.from_tket1(Circuit(4).CX(0, 1).H(1).CX(1, 2).CX(0, 3).H(0)) + pytket = pytest.importorskip("pytket") + + circ = CompilationState.from_tket1( + pytket.Circuit(4).CX(0, 1).H(1).CX(1, 2).CX(0, 3).H(0) + ) print(circ.circuit_cost(lambda op: int(op == TketOp.CX))) @@ -31,16 +35,19 @@ def test_cost(): def test_hash(): - circA = CompilationState.from_tket1(Circuit(4).CX(0, 1).CX(1, 2).CX(0, 3)) - circB = CompilationState.from_tket1(Circuit(4).CX(1, 2).CX(0, 1).CX(0, 3)) - circC = CompilationState.from_tket1(Circuit(4).CX(0, 1).CX(0, 3).CX(1, 2)) + pytket = pytest.importorskip("pytket") + + circA = CompilationState.from_tket1(pytket.Circuit(4).CX(0, 1).CX(1, 2).CX(0, 3)) + circB = CompilationState.from_tket1(pytket.Circuit(4).CX(1, 2).CX(0, 1).CX(0, 3)) + circC = CompilationState.from_tket1(pytket.Circuit(4).CX(0, 1).CX(0, 3).CX(1, 2)) assert hash(circA) != hash(circB) assert hash(circA) == hash(circC) def test_conversion(): - tk1 = Circuit(4).CX(0, 2).CX(1, 2).CX(1, 3) + pytket = pytest.importorskip("pytket") + tk1 = pytket.Circuit(4).CX(0, 2).CX(1, 2).CX(1, 3) tk2 = CompilationState.from_tket1(tk1) mermaid = tk2.render_mermaid() @@ -51,11 +58,12 @@ def test_conversion(): tk1_back = tk2.to_tket1() assert tk1_back == tk1 - assert type(tk1_back) is Circuit + assert type(tk1_back) is pytket.Circuit def test_conversion_qsystem(): - tk1 = Circuit(2).ZZPhase(0.75, 0, 1).PhasedX(0.25, 0.33, 1) + pytket = pytest.importorskip("pytket") + tk1 = pytket.Circuit(2).ZZPhase(0.75, 0, 1).PhasedX(0.25, 0.33, 1) tk2 = CompilationState.from_tket1(tk1) mermaid = tk2.render_mermaid() @@ -71,4 +79,4 @@ def test_conversion_qsystem(): tk1_back = tk2.to_tket1() assert tk1_back == tk1 - assert type(tk1_back) is Circuit + assert type(tk1_back) is pytket.Circuit diff --git a/tket-py/test/test_optimiser.py b/tket-py/test/test_optimiser.py index 5dfcfe88e..feeee83d9 100644 --- a/tket-py/test/test_optimiser.py +++ b/tket-py/test/test_optimiser.py @@ -1,4 +1,5 @@ -from pytket import Circuit +import pytest + from tket._state import CompilationState from tket._rewrite import ECCRewriter from tket._optimiser import BadgerOptimiser @@ -6,25 +7,29 @@ def test_simple_optimiser(): """a simple circuit matching test""" - tk = CompilationState.from_tket1(Circuit(3).CX(0, 1).CX(0, 1).CX(1, 2)) + pytket = pytest.importorskip("pytket") + tk = CompilationState.from_tket1(pytket.Circuit(3).CX(0, 1).CX(0, 1).CX(1, 2)) opt = BadgerOptimiser.compile_eccs("test_files/eccs/cx_cx_eccs.json") opt.optimise(tk._inner, max_circuit_count=3) cc = tk.to_tket1() - exp_c = Circuit(3).CX(1, 2) + exp_c = pytket.Circuit(3).CX(1, 2) assert cc == exp_c def test_compose_rewriter(): """test composing rewriters.""" - tk = CompilationState.from_tket1(Circuit(3).CX(0, 1).CX(0, 1).H(0).H(0).CX(0, 2)) + pytket = pytest.importorskip("pytket") + tk = CompilationState.from_tket1( + pytket.Circuit(3).CX(0, 1).CX(0, 1).H(0).H(0).CX(0, 2) + ) cx_rewriter = ECCRewriter.compile_eccs("test_files/eccs/cx_cx_eccs.json") h_rewriter = ECCRewriter.compile_eccs("test_files/eccs/h_h_eccs.json") opt = BadgerOptimiser([cx_rewriter, h_rewriter]) opt.optimise(tk._inner, max_circuit_count=3) cc = tk.to_tket1() - exp_c = Circuit(3).CX(0, 2) + exp_c = pytket.Circuit(3).CX(0, 2) assert cc == exp_c diff --git a/tket-py/test/test_pass.py b/tket-py/test/test_pass.py index bdd5e947b..547b37acc 100644 --- a/tket-py/test/test_pass.py +++ b/tket-py/test/test_pass.py @@ -1,4 +1,3 @@ -from pytket import Circuit, OpType from typing import Callable, Any from tket._ops import TketOp from tket.passes import ( @@ -16,14 +15,15 @@ from hypothesis import given, settings from tket.passes import PytketHugrPass -from pytket.passes import CliffordSimp, SquashRzPhasedX, SequencePass from hugr.build.base import Hugr import pytest - - from pathlib import Path +pytket = pytest.importorskip("pytket") +from pytket import Circuit, OpType # noqa: E402 +from pytket.passes import CliffordSimp, SquashRzPhasedX, SequencePass # noqa: E402 + normalize = NormalizeGuppy() diff --git a/tket-py/test/test_pauli_prop.py b/tket-py/test/test_pauli_prop.py index 4fee24a9c..2a9eb37ef 100644 --- a/tket-py/test/test_pauli_prop.py +++ b/tket-py/test/test_pauli_prop.py @@ -1,7 +1,6 @@ import itertools import pytest -from pytket._tket.circuit import Circuit from hugr.ops import Custom from hugr.hugr import Wire @@ -160,6 +159,8 @@ def map_op(op: Custom) -> str: @pytest.mark.skip(reason="Broken with hugr 0.8.0. See comment in `propagate_matcher`.") def test_simple_z_prop(propagate_matcher: RuleMatcher): + pytket = pytest.importorskip("pytket") + c = CircBuild.with_nqb(2) (h_node_e, *_) = c.extend(H(0), H(0), CX(0, 1)) @@ -169,17 +170,19 @@ def test_simple_z_prop(propagate_matcher: RuleMatcher): add_error_after(t2c, h_node_e[0], PauliX) - assert t2c.to_tket1() == Circuit(2).H(0).X(0).H(0).CX(0, 1) + assert t2c.to_tket1() == pytket.Circuit(2).H(0).X(0).H(0).CX(0, 1) assert apply_exhaustive(t2c, propagate_matcher) == 2 - assert t2c.to_tket1() == Circuit(2).H(0).H(0).CX(0, 1).Z(0) + assert t2c.to_tket1() == pytket.Circuit(2).H(0).H(0).CX(0, 1).Z(0) assert final_pauli_string(t2c) == "ZI" @pytest.mark.skip(reason="Broken with hugr 0.8.0. See comment in `propagate_matcher`.") def test_cat(propagate_matcher: RuleMatcher): + pytket = pytest.importorskip("pytket") + c = CircBuild.with_nqb(4) (h_node, *_) = c.extend( H(2), @@ -191,13 +194,13 @@ def test_cat(propagate_matcher: RuleMatcher): t2c = c.finish() add_error_after(t2c, h_node[0], PauliX) - assert t2c.to_tket1() == Circuit(4).H(2).X(2).CX(2, 1).CX(2, 3).CX(1, 0) + assert t2c.to_tket1() == pytket.Circuit(4).H(2).X(2).CX(2, 1).CX(2, 3).CX(1, 0) assert apply_exhaustive(t2c, propagate_matcher) == 3 - assert t2c.to_tket1() == Circuit(4).H(2).CX(2, 1).CX(2, 3).CX(1, 0).X(0).X(1).X( - 2 - ).X(3) + assert t2c.to_tket1() == pytket.Circuit(4).H(2).CX(2, 1).CX(2, 3).CX(1, 0).X(0).X( + 1 + ).X(2).X(3) assert final_pauli_string(t2c) == "XXXX" diff --git a/tket-py/test/test_portmatching.py b/tket-py/test/test_portmatching.py index 41a58790d..c3ddbfcce 100644 --- a/tket-py/test/test_portmatching.py +++ b/tket-py/test/test_portmatching.py @@ -1,8 +1,11 @@ -from pytket import Circuit -from pytket.qasm import circuit_from_qasm_str +import pytest from tket._pattern import CircuitPattern, PatternMatcher from tket._state import CompilationState +pytket = pytest.importorskip("pytket") +from pytket import Circuit # noqa: E402 +from pytket.qasm import circuit_from_qasm_str # noqa: E402 + def _tk(circ: Circuit): """Convert a pytket Circuit to a Rust CompilationState.""" diff --git a/tket-py/test/test_scopes.py b/tket-py/test/test_scopes.py index b60da86ef..7e98a9e5f 100644 --- a/tket-py/test/test_scopes.py +++ b/tket-py/test/test_scopes.py @@ -1,7 +1,7 @@ -from .test_pass import _hugr_from_path, _count_ops +import pytest +from .test_pass import _hugr_from_path, _count_ops -from pytket.passes import FullPeepholeOptimise from tket.passes import NormalizeGuppy, PytketHugrPass from hugr.passes.scope import GlobalScope, LocalScope @@ -9,10 +9,12 @@ def test_nested_function_opt_global() -> None: + pytket = pytest.importorskip("pytket") + h = _hugr_from_path("test_files/guppy_optimization/nested/nested.flat.hugr") h_normalized = normalize(h) - fpo = PytketHugrPass(FullPeepholeOptimise()) + fpo = PytketHugrPass(pytket.passes.FullPeepholeOptimise()) fpo_preserve_entrypoint = fpo.with_scope(GlobalScope.PRESERVE_ENTRYPOINT) opt_hugr = fpo_preserve_entrypoint(h_normalized) @@ -22,10 +24,12 @@ def test_nested_function_opt_global() -> None: def test_nested_function_opt_local() -> None: + pytket = pytest.importorskip("pytket") + h = _hugr_from_path("test_files/guppy_optimization/nested/nested.flat.hugr") h_normalized = normalize(h) - fpo = PytketHugrPass(FullPeepholeOptimise()) + fpo = PytketHugrPass(pytket.passes.FullPeepholeOptimise()) fpo_local_flat = fpo.with_scope(LocalScope.FLAT) flat_opt_hugr = fpo_local_flat(h_normalized) diff --git a/tket-py/tket/_tket/state.pyi b/tket-py/tket/_tket/state.pyi index 52544cb0a..2b84c4388 100644 --- a/tket-py/tket/_tket/state.pyi +++ b/tket-py/tket/_tket/state.pyi @@ -1,9 +1,14 @@ from typing import Any, Callable -from pytket._tket.circuit import Circuit as Tk1Circuit from tket._tket.ops import TketOp from hugr.envelope import EnvelopeConfig +try: + from pytket._tket.circuit import Circuit as Tk1Circuit +except ImportError: + # Pytket is only available as an optional dependency under the `pytket` extra. + from typing import Any as Tk1Circuit # type: ignore + class CompilationState: """Program state definition. diff --git a/uv.lock b/uv.lock index 36214ef23..c48338080 100644 --- a/uv.lock +++ b/uv.lock @@ -30,7 +30,6 @@ dev = [ { name = "pre-commit", specifier = "~=4.3" }, { name = "pytest", specifier = ">=8.3.2,<10" }, { name = "pytest-cov", specifier = "~=7.0" }, - { name = "pytket", specifier = ">=2.1.0,<3" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "ruff", specifier = ">=0.6.2,<0.16" }, ]