cmd2 4.1.2__tar.gz → 4.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cmd2-4.1.2 → cmd2-4.2.0}/.pre-commit-config.yaml +1 -1
- {cmd2-4.1.2 → cmd2-4.2.0}/CHANGELOG.md +29 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/PKG-INFO +2 -2
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/annotated.py +65 -47
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/argparse_completer.py +4 -2
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/PKG-INFO +2 -2
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/requires.txt +1 -1
- cmd2-4.2.0/cmd2.egg-info/scm_version.json +8 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/annotated.md +35 -10
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/annotated_example.py +35 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commandsets.py +3 -2
- {cmd2-4.1.2 → cmd2-4.2.0}/pyproject.toml +1 -1
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_annotated.py +173 -31
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_argparse_completer.py +52 -0
- cmd2-4.1.2/cmd2.egg-info/scm_version.json +0 -8
- {cmd2-4.1.2 → cmd2-4.2.0}/.prettierignore +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/.prettierrc +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/.typos.toml +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/GEMINI.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/LICENSE +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/MANIFEST.in +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/Makefile +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/README.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/__init__.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/argparse_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/clipboard.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/cmd2.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/colors.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/command_set.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/constants.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/decorators.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/exceptions.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/history.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/parsing.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/plugin.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/pt_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/py.typed +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/py_bridge.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/rich_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/string_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/styles.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/theme.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/types.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/SOURCES.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/dependency_links.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/scm_file_list.json +14 -14
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/top_level.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.png +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/codecov.yml +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/annotated.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/argparse_completer.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/argparse_utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/clipboard.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/cmd.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/colors.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/command_set.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/completion.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/constants.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/decorators.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/exceptions.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/history.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/parsing.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/plugin.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/pt_utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/py_bridge.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/rich_utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/string_utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/styles.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/theme.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/doc_conventions.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/examples/alternate_event_loops.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/examples/examples.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/examples/getting_started.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/examples/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/argument_processing.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/async_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/builtin_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/clipboard.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/completion.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/disable_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/embedded_python_shells.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/generating_output.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/help.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/history.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/hooks.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/initialization.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/misc.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/modular_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/multiline_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/os.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/packaging.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/plugins.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/prompt.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/redirection.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/scripting.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/settings.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/shortcuts_aliases_macros.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/startup_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/table_creation.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/theme.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/javascripts/readthedocs.js +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/incompatibilities.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/minimum.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/next_steps.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/why.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/mixins/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/mixins/mixin_template.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/overrides/main.html +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/alternatives.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/installation.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/integrating.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/resources.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/stylesheets/cmd2.css +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/testing.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/docs/upgrades.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/.cmd2rc +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/README.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/argparse_completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/argparse_example.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/async_call.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/async_commands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/async_printing.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/basic_completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/cmd_as_argument.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/color.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/command_sets.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/completion_item_choices.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/custom_parser.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/custom_types.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/default_categories.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/dynamic_commands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/environment.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/event_loops.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/exit_code.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/getting_started.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/hello_cmd2.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/help_categories.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/hooks.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/migrating.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/mixin.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commands/__init__.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commands/commandset_basic.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commands/commandset_complex.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commands/commandset_custominit.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/paged_output.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/persistent_history.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/pretty_print.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/python_scripting.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/read_input.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/remove_builtin_commands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/remove_settable.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/rich_tables.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/rich_theme.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/arg_printer.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/conditional.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/nested.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/quit.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/save_help_text.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/script.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/script.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/tmux_launch.sh +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/tmux_split.sh +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/examples/unicode_commands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/mkdocs.yml +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/package.json +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/ruff.toml +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/scripts/validate_tag.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/setup.cfg +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/.cmd2rc +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/__init__.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/conftest.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/echo.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/environment.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/help.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/py_locals.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/pyscript_dir.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/raises_exception.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/recursive.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/self_in_py.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/stop.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/test_print.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/relative_multiple.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/script.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/script.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/binary.bin +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/empty.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/help.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/nested.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/one_down.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/postcmds.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/precmds.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/utf8.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_argparse.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_argparse_subcommands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_argparse_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_categories.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_cmd2.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_commandset.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_dynamic_complete_style.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_future_annotations.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_history.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_parsing.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_plugin.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_pt_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_py_completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_rich_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_run_pyscript.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_string_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_theme.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_utils_defining_class.py +0 -0
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## 4.2.0 (August 6, 2026)
|
|
2
|
+
|
|
3
|
+
- Enhancements
|
|
4
|
+
- `@with_annotated` argument groups can now contain an `ArgumentBlock`'s arguments. A `Group`
|
|
5
|
+
member names a command-line argument, and a block expands into one argument per field, so its
|
|
6
|
+
fields are named: `Group("host", "port")`.
|
|
7
|
+
- Breaking Changes
|
|
8
|
+
- A `Group` member now names an argument rather than a parameter. The two differ only for an
|
|
9
|
+
`ArgumentBlock` parameter, which is expanded away and has no argument of its own:
|
|
10
|
+
`Group("conn")` now raises `ValueError` pointing at the block's fields. It previously produced
|
|
11
|
+
an empty argument group in `groups=` and a `KeyError` in `mutually_exclusive_groups=`.
|
|
12
|
+
- Whether a `Group` member exists is now validated when the parser is built rather than at
|
|
13
|
+
decoration time, because a block's field names cannot be known without resolving its type
|
|
14
|
+
hint, and resolving hints eagerly would break forward-referenced annotations. A typo in a
|
|
15
|
+
member name still raises `ValueError`, but on first use of that command rather than at class
|
|
16
|
+
definition. The spec-shape rules (a member listed twice, a member in two groups,
|
|
17
|
+
`required=True` on a plain group, the mutex nesting rules) are unaffected and still hard-fail
|
|
18
|
+
at decoration time.
|
|
19
|
+
- Bug Fixes
|
|
20
|
+
- Fixed `@with_annotated(base_command=True)` not listing its subcommands under the positional
|
|
21
|
+
arguments section of the parent command's `--help`, unlike `argparse` and
|
|
22
|
+
`Cmd2ArgumentParser`. They were placed in an untitled section of their own instead. Passing
|
|
23
|
+
`subcommand_title` or `subcommand_description` still gives the subcommands a dedicated section
|
|
24
|
+
([#1715](https://github.com/python-cmd2/cmd2/issues/1715)).
|
|
25
|
+
- Fix `@with_annotated` decorator so using `ArgumentBlock` works with groups
|
|
26
|
+
([#1718](https://github.com/python-cmd2/cmd2/issues/1718)).
|
|
27
|
+
- Fixed bug where already sorted `choices_provider` results were being re-sorted
|
|
28
|
+
([#1727](https://github.com/python-cmd2/cmd2/issues/1727)).
|
|
29
|
+
|
|
1
30
|
## 4.1.2 (July 16, 2026)
|
|
2
31
|
|
|
3
32
|
- Enhancements
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmd2
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python
|
|
5
5
|
Author: cmd2 Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: prompt-toolkit>=3.0.
|
|
24
|
+
Requires-Dist: prompt-toolkit>=3.0.53
|
|
25
25
|
Requires-Dist: pyperclip>=1.8.2
|
|
26
26
|
Requires-Dist: rich>=15.0.0
|
|
27
27
|
Requires-Dist: rich-argparse>=1.7.2
|
|
@@ -175,9 +175,11 @@ dataclass constructor at reconstruction time, so a ``default_factory`` yields a
|
|
|
175
175
|
argument. A field whose type is itself a block is not expanded (no recursion) and raises the
|
|
176
176
|
unsupported-type error. Because fields expand flat, every field name must be unique across the command's
|
|
177
177
|
own parameters and every other block's fields -- a collision (which would put two argparse actions on one
|
|
178
|
-
namespace dest) raises ``TypeError`` when the parser is built. Block fields
|
|
179
|
-
``groups=`` / ``mutually_exclusive_groups
|
|
180
|
-
|
|
178
|
+
namespace dest) raises ``TypeError`` when the parser is built. Block fields can participate in
|
|
179
|
+
``groups=`` / ``mutually_exclusive_groups=``: a group names arguments, and because a block expands to one
|
|
180
|
+
argument per field, you name its *field* names. Since resolving those field names needs the block's
|
|
181
|
+
type hints, this membership is checked when the parser is built rather than at decoration time. A block
|
|
182
|
+
must be the *bare* annotation of a regular parameter:
|
|
181
183
|
wrapping it in ``Annotated``/``Optional``/a union, or using it as ``*args`` / ``**kwargs``, raises
|
|
182
184
|
``TypeError`` (to use a dataclass as a single value instead, make it a plain ``@dataclass`` with an
|
|
183
185
|
``Argument(converter=...)``). An ``ArgumentBlock`` subclass that is not a ``@dataclass`` has no fields and
|
|
@@ -414,7 +416,7 @@ class _BaseArgMetadata:
|
|
|
414
416
|
"""
|
|
415
417
|
reserved = self._RESERVED_EXTRA_KWARGS & extra_kwargs.keys()
|
|
416
418
|
if reserved:
|
|
417
|
-
name =
|
|
419
|
+
name = min(reserved)
|
|
418
420
|
# Per-key remediation hint for the reserved kwarg.
|
|
419
421
|
hint = {
|
|
420
422
|
"type": (
|
|
@@ -552,25 +554,20 @@ class Group:
|
|
|
552
554
|
) -> None:
|
|
553
555
|
"""Initialise an argument group definition.
|
|
554
556
|
|
|
555
|
-
:param members:
|
|
557
|
+
:param members: argument names to place in the group (at least one). A plain parameter's name is its
|
|
558
|
+
argument name; for an ``ArgumentBlock`` name its fields.
|
|
556
559
|
:param title: group title shown as a help section header
|
|
557
560
|
:param description: group description shown under the title
|
|
558
561
|
:param required: ``mutually_exclusive_groups`` only -- require exactly one member.
|
|
559
562
|
On a ``groups=`` entry it raises ``ValueError``.
|
|
560
563
|
"""
|
|
561
564
|
if not members:
|
|
562
|
-
raise ValueError("Group requires at least one member
|
|
565
|
+
raise ValueError("Group requires at least one member argument name")
|
|
563
566
|
self.members = members
|
|
564
567
|
self.title = title
|
|
565
568
|
self.description = description
|
|
566
569
|
self.required = required
|
|
567
570
|
|
|
568
|
-
def _validate_members(self, *, all_param_names: set[str], group_type: str) -> None:
|
|
569
|
-
"""Validate that every referenced member parameter exists."""
|
|
570
|
-
for name in self.members:
|
|
571
|
-
if name not in all_param_names:
|
|
572
|
-
raise ValueError(f"{group_type} references nonexistent parameter {name!r}")
|
|
573
|
-
|
|
574
571
|
|
|
575
572
|
#: Metadata extracted from ``Annotated[T, meta]``, or ``None`` for plain types.
|
|
576
573
|
ArgMetadata = Argument | Option | None
|
|
@@ -2072,6 +2069,39 @@ def _shared_field_dest(dc_type: type, field_name: str) -> str:
|
|
|
2072
2069
|
return constants.cmd2_private_attr_name(f"shared_{id(dc_type):x}_{field_name}")
|
|
2073
2070
|
|
|
2074
2071
|
|
|
2072
|
+
def _check_group_members_exist(
|
|
2073
|
+
by_name: dict[str, _ArgparseArgument],
|
|
2074
|
+
block_param_names: set[str],
|
|
2075
|
+
func_qualname: str,
|
|
2076
|
+
*specs: tuple[Group, ...] | None,
|
|
2077
|
+
) -> None:
|
|
2078
|
+
"""Reject a ``Group`` member that names no command-line argument, once every argument is built.
|
|
2079
|
+
|
|
2080
|
+
:func:`_validate_group_specs` cannot do this at decoration time: an ArgumentBlock's arguments are its
|
|
2081
|
+
dataclass fields, which only resolving the block's type hint would reveal, and resolving hints there
|
|
2082
|
+
would break forward-referenced annotations. So this is where a bad member name is finally caught.
|
|
2083
|
+
|
|
2084
|
+
Naming the block parameter itself is the likely mistake -- it reads natural but the parameter is expanded
|
|
2085
|
+
away and has no argument -- so it gets its own message pointing at the fields to name instead.
|
|
2086
|
+
"""
|
|
2087
|
+
for spec_group in specs:
|
|
2088
|
+
for spec in spec_group or ():
|
|
2089
|
+
for name in spec.members:
|
|
2090
|
+
if name in by_name:
|
|
2091
|
+
continue
|
|
2092
|
+
if name in block_param_names:
|
|
2093
|
+
raise ValueError(
|
|
2094
|
+
f"group in {func_qualname} references {name!r}, which is an ArgumentBlock parameter "
|
|
2095
|
+
f"and not a command-line argument: a block is expanded into one argument per field, "
|
|
2096
|
+
f"so name those fields instead."
|
|
2097
|
+
)
|
|
2098
|
+
raise ValueError(
|
|
2099
|
+
f"group in {func_qualname} references {name!r}, which is not a command-line argument. "
|
|
2100
|
+
f"Members name arguments: a plain parameter is its own argument, and an ArgumentBlock's "
|
|
2101
|
+
f"arguments are its field names."
|
|
2102
|
+
)
|
|
2103
|
+
|
|
2104
|
+
|
|
2075
2105
|
def _link_mutex_group_membership(
|
|
2076
2106
|
by_name: dict[str, _ArgparseArgument],
|
|
2077
2107
|
mutually_exclusive_groups: tuple[Group, ...] | None,
|
|
@@ -2079,8 +2109,7 @@ def _link_mutex_group_membership(
|
|
|
2079
2109
|
"""Append each mutex group's 1-based index to its member arguments' ``mutex_group_indices``.
|
|
2080
2110
|
|
|
2081
2111
|
This membership is the fact behind the required-member constraint row. Member references are
|
|
2082
|
-
|
|
2083
|
-
resolves to a built argument.
|
|
2112
|
+
resolved to built arguments upstream by :func:`_check_group_members_exist` before this runs.
|
|
2084
2113
|
"""
|
|
2085
2114
|
if not mutually_exclusive_groups:
|
|
2086
2115
|
return
|
|
@@ -2352,6 +2381,7 @@ def _resolve_parameters(
|
|
|
2352
2381
|
*,
|
|
2353
2382
|
skip_params: frozenset[str] = _SKIP_PARAMS,
|
|
2354
2383
|
base_command: bool = False,
|
|
2384
|
+
groups: tuple[Group, ...] | None = None,
|
|
2355
2385
|
mutually_exclusive_groups: tuple[Group, ...] | None = None,
|
|
2356
2386
|
) -> tuple[list[_ArgparseArgument], set[type]]:
|
|
2357
2387
|
"""Resolve a function signature into argparse-argument builders and inheritable-block types.
|
|
@@ -2378,6 +2408,7 @@ def _resolve_parameters(
|
|
|
2378
2408
|
resolved: list[_ArgparseArgument] = []
|
|
2379
2409
|
inherited_field_names: set[str] = set()
|
|
2380
2410
|
base_args_types: set[type] = set()
|
|
2411
|
+
block_param_names: set[str] = set()
|
|
2381
2412
|
|
|
2382
2413
|
# Skip the first parameter by position (self/cls for methods)
|
|
2383
2414
|
params = list(sig.parameters.items())
|
|
@@ -2398,6 +2429,7 @@ def _resolve_parameters(
|
|
|
2398
2429
|
f"cannot have a default value; remove the default."
|
|
2399
2430
|
)
|
|
2400
2431
|
inherited_field_names.update(_init_field_names(inherited))
|
|
2432
|
+
block_param_names.add(name)
|
|
2401
2433
|
continue
|
|
2402
2434
|
|
|
2403
2435
|
# An ArgumentBlock-typed parameter is an argument block: expand its fields in place (flat) instead of
|
|
@@ -2416,6 +2448,7 @@ def _resolve_parameters(
|
|
|
2416
2448
|
block_hint, func_qualname=func.__qualname__, base_command=base_command, shared=name == NS_ATTR_BASE_ARGS
|
|
2417
2449
|
)
|
|
2418
2450
|
_reject_field_shadowing_block_param(block_hint, name, func.__qualname__)
|
|
2451
|
+
block_param_names.add(name)
|
|
2419
2452
|
resolved.extend(expanded)
|
|
2420
2453
|
continue
|
|
2421
2454
|
# The magic name requires a bare ArgumentBlock; a non-block annotation on it is a clear mistake.
|
|
@@ -2480,6 +2513,7 @@ def _resolve_parameters(
|
|
|
2480
2513
|
for arg in positionals[:-1]: # every positional except the last has a following positional
|
|
2481
2514
|
arg.has_following_positional = True
|
|
2482
2515
|
by_name = {arg.name: arg for arg in resolved}
|
|
2516
|
+
_check_group_members_exist(by_name, block_param_names, func.__qualname__, groups, mutually_exclusive_groups)
|
|
2483
2517
|
_link_mutex_group_membership(by_name, mutually_exclusive_groups)
|
|
2484
2518
|
for arg in resolved:
|
|
2485
2519
|
arg._check_constraints()
|
|
@@ -2539,30 +2573,22 @@ def _filtered_namespace_kwargs(
|
|
|
2539
2573
|
|
|
2540
2574
|
|
|
2541
2575
|
def _validate_group_specs(
|
|
2542
|
-
func: Callable[..., Any],
|
|
2543
|
-
*,
|
|
2544
|
-
skip_params: frozenset[str],
|
|
2545
2576
|
groups: tuple[Group, ...] | None,
|
|
2546
2577
|
mutually_exclusive_groups: tuple[Group, ...] | None,
|
|
2547
2578
|
) -> None:
|
|
2548
|
-
"""Validate ``groups=`` / ``mutually_exclusive_groups=`` specs
|
|
2549
|
-
|
|
2550
|
-
Runs at decoration time (from both the regular-command and subcommand decoration paths, and
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
group rule that needs the annotations (a required member in a mutually exclusive group) stays
|
|
2556
|
-
in :data:`_CONSTRAINTS` and fires when the parser is built.
|
|
2579
|
+
"""Validate the *shape* of ``groups=`` / ``mutually_exclusive_groups=`` specs.
|
|
2580
|
+
|
|
2581
|
+
Runs at decoration time (from both the regular-command and subcommand decoration paths, and again from
|
|
2582
|
+
:func:`build_parser_from_function` for direct callers), so a misshapen group hard-fails when the class is
|
|
2583
|
+
defined instead of on first command use, where cmd2's runtime handler turns the error into a printed
|
|
2584
|
+
message. Reads only the ``Group`` specs -- never the signature or the type hints -- so
|
|
2585
|
+
forward-referenced annotations still decorate.
|
|
2557
2586
|
"""
|
|
2558
2587
|
if not groups and not mutually_exclusive_groups:
|
|
2559
2588
|
return
|
|
2560
|
-
params = list(inspect.signature(func).parameters)[1:] # skip self/cls by position
|
|
2561
|
-
all_param_names = {name for name in params if name not in skip_params}
|
|
2562
2589
|
|
|
2563
2590
|
group_entry_for: dict[str, int] = {}
|
|
2564
2591
|
for index, spec in enumerate(groups or (), start=1):
|
|
2565
|
-
spec._validate_members(all_param_names=all_param_names, group_type="groups")
|
|
2566
2592
|
if spec.required:
|
|
2567
2593
|
raise ValueError(
|
|
2568
2594
|
"Group(required=True) is only valid in mutually_exclusive_groups; "
|
|
@@ -2580,7 +2606,6 @@ def _validate_group_specs(
|
|
|
2580
2606
|
|
|
2581
2607
|
mutex_entry_for: dict[str, int] = {}
|
|
2582
2608
|
for index, spec in enumerate(mutually_exclusive_groups or (), start=1):
|
|
2583
|
-
spec._validate_members(all_param_names=all_param_names, group_type="mutually_exclusive_groups")
|
|
2584
2609
|
for name in spec.members:
|
|
2585
2610
|
previous = mutex_entry_for.get(name)
|
|
2586
2611
|
if previous == index:
|
|
@@ -2721,7 +2746,7 @@ def build_parser_from_function(
|
|
|
2721
2746
|
from . import argparse_utils
|
|
2722
2747
|
|
|
2723
2748
|
# The decorator already ran this at decoration time; direct callers get the same checks here.
|
|
2724
|
-
_validate_group_specs(
|
|
2749
|
+
_validate_group_specs(groups, mutually_exclusive_groups)
|
|
2725
2750
|
|
|
2726
2751
|
parser_cls = parser_class or argparse_utils.DEFAULT_ARGUMENT_PARSER
|
|
2727
2752
|
if "description" not in parser_kwargs:
|
|
@@ -2735,6 +2760,7 @@ def build_parser_from_function(
|
|
|
2735
2760
|
resolved, base_args_types = _resolve_parameters(
|
|
2736
2761
|
func,
|
|
2737
2762
|
skip_params=skip_params,
|
|
2763
|
+
groups=groups,
|
|
2738
2764
|
mutually_exclusive_groups=mutually_exclusive_groups,
|
|
2739
2765
|
)
|
|
2740
2766
|
|
|
@@ -2827,15 +2853,17 @@ def _make_parser_builder(
|
|
|
2827
2853
|
**options.parser_kwargs,
|
|
2828
2854
|
)
|
|
2829
2855
|
if base_command:
|
|
2830
|
-
# dict[str, Any] is load-bearing:
|
|
2831
|
-
#
|
|
2856
|
+
# dict[str, Any] is load-bearing: title/description are added conditionally below,
|
|
2857
|
+
# which the typeshed overloads for add_subparsers cannot express.
|
|
2832
2858
|
kwargs: dict[str, Any] = {
|
|
2833
2859
|
"dest": "subcommand",
|
|
2834
2860
|
"metavar": options.subcommand_metavar,
|
|
2835
2861
|
"required": options.subcommand_required,
|
|
2836
|
-
"title": options.subcommand_title,
|
|
2837
|
-
"description": options.subcommand_description,
|
|
2838
2862
|
}
|
|
2863
|
+
if options.subcommand_title is not None:
|
|
2864
|
+
kwargs["title"] = options.subcommand_title
|
|
2865
|
+
if options.subcommand_description is not None:
|
|
2866
|
+
kwargs["description"] = options.subcommand_description
|
|
2839
2867
|
parser.add_subparsers(**kwargs)
|
|
2840
2868
|
return parser
|
|
2841
2869
|
|
|
@@ -2862,12 +2890,7 @@ def _build_subcommand_handler(
|
|
|
2862
2890
|
# Validate the group specs eagerly (decoration time) so a misconfigured group hard-fails when
|
|
2863
2891
|
# the class is defined; the name-only checks never resolve type hints, so forward-referenced
|
|
2864
2892
|
# annotations still decorate and the parser build stays deferred.
|
|
2865
|
-
_validate_group_specs(
|
|
2866
|
-
func,
|
|
2867
|
-
skip_params=_SKIP_PARAMS,
|
|
2868
|
-
groups=options.groups,
|
|
2869
|
-
mutually_exclusive_groups=options.mutually_exclusive_groups,
|
|
2870
|
-
)
|
|
2893
|
+
_validate_group_specs(options.groups, options.mutually_exclusive_groups)
|
|
2871
2894
|
if base_command:
|
|
2872
2895
|
# Validate eagerly (decoration time); the base-command rows in _CONSTRAINTS fire here.
|
|
2873
2896
|
# skip_params is spelled out so this call cannot silently diverge from the parser build below.
|
|
@@ -3051,12 +3074,7 @@ def with_annotated(
|
|
|
3051
3074
|
# Validate the group specs eagerly (decoration time) so a misconfigured group hard-fails when
|
|
3052
3075
|
# the class is defined; the name-only checks never resolve type hints, so forward-referenced
|
|
3053
3076
|
# annotations still decorate and the parser build stays deferred.
|
|
3054
|
-
_validate_group_specs(
|
|
3055
|
-
fn,
|
|
3056
|
-
skip_params=skip_params,
|
|
3057
|
-
groups=options.groups,
|
|
3058
|
-
mutually_exclusive_groups=options.mutually_exclusive_groups,
|
|
3059
|
-
)
|
|
3077
|
+
_validate_group_specs(options.groups, options.mutually_exclusive_groups)
|
|
3060
3078
|
if base_command:
|
|
3061
3079
|
# Validate eagerly (decoration time); the base-command rows in _CONSTRAINTS fire here.
|
|
3062
3080
|
_resolve_parameters(fn, skip_params=skip_params, base_command=True)
|
|
@@ -694,7 +694,7 @@ class ArgparseCompleter:
|
|
|
694
694
|
self._parser.print_help(file)
|
|
695
695
|
|
|
696
696
|
def _choices_to_items(self, arg_state: _ArgumentState) -> list[CompletionItem]:
|
|
697
|
-
"""Convert
|
|
697
|
+
"""Convert an action's choices to a list of CompletionItems."""
|
|
698
698
|
if arg_state.action.choices is None:
|
|
699
699
|
return []
|
|
700
700
|
|
|
@@ -785,13 +785,15 @@ class ArgparseCompleter:
|
|
|
785
785
|
cmd_set,
|
|
786
786
|
)
|
|
787
787
|
all_choices = list(choices_provider(*args, **kwargs))
|
|
788
|
+
sort = False # choices_provider results are already sorted, so don't re-sort them
|
|
788
789
|
else:
|
|
789
790
|
all_choices = self._choices_to_items(arg_state)
|
|
791
|
+
sort = True
|
|
790
792
|
|
|
791
793
|
# Filter used values and run basic completion
|
|
792
794
|
used_values = consumed_arg_values.get(arg_state.action.dest, [])
|
|
793
795
|
filtered = [choice for choice in all_choices if choice.text not in used_values]
|
|
794
|
-
completions = self._cmd_app.basic_complete(text, line, begidx, endidx, filtered)
|
|
796
|
+
completions = self._cmd_app.basic_complete(text, line, begidx, endidx, filtered, sort=sort)
|
|
795
797
|
|
|
796
798
|
return self._build_completion_table(arg_state, completions)
|
|
797
799
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmd2
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python
|
|
5
5
|
Author: cmd2 Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: prompt-toolkit>=3.0.
|
|
24
|
+
Requires-Dist: prompt-toolkit>=3.0.53
|
|
25
25
|
Requires-Dist: pyperclip>=1.8.2
|
|
26
26
|
Requires-Dist: rich>=15.0.0
|
|
27
27
|
Requires-Dist: rich-argparse>=1.7.2
|
|
@@ -436,14 +436,17 @@ token to convert. Both raise `TypeError` at decoration time.
|
|
|
436
436
|
default `True`)
|
|
437
437
|
- `subcommand_metavar` -- metavar shown for the subcommands group (only with `base_command=True`,
|
|
438
438
|
default `"SUBCOMMAND"`)
|
|
439
|
-
- `subcommand_title` -- title for the subcommands `--help` section (only with `base_command=True`)
|
|
439
|
+
- `subcommand_title` -- title for the subcommands `--help` section (only with `base_command=True`).
|
|
440
|
+
Setting either this or `subcommand_description` moves the subcommands out of the positional
|
|
441
|
+
arguments section into a section of their own, as it does in argparse.
|
|
440
442
|
- `subcommand_description` -- description for the subcommands `--help` section (only with
|
|
441
|
-
`base_command=True`)
|
|
443
|
+
`base_command=True`). Supplying this without `subcommand_title` gives that section argparse's
|
|
444
|
+
default title, `subcommands`.
|
|
442
445
|
- `help` -- help text for an annotated subcommand (only valid with `subcommand_to`)
|
|
443
446
|
- `aliases` -- aliases for an annotated subcommand (only valid with `subcommand_to`)
|
|
444
447
|
- `deprecated` -- mark the subcommand as deprecated in `--help` (only valid with `subcommand_to`)
|
|
445
|
-
- `groups` -- `Group` instances assigning
|
|
446
|
-
- `mutually_exclusive_groups` -- `Group` instances of mutually exclusive
|
|
448
|
+
- `groups` -- `Group` instances assigning argument names to argument groups
|
|
449
|
+
- `mutually_exclusive_groups` -- `Group` instances of mutually exclusive arguments
|
|
447
450
|
- `parser_class` -- a custom parser class (defaults to the configured default)
|
|
448
451
|
- `**parser_kwargs` -- every other `Cmd2ArgumentParser` constructor kwarg, forwarded through PEP 692
|
|
449
452
|
[`Unpack[Cmd2ParserKwargs]`][cmd2.annotated.Cmd2ParserKwargs]. See
|
|
@@ -546,13 +549,35 @@ both places, a mutex that sits only partly in a `groups=` entry, or one that spa
|
|
|
546
549
|
raises `ValueError`. The other three nestings (an argument group inside another group or a mutex, or
|
|
547
550
|
a mutex inside a mutex) are removed in argparse on Python 3.14 and cannot be expressed here.
|
|
548
551
|
|
|
549
|
-
|
|
552
|
+
A `Group` member names a command-line **argument**. For a plain parameter the argument is the
|
|
553
|
+
parameter, so the name is the same either way. An `ArgumentBlock` parameter is different: it is
|
|
554
|
+
expanded into one argument per field (field name == argument name), so you name its fields rather
|
|
555
|
+
than the block:
|
|
556
|
+
|
|
557
|
+
```python
|
|
558
|
+
@dataclass
|
|
559
|
+
class Conn(ArgumentBlock):
|
|
560
|
+
host: Annotated[str, Option("--host")] = "localhost"
|
|
561
|
+
port: Annotated[int, Option("--port")] = 8080
|
|
562
|
+
|
|
563
|
+
@with_annotated(groups=(Group("host", "port", title="connection"),))
|
|
564
|
+
def do_connect(self, conn: Conn) -> None: ...
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Naming the block parameter itself (`Group("conn")`) raises `ValueError`: the parameter is expanded
|
|
568
|
+
away and has no argument of its own. Naming a block's fields individually is also what lets you say
|
|
569
|
+
which fields are alternatives -- put `Group("json", "csv")` in `mutually_exclusive_groups` and only
|
|
570
|
+
those two exclude each other, rather than every field of the block they happen to live in.
|
|
571
|
+
|
|
572
|
+
The group-spec **shape** rules -- a member listed twice, a member assigned to two groups,
|
|
550
573
|
`required=True` on a plain group, and the mutex nesting rules above -- are validated when the
|
|
551
|
-
decorator runs, so a
|
|
552
|
-
first command use.
|
|
553
|
-
annotations still decorate cleanly.
|
|
554
|
-
|
|
555
|
-
parser is built
|
|
574
|
+
decorator runs, so a misshapen group raises `ValueError` at class definition time instead of on
|
|
575
|
+
first command use. Those checks read only the `Group` specs, never the signature or the type hints,
|
|
576
|
+
so forward-referenced annotations still decorate cleanly.
|
|
577
|
+
|
|
578
|
+
The rules that depend on what a member _is_ fire when the parser is built, because a block's field
|
|
579
|
+
names cannot be known without resolving its type hint: that every member names a real argument, and
|
|
580
|
+
that a member of a mutually exclusive group is omittable (has a default or is `T | None`).
|
|
556
581
|
|
|
557
582
|
`parents=` mirrors argparse's standard parents mechanism for sharing argument definitions across
|
|
558
583
|
parsers. `argument_default=argparse.SUPPRESS` is not supported and raises `TypeError`. It removes an
|
|
@@ -116,6 +116,19 @@ class RunOpts(ArgumentBlock):
|
|
|
116
116
|
dry_run: Annotated[bool, Option("--dry-run", help_text="don't actually run")] = False
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
@dataclass
|
|
120
|
+
class ConnOpts(ArgumentBlock):
|
|
121
|
+
"""A block whose fields are placed in an argument group by name.
|
|
122
|
+
|
|
123
|
+
A ``Group`` names command-line arguments, and a block expands into one argument
|
|
124
|
+
per field -- so a group names the block's *fields* (``host``, ``port``), not the
|
|
125
|
+
block parameter (``Group("conn")`` is rejected). See ``bind`` below.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
host: Annotated[str, Option("--host", help_text="host to connect to")] = "localhost"
|
|
129
|
+
port: Annotated[int, Option("--port", help_text="port to connect on")] = 8080
|
|
130
|
+
|
|
131
|
+
|
|
119
132
|
class AnnotatedExample(Cmd):
|
|
120
133
|
"""Demonstrates @with_annotated strengths over @with_argparser."""
|
|
121
134
|
|
|
@@ -641,6 +654,28 @@ class AnnotatedExample(Cmd):
|
|
|
641
654
|
msg = f"Connecting to {host}:{port}"
|
|
642
655
|
self.poutput(f"{msg} (verbose)" if verbose else msg)
|
|
643
656
|
|
|
657
|
+
# An ``ArgumentBlock``'s fields can go in a group too: a ``Group`` names the
|
|
658
|
+
# arguments a block expands into (its fields), not the block parameter itself.
|
|
659
|
+
|
|
660
|
+
@with_annotated(
|
|
661
|
+
description="Open a connection whose grouped flags come from an argument block.",
|
|
662
|
+
groups=(Group("host", "port", title="connection", description="where to connect"),),
|
|
663
|
+
)
|
|
664
|
+
@cmd2.with_category(ANNOTATED_CATEGORY)
|
|
665
|
+
def do_bind(self, conn: ConnOpts, verbose: bool = False) -> None:
|
|
666
|
+
"""Group a block's fields: ``Group("host", "port")`` names the ``ConnOpts`` fields.
|
|
667
|
+
|
|
668
|
+
``--host`` / ``--port`` come from the block yet still render under the ``connection``
|
|
669
|
+
help section, because a group names the arguments a block expands into -- not the
|
|
670
|
+
``conn`` parameter (naming that, ``Group("conn")``, is rejected).
|
|
671
|
+
|
|
672
|
+
Try:
|
|
673
|
+
help bind
|
|
674
|
+
bind --host example.com --port 2222 --verbose
|
|
675
|
+
"""
|
|
676
|
+
msg = f"Binding to {conn.host}:{conn.port}"
|
|
677
|
+
self.poutput(f"{msg} (verbose)" if verbose else msg)
|
|
678
|
+
|
|
644
679
|
# -- Mutually exclusive groups -------------------------------------------
|
|
645
680
|
# A plain (untitled) mutex rejects combinations of its members; required=True
|
|
646
681
|
# makes exactly one of them mandatory.
|
|
@@ -18,6 +18,7 @@ from modular_commands.commandset_custominit import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
from cmd2 import (
|
|
21
|
+
Choices,
|
|
21
22
|
Cmd,
|
|
22
23
|
Cmd2ArgumentParser,
|
|
23
24
|
CommandSet,
|
|
@@ -31,9 +32,9 @@ class WithCommandSets(Cmd):
|
|
|
31
32
|
super().__init__(command_sets=command_sets)
|
|
32
33
|
self.sport_item_strs = ["Bat", "Basket", "Basketball", "Football", "Space Ball"]
|
|
33
34
|
|
|
34
|
-
def choices_provider(self) ->
|
|
35
|
+
def choices_provider(self) -> Choices:
|
|
35
36
|
"""A choices provider is useful when the choice list is based on instance data of your application."""
|
|
36
|
-
return self.sport_item_strs
|
|
37
|
+
return Choices.from_values(self.sport_item_strs)
|
|
37
38
|
|
|
38
39
|
# Parser for example command
|
|
39
40
|
example_parser = Cmd2ArgumentParser(
|