Not a bug in cw's grammar — cw is faithful to what it says it implements. It is a
documentation gap that silently breaks migrations, found while migrating
i2mint/http2py (wave apps-2).
The trap
"This repo used argh, so cw.ARGH reproduces it" is false. It depends on which argh
entry point the repo called, because argh's own default is not one thing:
| what the repo wrote |
policy argh actually used |
argh.dispatch_commands(funcs) |
explicitly BY_NAME_IF_HAS_DEFAULT (from argh's own old_name_mapping_policy=True default) |
ArghParser(); parser.add_commands(funcs) |
None — argh's legacy mode |
Those two are not the same grammar. They diverge on one shape: a keyword-only
parameter with no default.
def greet(*, who: str, punct: str = "!"):
"""Greet WHO."""
argh, dispatch_commands (=BY_NAME_IF_HAS_DEFAULT): greet [-h] [-p PUNCT] who
argh, add_commands with no policy (legacy): greet [-h] -w WHO [-p PUNCT]
cw.ARGH (naming='by_name_if_has_default'): greet [-h] [-p PUNCT] who
cw + naming=BY_NAME_IF_KWONLY: greet [-h] -w WHO [-p PUNCT]
cw.ARGH matches argh's explicit BY_NAME_IF_HAS_DEFAULT exactly — correct, and
cw.grammar's KEYWORD_ONLY branch is doing precisely what the policy name says. But a
repo in the second row that migrates to plain cw.dispatch gets a silently different
command line: every required argument stops being -w WHO and becomes a bare positional.
No error, no warning.
Why this is not theoretical
http2py.cli_maker.mk_cli is exactly the second row, and every function it generates is
all-keyword-only (Sig.merge_with_sig(..., kind=KO)). A naive migration would have changed
the spelling of every required argument of every generated API client:
before: get-thing -u UID -p PID [-l LIMIT]
after: get-thing [-l LIMIT] uid pid
Caught only by diffing argh against cw before editing. i2mint/http2py#17 pins
BY_NAME_IF_KWONLY for that module and carries a regression test.
The two repos in the same wave landed on opposite answers, which is the point:
thorwhalen/muvid — argh.dispatch_commands → cw.ARGH, no override. Correct.
i2mint/http2py — ArghParser().add_commands → BY_NAME_IF_KWONLY required.
i2mint/ir — explicit BY_NAME_IF_KWONLY already, preserved.
Suggested fix
Docs, not code — the grammar is right.
- In
cw.convention / cw.grammar next to BY_NAME_IF_HAS_DEFAULT, state that it
reproduces argh's explicit BY_NAME_IF_HAS_DEFAULT (and therefore
argh.dispatch_commands), and that argh's no-policy legacy mode differs for
keyword-only parameters without defaults — for which BY_NAME_IF_KWONLY is the match.
- In
cw.compat, if ArghParser.add_commands is emulated, consider defaulting it to the
legacy-equivalent convention rather than cw.ARGH, since that is what the code it is
replacing did.
- Add the shape to
cw/tests/ as a pinned fixture: def f(*, who: str) under both
namings. It currently has no direct coverage that names this divergence.
Migration-guide wording
Check which argh entry point the repo used before choosing a convention.
argh.dispatch_commands → cw.ARGH. ArghParser() + add_commands() with no
name_mapping_policy → argh's legacy mode; if any command has a keyword-only parameter
with no default, you need naming=cw.BY_NAME_IF_KWONLY. Diff --help either way.
Not a bug in cw's grammar — cw is faithful to what it says it implements. It is a
documentation gap that silently breaks migrations, found while migrating
i2mint/http2py(wave apps-2).The trap
"This repo used argh, so
cw.ARGHreproduces it" is false. It depends on which arghentry point the repo called, because argh's own default is not one thing:
argh.dispatch_commands(funcs)BY_NAME_IF_HAS_DEFAULT(from argh's ownold_name_mapping_policy=Truedefault)ArghParser(); parser.add_commands(funcs)None— argh's legacy modeThose two are not the same grammar. They diverge on one shape: a keyword-only
parameter with no default.
cw.ARGH matches argh's explicit
BY_NAME_IF_HAS_DEFAULTexactly — correct, andcw.grammar'sKEYWORD_ONLYbranch is doing precisely what the policy name says. But arepo in the second row that migrates to plain
cw.dispatchgets a silently differentcommand line: every required argument stops being
-w WHOand becomes a bare positional.No error, no warning.
Why this is not theoretical
http2py.cli_maker.mk_cliis exactly the second row, and every function it generates isall-keyword-only (
Sig.merge_with_sig(..., kind=KO)). A naive migration would have changedthe spelling of every required argument of every generated API client:
Caught only by diffing argh against cw before editing.
i2mint/http2py#17pinsBY_NAME_IF_KWONLYfor that module and carries a regression test.The two repos in the same wave landed on opposite answers, which is the point:
thorwhalen/muvid—argh.dispatch_commands→cw.ARGH, no override. Correct.i2mint/http2py—ArghParser().add_commands→BY_NAME_IF_KWONLYrequired.i2mint/ir— explicitBY_NAME_IF_KWONLYalready, preserved.Suggested fix
Docs, not code — the grammar is right.
cw.convention/cw.grammarnext toBY_NAME_IF_HAS_DEFAULT, state that itreproduces argh's explicit
BY_NAME_IF_HAS_DEFAULT(and thereforeargh.dispatch_commands), and that argh's no-policy legacy mode differs forkeyword-only parameters without defaults — for which
BY_NAME_IF_KWONLYis the match.cw.compat, ifArghParser.add_commandsis emulated, consider defaulting it to thelegacy-equivalent convention rather than
cw.ARGH, since that is what the code it isreplacing did.
cw/tests/as a pinned fixture:def f(*, who: str)under bothnamings. It currently has no direct coverage that names this divergence.
Migration-guide wording