Skip to content

Modernize packaging + CI so a release can actually reach PyPI (#14) - #15

Merged
thorwhalen merged 1 commit into
masterfrom
claude/issue-14-modernize-packaging-ci
Aug 17, 2026
Merged

Modernize packaging + CI so a release can actually reach PyPI (#14)#15
thorwhalen merged 1 commit into
masterfrom
claude/issue-14-modernize-packaging-ci

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Advances #14. Not closing it — two questions it raises are still open (see "What is left" at the bottom).

The headline, which #14 understates

The ho deprecation pointer has never reached PyPI. The README on master tells people to use ho instead. PyPI does not. Verified:

http2py 0.1.31 uploaded to PyPI 2026-03-06T11:46:58Z
commit adding the ho note to README 2026-03-06 11:47:05Z (7 seconds later)
ho pointer in the PyPI 0.1.31 long_description absent

Every publish since has failed, so anyone running pip install http2py today still gets a package that never mentions its successor. Getting a green CI that can publish is therefore the real deliverable here — the deprecation notice ships as a side effect of the next successful merge.

Why Publish was red

Both master runs on 2026-03-06 show Validation (3.10) => success, Publish (3.10) => failure, with Push Changes and Tag Repository skipped. The failing step is named Publish, and it ran two commands as one step:

twine upload dist/... && epythet make . github

The upload succeeded (that is why 0.1.31 exists on PyPI). epythet make . github then failed, because commit 43aef24 had deleted docs/ (22 files) and docsrc/. Because the step failed, the version push-back and tag were skipped — which is why the tree still said 0.1.30 while PyPI said 0.1.31.

What changed

Packaging

  • New pyproject.toml (hatchling) carrying the metadata ported from setup.cfg: description, keywords, the api-pkg-maker console script, and the dependency set (glom, i2, ju, requests, argh, PyYAML, importlib_resources).
  • SPDX license = "Apache-2.0", no License :: classifier (PEP 639 conflict). Confirmed in the built wheel: License-Expression: Apache-2.0.
  • setup.py and setup.cfg deleted.
  • Version 0.1.320.1.31 is burned. Verified isee gen-semver resolves to 0.1.33 from this state (it reads pyproject 0.1.32 + PyPI 0.1.31), safely past the burned number.

CI

  • .github/workflows/ci.yml replaced wholesale with the current uv-based standard: actions/checkout@v6, astral-sh/setup-uv@v7, and the wads composites setup-python-uv / install-deps-uv / run-tests-uv / build-dist-uv / pypi-publish-uv.
  • Drops actions/checkout@v2, actions/setup-python@v2, and the dead SCRIPTS_REPOSITORY_URL env pointing at a host that no longer resolves.
  • No docs builder. [tool.wads.ci.docs].enabled = false; the pages job is gated on it. docs/ and docsrc/ no longer exist, and re-enabling a docs build re-creates the exact failure above. Verified read-ci-config emits docs-enabled=false (see evidence below) — if it had emitted an empty string the gate != 'false' would have let the job run, which is the trap this note exists for.
  • Test matrix ["3.10", "3.12"]; Windows tests off; metrics off. No system dependencies (pure python), so no [tool.wads.ops.*].

Tests — there were none that ran

tests/ is new. Previously the only test module imported api_pkg_maker and could not be collected at all.

  • tests/test_smoke.py — the package imports; its public names are exported; mk_request_function fills a url_template from path args (asserted through the injectable dispatch seam, so nothing leaves the machine); HttpClient binds the routes an OpenAPI spec declares.
  • tests/test_ci_collection_contract.py — guards the failure mode that was actually biting: under --doctest-modules an unimportable module is not one red test, it aborts the whole session.
  • tests/test_dependency_workarounds.py — see the ju note below.

api_pkg_maker — left in place, excluded from collection

It imports setuptools.sandbox, which modern setuptools no longer ships, so importing it raises ImportError. Per the open question on #14 this PR takes the reversible option: the module is neither rewritten nor deleted, just excluded from collection, in two places that a test keeps in agreement — [tool.wads.ci.testing].exclude_paths and the repo-root conftest.py. A tripwire test fails the moment the module becomes importable again, so a stale exclusion cannot quietly outlive the decision.

Two traps found while verifying, both of which would have kept CI red anyway

1. http2py/tests/conftest.py imported py2http at module level. pytest eagerly imports the conftest.py of any test* sub-directory of a collection root before --ignore is applied — so excluding the directory was not enough. On any machine without py2http (CI included) the session aborted with ImportError while loading conftest before a single test ran. The import now lives inside the fixture that needs it. This one is why "exclude the directory" was not a complete fix, and it is the mutation I care most about.

2. A cold pip install http2py is broken today, upstream of this repo. ju/oas.py imports dill at module level, never uses it, and does not declare it; ju/__init__.py imports ju.oas eagerly. So import http2py -> import ju -> ModuleNotFoundError: No module named 'dill'. Reproduced on a clean Python 3.10 venv against ju==0.1.31 (current PyPI). Filed as i2mint/ju#6; not fixed in passing, since ju is a foundation package with its own release cadence.

This PR carries dill as an explicitly-marked temporary dependency so http2py can be installed and published now, plus a test that goes red as soon as ju is fixed — a workaround with no expiry becomes permanent, and this one has one.

Gates actually run

ruff format / ruff check (D100 enabled — the missing module docstrings were added rather than the rule disabled):

$ uvx ruff format --check .
16 files already formatted
$ uvx ruff check --output-format=concise http2py
All checks passed!

The exact command CI runs, on Python 3.12:

16 passed, 1 warning in 0.67s

The same, on a fresh Python 3.10 venv built the way CI builds it (uv venv --python 3.10, uv pip install -e ".[dev]", no local-source constraints), which is what proves the declared dependency set is complete:

13 passed, 3 skipped, 1 warning in 0.60s

(the 3 skips are tomllib-gated config assertions; tomllib is 3.11+, and the 3.12 leg covers them.)

Build + cold install of the artifact:

Successfully built http2py-0.1.32.tar.gz and http2py-0.1.32-py3-none-any.whl
...
OK  python 3.10.13 | http2py 0.1.32

Wheel metadata confirms the deliverable: the long_description begins with the ho pointer, and 'github.com/i2mint/ho' in description is True.

read-ci-config against the new pyproject:

docs-enabled=false
exclude-paths=http2py/api_pkg_maker.py,http2py/tests
python-versions=["3.10", "3.12"]
publish-enabled=true

Mutation testing

Every guard was re-checked by reintroducing the bug it claims to catch and confirming it goes red; each file was restored from a byte-for-byte backup afterwards. 10/10 mutations turned their guard red.

Bug reintroduced Guard
conftest imports py2http at module level again test_excluded_test_dir_conftest_imports_without_undeclared_deps
api_pkg_maker dropped from the known-broken set test_every_package_module_imports_except_the_known_broken_one
api_pkg_maker made importable test_known_broken_module_is_still_broken
conftest collect_ignore drifts from exclude_paths test_pyproject_and_conftest_exclusions_agree
docs builder re-enabled test_docs_builder_is_disabled
dill dropped while ju#6 is open test_dill_is_declared_while_the_workaround_stands
ju fixed upstream (simulated) test_dill_workaround_is_still_needed
url_template no longer formatted with path args test_mk_request_function_formats_the_url_template_from_path_args
mk_cli dropped from the public API test_public_names_are_exported
HttpClient stops binding spec-declared routes test_http_client_from_openapi_spec_binds_declared_methods

The first run of the harness reported the conftest guard as green, which was a flaw in my mutation (it deleted the names instead of restoring the module-level import) rather than a vacuous test; corrected, it goes red.

Publish credentials

This repo has no repository-level secrets. It inherits the organization-level PYPI_PASSWORD and SSH_PRIVATE_KEY (visibility: all). That pair is confirmed working with uv publishdol, in the same org and with no repo-level secrets either, published 0.3.63 through this identical workflow on 2026-08-10. No secrets were created or modified.

What is left on #14 (why this does not close it)

  • The api_pkg_maker question is unanswered. (a) rewrite it for modern setuptools, or (b) delete it. This PR takes (c) — leave it, exclude it — precisely because neither answer has arrived. The console-script entry point is preserved for the same reason.
  • The ho pointer is not on PyPI until this merges and the Publish job goes green. The claim in this PR is that CI can now publish; the proof is the first green Publish run.
  • ju/oas.py has an unconditional import dill that is never used and is not a declared dependency ju#6 must be fixed and released before the dill dependency here can be dropped.
  • Test coverage is deliberately thin (45%), per the issue's "do not invest in test expansion here" — what was added is the minimum that makes CI meaningful rather than vacuous.

https://claude.ai/code/session_01GPpn5ixPgqGqk7uJH7cC6o

The user-visible point of this change: the README's "use `ho` instead"
pointer has never reached PyPI. 0.1.31 was uploaded at 11:46:58Z on
2026-03-06; the commit adding that note landed 7 seconds later, and every
publish since has failed, so `pip install http2py` still hands people a
package that never mentions its successor. Getting a green, publishable
CI is what ships the deprecation notice.

Why Publish was red: the job ran `twine upload && epythet make . github`
as a single step. The upload succeeded (hence 0.1.31 on PyPI) and the
docs build failed, because an earlier commit had deleted docs/ and
docsrc/. The failure also skipped the push-back and tag steps, which is
why the tree still said 0.1.30.

Packaging
- pyproject.toml (hatchling) carries the metadata ported from setup.cfg:
  deps, keywords, the api-pkg-maker console script, SPDX
  `license = "Apache-2.0"` with no License:: classifier. setup.py and
  setup.cfg deleted.
- Version 0.1.32: 0.1.31 is burned on PyPI. Verified the CI bump lands on
  0.1.33 from here, safely past it.

CI
- .github/workflows/ci.yml replaced with the current uv-based standard:
  checkout@v6, setup-uv@v7, the wads setup-python-uv / install-deps-uv /
  run-tests-uv composites. Drops checkout@v2, setup-python@v2, and the
  dead SCRIPTS_REPOSITORY_URL env pointing at a host that no longer
  resolves.
- No docs builder: `[tool.wads.ci.docs].enabled = false` gates the pages
  job off. docs/ and docsrc/ are gone; re-enabling it re-creates the
  exact failure above.
- Tested on 3.10 and 3.12.

Tests (there were none that ran)
- New tests/ with a real smoke surface: the package imports, its public
  names are exported, mk_request_function fills a url_template from path
  args (via the injectable dispatch seam, so no network), and HttpClient
  binds the routes an OpenAPI spec declares.
- tests/test_ci_collection_contract.py guards the thing that was broken:
  under --doctest-modules an unimportable module is not one red test, it
  aborts the whole session.

api_pkg_maker stays, excluded from collection
- It imports `setuptools.sandbox`, removed from modern setuptools.
  Rewriting it or deleting it is still an open question, so it is left in
  place and excluded from collection instead - the reversible option.
  Excluded in two places that a test keeps in agreement:
  [tool.wads.ci.testing].exclude_paths and the repo-root conftest.

Two traps found while verifying, both of which would have kept CI red
- http2py/tests/conftest.py imported py2http at module level. pytest
  eagerly imports the conftest of any test* subdirectory *before*
  --ignore is applied, so excluding the directory was not enough: a
  machine without py2http (CI included) aborted with "ImportError while
  loading conftest" before any test ran. The import is now inside the
  fixture that needs it.
- A cold `pip install http2py` fails on `import ju`: ju/oas.py imports
  dill at module level, never uses it, and does not declare it. Filed
  upstream as i2mint/ju#6. Carrying `dill` here as a temporary
  dependency, with a test that goes red as soon as upstream is fixed so
  the workaround does not become permanent.

Also added the module docstrings that were missing (they are extracted
for generated docs, and D100 is enabled).

https://claude.ai/code/session_01GPpn5ixPgqGqk7uJH7cC6o
@thorwhalen
thorwhalen merged commit c0cf892 into master Aug 17, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the claude/issue-14-modernize-packaging-ci branch August 17, 2026 19:51
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.

1 participant