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.
Files changed (221) hide show
  1. {cmd2-4.1.2 → cmd2-4.2.0}/.pre-commit-config.yaml +1 -1
  2. {cmd2-4.1.2 → cmd2-4.2.0}/CHANGELOG.md +29 -0
  3. {cmd2-4.1.2 → cmd2-4.2.0}/PKG-INFO +2 -2
  4. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/annotated.py +65 -47
  5. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/argparse_completer.py +4 -2
  6. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/PKG-INFO +2 -2
  7. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/requires.txt +1 -1
  8. cmd2-4.2.0/cmd2.egg-info/scm_version.json +8 -0
  9. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/annotated.md +35 -10
  10. {cmd2-4.1.2 → cmd2-4.2.0}/examples/annotated_example.py +35 -0
  11. {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commandsets.py +3 -2
  12. {cmd2-4.1.2 → cmd2-4.2.0}/pyproject.toml +1 -1
  13. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_annotated.py +173 -31
  14. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_argparse_completer.py +52 -0
  15. cmd2-4.1.2/cmd2.egg-info/scm_version.json +0 -8
  16. {cmd2-4.1.2 → cmd2-4.2.0}/.prettierignore +0 -0
  17. {cmd2-4.1.2 → cmd2-4.2.0}/.prettierrc +0 -0
  18. {cmd2-4.1.2 → cmd2-4.2.0}/.typos.toml +0 -0
  19. {cmd2-4.1.2 → cmd2-4.2.0}/GEMINI.md +0 -0
  20. {cmd2-4.1.2 → cmd2-4.2.0}/LICENSE +0 -0
  21. {cmd2-4.1.2 → cmd2-4.2.0}/MANIFEST.in +0 -0
  22. {cmd2-4.1.2 → cmd2-4.2.0}/Makefile +0 -0
  23. {cmd2-4.1.2 → cmd2-4.2.0}/README.md +0 -0
  24. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/__init__.py +0 -0
  25. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/argparse_utils.py +0 -0
  26. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/clipboard.py +0 -0
  27. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/cmd2.py +0 -0
  28. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/colors.py +0 -0
  29. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/command_set.py +0 -0
  30. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/completion.py +0 -0
  31. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/constants.py +0 -0
  32. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/decorators.py +0 -0
  33. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/exceptions.py +0 -0
  34. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/history.py +0 -0
  35. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/parsing.py +0 -0
  36. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/plugin.py +0 -0
  37. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/pt_utils.py +0 -0
  38. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/py.typed +0 -0
  39. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/py_bridge.py +0 -0
  40. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/rich_utils.py +0 -0
  41. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/string_utils.py +0 -0
  42. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/styles.py +0 -0
  43. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/theme.py +0 -0
  44. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/types.py +0 -0
  45. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2/utils.py +0 -0
  46. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/SOURCES.txt +0 -0
  47. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/dependency_links.txt +0 -0
  48. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/scm_file_list.json +14 -14
  49. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.egg-info/top_level.txt +0 -0
  50. {cmd2-4.1.2 → cmd2-4.2.0}/cmd2.png +0 -0
  51. {cmd2-4.1.2 → cmd2-4.2.0}/codecov.yml +0 -0
  52. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/annotated.md +0 -0
  53. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/argparse_completer.md +0 -0
  54. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/argparse_utils.md +0 -0
  55. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/clipboard.md +0 -0
  56. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/cmd.md +0 -0
  57. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/colors.md +0 -0
  58. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/command_set.md +0 -0
  59. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/completion.md +0 -0
  60. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/constants.md +0 -0
  61. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/decorators.md +0 -0
  62. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/exceptions.md +0 -0
  63. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/history.md +0 -0
  64. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/index.md +0 -0
  65. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/parsing.md +0 -0
  66. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/plugin.md +0 -0
  67. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/pt_utils.md +0 -0
  68. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/py_bridge.md +0 -0
  69. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/rich_utils.md +0 -0
  70. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/string_utils.md +0 -0
  71. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/styles.md +0 -0
  72. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/theme.md +0 -0
  73. {cmd2-4.1.2 → cmd2-4.2.0}/docs/api/utils.md +0 -0
  74. {cmd2-4.1.2 → cmd2-4.2.0}/docs/doc_conventions.md +0 -0
  75. {cmd2-4.1.2 → cmd2-4.2.0}/docs/examples/alternate_event_loops.md +0 -0
  76. {cmd2-4.1.2 → cmd2-4.2.0}/docs/examples/examples.md +0 -0
  77. {cmd2-4.1.2 → cmd2-4.2.0}/docs/examples/getting_started.md +0 -0
  78. {cmd2-4.1.2 → cmd2-4.2.0}/docs/examples/index.md +0 -0
  79. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/argument_processing.md +0 -0
  80. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/async_commands.md +0 -0
  81. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/builtin_commands.md +0 -0
  82. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/clipboard.md +0 -0
  83. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/commands.md +0 -0
  84. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/completion.md +0 -0
  85. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/disable_commands.md +0 -0
  86. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/embedded_python_shells.md +0 -0
  87. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/generating_output.md +0 -0
  88. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/help.md +0 -0
  89. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/history.md +0 -0
  90. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/hooks.md +0 -0
  91. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/index.md +0 -0
  92. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/initialization.md +0 -0
  93. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/misc.md +0 -0
  94. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/modular_commands.md +0 -0
  95. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/multiline_commands.md +0 -0
  96. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/os.md +0 -0
  97. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/packaging.md +0 -0
  98. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/plugins.md +0 -0
  99. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/prompt.md +0 -0
  100. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/redirection.md +0 -0
  101. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/scripting.md +0 -0
  102. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/settings.md +0 -0
  103. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/shortcuts_aliases_macros.md +0 -0
  104. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/startup_commands.md +0 -0
  105. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/table_creation.md +0 -0
  106. {cmd2-4.1.2 → cmd2-4.2.0}/docs/features/theme.md +0 -0
  107. {cmd2-4.1.2 → cmd2-4.2.0}/docs/index.md +0 -0
  108. {cmd2-4.1.2 → cmd2-4.2.0}/docs/javascripts/readthedocs.js +0 -0
  109. {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/incompatibilities.md +0 -0
  110. {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/index.md +0 -0
  111. {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/minimum.md +0 -0
  112. {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/next_steps.md +0 -0
  113. {cmd2-4.1.2 → cmd2-4.2.0}/docs/migrating/why.md +0 -0
  114. {cmd2-4.1.2 → cmd2-4.2.0}/docs/mixins/index.md +0 -0
  115. {cmd2-4.1.2 → cmd2-4.2.0}/docs/mixins/mixin_template.md +0 -0
  116. {cmd2-4.1.2 → cmd2-4.2.0}/docs/overrides/main.html +0 -0
  117. {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/alternatives.md +0 -0
  118. {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/index.md +0 -0
  119. {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/installation.md +0 -0
  120. {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/integrating.md +0 -0
  121. {cmd2-4.1.2 → cmd2-4.2.0}/docs/overview/resources.md +0 -0
  122. {cmd2-4.1.2 → cmd2-4.2.0}/docs/stylesheets/cmd2.css +0 -0
  123. {cmd2-4.1.2 → cmd2-4.2.0}/docs/testing.md +0 -0
  124. {cmd2-4.1.2 → cmd2-4.2.0}/docs/upgrades.md +0 -0
  125. {cmd2-4.1.2 → cmd2-4.2.0}/examples/.cmd2rc +0 -0
  126. {cmd2-4.1.2 → cmd2-4.2.0}/examples/README.md +0 -0
  127. {cmd2-4.1.2 → cmd2-4.2.0}/examples/argparse_completion.py +0 -0
  128. {cmd2-4.1.2 → cmd2-4.2.0}/examples/argparse_example.py +0 -0
  129. {cmd2-4.1.2 → cmd2-4.2.0}/examples/async_call.py +0 -0
  130. {cmd2-4.1.2 → cmd2-4.2.0}/examples/async_commands.py +0 -0
  131. {cmd2-4.1.2 → cmd2-4.2.0}/examples/async_printing.py +0 -0
  132. {cmd2-4.1.2 → cmd2-4.2.0}/examples/basic_completion.py +0 -0
  133. {cmd2-4.1.2 → cmd2-4.2.0}/examples/cmd_as_argument.py +0 -0
  134. {cmd2-4.1.2 → cmd2-4.2.0}/examples/color.py +0 -0
  135. {cmd2-4.1.2 → cmd2-4.2.0}/examples/command_sets.py +0 -0
  136. {cmd2-4.1.2 → cmd2-4.2.0}/examples/completion_item_choices.py +0 -0
  137. {cmd2-4.1.2 → cmd2-4.2.0}/examples/custom_parser.py +0 -0
  138. {cmd2-4.1.2 → cmd2-4.2.0}/examples/custom_types.py +0 -0
  139. {cmd2-4.1.2 → cmd2-4.2.0}/examples/default_categories.py +0 -0
  140. {cmd2-4.1.2 → cmd2-4.2.0}/examples/dynamic_commands.py +0 -0
  141. {cmd2-4.1.2 → cmd2-4.2.0}/examples/environment.py +0 -0
  142. {cmd2-4.1.2 → cmd2-4.2.0}/examples/event_loops.py +0 -0
  143. {cmd2-4.1.2 → cmd2-4.2.0}/examples/exit_code.py +0 -0
  144. {cmd2-4.1.2 → cmd2-4.2.0}/examples/getting_started.py +0 -0
  145. {cmd2-4.1.2 → cmd2-4.2.0}/examples/hello_cmd2.py +0 -0
  146. {cmd2-4.1.2 → cmd2-4.2.0}/examples/help_categories.py +0 -0
  147. {cmd2-4.1.2 → cmd2-4.2.0}/examples/hooks.py +0 -0
  148. {cmd2-4.1.2 → cmd2-4.2.0}/examples/migrating.py +0 -0
  149. {cmd2-4.1.2 → cmd2-4.2.0}/examples/mixin.py +0 -0
  150. {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commands/__init__.py +0 -0
  151. {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commands/commandset_basic.py +0 -0
  152. {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commands/commandset_complex.py +0 -0
  153. {cmd2-4.1.2 → cmd2-4.2.0}/examples/modular_commands/commandset_custominit.py +0 -0
  154. {cmd2-4.1.2 → cmd2-4.2.0}/examples/paged_output.py +0 -0
  155. {cmd2-4.1.2 → cmd2-4.2.0}/examples/persistent_history.py +0 -0
  156. {cmd2-4.1.2 → cmd2-4.2.0}/examples/pretty_print.py +0 -0
  157. {cmd2-4.1.2 → cmd2-4.2.0}/examples/python_scripting.py +0 -0
  158. {cmd2-4.1.2 → cmd2-4.2.0}/examples/read_input.py +0 -0
  159. {cmd2-4.1.2 → cmd2-4.2.0}/examples/remove_builtin_commands.py +0 -0
  160. {cmd2-4.1.2 → cmd2-4.2.0}/examples/remove_settable.py +0 -0
  161. {cmd2-4.1.2 → cmd2-4.2.0}/examples/rich_tables.py +0 -0
  162. {cmd2-4.1.2 → cmd2-4.2.0}/examples/rich_theme.py +0 -0
  163. {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/arg_printer.py +0 -0
  164. {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/conditional.py +0 -0
  165. {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/nested.txt +0 -0
  166. {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/quit.txt +0 -0
  167. {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/save_help_text.py +0 -0
  168. {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/script.py +0 -0
  169. {cmd2-4.1.2 → cmd2-4.2.0}/examples/scripts/script.txt +0 -0
  170. {cmd2-4.1.2 → cmd2-4.2.0}/examples/tmux_launch.sh +0 -0
  171. {cmd2-4.1.2 → cmd2-4.2.0}/examples/tmux_split.sh +0 -0
  172. {cmd2-4.1.2 → cmd2-4.2.0}/examples/unicode_commands.py +0 -0
  173. {cmd2-4.1.2 → cmd2-4.2.0}/mkdocs.yml +0 -0
  174. {cmd2-4.1.2 → cmd2-4.2.0}/package.json +0 -0
  175. {cmd2-4.1.2 → cmd2-4.2.0}/ruff.toml +0 -0
  176. {cmd2-4.1.2 → cmd2-4.2.0}/scripts/validate_tag.py +0 -0
  177. {cmd2-4.1.2 → cmd2-4.2.0}/setup.cfg +0 -0
  178. {cmd2-4.1.2 → cmd2-4.2.0}/tests/.cmd2rc +0 -0
  179. {cmd2-4.1.2 → cmd2-4.2.0}/tests/__init__.py +0 -0
  180. {cmd2-4.1.2 → cmd2-4.2.0}/tests/conftest.py +0 -0
  181. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/echo.py +0 -0
  182. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/environment.py +0 -0
  183. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/help.py +0 -0
  184. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/py_locals.py +0 -0
  185. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/pyscript_dir.py +0 -0
  186. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/raises_exception.py +0 -0
  187. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/recursive.py +0 -0
  188. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/self_in_py.py +0 -0
  189. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/stop.py +0 -0
  190. {cmd2-4.1.2 → cmd2-4.2.0}/tests/pyscript/test_print.py +0 -0
  191. {cmd2-4.1.2 → cmd2-4.2.0}/tests/relative_multiple.txt +0 -0
  192. {cmd2-4.1.2 → cmd2-4.2.0}/tests/script.py +0 -0
  193. {cmd2-4.1.2 → cmd2-4.2.0}/tests/script.txt +0 -0
  194. {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/binary.bin +0 -0
  195. {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/empty.txt +0 -0
  196. {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/help.txt +0 -0
  197. {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/nested.txt +0 -0
  198. {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/one_down.txt +0 -0
  199. {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/postcmds.txt +0 -0
  200. {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/precmds.txt +0 -0
  201. {cmd2-4.1.2 → cmd2-4.2.0}/tests/scripts/utf8.txt +0 -0
  202. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_argparse.py +0 -0
  203. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_argparse_subcommands.py +0 -0
  204. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_argparse_utils.py +0 -0
  205. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_categories.py +0 -0
  206. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_cmd2.py +0 -0
  207. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_commandset.py +0 -0
  208. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_completion.py +0 -0
  209. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_dynamic_complete_style.py +0 -0
  210. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_future_annotations.py +0 -0
  211. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_history.py +0 -0
  212. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_parsing.py +0 -0
  213. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_plugin.py +0 -0
  214. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_pt_utils.py +0 -0
  215. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_py_completion.py +0 -0
  216. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_rich_utils.py +0 -0
  217. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_run_pyscript.py +0 -0
  218. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_string_utils.py +0 -0
  219. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_theme.py +0 -0
  220. {cmd2-4.1.2 → cmd2-4.2.0}/tests/test_utils.py +0 -0
  221. {cmd2-4.1.2 → cmd2-4.2.0}/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.15.21"
17
+ rev: "v0.16.1"
18
18
  hooks:
19
19
  - id: ruff-format
20
20
  args: [--config=ruff.toml]
@@ -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.1.2
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.52
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 cannot participate in
179
- ``groups=`` / ``mutually_exclusive_groups=`` (those reference the function's own parameter names, which are
180
- validated without resolving type hints). A block must be the *bare* annotation of a regular parameter:
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 = sorted(reserved)[0]
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: parameter names to place in the group (at least one)
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 parameter name")
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
- validated upstream by :func:`_validate_group_specs` before this runs, so every member name
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 from parameter names alone.
2549
-
2550
- Runs at decoration time (from both the regular-command and subcommand decoration paths, and
2551
- again from :func:`build_parser_from_function` for direct callers), so a misconfigured group
2552
- hard-fails when the class is defined instead of on first command use, where cmd2's runtime
2553
- handler turns the error into a printed message. Reads only parameter names and the ``Group``
2554
- specs -- never the type hints -- so forward-referenced annotations still decorate. The one
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(func, skip_params=skip_params, groups=groups, mutually_exclusive_groups=mutually_exclusive_groups)
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: the typeshed stub types title/metavar as non-None,
2831
- # but argparse accepts None at runtime, so splatting avoids a false overload error.
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 choices from action to list of CompletionItems."""
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.1.2
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.52
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
@@ -1,4 +1,4 @@
1
- prompt-toolkit>=3.0.52
1
+ prompt-toolkit>=3.0.53
2
2
  pyperclip>=1.8.2
3
3
  rich>=15.0.0
4
4
  rich-argparse>=1.7.2
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "4.2.0",
3
+ "distance": 0,
4
+ "node": "g1dcd944e8db02c81ba451fada939c110cf1aca7e",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-08-06"
8
+ }
@@ -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 parameter names to argument groups
446
- - `mutually_exclusive_groups` -- `Group` instances of mutually exclusive parameters
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
- All of these group-spec rules -- member references, a parameter assigned to two groups,
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 misconfigured group raises `ValueError` at class definition time instead of on
552
- first command use. The checks read only parameter names, never the type hints, so forward-referenced
553
- annotations still decorate cleanly. The one group rule that depends on the annotations (a member of
554
- a mutually exclusive group must be omittable -- have a default or be `T | None`) fires when the
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) -> list[str]:
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(
@@ -28,7 +28,7 @@ classifiers = [
28
28
  "Topic :: Software Development :: Libraries :: Python Modules",
29
29
  ]
30
30
  dependencies = [
31
- "prompt-toolkit>=3.0.52",
31
+ "prompt-toolkit>=3.0.53",
32
32
  "pyperclip>=1.8.2",
33
33
  "rich>=15.0.0",
34
34
  "rich-argparse>=1.7.2",