Skip to content

Commit 08a3019

Browse files
authored
[core] Add auto dispatch + Extend the mgit example (#49)
This PR adds Cobra-style auto-dispatch to ArgMojo commands (register per-command handlers and run via `execute()`), expands the `mgit` example to use the new dispatch model (including nested subcommands), and wires in tests + tooling updates to exercise the feature. **Changes:** - Introduce `Command.set_run_function(handler)` and `Command.execute()` with recursive subcommand dispatch (plus `_execute_with_arguments()` test helper). - Add a dedicated test suite validating dispatch behavior across root/subcommands/nested commands, aliases, persistent flags, and error cases. - Extend documentation and examples (`mgit`) to demonstrate and describe auto-dispatch; adjust Pixi tasks to include the new tests and use a new example build script.
1 parent a3ccfcd commit 08a3019

9 files changed

Lines changed: 1578 additions & 176 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ ArgMojo currently supports:
8383
- **Partial parsing**: `parse_known_arguments()` collects unrecognised options instead of erroring
8484
- **Compile-time validation**: builder parameters validated at `mojo build` time via `comptime assert`
8585
- **Registration-time validation**: group constraint typos caught when the program starts, not when the user runs it
86+
- **Auto-dispatch**: `set_run_function(handler)` + `execute()` for Cobra-style automatic subcommand dispatch — no manual `if/elif` chains
8687

8788
---
8889

docs/argmojo_overall_planning.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ These features appear across multiple libraries and depend only on string operat
8585
| Typed retrieval (`get_int()` etc.) |||||| | **Done** |
8686
| Comptime `StringLiteral` params |||||| clap derive macros | **Done** |
8787
| Registration-time name validation |||||| clap panic on unknown ID | **Done** |
88-
| Struct-based schema (reflection) |||||| swift-argument-parser | Phase 7a |
88+
| Struct-based schema (reflection) |||||| swift-argument-parser | **Done** |
89+
| Auto-dispatch (run functions) |||||| Cobra / swift | **Done** |
8990
| Pre/Post run hooks |||||| Cobra | Phase 7a |
9091
| Derive (macro/decorator-based) |||||| clap `#[derive(Parser)]` | Phase 7b |
9192
| Enum → type mapping (real enums) |||||| Requires reflection | Phase 7b |
@@ -289,14 +290,14 @@ Positional arguments and named options are validated **independently** — a com
289290

290291
#### Per-Dimension Behavior
291292

292-
**Positional arguments:**
293+
##### Positional arguments
293294

294295
| Command config ↓ \ User input → | Enough positionals provided | Not enough positionals provided |
295296
| ------------------------------- | --------------------------- | ------------------------------- |
296297
| **Has required positional(s)** | ✓ Proceed | ✗ Error + usage |
297298
| **No required positional(s)** | ✓ Proceed | N/A — always "enough" |
298299

299-
**Named options:**
300+
##### Named options
300301

301302
| Command config ↓ \ User input → | Enough options provided | Not enough options provided |
302303
| ------------------------------- | ----------------------- | --------------------------- |
@@ -569,7 +570,7 @@ Before adding Phase 5 features, further decompose `parse_arguments()` for readab
569570
- [x] **Interactive prompting** — prompt user for missing required args instead of erroring (Click `prompt=True`) (PR #23)
570571
- [x] **Password / masked input** — hide typed characters for sensitive values (Click `hide_input=True`)
571572
- [x] **Confirmation option**`confirmation_option()` or `confirmation_option["prompt"]()` auto-registers `--yes`/`-y` flag; prompts user for confirmation after parsing; aborts on decline or non-interactive stdin (Click `confirmation_option`) (PR #26)
572-
- [ ] **Pre/Post run hooks**callbacks before/after main logic (cobra `PreRun`/`PostRun`)
573+
- [x] **Auto-dispatch**`set_run_function(handler)` registers a `def (ParseResult) raises` function pointer on a `Command`; `execute()` parses and walks the subcommand chain to invoke the matching handler; `_execute_with_arguments(args)` provides the same dispatch for testing. **Lifecycle hooks** (PersistentPreRun/PreRun/PostRun/PersistentPostRun) are not yet implemented — they depend on auto-dispatch and will be added in a future release.
573574
- [x] **Remainder positional**`.remainder()` consumes ALL remaining tokens (including `-` prefixed); at most one per command, must be last positional (argparse `nargs=REMAINDER`, clap `trailing_var_arg`) (PR #13)
574575
- [x] **Allow hyphen values**`.allow_hyphen_values()` on positional accepts dash-prefixed tokens as values without `--`; remainder enables this automatically (clap `allow_hyphen_values`) (PR #13)
575576
- [ ] **Regex validation**`.pattern(r"^\d{4}-\d{2}-\d{2}$")` validates value format (no major library has this)
@@ -580,6 +581,7 @@ Before adding Phase 5 features, further decompose `parse_arguments()` for readab
580581
- [x] **`NO_COLOR` env variable** — honour the [no-color.org](https://no-color.org/) standard: if env `NO_COLOR` is set (any value, including empty), suppress all ANSI colour output; lower priority than explicit `.color(False)` API call (PR #9)
581582
- [x] **Value-name wrapping control**`.value_name[wrapped: Bool = True]("NAME")` displays custom value names in `<NAME>` by default (matching clap/cargo/pixi/git convention); pass `False` for bare display (PR #17)
582583
- [ ] **Extend `implies()`** - support value-taking options with a default value, e.g., `cmd.implies("debug", "output", "debug.log")` — when `--debug` is set, auto-set `--output` to `"debug.log"`. Currently `implies()` only supports flag/count targets (same as cobra in Go). Revisit when there is a concrete use case.
584+
- [ ] **80-character help formatting** — wrap help descriptions at 80 columns with proper indentation (no major library does this by default; users typically pipe through `less` or rely on terminal wrapping)
583585
584586
#### Explicitly Out of Scope in This Phase
585587
@@ -606,7 +608,7 @@ ArgMojo's differentiating features — no other CLI library addresses CJK-specif
606608
--ling 使用宇浩靈明編碼 ← CJK chars each take 2 columns, misaligned
607609
```
608610
609-
**Implementation:**
611+
##### Implementation (CJK alignment)
610612
611613
- [x] Implement `_display_width(s: String) -> Int` in `utils.mojo`, traversing each code point:
612614
- CJK Unified Ideographs, CJK Ext-A/B/C/D/E/F/G/H/I/J, fullwidth forms → width 2
@@ -623,7 +625,7 @@ ArgMojo's differentiating features — no other CLI library addresses CJK-specif
623625
- `--verbose` instead of `--verbose`
624626
- `=` instead of `=`
625627
626-
**Implementation:**
628+
##### Implementation (fullwidth correction)
627629
628630
- [x] Implement `_fullwidth_to_halfwidth(token: String) -> String` in `utils.mojo`:
629631
- Full-width ASCII range: `U+FF01`–`U+FF5E` → subtract `0xFEE0` to get half-width
@@ -652,7 +654,7 @@ Note that the following punctuation characters are already handled by the full-w
652654
- `——verbose` (em-dash `U+2014` × 2) instead of `--verbose`
653655
- `--key:value` (full-width colon `U+FF1A`) instead of `--key=value`
654656
655-
**Implementation:**
657+
##### Implementation (CJK punctuation)
656658
657659
- [x] Integrate with typo suggestion system — when a token fails to match any known option, check for common CJK punctuation patterns before running Levenshtein:
658660
- `——` (`U+2014 U+2014`, 破折號) → `--` (note that `U+FF0D` full-width hyphen-minus is already handled by the full-width correction step)
@@ -679,10 +681,11 @@ The features below are **not part of the core builder API**. They are split into
679681
680682
These features use capabilities already available in Mojo 0.26.2 and can be experimented with immediately.
681683
682-
| Feature | Inspiration | Status | Planning doc |
683-
| ------------------------------ | -------------------------- | ------------- | ---------------------------------------------------------- |
684-
| Declarative / struct-based API | swift-argument-parser | Investigating | [declarative_api_planning.md](declarative_api_planning.md) |
685-
| Pre/Post run hooks | cobra `PreRun` / `PostRun` | Investigating | TBD |
684+
| Feature | Inspiration | Status | Planning doc |
685+
| ------------------------------ | --------------------------- | -------- | ---------------------------------------------------------- |
686+
| Declarative / struct-based API | swift-argument-parser | **Done** | [declarative_api_planning.md](declarative_api_planning.md) |
687+
| Auto-dispatch (run functions) | cobra `Run` / swift `run()` | **Done** | Implemented in command.mojo |
688+
| Pre/Post lifecycle hooks | cobra `PreRun` / `PostRun` | Planned | Depends on auto-dispatch (now available) |
686689
687690
**Declarative API summary** (see [full design doc](declarative_api_planning.md)):
688691
@@ -691,7 +694,9 @@ These features use capabilities already available in Mojo 0.26.2 and can be expe
691694
- **Two innovations beyond Swift**: (1) `to_command()` exposes the underlying `Command` for builder-level tweaks (groups, implications, coloured help); (2) `parse_full()` returns both typed struct + `ParseResult` for hybrid workflows.
692695
- **Optional** — Users who prefer the builder API are completely unaffected. Zero change to existing code.
693696
694-
**Pre/Post run hooks** — Straightforward callback mechanism (`def(ParseResult) raises`). No special language features needed; just needs API design and a decision on execution order with subcommands.
697+
**Auto-dispatch** — Implemented via `set_run_function()` + `execute()`. Registers a non-capturing function pointer (`def (ParseResult) raises`) on each `Command`. `execute()` parses `sys.argv()`, walks the subcommand chain, and invokes the leaf handler. `_execute_with_arguments(args)` provides the same dispatch for testing with explicit argument lists. Works with aliases, nested subcommands, and persistent flags. Closures cannot be stored as struct fields in Mojo 0.26.2 (only non-capturing function pointers via the `def (...) raises` type), so handlers must be free functions.
698+
699+
**Pre/Post lifecycle hooks** — Now unblocked by auto-dispatch. Straightforward extension: add `_pre_run_function` / `_post_run_function` fields with the same function pointer type. Execution order: PersistentPreRun → PreRun → Run → PostRun → PersistentPostRun. Will be implemented in a future release.
695700
696701
#### Phase 7b: Blocked on Mojo Language Features
697702

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This document tracks all notable changes to ArgMojo, including new features, API
66
Unreleased changes should be commented out from here. This file will be edited just before each release to reflect the final changelog for that version. Otherwise, the users would be confused.
77
88
- Add `allow_negative_expressions()` on `Command` — treats single-hyphen tokens as positional arguments when they don't conflict with registered short options. Handles mathematical expressions like `-1/3*pi`, `-sin(2)`, `-e^2`. Superset of `allow_negative_numbers()`.
9+
- Add **auto-dispatch** — `set_run_function(handler)` registers a `def (ParseResult) raises` handler on a `Command`; `execute()` parses and auto-dispatches to the matching handler, eliminating manual `if/elif` subcommand chains. `_execute_with_arguments(args)` provides the same dispatch for testing with explicit argument lists. Works with nested subcommands, aliases, and persistent flags.
910
-->
1011

1112
## 20260404 (v0.5.0)

0 commit comments

Comments
 (0)