cmd2 4.1.2__tar.gz → 4.2.1__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.1}/.pre-commit-config.yaml +2 -2
- {cmd2-4.1.2 → cmd2-4.2.1}/CHANGELOG.md +38 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/Makefile +9 -9
- {cmd2-4.1.2 → cmd2-4.2.1}/PKG-INFO +2 -2
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/annotated.py +72 -51
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/argparse_completer.py +4 -2
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/argparse_utils.py +7 -6
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/cmd2.py +67 -38
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/decorators.py +3 -3
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/parsing.py +4 -4
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/pt_utils.py +1 -1
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/rich_utils.py +1 -1
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2.egg-info/PKG-INFO +2 -2
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2.egg-info/SOURCES.txt +1 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2.egg-info/requires.txt +1 -1
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2.egg-info/scm_file_list.json +127 -126
- cmd2-4.2.1/cmd2.egg-info/scm_version.json +8 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/annotated.md +35 -10
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/argument_processing.md +8 -8
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/upgrades.md +9 -9
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/annotated_example.py +35 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/modular_commandsets.py +3 -2
- {cmd2-4.1.2 → cmd2-4.2.1}/pyproject.toml +3 -29
- {cmd2-4.1.2 → cmd2-4.2.1}/ruff.toml +2 -1
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_annotated.py +216 -31
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_argparse_completer.py +52 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_cmd2.py +91 -1
- cmd2-4.2.1/ty.toml +8 -0
- cmd2-4.1.2/cmd2.egg-info/scm_version.json +0 -8
- {cmd2-4.1.2 → cmd2-4.2.1}/.prettierignore +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/.prettierrc +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/.typos.toml +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/GEMINI.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/LICENSE +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/MANIFEST.in +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/README.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/__init__.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/clipboard.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/colors.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/command_set.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/constants.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/exceptions.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/history.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/plugin.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/py.typed +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/py_bridge.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/string_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/styles.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/theme.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/types.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2/utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2.egg-info/dependency_links.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2.egg-info/top_level.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/cmd2.png +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/codecov.yml +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/annotated.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/argparse_completer.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/argparse_utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/clipboard.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/cmd.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/colors.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/command_set.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/completion.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/constants.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/decorators.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/exceptions.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/history.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/parsing.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/plugin.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/pt_utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/py_bridge.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/rich_utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/string_utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/styles.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/theme.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/api/utils.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/doc_conventions.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/examples/alternate_event_loops.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/examples/examples.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/examples/getting_started.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/examples/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/async_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/builtin_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/clipboard.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/completion.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/disable_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/embedded_python_shells.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/generating_output.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/help.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/history.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/hooks.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/initialization.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/misc.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/modular_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/multiline_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/os.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/packaging.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/plugins.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/prompt.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/redirection.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/scripting.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/settings.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/shortcuts_aliases_macros.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/startup_commands.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/table_creation.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/features/theme.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/javascripts/readthedocs.js +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/migrating/incompatibilities.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/migrating/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/migrating/minimum.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/migrating/next_steps.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/migrating/why.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/mixins/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/mixins/mixin_template.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/overrides/main.html +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/overview/alternatives.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/overview/index.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/overview/installation.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/overview/integrating.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/overview/resources.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/stylesheets/cmd2.css +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/docs/testing.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/.cmd2rc +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/README.md +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/argparse_completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/argparse_example.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/async_call.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/async_commands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/async_printing.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/basic_completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/cmd_as_argument.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/color.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/command_sets.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/completion_item_choices.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/custom_parser.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/custom_types.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/default_categories.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/dynamic_commands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/environment.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/event_loops.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/exit_code.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/getting_started.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/hello_cmd2.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/help_categories.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/hooks.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/migrating.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/mixin.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/modular_commands/__init__.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/modular_commands/commandset_basic.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/modular_commands/commandset_complex.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/modular_commands/commandset_custominit.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/paged_output.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/persistent_history.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/pretty_print.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/python_scripting.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/read_input.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/remove_builtin_commands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/remove_settable.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/rich_tables.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/rich_theme.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/scripts/arg_printer.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/scripts/conditional.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/scripts/nested.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/scripts/quit.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/scripts/save_help_text.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/scripts/script.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/scripts/script.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/tmux_launch.sh +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/tmux_split.sh +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/examples/unicode_commands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/mkdocs.yml +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/package.json +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/scripts/validate_tag.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/setup.cfg +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/.cmd2rc +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/__init__.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/conftest.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/echo.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/environment.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/help.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/py_locals.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/pyscript_dir.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/raises_exception.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/recursive.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/self_in_py.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/stop.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/pyscript/test_print.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/relative_multiple.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/script.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/script.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/scripts/binary.bin +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/scripts/empty.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/scripts/help.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/scripts/nested.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/scripts/one_down.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/scripts/postcmds.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/scripts/precmds.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/scripts/utf8.txt +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_argparse.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_argparse_subcommands.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_argparse_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_categories.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_commandset.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_dynamic_complete_style.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_future_annotations.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_history.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_parsing.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_plugin.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_pt_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_py_completion.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_rich_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_run_pyscript.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_string_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_theme.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_utils.py +0 -0
- {cmd2-4.1.2 → cmd2-4.2.1}/tests/test_utils_defining_class.py +0 -0
|
@@ -14,7 +14,7 @@ repos:
|
|
|
14
14
|
- id: trailing-whitespace
|
|
15
15
|
|
|
16
16
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
17
|
-
rev: "v0.
|
|
17
|
+
rev: "v0.16.3"
|
|
18
18
|
hooks:
|
|
19
19
|
- id: ruff-format
|
|
20
20
|
args: [--config=ruff.toml]
|
|
@@ -30,7 +30,7 @@ repos:
|
|
|
30
30
|
- prettier-plugin-toml@2.0.6
|
|
31
31
|
|
|
32
32
|
- repo: https://github.com/crate-ci/typos
|
|
33
|
-
rev: v1.
|
|
33
|
+
rev: v1.49.0
|
|
34
34
|
hooks:
|
|
35
35
|
- id: typos
|
|
36
36
|
exclude: |
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
## 4.2.1 (August 22, 2026)
|
|
2
|
+
|
|
3
|
+
- Enhancements
|
|
4
|
+
- Added bracketed paste support so multiple pasted commands execute sequentially and multiline
|
|
5
|
+
commands continue as expected.
|
|
6
|
+
|
|
7
|
+
- Bug Fixes
|
|
8
|
+
- Enabled Ctrl-Z suspension at the prompt
|
|
9
|
+
|
|
10
|
+
## 4.2.0 (August 6, 2026)
|
|
11
|
+
|
|
12
|
+
- Enhancements
|
|
13
|
+
- `@with_annotated` argument groups can now contain an `ArgumentBlock`'s arguments. A `Group`
|
|
14
|
+
member names a command-line argument, and a block expands into one argument per field, so its
|
|
15
|
+
fields are named: `Group("host", "port")`.
|
|
16
|
+
- Breaking Changes
|
|
17
|
+
- A `Group` member now names an argument rather than a parameter. The two differ only for an
|
|
18
|
+
`ArgumentBlock` parameter, which is expanded away and has no argument of its own:
|
|
19
|
+
`Group("conn")` now raises `ValueError` pointing at the block's fields. It previously produced
|
|
20
|
+
an empty argument group in `groups=` and a `KeyError` in `mutually_exclusive_groups=`.
|
|
21
|
+
- Whether a `Group` member exists is now validated when the parser is built rather than at
|
|
22
|
+
decoration time, because a block's field names cannot be known without resolving its type
|
|
23
|
+
hint, and resolving hints eagerly would break forward-referenced annotations. A typo in a
|
|
24
|
+
member name still raises `ValueError`, but on first use of that command rather than at class
|
|
25
|
+
definition. The spec-shape rules (a member listed twice, a member in two groups,
|
|
26
|
+
`required=True` on a plain group, the mutex nesting rules) are unaffected and still hard-fail
|
|
27
|
+
at decoration time.
|
|
28
|
+
- Bug Fixes
|
|
29
|
+
- Fixed `@with_annotated(base_command=True)` not listing its subcommands under the positional
|
|
30
|
+
arguments section of the parent command's `--help`, unlike `argparse` and
|
|
31
|
+
`Cmd2ArgumentParser`. They were placed in an untitled section of their own instead. Passing
|
|
32
|
+
`subcommand_title` or `subcommand_description` still gives the subcommands a dedicated section
|
|
33
|
+
([#1715](https://github.com/python-cmd2/cmd2/issues/1715)).
|
|
34
|
+
- Fix `@with_annotated` decorator so using `ArgumentBlock` works with groups
|
|
35
|
+
([#1718](https://github.com/python-cmd2/cmd2/issues/1718)).
|
|
36
|
+
- Fixed bug where already sorted `choices_provider` results were being re-sorted
|
|
37
|
+
([#1727](https://github.com/python-cmd2/cmd2/issues/1727)).
|
|
38
|
+
|
|
1
39
|
## 4.1.2 (July 16, 2026)
|
|
2
40
|
|
|
3
41
|
- Enhancements
|
|
@@ -18,8 +18,8 @@ check: ## Run code quality tools.
|
|
|
18
18
|
@uv lock --locked
|
|
19
19
|
@echo "🚀 Auto-formatting/Linting code and documentation: Running prek"
|
|
20
20
|
@uv run prek run -a
|
|
21
|
-
@echo "🚀 Static type checking: Running
|
|
22
|
-
@uv run
|
|
21
|
+
@echo "🚀 Static type checking: Running ty"
|
|
22
|
+
@uv run ty check
|
|
23
23
|
|
|
24
24
|
.PHONY: format
|
|
25
25
|
format: ## Perform ruff formatting
|
|
@@ -31,7 +31,7 @@ lint: ## Perform ruff linting
|
|
|
31
31
|
|
|
32
32
|
.PHONY: typecheck
|
|
33
33
|
typecheck: ## Perform type checking
|
|
34
|
-
@uv run
|
|
34
|
+
@uv run ty check
|
|
35
35
|
|
|
36
36
|
.PHONY: test
|
|
37
37
|
test: ## Test the code with pytest.
|
|
@@ -76,7 +76,7 @@ publish: validate-tag build ## Publish a release to PyPI, uses token from ~/.pyp
|
|
|
76
76
|
# Define variables for files/directories to clean
|
|
77
77
|
BUILD_DIRS = build dist *.egg-info
|
|
78
78
|
DOC_DIRS = build
|
|
79
|
-
|
|
79
|
+
TY_DIRS = .ty_cache .red_knot_cache
|
|
80
80
|
TEST_DIRS = .cache .pytest_cache htmlcov
|
|
81
81
|
TEST_FILES = .coverage coverage.xml
|
|
82
82
|
|
|
@@ -90,10 +90,10 @@ clean-docs: ## Clean documentation artifacts
|
|
|
90
90
|
@echo "🚀 Removing documentation artifacts"
|
|
91
91
|
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(DOC_DIRS)'.split() if os.path.isdir(d)]"
|
|
92
92
|
|
|
93
|
-
.PHONY: clean-
|
|
94
|
-
clean-
|
|
95
|
-
@echo "🚀 Removing
|
|
96
|
-
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(
|
|
93
|
+
.PHONY: clean-ty
|
|
94
|
+
clean-ty: ## Clean ty artifacts
|
|
95
|
+
@echo "🚀 Removing ty artifacts"
|
|
96
|
+
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(TY_DIRS)'.split() if os.path.isdir(d)]"
|
|
97
97
|
|
|
98
98
|
.PHONY: clean-pycache
|
|
99
99
|
clean-pycache: ## Clean pycache artifacts
|
|
@@ -112,7 +112,7 @@ clean-test: ## Clean test artifacts
|
|
|
112
112
|
@uv run python -c "from pathlib import Path; [Path(f).unlink(missing_ok=True) for f in '$(TEST_FILES)'.split()]"
|
|
113
113
|
|
|
114
114
|
.PHONY: clean
|
|
115
|
-
clean: clean-build clean-docs clean-
|
|
115
|
+
clean: clean-build clean-docs clean-ty clean-pycache clean-ruff clean-test ## Clean all artifacts
|
|
116
116
|
@echo "🚀 Cleaned all artifacts"
|
|
117
117
|
|
|
118
118
|
.PHONY: help
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmd2
|
|
3
|
-
Version: 4.1
|
|
3
|
+
Version: 4.2.1
|
|
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
|
|
@@ -711,10 +708,10 @@ class _CollectionCastingAction(argparse._StoreAction):
|
|
|
711
708
|
|
|
712
709
|
def __call__(
|
|
713
710
|
self,
|
|
714
|
-
|
|
711
|
+
parser: argparse.ArgumentParser, # noqa: ARG002
|
|
715
712
|
namespace: argparse.Namespace,
|
|
716
713
|
values: Any,
|
|
717
|
-
|
|
714
|
+
option_string: str | None = None, # noqa: ARG002
|
|
718
715
|
) -> None:
|
|
719
716
|
result = values
|
|
720
717
|
if self._container_factory is not None and isinstance(values, list):
|
|
@@ -882,7 +879,7 @@ def _resolve_union(
|
|
|
882
879
|
raise TypeError(f"Union type {type_names} is ambiguous for auto-resolution.")
|
|
883
880
|
|
|
884
881
|
parts = [_resolve_base_type(member, allow_unknown_entry=allow_unknown_entry) for member in non_none]
|
|
885
|
-
# Every part is an Enum (guarded above), so each has a converter; the None-filter keeps
|
|
882
|
+
# Every part is an Enum (guarded above), so each has a converter; the None-filter keeps the type checker happy.
|
|
886
883
|
converters = [part.converter for part in parts if part.converter is not None]
|
|
887
884
|
choices = _dedupe_choices(choice for part in parts for choice in (part.choices or []))
|
|
888
885
|
|
|
@@ -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
|
|
@@ -2160,7 +2189,7 @@ def _find_argument_block(hint: Any) -> type[ArgumentBlock] | None:
|
|
|
2160
2189
|
return None
|
|
2161
2190
|
|
|
2162
2191
|
|
|
2163
|
-
def _init_field_names(dc_type:
|
|
2192
|
+
def _init_field_names(dc_type: Any) -> list[str]:
|
|
2164
2193
|
"""Names of a dataclass's ``init`` fields in definition order (the flat argument names of a block)."""
|
|
2165
2194
|
return [f.name for f in fields(dc_type) if f.init]
|
|
2166
2195
|
|
|
@@ -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)
|
|
@@ -3095,6 +3113,9 @@ def with_annotated(
|
|
|
3095
3113
|
except SystemExit as exc:
|
|
3096
3114
|
raise Cmd2ArgparseError from exc
|
|
3097
3115
|
|
|
3116
|
+
if ns is None:
|
|
3117
|
+
raise ValueError("ns is None")
|
|
3118
|
+
|
|
3098
3119
|
setattr(ns, constants.NS_ATTR_STATEMENT, statement)
|
|
3099
3120
|
handler = getattr(ns, constants.NS_ATTR_SUBCOMMAND_FUNC, None)
|
|
3100
3121
|
if base_command and handler is not None:
|
|
@@ -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
|
|
|
@@ -747,8 +747,8 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
|
|
|
747
747
|
super().__init__(
|
|
748
748
|
prog=prog,
|
|
749
749
|
usage=usage,
|
|
750
|
-
description=description, # type: ignore[arg-type]
|
|
751
|
-
epilog=epilog, # type: ignore[arg-type]
|
|
750
|
+
description=description, # type: ignore[arg-type, ty:invalid-argument-type]
|
|
751
|
+
epilog=epilog, # type: ignore[arg-type, ty:invalid-argument-type]
|
|
752
752
|
parents=parents,
|
|
753
753
|
formatter_class=formatter_class,
|
|
754
754
|
prefix_chars=prefix_chars,
|
|
@@ -772,7 +772,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
|
|
|
772
772
|
self.description: HelpContent | None # type: ignore[assignment]
|
|
773
773
|
self.epilog: HelpContent | None # type: ignore[assignment]
|
|
774
774
|
|
|
775
|
-
def print_usage(self, file: IO[str] | None = None) -> None: # type:ignore[override]
|
|
775
|
+
def print_usage(self, file: IO[str] | None = None) -> None: # type: ignore[override, ty:invalid-method-override]
|
|
776
776
|
"""Override to ensure the formatter is aware of the target file."""
|
|
777
777
|
if file is None:
|
|
778
778
|
file = self._thread_locals.current_output_file
|
|
@@ -780,7 +780,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
|
|
|
780
780
|
with self.output_to(file):
|
|
781
781
|
super().print_usage(file)
|
|
782
782
|
|
|
783
|
-
def print_help(self, file: IO[str] | None = None) -> None: # type:ignore[override]
|
|
783
|
+
def print_help(self, file: IO[str] | None = None) -> None: # type: ignore[override, ty:invalid-method-override]
|
|
784
784
|
"""Override to ensure the formatter is aware of the target file."""
|
|
785
785
|
if file is None:
|
|
786
786
|
file = self._thread_locals.current_output_file
|
|
@@ -831,7 +831,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
|
|
|
831
831
|
temp_parser = Cmd2ArgumentParser(
|
|
832
832
|
prog=self.prog,
|
|
833
833
|
usage=None,
|
|
834
|
-
formatter_class=self.formatter_class,
|
|
834
|
+
formatter_class=cast(type[Cmd2HelpFormatter], self.formatter_class),
|
|
835
835
|
add_help=False,
|
|
836
836
|
)
|
|
837
837
|
|
|
@@ -1037,7 +1037,8 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
|
|
|
1037
1037
|
|
|
1038
1038
|
def _get_formatter(self, *_args: Any, **_kwargs: Any) -> Cmd2HelpFormatter:
|
|
1039
1039
|
"""Override with customizations for Cmd2HelpFormatter."""
|
|
1040
|
-
|
|
1040
|
+
formatter_class = cast(type[Cmd2HelpFormatter], self.formatter_class)
|
|
1041
|
+
return formatter_class(prog=self.prog, file=self._thread_locals.current_output_file)
|
|
1041
1042
|
|
|
1042
1043
|
def format_help(self, *args: Any, **kwargs: Any) -> str:
|
|
1043
1044
|
"""Override to add a newline."""
|