Skip to content

Consolidate packaging metadata into pyproject.toml #344

Description

@dave-doty

Packaging metadata is spread across four files, three of which are legacy setuptools/distutils formats:

file contents status
setup.py all real metadata: name, version, deps, author, URLs, long_description imperative, PEP 517 discourages it
setup.cfg only [metadata] description_file = README.md dead — setup.py already sets long_description, so this does nothing
MANIFEST a tracked file whose first line reads # file GENERATED by distutils, do NOT edit stale build artifact, checked in by accident
doc/requirements.txt Sphinx docs dependencies separate file, hand-synced with install_requires

Consolidate all of it into a single declarative PEP 621 pyproject.toml.

Because setup.py goes away, 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 subsumes #326, which therefore does not need doing separately.

Where the version lives

Decision: version under [project] in pyproject.toml is the single source of truth, and scadnano.__version__ is read back from the installed distribution's metadata via importlib.metadata. Bumping a release is a one-line edit.

This replaces the previous arrangement, where the truth was a __version__ string literal in scadnano/scadnano.py that setup.py and doc/conf.py each parsed with their own copy of an extract_version() helper that searched for a magic trailing comment and split the line on =.

An earlier draft of this issue proposed keeping the literal in source and having pyproject.toml read it via dynamic + version = {attr = "scadnano.__version__"}. That attribute path was wrong — scadnano/__init__.py does from scadnano.scadnano import *, and import * skips names beginning with an underscore, so scadnano.__version__ did not exist at all. Fixed separately by exporting it explicitly.

Consequence: installation becomes mandatory

Reading the version from distribution metadata means import scadnano requires the package to be installed. Copying scadnano.py into a working directory and importing it with nothing installed — previously a documented, supported workflow — now raises PackageNotFoundError. That is intended: scadnano becomes a normal pip-installed package. It requires retiring that workflow from README.md and CONTRIBUTING.md, and fixing 21 example scripts that import origami_rectangle and modifications as top-level modules (which only resolves when the package directory itself is on sys.path, so those were already broken for every pip-installed user).

Consequence: stale metadata under editable installs

pip install -e . makes the source live, not the metadata. pip writes scadnano-<version>.dist-info/METADATA once, at install time, with the version baked into the directory name. Bump the version without reinstalling and scadnano.__version__ keeps reporting the old value — which is then written into the "version" field of every .sc file produced.

Two ways to handle it were considered:

  • A. Detect it. A test asserting scadnano.__version__ equals the pyproject.toml version, turning silent staleness into an immediate failure naming the fix.
  • B. Avoid it. Keep the literal in scadnano.py and have pyproject.toml read it via dynamic + attr:. Eliminates the problem entirely, since an editable install reads the live source — but re-enables importing from a bare checkout, undoing the mandatory-install property above.

Going with A. The window is narrow (between bumping and reinstalling), pip install -e . takes seconds, and run_unit_tests.yml installs immediately before running the suite, so the guard cannot fire spuriously in CI.

Also worth fixing while in there

  • tests_require is silently ignored. setup.py passes tests_require=['openpyxl'], which setuptools no longer recognizes — it warns UserWarning: Unknown distribution option: 'tests_require' on every build. Becomes a tests extra. Noted previously in check_pypi_packaging.yml uses deprecated python setup.py sdist bdist_wheel #326.
  • doc/requirements.txt folds in as a docs extra, so Read the Docs and the Docs Check workflow install one declared list instead of two hand-synced ones.
  • MANIFEST should be deleted and gitignored. It is a generated distutils artifact, not MANIFEST.in, and nothing reads it.
  • CI never installed the package. run_unit_tests.yml installed only openpyxl and tabulate and relied on the source tree being importable — exactly the workflow being removed. It needs pip install -e .[tests].
  • check_pypi_packaging.yml built the artifacts and threw them away. It should install the built wheel into a clean virtualenv and import it from a directory containing no source tree, asserting the version matches and a design serializes. Running from the repo root proves nothing: the flat layout means the checkout shadows the installed package. (A fuller fix for that shadowing is move the package to a src/ layout so tests exercise the installed package #345.)

Verifying the migration

Compare artifacts before and after:

  • python -m build produces scadnano-<version>.tar.gz and scadnano-<version>-py3-none-any.whl, and the wheel contains __init__.py, scadnano.py, modifications.py, origami_rectangle.py.
  • twine check dist/* passes, confirming the README still renders on PyPI — worth checking explicitly, since long_description_content_type='text/markdown; variant=GFM' carries over into readme.content-type and a mistake there is only visible on the PyPI page.
  • Expect metadata to move to the modern PEP 621/639 spellings: Home-page → Project-URL, Author/Author-email combined, License: MIT → License-Expression: MIT. One field is genuinely lost: Download-URL, which was an f-string interpolating the version into a GitHub archive zip. That cannot be expressed in static TOML, and PyPI does not use it.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions