Drive the CLI from an argtree Cli spec (argtree 0.1.2)#31
Conversation
Port camas.main.parser's hand-rolled argparse construction to a declarative argtree `Cli` NamedTuple, and consume the typed result in dispatch instead of an `Any`-typed argparse.Namespace. - parser.py: `Cli` spec built by argtree, bridged to CamasArgumentParser (custom --help) via argparse `parents=`. The dynamic per-axis flags, the state-dependent positional metavar, and the env-dependent --effects default stay imperative around the declarative core. - dispatch.py: read a typed `Cli` (reconstruct/from_namespace) rather than poking the namespace; resolve the --effects env default here. - parser.py excluded from mypyc — argtree resolves field types at runtime via get_type_hints, which mypyc erases — and argtree added to build-system requires so the compiled dispatcher's import still type-checks. Behavior unchanged: --help byte-identical, 639 tests pass, 100% coverage, all five type-checkers clean, and the mypyc wheel builds and runs end-to-end. Blocked on argtree enhancements before this is merge-ready: JPHutchins/argtree#9, JPHutchins/argtree#10, JPHutchins/argtree#11. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #31 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 54 54
Lines 5103 5115 +12
Branches 275 277 +2
=========================================
+ Hits 5103 5115 +12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
argtree 0.1.2 ships the three enhancements this branch was waiting on (JPHutchins/argtree#9, #10, #11). Swap the workarounds for the real APIs: - #9: build the parser with `parser_class=CamasArgumentParser` instead of the `parents=[vanilla]` adoption dance. The overload types the result as the subclass, so `.state` assigns without a cast. - #11: the builtin-shadow case is now a clear ConfigError; the `list_` field rename stays (the flag is still `--list`). - #10: argtree's guidance is to keep the spec in an interpreted module, which is exactly what `_main_excluded` already does — no change, now the blessed pattern. Bump the build-system argtree pin to >=0.1.2 since the build-env mypy analyzes parser.py's `parser_class=` usage. Re-verified: --help byte-identical, 639 tests pass, 100% coverage, all five type-checkers clean, mypyc wheel builds and runs (build_parser() returns a CamasArgumentParser through the compiled dispatcher). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates camas’s CLI definition from a hand-written argparse setup to a declarative argtree Cli spec, then updates dispatch to consume a typed CLI object instead of an argparse.Namespace with Any-typed fields.
Changes:
- Introduces an
argtree-basedCliNamedTuplespec and builds the parser viaargtree.build_parser(..., parser_class=CamasArgumentParser). - Updates the dispatcher to use the typed
Cli(viareconstruct(from_namespace)) and resolves the env-dependent--effectsdefault in dispatch. - Updates packaging metadata to include
argtree>=0.1.2at runtime and in[build-system], and excludesparser.pyfrom mypyc compilation.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Locks the new argtree==0.1.2 dependency. |
| tests/main/test_parser.py | Adjusts tests to reconstruct and assert against the typed Cli. |
| src/camas/main/parser.py | Adds the Cli argtree spec, switches parser construction to argtree, and adds reconstruct(). |
| src/camas/main/dispatch.py | Switches to typed CLI consumption and moves --effects default resolution into dispatch. |
| setup.py | Excludes parser.py from mypyc compilation due to runtime type-hint resolution needs. |
| pyproject.toml | Adds argtree>=0.1.2 to runtime deps and build-system requirements. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| matrix: list[str] = arg( | ||
| "--matrix", | ||
| action="append", | ||
| default=[], | ||
| metavar="KEY=VAL[,VAL...]", | ||
| help="override a matrix axis (repeatable; e.g. --matrix PY=3.13)", |
The argtree runtime dependency broke `nix flake check`: nixpkgs' pythonRuntimeDepsCheck reports `Missing dependencies: argtree<1,>=0.1.2` because argtree isn't in the pinned nixpkgs and nix/package.nix never provided it. (Every other CI job was green — only the nix jobs failed.) - nix/argtree.nix: buildPythonPackage for argtree 0.1.2 (hatchling/hatch-vcs, no runtime deps, sdist pinned by hash). doCheck=false — argtree's dev tests depend on camas, so running them here would be a build cycle. - flake.nix: inject argtree into python3Packages via pythonPackagesExtensions so package.nix references it as plain `python3Packages.argtree` (the form a future nixpkgs build uses); extend the nixfmt check to cover argtree.nix. - nix/package.nix: add argtree as a base dependency. Verified locally with the CI command: `nix flake check` passes — all five package variants build (incl. the four mypyc ones), the runtime-deps check passes, nixfmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| effects: str | None = arg( | ||
| "--effects", | ||
| nargs="?", | ||
| const="", | ||
| help="tuple of Effect instances; pass with no value to list available Effects " | ||
| "(default: Termtree, or Status('github') when GITHUB_ACTIONS=true)", | ||
| ) |
There was a problem hiding this comment.
This is tricky. Does the | None really stand in for an empty tuple? Anyway, that's not really what happens. The default is TermTree, unless GitHub CI environment variable is set, and then there's the feature of adding configurable defaults.
My point is that there usually is a default, and it can't be known until runtime, and it should be displayed here, correctly, rather than giving the misleading "| None".
There was a problem hiding this comment.
Aw, the "| None" is for the purpose of listing effects. IDK if we should do this, it feels like help has everything that's needed. Is there precedent for a CLI param meaning "show me the possible args" when left empty?
|
Holding off, unsure of value. Maybe useful if sub commands are added. |
What this is
Replaces camas's hand-rolled
argparseparser with a declarativeargtreeClispec, and consumes the typed result in dispatch instead of anAny-typedargparse.Namespace.Originally opened as an experiment gated on three argtree gaps. Those shipped in argtree 0.1.2, so this now uses the real APIs and is ready for review.
✅ Blockers resolved in argtree 0.1.2
build_parser(..., parser_class=)+ publicadd_arguments()build_parser(Cli, parser_class=CamasArgumentParser, ...)— no moreparents=[vanilla]danceparser.pyexcluded frommypycify; argtree added to[build-system] requiresConfigErrornaming the shadowing fieldlist→list_rename (flag stays--list)What changed
src/camas/main/parser.pyCliNamedTuple spec (one field per arg);build_parserconstructs it directly intoCamasArgumentParserviaparser_class=; newreconstruct()wrapsfrom_namespace. Stays interpreted (excluded from mypyc).src/camas/main/dispatch.pyCliinstead of poking the namespace; resolves the env-dependent--effectsdefault here (None⇒ absent ⇒default_effects_expr(),""⇒ list Effects).setup.pyparser.pyadded to_main_excluded(mypyc).pyproject.tomlargtree>=0.1.2in runtime deps and[build-system] requires.tests/main/test_parser.pyCliviareconstructrather than the raw namespace.The declarative spec is the core; three runtime/state-dependent behaviors stay imperative around it — the dynamic per-matrix-axis
--PYflags, thetask | expressionpositional metavar, and the--effectsdefault. argtree gives the real parser back, so these escape hatches remain available; camas just lands as "declarative core + imperative shell."Why bother — the payoff
The win isn't the prettier spec; it's types.
dispatchnow consumesCli(expression: str | None,dry_run: bool, …) instead of anargparse.Namespacewhere every field isAny. Under five strict type-checkers that removes a class of implicit-Anyreads.Verification (all green)
--helpoutput byte-identical to the current parserdispatch.so→ interpretedparser.py;build_parser()returns aCamasArgumentParserthroughparser_class=, dynamic--PYaxis flags workOpen question for review
Is the typed-dispatch readability worth (a) a runtime dependency on argtree and (b)
parser.pyno longer being mypyc-compiled? Leaning yes; not an obvious slam-dunk over the working argparse.🤖 Generated with Claude Code