feat: Experimental annotated argparse#1666
Conversation
Adds @with_annotated decorator that builds argparse parsers from type-annotated function signatures. Supports Annotated[T, Argument(...)] / Annotated[T, Option(...)] metadata, automatic positional/option detection, optional unwrapping, collections, enums, literals, Path completion, subcommands via subcommand_to=, base_command=True with cmd2_handler dispatch, and argument/mutually-exclusive groups. - New module cmd2/annotated.py with Argument, Option, with_annotated, and build_parser_from_function helpers - Comprehensive test suite in tests/test_annotated.py - Example in examples/annotated_example.py - Docs updates in docs/features/argument_processing.md
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1666 +/- ##
==========================================
+ Coverage 99.55% 99.59% +0.04%
==========================================
Files 22 23 +1
Lines 4920 5414 +494
==========================================
+ Hits 4898 5392 +494
Misses 22 22
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
|
I plan to review this PR this weekend. @kmvanbrunt @bambu I'd very much appreciate your feedback as well. |
|
How can I do the following?
|
- aliases param: Sequence[str] = () to match as_subcommand_to() - update aliases None checks now that it defaults to () - type with_annotated via @overload (no longer untyped decorator) - drop experimental annotated exports from cmd2/__init__.py - import from cmd2.annotated in example/tests/docs; fix example mypy
- Group(*members, title=, description=) for titled argument-group sections (groups= now accepts bare tuples or Group) - description= and epilog= for the generated parser - formatter_class= for a custom help formatter - parser_class= for a custom parser class Includes tests, example command, and docs.
Adds VerbatimHelpFormatter and StrictArgumentParser subclasses and a do_report command so all four parser-customization features have a runnable demonstration, not just docs/tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop bare tuple[str, ...] support; entries must be Group instances. Removes the _group_members shim and adds an explicit TypeError guard (_require_group) so a wrong type fails clearly instead of with an AttributeError. Updates tests and docs accordingly.
Adds a do_export command so mutually_exclusive_groups has a runnable demo alongside the existing groups demo.
…ally subtle conversion edge-case bug
| kwargs["default"] = default | ||
|
|
||
| if is_kw_only and not has_default: | ||
| kwargs["required"] = True |
There was a problem hiding this comment.
Options created via Option() metadata without a default bypass type safety by defaulting to None.
When a required parameter (no default) is converted to an option via Annotated[str, Option("-c")], is_kw_only is False, so kwargs["required"] = True is not set. Argparse makes the flag optional and passes None if it is omitted on the CLI. This violates the str type hint (since it's not str | None) and circumvents Python's standard requirement that parameters without defaults must be provided. Expanding the check to include any non-positional parameter without a default will enforce type safety while preserving the developer's intent.
Possible fix:
if not is_positional and not has_default:
kwargs["required"] = TrueThere was a problem hiding this comment.
I will spend a bit more time and do a larger refactor to catch this type of leaking/edge case combination. I don't think keeping patching various if statements to reach the desired behaviour is ideal.
Adds
@with_annotated, a type-hint-driven alternative to@with_argparserthat builds the parser automatically from a command's signature (positional/option inference, enum/literal/path/collection handling, subcommands, groups, mutex). Marked experimental.cmd2/annotated.pyplusArgument/Optionmetadata classes exported fromcmd2dry_run→--dry-run); opt out viaOption("--my_flag")docs/features/annotated.mdwith an experimental admonition;argument_processing.mdkeeps a short pointerexamples/annotated_example.pyand test suite intests/test_annotated.py