Poetry CLI wrapper consumes positional arguments intended for face library subcommand
A Python CLI built with face declares post_posargs=True so a subcommand can take names after the conventional -- separator (the git/tox style; face injects them as post_posargs_). Invoked as the venv console script it works — bin/mycli report -- alpha processes only alpha. Invoked the way the project's docs mandate, it silently processes every item instead and prints a plausible-looking full report that then gets pasted into the wrong place:
$ poetry -C proj run mycli report -- alpha
"alpha": ...
"beta": ...
"gamma": ... # every item, no error, exit 0Assumed the CLI framework was at fault and read face's parser: post_posargs splits at the first -- and injects the tail, and face's own tests confirm the slot is None when no -- appears. That checked out, and the direct-binary invocation proved parsing was fine, so the framework was exonerated. Then assumed the project's docs were wrong and nearly rewrote them.
Minimal probe with no framework involved, Poetry 2.4.1:
$ printf 'import sys\nprint(sys.argv[1:])\n' > /tmp/argvprobe.py
$ python3 /tmp/argvprobe.py -- a b
['--', 'a', 'b']
$ poetry run python /tmp/argvprobe.py -- a b
['a', 'b']No error, no warning, exit 0 either way. A separate clue pointing the same direction — the wrapper reads the inner command's flags as its own:
$ poetry run python -c 'print(1)'
The option "-c" does not existpoetry run consumes the first -- in the command line instead of forwarding it to the child process.
Poetry's CLI (cleo/Symfony-console lineage) parses the entire argv itself, and -- is that parser's own "end of options" marker. It is stripped when building the child's argv, so the child sees the tail without the separator. Confirmed on Poetry 2.4.1:
$ poetry run python /tmp/argvprobe.py -- a b
['a', 'b'] # separator gone
$ poetry run python /tmp/argvprobe.py -- -- a b
['--', 'a', 'b'] # doubling restores it
$ python3 /tmp/argvprobe.py -- a b
['--', 'a', 'b'] # direct invocation, unchangedThe same parser explains the The option "-c" does not exist failure: flags after the executable are matched against Poetry's own options first.
Three fixes, in order of durability:
- Don't depend on the separator surviving. If the post-
--slot is the only way to pass values, accept the plain positional slot too. With face, request both injectables and concatenate:
def report(posargs_: list[str], post_posargs_: list[str]):
names = [*(posargs_ or ()), *(post_posargs_ or ())] or _all_names() post_posargs_ is None (not ()) when no -- was present, so guard with or () before unpacking.
Double the separator at the call site:
poetry run mycli report -- -- alpha. Works, but every caller and every doc line has to remember it.Bypass the wrapper: invoke the venv console script directly, which forwards argv untouched:
"$(poetry env info --path)"/bin/mycli report -- alphaWhy this deserves a guard rather than a doc note: a command whose empty post--- slot falls back to "do everything" turns a dropped separator into a confident wrong answer instead of a usage error. Any fallback reached because an argument slot came back empty should either error or be reachable from every slot the caller might plausibly land in. The same trap applies to poetry run pytest -- -x, poetry run tox -- args, and any git-style passthrough behind poetry run.