fixes #344; fixes #326; consolidate packaging into pyproject.toml and make it the single source of truth for the version - #347
Merged
dave-doty merged 4 commits intoAug 10, 2026
Conversation
….toml Packaging metadata was spread across setup.py (all the real metadata), setup.cfg (a single dead `description_file` key, since setup.py set long_description explicitly), MANIFEST (a checked-in distutils artifact whose own first line says "do NOT edit") and doc/requirements.txt (docs dependencies, kept in sync with install_requires by hand). All of it now lives in one declarative PEP 621 pyproject.toml; the other four files are deleted. Because setup.py is gone, check_pypi_packaging.yml can no longer build with `python setup.py sdist bdist_wheel`, so it moves to `python -m build`, the PEP 517 front end release.yml already uses. That is #326, which this necessarily subsumes. The version stays exactly where it was, the __version__ line in scadnano/scadnano.py, and remains the single place to edit when releasing. What goes away is the hand-rolled parsing of it: setup.py and doc/conf.py each had a copy of an extract_version() that searched for a magic trailing comment and split the line on "=". Now pyproject.toml declares the version dynamic and reads the attribute, and doc/conf.py imports it from the module autodoc is already importing to document. release.yml still reads the line with sed, since it runs before any Python environment exists. The version is deliberately not moved into pyproject.toml. scadnano.py uses __version__ at run time to stamp designs it writes, and CONTRIBUTING.md commits to users being able to copy scadnano.py on its own and import it without installing the package; sourcing the version from installed distribution metadata would break that. Also: - docs dependencies become the `docs` extra, so Read the Docs and the "Docs Check" workflow install one declared list rather than two hand-synced ones; - `tests_require`, which setuptools stopped recognizing and warned about on every build, becomes the `tests` extra; - run_unit_tests.yml keeps installing openpyxl and tabulate directly rather than via the extra, so a packaging break cannot take the unit tests down with it; - publish_to_pypi.txt described a manual `python setup.py sdist` + twine upload that would now conflict with the automated release; it points at that workflow instead; - AutoStaple.md told contributors to run the long-removed `python setup.py install`. Verified against artifacts built from the previous setup.py: the wheel contents are identical (same four modules plus dist-info), and `twine check` passes on both sdist and wheel. Metadata differences are the expected PEP 621/639 forms: Home-page becomes Project-URL, Author/Author-email combine, and License: MIT becomes License-Expression: MIT. One field is genuinely lost, Download-URL, which pointed at a version-interpolated GitHub archive zip; an f-string cannot be expressed in static TOML, and PyPI does not use the field. Also verified: 375 unit tests pass, and the html, epub and latex builders all succeed with warnings treated as errors, with the version correctly resolving to 0.21.1 in the built docs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rop standalone scadnano.py usage Follows on from the pyproject.toml migration in the previous commit. The version now lives in one place, `version` under [project] in pyproject.toml, and `scadnano.__version__` is read back from the installed distribution's metadata with importlib.metadata. Bumping a release is a one-line edit to pyproject.toml. This makes installation mandatory. scadnano could previously be used by copying scadnano.py into a working directory and importing it with nothing installed; that workflow is now gone, and importing from a bare source checkout raises `PackageNotFoundError: No package metadata was found for scadnano`. The package behaves like any other pip-installed package instead. Version plumbing: - pyproject.toml gains a static `version` and loses `dynamic = ["version"]` and the [tool.setuptools.dynamic] table. Keeping either alongside a static version is a PEP 621 conflict that aborts `python -m build`. - release.yml extracted the version by sed-matching `^__version__ = "..."` in scadnano/scadnano.py. That line no longer holds a literal, so the regex would have matched nothing, the semver guard would have failed, and EVERY release would have died at its first step with nothing tagged or published. It now parses pyproject.toml with tomllib, which is immune to reformatting in a way the regex was not. - scadnano/__init__.py exports __version__ explicitly. `import *` skips names beginning with an underscore, so scadnano.__version__ was not actually reachable for an installed user before this, despite being where anyone would look for it. CI now installs the package, which it never did: - run_unit_tests.yml installed only openpyxl and tabulate and relied on the source tree being importable, i.e. on exactly the workflow being removed here. It installs `-e .[tests]` now; editable so the tests still exercise the checked-out source, which they require anyway since they read data from tests_inputs/ relative to the repo root. - check_pypi_packaging.yml built the artifacts and threw them away. It now installs the built wheel into a clean virtualenv and imports it from a directory containing no source tree, asserting that scadnano.__version__ matches pyproject.toml and that a real design serializes. Running from the repo root would prove nothing, because the flat layout means the checkout shadows the installed package. Documentation and examples assumed the copy workflow: - README.md described two installation methods; the second told users to download scadnano.py onto their PYTHONPATH. Removed, leaving pip as the only path, and the troubleshooting section unnested from the list. That section also claimed xlwt is needed to write Excel files, which has been wrong for some time: the code uses openpyxl, an ordinary declared dependency. - 21 example scripts imported `origami_rectangle` and `modifications` as top-level modules, which only resolves when the package directory itself is on sys.path. They now use `scadnano.origami_rectangle` and `scadnano.modifications`, as does the README's example. - CONTRIBUTING.md dropped the "all required code is in a single file so it can be copied" constraint, and told contributors to skip installing; it now documents `pip install -e .[tests]`. Verified: `python -m build` succeeds and twine check passes on both artifacts; the built wheel installs into a clean virtualenv and imports correctly from a neutral directory reporting 0.21.1; a bare checkout with no install fails loudly as intended; 375 unit tests pass under `pip install -e .[tests]`; the docs build with warnings as errors and resolve the version to 0.21.1; and two rewritten example scripts run to completion. One consequence to be aware of, now documented in CONTRIBUTING.md: because the version comes from installed metadata, bumping it in pyproject.toml without reinstalling leaves the old value in scadnano.__version__, and therefore in the "version" field of any .sc file written before the reinstall. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…le install fails loudly The version is declared once, under [project] in pyproject.toml, and read back at import time from the installed distribution's metadata. `pip install -e .` makes the source live but not that metadata: pip writes it once, at install time, into scadnano-<version>.dist-info/METADATA, with the version baked into the directory name. So bumping the version without reinstalling leaves scadnano.__version__ reporting the previous value, and that stale value is what gets written into the "version" field of every .sc file produced in the meantime. Nothing detected that. This test does, at the moment it matters: it compares the declared version against the installed one and fails with an explanation and the command that fixes it. run_unit_tests.yml installs immediately before running the suite, so it cannot fire spuriously in CI. Skipped on Python 3.10, which the test matrix still covers and which predates stdlib tomllib, and skipped when not run from a source checkout, where there is no pyproject.toml to compare against and the installed metadata is authoritative by definition. Verified both directions: passes in the synced state, and with pyproject.toml temporarily bumped to 0.99.0 against an install of 0.21.1 it fails with "pyproject.toml declares version 0.99.0, but the installed scadnano distribution reports 0.21.1". Full suite is 376 tests, all passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ject-toml #346 landed the same switch to `python -m build` in check_pypi_packaging.yml that this branch already contained. Those two lines merged cleanly; the conflict was only that this branch appends the wheel smoke-test step immediately after them, so the hunks overlapped. Resolved in favour of this branch, whose version of the file is a superset: identical build step, plus the smoke test.
dave-doty
deleted the
344-consolidate-packaging-metadata-into-pyproject-toml
branch
August 10, 2026 17:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #344. Also fixes #326 (deleting
setup.pyforces that change, so it is included here).Three commits, each reviewable on its own.
1.
2c49119— consolidate packaging metadata intopyproject.tomlMetadata lived in four files:
setup.py(everything real),setup.cfg(one deaddescription_filekey),MANIFEST(a checked-in distutils artifact whose own first line saysdo NOT edit), anddoc/requirements.txt(docs deps, hand-synced withinstall_requires). All four are deleted in favour of one declarative PEP 621 file.Docs dependencies become a
docsextra;tests_require— which setuptools stopped recognizing and warned about on every build — becomes atestsextra.Verified against artifacts built from the old
setup.py: the wheel contents are byte-identical, andtwine checkpasses on both. Metadata differences are the expected PEP 621/639 spellings (Home-page→Project-URL,Author/Author-emailcombined,License: MIT→License-Expression: MIT). One field is genuinely lost —Download-URL, an f-string interpolating the version into a GitHub archive zip, which static TOML cannot express and PyPI does not use.2.
88d0c9a— version single-sourced inpyproject.toml; standalonescadnano.pyusage droppedversionunder[project]is now the only place to edit.scadnano.__version__reads it back from installed distribution metadata viaimportlib.metadata.This makes installation mandatory — importing from a bare checkout now raises
PackageNotFoundErrorinstead of silently working. That is the intended trade: scadnano behaves like a normal pip-installed package.Three things this turned up that were already broken:
release.ymlwould have failed every release. It extracted the version by sed-matching^__version__ = "..."inscadnano.py. Once that line stopped holding a literal, the regex would match nothing, the semver guard would fail, and the job wouldexit 1— no tag, no release, nothing published. Now parsespyproject.tomlwithtomllib.scadnano.__version__was never reachable for installed users.__init__.pydoesfrom scadnano.scadnano import *, andimport *skips underscore-prefixed names. Now exported explicitly.origami_rectangleandmodificationsas top-level modules, which only resolves when the package directory itself is onsys.path. Broken for every pip-installed user; now package-qualified.CI never installed the package.
run_unit_tests.ymlnow usespip install -e .[tests](editable, since the tests readtests_inputs/relative to the repo root).check_pypi_packaging.ymlnow installs the built wheel into a clean virtualenv and imports it from a directory containing no source tree — running from the repo root would prove nothing, because the flat layout means the checkout shadows the installed package.README.md and CONTRIBUTING.md drop the copy-
scadnano.pyworkflow. The README's install section also claimedxlwtis needed to write Excel files, which has been wrong for some time — the code usesopenpyxl, an ordinary declared dependency.3.
762374c— guard against stale metadata under editable installspip install -e .makes the source live, not the metadata: pip writesscadnano-<version>.dist-info/METADATAonce, at install time. Bump the version without reinstalling and__version__keeps reporting the old value, which then lands in the"version"field of every.scfile written.A test now compares the two and fails with the command that fixes it. Verified in both directions — passes when synced, and with
pyproject.tomlbumped to0.99.0against an install of0.21.1it fails withpyproject.toml declares version 0.99.0, but the installed scadnano distribution reports 0.21.1.Verification
python -m buildsucceeds;twine checkpasses on sdist and wheel0.21.1pip install -e .[tests]html,epub,latex), version resolving to0.21.1Follow-up
#345 tracks moving to a
src/layout, which is the structural fix for the shadowing described above — the flat layout is why the test suite cannot exercise the wheel, and why the two bugs listed in commit 2 shipped green.🤖 Generated with Claude Code