Skip to content

Replace argh with cw, and fix the api-pkg-maker console script - #17

Merged
thorwhalen merged 2 commits into
masterfrom
drop-argh-for-cw
Sep 4, 2026
Merged

Replace argh with cw, and fix the api-pkg-maker console script#17
thorwhalen merged 2 commits into
masterfrom
drop-argh-for-cw

Conversation

@thorwhalen

@thorwhalen thorwhalen commented Sep 4, 2026

Copy link
Copy Markdown
Member

Closes #16. Advances #14 — its open item 1 ("rewrite api_pkg_maker for modern
setuptools, 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-delete

The brief asked me to decide. Migrate, and it is not a close call:

mk_cli and dispatch_cli are re-exported from http2py/__init__.py, and
tests/test_smoke.py already asserts them by name as "the advertised public surface".
Deleting them would break import http2py for anyone using the documented API, and turn an
existing test red. It looks like scrap — a half-built ancestor of cw — but it is
reachable public API, so it gets migrated.

The trap: which argh policy was actually in force

mk_cli called ArghParser().add_commands(cli_methods) with no name_mapping_policy.
That is argh's legacy mode, and it is not what argh.dispatch_commands does — that
one explicitly passes BY_NAME_IF_HAS_DEFAULT.

Every function register_cli_method builds is all keyword-only (Sig.merge_with_sig
is called with kind=KO). For a keyword-only parameter with no default, the two modes
disagree:

grammar
argh legacy (what this module used) get-thing -u UID -p PID [-l LIMIT]
BY_NAME_IF_HAS_DEFAULT (what cw.ARGH implements) get-thing [-l LIMIT] uid pid

So a naive cw.mk_parser(cli_methods) would have silently changed the spelling of every
required argument of every generated API client
. No error, no warning — the grammar just
quietly moves. CLI_CONVENTION pins cw.BY_NAME_IF_KWONLY, which reproduces the legacy
grammar 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 which
cw convention to use — the argh entry point does, and this fleet contains both shapes.

dispatch_cli re-raises a non-zero code, because cw.run returns it where argh's
parser.dispatch() exited. Success still returns None and still prints — both of argh's
observable outcomes are preserved.

Verification

Recorded argh's usage line for every command generated from tests/spec_fixture.py, a spec
exercising two required path args (one typed integer), an optional query arg, a query arg
with a schema default, a JSON request body with a required list, an apiKey security
scheme, and a route with no arguments at all (which yields a *args/**kwargs
signature). Replayed under cw: zero diffs across all four surfaces.

Sabotage check, reverted: switching CLI_CONVENTION back to cw's default → 3 tests
fail
.

2. example_cli.py — migrated

Same treatment; still runs, help unchanged.

3. api_pkg_maker.py — the one that was actually broken

The report was confirmed, but the cause is not argh:

ImportError: cannot import name 'sandbox' from 'setuptools'

from setuptools import sandbox at line 18 raises at import time, before main() is
entered — so argh.dispatch_command below it had never been reached. api-pkg-maker is
this 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 sdist in a subprocess. Also
    removes the bare os.chdir(tempdir) that never chdir'd back — a global side effect that
    broke any caller using relative paths afterwards.
  • OUTPUT_DIR now uses expanduser("~") instead of os.environ["HOME"], which was an
    import-time KeyError on Windows.

api-pkg-maker --help now works. mk_api_pkg is covered end-to-end by a new test that
builds 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.toml and conftest.py. The tripwire test that asserted "it is still broken" is
inverted 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/tests stays
excluded; that is a separate concern (it needs py2http, a test-only dependency CI does
not install).

Test results

28 passed under pytest --doctest-modules (what CI runs), up from 15 — and the increase is
partly because api_pkg_maker is 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 the
ho deprecation pointer — the README commit landed 7 seconds after that upload. This
release will carry it.


Follow-up commit: the setuptools requirement CI surfaced

The first push went red, and the failure was worth having. mk_api_pkg shells out to
setup.py sdist, and a uv-created virtualenv does not ship setuptools — so the
subprocess exited 1 and check=True reported only the exit status, with the captured
output discarded.

That requirement was always there; the old code just hid it, because
from setuptools import sandbox made setuptools an undeclared import-time dependency of
the 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 actionable RuntimeError naming setuptools and the
    exact pip install command, before any temp directory is created.
  • A failed setup.py sdist now raises with the captured stdout and stderr.
  • 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.

Confirmed: test_mk_api_pkg_builds_a_source_distribution PASSED on Validation (3.10)
and Validation (3.12), not skipped. The test also carries an importorskip so a bare
environment skips it cleanly rather than failing for the wrong reason.

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
@thorwhalen
thorwhalen merged commit f164e9c into master Sep 4, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the drop-argh-for-cw branch September 4, 2026 12:05
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.

Replace argh (LGPL) with cw, and fix the api-pkg-maker console script it hides behind

1 participant