Replace argh with cw, and fix the api-pkg-maker console script - #17
Merged
Conversation
http2py depended on argh (LGPL) in three places. Move to cw (MIT, zero runtime
deps). The three were not the same job.
cli_maker.py -- MIGRATED, not deleted. It looks like scrap (a half-built
ancestor of cw), but mk_cli and dispatch_cli are re-exported from
http2py/__init__.py and tests/test_smoke.py asserts them as advertised public
API. Deleting them would break `import http2py`.
The care point there is the naming policy, and getting it wrong would have been
silent. mk_cli called ArghParser().add_commands(...) with NO name_mapping_policy
-- argh's LEGACY mode, which is not what argh.dispatch_commands does (that
explicitly passes BY_NAME_IF_HAS_DEFAULT). Every function register_cli_method
builds is all-keyword-only, and for a keyword-only parameter with no default the
two modes disagree:
argh legacy -> get-thing -u UID -p PID (required options)
BY_NAME_IF_HAS_DEFAULT -> get-thing uid pid (positionals)
cw.ARGH implements the latter, so plain cw.mk_parser would have changed the
spelling of every required API argument with no error anywhere. CLI_CONVENTION
pins cw.BY_NAME_IF_KWONLY, which reproduces the legacy grammar for this
signature shape, and a test asserts the difference so the line cannot be
dropped.
dispatch_cli re-raises a non-zero code: cw.run RETURNS it where argh's
parser.dispatch() exited. Success still returns None and still prints, exactly
as before.
api_pkg_maker.py -- FIXED, which is issue #14's open item 1. `api-pkg-maker` is
this package's only console script and it could not start at all:
`from setuptools import sandbox` raised ImportError at import time, before
main() was ever entered, so the argh call below it had never been reached.
sandbox.run_setup is now `setup.py sdist` in a subprocess -- which also removes
the bare os.chdir(tempdir) that never chdir'd back. OUTPUT_DIR now uses
expanduser rather than os.environ["HOME"], an import-time KeyError on Windows.
The module is therefore importable again and collected by CI again, so its
exclusion is removed from both pyproject.toml and conftest.py. The tripwire test
that guarded "it is still broken" is inverted into one asserting it imports and
that the console script's entry point exists -- which is what that tripwire was
written to force.
Verification: recorded argh's usage line for every command generated from a spec
fixture exercising path args, typed query args, a request body with a required
property, an apiKey scheme, and a no-argument route. Replayed under cw: zero
diffs across all four surfaces. Proven load-bearing -- reverting the convention
fails 3 tests. mk_api_pkg is covered end-to-end by a test that builds a real
sdist offline and asserts the working directory survives.
Closes #16. Advances #14 (item 1 answered: rewrite, not delete).
Claude-Session: https://claude.ai/code/session_01K6LB3AwUmKDxaFNZ2NqPGr
CI caught a real gap in the previous commit: uv-created virtualenvs do not ship setuptools, so `setup.py sdist` exited 1 and `check=True` reported only the exit status -- no cause. The old implementation hid this: `from setuptools import sandbox` made setuptools an undeclared IMPORT-time requirement of the module. Three changes, all about making the requirement visible rather than adding it to the package: - _check_build_backend() raises an actionable RuntimeError naming setuptools and the exact pip command, before any temp directory is created. - A failed `setup.py sdist` now raises with the captured stdout and stderr. check=True discarded exactly the output that says what went wrong. - setuptools added to the `dev` extra, NOT to the runtime dependencies. It is needed by this one function, not by `import http2py`, and CI installs `-e ".[dev]"` -- so the end-to-end test really runs there rather than skipping. The test also gained an importorskip so a bare environment skips it cleanly instead of failing for the wrong reason. Claude-Session: https://claude.ai/code/session_01K6LB3AwUmKDxaFNZ2NqPGr
This was referenced Sep 4, 2026
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.
Closes #16. Advances #14 — its open item 1 ("rewrite
api_pkg_makerfor modernsetuptools, or delete it") is answered: rewrite.
Three modules imported
argh, and they turned out to be three different jobs.1.
cli_maker.py— MIGRATE, not harvest-and-deleteThe brief asked me to decide. Migrate, and it is not a close call:
mk_clianddispatch_cliare re-exported fromhttp2py/__init__.py, andtests/test_smoke.pyalready asserts them by name as "the advertised public surface".Deleting them would break
import http2pyfor anyone using the documented API, and turn anexisting test red. It looks like scrap — a half-built ancestor of
cw— but it isreachable public API, so it gets migrated.
The trap: which argh policy was actually in force
mk_clicalledArghParser().add_commands(cli_methods)with noname_mapping_policy.That is argh's legacy mode, and it is not what
argh.dispatch_commandsdoes — thatone explicitly passes
BY_NAME_IF_HAS_DEFAULT.Every function
register_cli_methodbuilds is all keyword-only (Sig.merge_with_sigis called with
kind=KO). For a keyword-only parameter with no default, the two modesdisagree:
get-thing -u UID -p PID [-l LIMIT]BY_NAME_IF_HAS_DEFAULT(whatcw.ARGHimplements)get-thing [-l LIMIT] uid pidSo a naive
cw.mk_parser(cli_methods)would have silently changed the spelling of everyrequired argument of every generated API client. No error, no warning — the grammar just
quietly moves.
CLI_CONVENTIONpinscw.BY_NAME_IF_KWONLY, which reproduces the legacygrammar for this signature shape.
This is not a cw bug. cw faithfully reproduces argh's explicit
BY_NAME_IF_HAS_DEFAULT. The lesson is that "the repo used argh" does not tell you whichcw convention to use — the argh entry point does, and this fleet contains both shapes.
dispatch_clire-raises a non-zero code, becausecw.runreturns it where argh'sparser.dispatch()exited. Success still returnsNoneand still prints — both of argh'sobservable outcomes are preserved.
Verification
Recorded argh's usage line for every command generated from
tests/spec_fixture.py, a specexercising two required path args (one typed
integer), an optional query arg, a query argwith a schema default, a JSON request body with a
requiredlist, anapiKeysecurityscheme, and a route with no arguments at all (which yields a
*args/**kwargssignature). Replayed under cw: zero diffs across all four surfaces.
Sabotage check, reverted: switching
CLI_CONVENTIONback to cw's default → 3 testsfail.
2.
example_cli.py— migratedSame treatment; still runs, help unchanged.
3.
api_pkg_maker.py— the one that was actually brokenThe report was confirmed, but the cause is not argh:
from setuptools import sandboxat line 18 raises at import time, beforemain()isentered — so
argh.dispatch_commandbelow it had never been reached.api-pkg-makeristhis package's only console script, and it has been dead.
I fixed it rather than filing it again, for two reasons: #14 has been holding this exact
(a)/(b) decision open, and leaving a module unimportable while editing its import block to
remove argh is not a defensible end state.
sandbox.run_setup("setup.py", ["sdist"])→setup.py sdistin a subprocess. Alsoremoves the bare
os.chdir(tempdir)that never chdir'd back — a global side effect thatbroke any caller using relative paths afterwards.
OUTPUT_DIRnow usesexpanduser("~")instead ofos.environ["HOME"], which was animport-time
KeyErroron Windows.api-pkg-maker --helpnow works.mk_api_pkgis covered end-to-end by a new test thatbuilds a real sdist offline, inspects the archive, and asserts the working directory
survives.
Because the module imports again, its exclusion is removed from both
pyproject.tomlandconftest.py. The tripwire test that asserted "it is still broken" isinverted into one asserting it imports and that the entry point exists — which is exactly
the cleanup that tripwire's own docstring said it existed to force.
http2py/testsstaysexcluded; that is a separate concern (it needs
py2http, a test-only dependency CI doesnot install).
Test results
28 passed under
pytest --doctest-modules(what CI runs), up from 15 — and the increase ispartly because
api_pkg_makeris being collected again for the first time.ruff check .clean. No test weakened; the one test whose meaning changed is discussed above.
Note on the release
Merging publishes. Per #14,
0.1.33's long_description on PyPI still does not carry thehodeprecation pointer — the README commit landed 7 seconds after that upload. Thisrelease will carry it.
Follow-up commit: the setuptools requirement CI surfaced
The first push went red, and the failure was worth having.
mk_api_pkgshells out tosetup.py sdist, and a uv-created virtualenv does not ship setuptools — so thesubprocess exited 1 and
check=Truereported only the exit status, with the capturedoutput discarded.
That requirement was always there; the old code just hid it, because
from setuptools import sandboxmade setuptools an undeclared import-time dependency ofthe module. Replacing it with a subprocess moved the requirement from import time to call
time without making it visible. Three changes fix that:
_check_build_backend()raises an actionableRuntimeErrornaming setuptools and theexact
pip installcommand, before any temp directory is created.setup.py sdistnow raises with the captured stdout and stderr.setuptoolsadded to thedevextra, not to the runtime dependencies. It is neededby this one function, not by
import http2py, and CI installs-e ".[dev]"— so theend-to-end test really runs there.
Confirmed:
test_mk_api_pkg_builds_a_source_distributionPASSED on Validation (3.10)and Validation (3.12), not skipped. The test also carries an
importorskipso a bareenvironment skips it cleanly rather than failing for the wrong reason.