Skip to content

chore: migrate to modern Python tooling (pyproject.toml + uv) - #267

Open
natestemen wants to merge 10 commits into
mainfrom
265-modern-python-tooling
Open

chore: migrate to modern Python tooling (pyproject.toml + uv)#267
natestemen wants to merge 10 commits into
mainfrom
265-modern-python-tooling

Conversation

@natestemen

Copy link
Copy Markdown
Collaborator

Closes #265.

This migrates the project to modern Python packaging and tooling, consolidating six config files (setup.py, setup.cfg, tox.ini, .pylintrc, MANIFEST.in, three requirements*.txt) into pyproject.toml and adopting uv across CI and the contributor workflow.

What changed

Packaging (2d2a1ae)

  • All metadata moved from setup.py/setup.cfg into [project], with an SPDX license expression
  • The three requirements*.txt files became PEP 735 [dependency-groups] (test, docs, and a dev group that pulls in both)
  • pytest config from setup.cfg + tox.ini merged into [tool.pytest.ini_options]
  • Dropped the importlib-metadata backport in favor of stdlib importlib.metadata
  • uv.lock committed for reproducible CI installs

Versioning (6a6df1f, 6b79b6b)

  • version.py (~100 lines shelling out to git at import time) replaced with setuptools-scm; the version derives from the existing v* tags, so tagged releases need no version-bump commit, and dev installs get the same 1.1.2.dev5+g<sha> flavor of version as before
  • __version__ and the Sphinx release string now come from installed-distribution metadata

Lint config (b082b44, 682469b)

  • .pylintrc translated into [tool.pylint], carrying only non-default values; verified for parity (zero diagnostics before and after)
  • tox.ini deleted; the CI matrix already covers multi-Python, so tox was pure indirection
  • ruff's excludes moved from the pre-commit hook into [tool.ruff]
  • the license-header checker now skips the setuptools-scm-generated _version.py

CI (2ba7fa6)

  • All workflows use astral-sh/setup-uv with locked installs of only the dependency group each job needs; release builds with uv build (with fetch-depth: 0 so setuptools-scm sees the tag)

Docs (0e4b173)

  • CONTRIBUTING.md gains a real dev-environment section (uv sync, uv run pytest, lint/format commands, pre-commit setup); the deprecated python setup.py test instructions are gone from CONTRIBUTING.md and README.md

Heads-up: qiskit 2.5.0 regression (pre-existing, not caused by this PR)

While verifying, the test suite hung at test_forte_rzz_transpiles_to_zz: qiskit 2.5.0 (released 2026-07-02) deadlocks when transpiling to the IonQ native target at optimization_level=3 (the main thread parks forever in a rayon lock inside _accelerate). This reproduces identically on unmodified main with the old pip-based setup — the last green CI run on main (June 5) simply predates the 2.5.0 release, so main's next CI run would hang too. qiskit 2.4.2 is fine.

This PR constrains the locked dev/CI environment to qiskit<2.5 via [tool.uv] constraint-dependencies (published metadata still allows qiskit>=2.0.0). This deserves its own issue — both to remove the constraint once fixed and probably to report upstream to Qiskit.

Deviation from the issue

The issue proposed replacing pylint with ruff. Pylint is intentionally kept (alongside ruff, which already runs via pre-commit): its docparams/docstyle extensions validate docstring parameter lists against signatures, which ruff's stable rules don't cover yet. Its config now lives in pyproject.toml like everything else.

Verification

  • uv run pytest: 417 passed, 89.81% branch coverage (above the 80% floor) on py3.13, plus a py3.10 smoke run against the built wheel
  • uv run pylint -rn qiskit_ionq test: 10.00/10, zero diagnostics (parity with .pylintrc)
  • uv run pre-commit run --all-files: all hooks pass (ruff, ruff-format, mypy, zizmor)
  • uv build: wheel contains py.typed + generated _version.py, excludes test/; installed into a fresh venv and smoke-tested (IonQProvider, gates, __version__)
  • uv run make html: docs build succeeds and the page title shows the scm-derived version

🤖 Generated with Claude Code

@natestemen
natestemen force-pushed the 265-modern-python-tooling branch from 2dbfe9c to 8d5a8f4 Compare July 9, 2026 23:43
@natestemen
natestemen requested a review from splch July 9, 2026 23:44
@natestemen
natestemen marked this pull request as ready for review July 9, 2026 23:44
@natestemen
natestemen requested a review from antalszava July 10, 2026 00:11

@antalszava antalszava left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! 👏 This is a great change, amazing one 💯

Some comments are targetting slight changes to be made before merging, but none are too major to block the PR.

Comment thread CONTRIBUTING.md

```bash
pytest [pytest-args]
uv run pylint -rn qiskit_ionq test

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command is giving some formatting issues atm. for the repo and it's not the one that's being used in the CI (see line 46 of ci.yml):

          # Tests lint with the same config, minus the 50-char cap on function names:
          uv run --no-sync pylint -rn --function-rgx='[a-z_][a-z0-9_]+$' test

Comment thread pyproject.toml
version_file = "qiskit_ionq/_version.py"

[tool.ruff]
extend-exclude = ["docs/conf.py", "tools/verify_headers.py"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note from Claude:

ruff format silently lost coverage of two files. On main, the exclude for docs/conf.py and tools/verify_headers.py applied only to the ruff lint hook; moving it to top-level [tool.ruff] extend-exclude makes ruff format skip them as well (the hook passes --force-exclude). Nothing breaks today (both are format-clean under the pinned ruff 0.8.2), but future edits to those files go unformatted. If lint-only exclusion was the intent, use [tool.ruff.lint] exclude instead.

Comment thread pyproject.toml Outdated
Comment on lines +66 to +69
# qiskit 2.5.0 deadlocks when transpiling to the IonQ native target at
# optimization_level=3 (e.g. test_forte_rzz_transpiles_to_zz); keep the
# locked dev/CI environment on 2.4.x until that is fixed upstream.
constraint-dependencies = ["qiskit<2.5"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may not be relevant anymore with #268 merged and [project] dependencies having the same pin, right?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: was this resolved in 2.5.1 (like in Qiskit/qiskit#16592)?

Comment thread pyproject.toml
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor and may be out of scope: what about 3.14? qiskit supports that (at least on their GH main)

Comment thread pyproject.toml
qiskit_ionq = ["py.typed"]

[tool.setuptools_scm]
version_file = "qiskit_ionq/_version.py"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm gathering is that this file is not being used anywhere in this repo atm. - is that true?

As far as my findings go, from importlib.metadata import version … version("qiskit-ionq") reads installed-package metadata. If that's true then generating "qiskit_ionq/_version.py" is not necessary and the [tool.setuptools_scm] section should be empty for simplicity.

Comment thread CONTRIBUTING.md
pytest [pytest-args]
uv run pylint -rn qiskit_ionq test
uv run ruff check
uv run ruff format

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running this on the repo atm. is producing changes.

@natestemen

Copy link
Copy Markdown
Collaborator Author

Before addressing the changes here, I think it's worth strategizing. From what I recall, @splch wanted to move the repo to use ionq-core internally before this. I don't remember the reason for that, however. Spencer can you provide your thoughts?

natestemen and others added 10 commits August 18, 2026 16:27
Move all package metadata from setup.py/setup.cfg into [project],
replace the three requirements*.txt files with PEP 735
[dependency-groups] (test, docs, dev), and merge the pytest config
from setup.cfg and tox.ini into [tool.pytest.ini_options].

The importlib-metadata backport is dropped from dependencies in favor
of the stdlib importlib.metadata (available since Python 3.8), which
requires a one-line import change in helpers.py. The unused
[tool.isort] block is removed (nothing invokes isort; ruff handles
import sorting via pre-commit). MANIFEST.in is no longer needed:
README and LICENSE are picked up automatically and py.typed ships via
[tool.setuptools.package-data].

Versioning becomes dynamic via setuptools-scm so the package remains
buildable without setup.py; the legacy version.py is removed in a
follow-up commit. A uv.lock is committed for reproducible CI installs.

Part of #265.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete the ~100-line version.py that shelled out to git to compute a
dev version. setuptools-scm now derives the version from git tags at
build time (writing qiskit_ionq/_version.py, which is gitignored), and
runtime consumers read it from the installed distribution metadata via
importlib.metadata: __init__.py exposes __version__ from it and
docs/conf.py uses it for the Sphinx release string.

Dev installs keep the same flavor of version string as before
(e.g. 1.1.2.dev1+g2d2a1ae99) without any subprocess calls at import
time, and tagged releases (v1.1.1-style tags) resolve to the plain
version automatically -- no manual version bump commit needed.

Part of #265.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Translate .pylintrc into [tool.pylint] tables, carrying only values
that differ from pylint's defaults (the [DESIGN] limits were dropped
since every corresponding too-many-* message is disabled anyway) and
additionally ignoring the setuptools-scm-generated _version.py.
Verified for parity: pylint reports zero diagnostics before and after
the translation.

Delete tox.ini -- its pytest config already moved to pyproject.toml
and CI will invoke pytest/pylint through uv directly, with the GitHub
Actions matrix covering multiple Python versions.

Ruff's file exclusions move from the pre-commit hook definition into
[tool.ruff] so they hold anywhere ruff is run. The importlib.metadata
import in helpers.py moves up into the stdlib import block to satisfy
pylint's wrong-import-order now that it no longer comes from the
third-party importlib_metadata backport.

Part of #265.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the setup-python + pip install tox/tox-gh-actions steps with
astral-sh/setup-uv and locked installs from uv.lock:

- tests: uv sync with the test dependency group, then pytest with the
  coverage floor (--cov-fail-under=80) that previously lived in tox.ini
- lint: pylint (config now in pyproject.toml) plus the license-header
  check, from the same test group environment
- docs (PR build and publish): uv sync with the docs group, then
  make html
- pre-commit: run via uvx instead of a pip-installed pre-commit
- release: build the sdist/wheel with uv build; checkout now uses
  fetch-depth: 0 so setuptools-scm can resolve the version from the
  pushed tag

Part of #265.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a development environment section to CONTRIBUTING.md covering
uv sync, pre-commit setup, running tests, linting/formatting, and
building the docs. Remove the deprecated `python setup.py test`
instructions from CONTRIBUTING.md and README.md and replace the
`pip install -r requirements-*.txt` steps with their uv equivalents.

ruff and pre-commit are added to the dev dependency group so the
documented `uv run ruff ...` / `uv run pre-commit ...` commands work
out of the box after `uv sync`.

Closes #265.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The setuptools-scm-generated qiskit_ionq/_version.py carries no
license header and exists in any synced checkout, so verify_headers.py
now skips generated files during discovery.

Part of #265.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`from importlib.metadata import version` left a function bound to the
module-level name `version`, which Sphinx reads as its reserved
version config string and crashes on. Import the module instead.

Part of #265.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
qiskit 2.5.0 (released 2026-07-02) deadlocks when transpiling to the
IonQ native target at optimization_level=3, hanging the test suite at
test_forte_rzz_transpiles_to_zz. This reproduces on main with the old
pip-based setup as well, so it is an upstream regression rather than
anything in this migration -- but any freshly resolved environment
(including CI on main, whose last green run predates the 2.5.0
release) will hit it.

Constrain uv's resolution to qiskit<2.5 so the locked dev/CI
environment stays on 2.4.x. This does not restrict the published
package metadata, which still allows qiskit>=2.0.0. Remove the
constraint once the upstream hang is fixed.

Part of #265.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ugh rebase

Main (PR #268) pinned qiskit<2.5.0 in the requirements files this branch
consolidated into pyproject.toml, and relaxed pylint's function-name length
cap for the test suite via a second tox invocation. Reflect both here:
refresh uv.lock for the new metadata pin and split the CI pylint run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main (PR #272) relaxed the qiskit pin from <2.5.0 to !=2.5.0, treating
the 2.5.0 transpiler hang as fixed in any later release. The rebase
carried that into the consolidated pyproject metadata, so remove the
matching uv constraint-dependencies workaround (added with a note to
delete it once upstream was fixed) and re-lock. The lock still resolves
to qiskit 2.4.2 because no release newer than 2.5.0 exists yet.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
@natestemen
natestemen force-pushed the 265-modern-python-tooling branch from 8d5a8f4 to 5a65d97 Compare August 18, 2026 23:31
@natestemen

Copy link
Copy Markdown
Collaborator Author

Just rebased so this is up to date, but would still like @splch's comments before we push on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate to modern Python project tooling

2 participants