cmd2 4.1.1__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 (222) hide show
  1. {cmd2-4.1.1 → cmd2-4.2.0}/.pre-commit-config.yaml +1 -1
  2. {cmd2-4.1.1 → cmd2-4.2.0}/CHANGELOG.md +35 -0
  3. {cmd2-4.1.1 → cmd2-4.2.0}/PKG-INFO +2 -2
  4. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/annotated.py +65 -47
  5. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/argparse_completer.py +4 -2
  6. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/cmd2.py +4 -13
  7. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/utils.py +6 -3
  8. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2.egg-info/PKG-INFO +2 -2
  9. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2.egg-info/SOURCES.txt +0 -1
  10. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2.egg-info/requires.txt +1 -1
  11. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2.egg-info/scm_file_list.json +186 -186
  12. cmd2-4.2.0/cmd2.egg-info/scm_version.json +8 -0
  13. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/annotated.md +35 -10
  14. {cmd2-4.1.1 → cmd2-4.2.0}/examples/annotated_example.py +35 -0
  15. {cmd2-4.1.1 → cmd2-4.2.0}/examples/default_categories.py +6 -7
  16. {cmd2-4.1.1 → cmd2-4.2.0}/examples/getting_started.py +28 -25
  17. {cmd2-4.1.1 → cmd2-4.2.0}/examples/hello_cmd2.py +2 -2
  18. {cmd2-4.1.1 → cmd2-4.2.0}/examples/modular_commandsets.py +3 -2
  19. {cmd2-4.1.1 → cmd2-4.2.0}/pyproject.toml +1 -1
  20. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_annotated.py +173 -31
  21. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_argparse_completer.py +52 -0
  22. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_cmd2.py +26 -1
  23. cmd2-4.1.1/cmd2.egg-info/scm_version.json +0 -8
  24. cmd2-4.1.1/examples/cmd2_history.dat +0 -0
  25. {cmd2-4.1.1 → cmd2-4.2.0}/.prettierignore +0 -0
  26. {cmd2-4.1.1 → cmd2-4.2.0}/.prettierrc +0 -0
  27. {cmd2-4.1.1 → cmd2-4.2.0}/.typos.toml +0 -0
  28. {cmd2-4.1.1 → cmd2-4.2.0}/GEMINI.md +0 -0
  29. {cmd2-4.1.1 → cmd2-4.2.0}/LICENSE +0 -0
  30. {cmd2-4.1.1 → cmd2-4.2.0}/MANIFEST.in +0 -0
  31. {cmd2-4.1.1 → cmd2-4.2.0}/Makefile +0 -0
  32. {cmd2-4.1.1 → cmd2-4.2.0}/README.md +0 -0
  33. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/__init__.py +0 -0
  34. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/argparse_utils.py +0 -0
  35. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/clipboard.py +0 -0
  36. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/colors.py +0 -0
  37. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/command_set.py +0 -0
  38. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/completion.py +0 -0
  39. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/constants.py +0 -0
  40. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/decorators.py +0 -0
  41. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/exceptions.py +0 -0
  42. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/history.py +0 -0
  43. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/parsing.py +0 -0
  44. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/plugin.py +0 -0
  45. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/pt_utils.py +0 -0
  46. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/py.typed +0 -0
  47. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/py_bridge.py +0 -0
  48. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/rich_utils.py +0 -0
  49. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/string_utils.py +0 -0
  50. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/styles.py +0 -0
  51. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/theme.py +0 -0
  52. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2/types.py +0 -0
  53. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2.egg-info/dependency_links.txt +0 -0
  54. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2.egg-info/top_level.txt +0 -0
  55. {cmd2-4.1.1 → cmd2-4.2.0}/cmd2.png +0 -0
  56. {cmd2-4.1.1 → cmd2-4.2.0}/codecov.yml +0 -0
  57. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/annotated.md +0 -0
  58. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/argparse_completer.md +0 -0
  59. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/argparse_utils.md +0 -0
  60. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/clipboard.md +0 -0
  61. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/cmd.md +0 -0
  62. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/colors.md +0 -0
  63. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/command_set.md +0 -0
  64. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/completion.md +0 -0
  65. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/constants.md +0 -0
  66. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/decorators.md +0 -0
  67. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/exceptions.md +0 -0
  68. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/history.md +0 -0
  69. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/index.md +0 -0
  70. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/parsing.md +0 -0
  71. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/plugin.md +0 -0
  72. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/pt_utils.md +0 -0
  73. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/py_bridge.md +0 -0
  74. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/rich_utils.md +0 -0
  75. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/string_utils.md +0 -0
  76. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/styles.md +0 -0
  77. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/theme.md +0 -0
  78. {cmd2-4.1.1 → cmd2-4.2.0}/docs/api/utils.md +0 -0
  79. {cmd2-4.1.1 → cmd2-4.2.0}/docs/doc_conventions.md +0 -0
  80. {cmd2-4.1.1 → cmd2-4.2.0}/docs/examples/alternate_event_loops.md +0 -0
  81. {cmd2-4.1.1 → cmd2-4.2.0}/docs/examples/examples.md +0 -0
  82. {cmd2-4.1.1 → cmd2-4.2.0}/docs/examples/getting_started.md +0 -0
  83. {cmd2-4.1.1 → cmd2-4.2.0}/docs/examples/index.md +0 -0
  84. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/argument_processing.md +0 -0
  85. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/async_commands.md +0 -0
  86. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/builtin_commands.md +0 -0
  87. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/clipboard.md +0 -0
  88. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/commands.md +0 -0
  89. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/completion.md +0 -0
  90. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/disable_commands.md +0 -0
  91. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/embedded_python_shells.md +0 -0
  92. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/generating_output.md +0 -0
  93. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/help.md +0 -0
  94. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/history.md +0 -0
  95. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/hooks.md +0 -0
  96. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/index.md +0 -0
  97. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/initialization.md +0 -0
  98. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/misc.md +0 -0
  99. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/modular_commands.md +0 -0
  100. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/multiline_commands.md +0 -0
  101. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/os.md +0 -0
  102. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/packaging.md +0 -0
  103. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/plugins.md +0 -0
  104. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/prompt.md +0 -0
  105. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/redirection.md +0 -0
  106. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/scripting.md +0 -0
  107. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/settings.md +0 -0
  108. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/shortcuts_aliases_macros.md +0 -0
  109. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/startup_commands.md +0 -0
  110. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/table_creation.md +0 -0
  111. {cmd2-4.1.1 → cmd2-4.2.0}/docs/features/theme.md +0 -0
  112. {cmd2-4.1.1 → cmd2-4.2.0}/docs/index.md +0 -0
  113. {cmd2-4.1.1 → cmd2-4.2.0}/docs/javascripts/readthedocs.js +0 -0
  114. {cmd2-4.1.1 → cmd2-4.2.0}/docs/migrating/incompatibilities.md +0 -0
  115. {cmd2-4.1.1 → cmd2-4.2.0}/docs/migrating/index.md +0 -0
  116. {cmd2-4.1.1 → cmd2-4.2.0}/docs/migrating/minimum.md +0 -0
  117. {cmd2-4.1.1 → cmd2-4.2.0}/docs/migrating/next_steps.md +0 -0
  118. {cmd2-4.1.1 → cmd2-4.2.0}/docs/migrating/why.md +0 -0
  119. {cmd2-4.1.1 → cmd2-4.2.0}/docs/mixins/index.md +0 -0
  120. {cmd2-4.1.1 → cmd2-4.2.0}/docs/mixins/mixin_template.md +0 -0
  121. {cmd2-4.1.1 → cmd2-4.2.0}/docs/overrides/main.html +0 -0
  122. {cmd2-4.1.1 → cmd2-4.2.0}/docs/overview/alternatives.md +0 -0
  123. {cmd2-4.1.1 → cmd2-4.2.0}/docs/overview/index.md +0 -0
  124. {cmd2-4.1.1 → cmd2-4.2.0}/docs/overview/installation.md +0 -0
  125. {cmd2-4.1.1 → cmd2-4.2.0}/docs/overview/integrating.md +0 -0
  126. {cmd2-4.1.1 → cmd2-4.2.0}/docs/overview/resources.md +0 -0
  127. {cmd2-4.1.1 → cmd2-4.2.0}/docs/stylesheets/cmd2.css +0 -0
  128. {cmd2-4.1.1 → cmd2-4.2.0}/docs/testing.md +0 -0
  129. {cmd2-4.1.1 → cmd2-4.2.0}/docs/upgrades.md +0 -0
  130. {cmd2-4.1.1 → cmd2-4.2.0}/examples/.cmd2rc +0 -0
  131. {cmd2-4.1.1 → cmd2-4.2.0}/examples/README.md +0 -0
  132. {cmd2-4.1.1 → cmd2-4.2.0}/examples/argparse_completion.py +0 -0
  133. {cmd2-4.1.1 → cmd2-4.2.0}/examples/argparse_example.py +0 -0
  134. {cmd2-4.1.1 → cmd2-4.2.0}/examples/async_call.py +0 -0
  135. {cmd2-4.1.1 → cmd2-4.2.0}/examples/async_commands.py +0 -0
  136. {cmd2-4.1.1 → cmd2-4.2.0}/examples/async_printing.py +0 -0
  137. {cmd2-4.1.1 → cmd2-4.2.0}/examples/basic_completion.py +0 -0
  138. {cmd2-4.1.1 → cmd2-4.2.0}/examples/cmd_as_argument.py +0 -0
  139. {cmd2-4.1.1 → cmd2-4.2.0}/examples/color.py +0 -0
  140. {cmd2-4.1.1 → cmd2-4.2.0}/examples/command_sets.py +0 -0
  141. {cmd2-4.1.1 → cmd2-4.2.0}/examples/completion_item_choices.py +0 -0
  142. {cmd2-4.1.1 → cmd2-4.2.0}/examples/custom_parser.py +0 -0
  143. {cmd2-4.1.1 → cmd2-4.2.0}/examples/custom_types.py +0 -0
  144. {cmd2-4.1.1 → cmd2-4.2.0}/examples/dynamic_commands.py +0 -0
  145. {cmd2-4.1.1 → cmd2-4.2.0}/examples/environment.py +0 -0
  146. {cmd2-4.1.1 → cmd2-4.2.0}/examples/event_loops.py +0 -0
  147. {cmd2-4.1.1 → cmd2-4.2.0}/examples/exit_code.py +0 -0
  148. {cmd2-4.1.1 → cmd2-4.2.0}/examples/help_categories.py +0 -0
  149. {cmd2-4.1.1 → cmd2-4.2.0}/examples/hooks.py +0 -0
  150. {cmd2-4.1.1 → cmd2-4.2.0}/examples/migrating.py +0 -0
  151. {cmd2-4.1.1 → cmd2-4.2.0}/examples/mixin.py +0 -0
  152. {cmd2-4.1.1 → cmd2-4.2.0}/examples/modular_commands/__init__.py +0 -0
  153. {cmd2-4.1.1 → cmd2-4.2.0}/examples/modular_commands/commandset_basic.py +0 -0
  154. {cmd2-4.1.1 → cmd2-4.2.0}/examples/modular_commands/commandset_complex.py +0 -0
  155. {cmd2-4.1.1 → cmd2-4.2.0}/examples/modular_commands/commandset_custominit.py +0 -0
  156. {cmd2-4.1.1 → cmd2-4.2.0}/examples/paged_output.py +0 -0
  157. {cmd2-4.1.1 → cmd2-4.2.0}/examples/persistent_history.py +0 -0
  158. {cmd2-4.1.1 → cmd2-4.2.0}/examples/pretty_print.py +0 -0
  159. {cmd2-4.1.1 → cmd2-4.2.0}/examples/python_scripting.py +0 -0
  160. {cmd2-4.1.1 → cmd2-4.2.0}/examples/read_input.py +0 -0
  161. {cmd2-4.1.1 → cmd2-4.2.0}/examples/remove_builtin_commands.py +0 -0
  162. {cmd2-4.1.1 → cmd2-4.2.0}/examples/remove_settable.py +0 -0
  163. {cmd2-4.1.1 → cmd2-4.2.0}/examples/rich_tables.py +0 -0
  164. {cmd2-4.1.1 → cmd2-4.2.0}/examples/rich_theme.py +0 -0
  165. {cmd2-4.1.1 → cmd2-4.2.0}/examples/scripts/arg_printer.py +0 -0
  166. {cmd2-4.1.1 → cmd2-4.2.0}/examples/scripts/conditional.py +0 -0
  167. {cmd2-4.1.1 → cmd2-4.2.0}/examples/scripts/nested.txt +0 -0
  168. {cmd2-4.1.1 → cmd2-4.2.0}/examples/scripts/quit.txt +0 -0
  169. {cmd2-4.1.1 → cmd2-4.2.0}/examples/scripts/save_help_text.py +0 -0
  170. {cmd2-4.1.1 → cmd2-4.2.0}/examples/scripts/script.py +0 -0
  171. {cmd2-4.1.1 → cmd2-4.2.0}/examples/scripts/script.txt +0 -0
  172. {cmd2-4.1.1 → cmd2-4.2.0}/examples/tmux_launch.sh +0 -0
  173. {cmd2-4.1.1 → cmd2-4.2.0}/examples/tmux_split.sh +0 -0
  174. {cmd2-4.1.1 → cmd2-4.2.0}/examples/unicode_commands.py +0 -0
  175. {cmd2-4.1.1 → cmd2-4.2.0}/mkdocs.yml +0 -0
  176. {cmd2-4.1.1 → cmd2-4.2.0}/package.json +0 -0
  177. {cmd2-4.1.1 → cmd2-4.2.0}/ruff.toml +0 -0
  178. {cmd2-4.1.1 → cmd2-4.2.0}/scripts/validate_tag.py +0 -0
  179. {cmd2-4.1.1 → cmd2-4.2.0}/setup.cfg +0 -0
  180. {cmd2-4.1.1 → cmd2-4.2.0}/tests/.cmd2rc +0 -0
  181. {cmd2-4.1.1 → cmd2-4.2.0}/tests/__init__.py +0 -0
  182. {cmd2-4.1.1 → cmd2-4.2.0}/tests/conftest.py +0 -0
  183. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/echo.py +0 -0
  184. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/environment.py +0 -0
  185. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/help.py +0 -0
  186. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/py_locals.py +0 -0
  187. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/pyscript_dir.py +0 -0
  188. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/raises_exception.py +0 -0
  189. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/recursive.py +0 -0
  190. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/self_in_py.py +0 -0
  191. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/stop.py +0 -0
  192. {cmd2-4.1.1 → cmd2-4.2.0}/tests/pyscript/test_print.py +0 -0
  193. {cmd2-4.1.1 → cmd2-4.2.0}/tests/relative_multiple.txt +0 -0
  194. {cmd2-4.1.1 → cmd2-4.2.0}/tests/script.py +0 -0
  195. {cmd2-4.1.1 → cmd2-4.2.0}/tests/script.txt +0 -0
  196. {cmd2-4.1.1 → cmd2-4.2.0}/tests/scripts/binary.bin +0 -0
  197. {cmd2-4.1.1 → cmd2-4.2.0}/tests/scripts/empty.txt +0 -0
  198. {cmd2-4.1.1 → cmd2-4.2.0}/tests/scripts/help.txt +0 -0
  199. {cmd2-4.1.1 → cmd2-4.2.0}/tests/scripts/nested.txt +0 -0
  200. {cmd2-4.1.1 → cmd2-4.2.0}/tests/scripts/one_down.txt +0 -0
  201. {cmd2-4.1.1 → cmd2-4.2.0}/tests/scripts/postcmds.txt +0 -0
  202. {cmd2-4.1.1 → cmd2-4.2.0}/tests/scripts/precmds.txt +0 -0
  203. {cmd2-4.1.1 → cmd2-4.2.0}/tests/scripts/utf8.txt +0 -0
  204. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_argparse.py +0 -0
  205. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_argparse_subcommands.py +0 -0
  206. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_argparse_utils.py +0 -0
  207. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_categories.py +0 -0
  208. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_commandset.py +0 -0
  209. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_completion.py +0 -0
  210. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_dynamic_complete_style.py +0 -0
  211. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_future_annotations.py +0 -0
  212. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_history.py +0 -0
  213. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_parsing.py +0 -0
  214. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_plugin.py +0 -0
  215. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_pt_utils.py +0 -0
  216. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_py_completion.py +0 -0
  217. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_rich_utils.py +0 -0
  218. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_run_pyscript.py +0 -0
  219. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_string_utils.py +0 -0
  220. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_theme.py +0 -0
  221. {cmd2-4.1.1 → cmd2-4.2.0}/tests/test_utils.py +0 -0
  222. {cmd2-4.1.1 → 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,38 @@
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
+
30
+ ## 4.1.2 (July 16, 2026)
31
+
32
+ - Enhancements
33
+ - Added possibility to use rich Text objects to set the description argument of a `Settable`
34
+ - Removed redundant `ipython` import which slowed application startup time
35
+
1
36
  ## 4.1.1 (July 9, 2026)
2
37
 
3
38
  - Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmd2
3
- Version: 4.1.1
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
 
@@ -177,9 +177,6 @@ from .types import (
177
177
  UnboundCompleter,
178
178
  )
179
179
 
180
- with contextlib.suppress(ImportError):
181
- from IPython import start_ipython
182
-
183
180
  try:
184
181
  if sys.platform == "win32":
185
182
  from prompt_toolkit.output.win32 import NoConsoleScreenBufferError # type: ignore[attr-defined]
@@ -1382,7 +1379,7 @@ class Cmd:
1382
1379
  Settable(
1383
1380
  "allow_style",
1384
1381
  allow_style_type,
1385
- ru.rich_text_to_string(settable_description),
1382
+ settable_description,
1386
1383
  self,
1387
1384
  choices_provider=get_allow_style_choices,
1388
1385
  )
@@ -1399,7 +1396,7 @@ class Cmd:
1399
1396
  Settable(
1400
1397
  "editor",
1401
1398
  str,
1402
- ru.rich_text_to_string(editor_description),
1399
+ editor_description,
1403
1400
  self,
1404
1401
  )
1405
1402
  )
@@ -1434,7 +1431,7 @@ class Cmd:
1434
1431
  Settable(
1435
1432
  "traceback_width",
1436
1433
  utils.optional_int,
1437
- ru.rich_text_to_string(traceback_width_description),
1434
+ traceback_width_description,
1438
1435
  self,
1439
1436
  )
1440
1437
  )
@@ -5151,13 +5148,7 @@ class Cmd:
5151
5148
  # Detect whether IPython is installed
5152
5149
  try:
5153
5150
  import traitlets.config.loader as traitlets_loader
5154
-
5155
- # Allow users to install ipython from a cmd2 prompt when needed and still have ipy command work
5156
- try:
5157
- _dummy = start_ipython # noqa: F823
5158
- except NameError:
5159
- from IPython import start_ipython
5160
-
5151
+ from IPython import start_ipython
5161
5152
  from IPython.terminal.interactiveshell import TerminalInteractiveShell
5162
5153
  from IPython.terminal.ipapp import TerminalIPythonApp
5163
5154
  except ImportError:
@@ -25,7 +25,10 @@ from typing import (
25
25
  cast,
26
26
  )
27
27
 
28
+ from rich.text import Text
29
+
28
30
  from . import constants
31
+ from . import rich_utils as ru
29
32
  from . import string_utils as su
30
33
  from .types import (
31
34
  CmdOrSet,
@@ -88,7 +91,7 @@ class Settable:
88
91
  self,
89
92
  name: str,
90
93
  val_type: type[Any] | Callable[[Any], Any],
91
- description: str,
94
+ description: str | Text,
92
95
  settable_object: object,
93
96
  *,
94
97
  settable_attrib_name: str | None = None,
@@ -108,7 +111,7 @@ class Settable:
108
111
  input is a valid integer. Specifying bool automatically provides
109
112
  completion for 'true' and 'false' and uses a built-in function
110
113
  for conversion and validation.
111
- :param description: A concise string that describes the purpose of this setting.
114
+ :param description: A concise string or rich Text object that describes the purpose of this setting.
112
115
  :param settable_object: The object that owns the attribute being made settable (e.g. self).
113
116
  :param settable_attrib_name: The name of the attribute on the settable_object that
114
117
  will be modified. This defaults to the value of the name
@@ -142,7 +145,7 @@ class Settable:
142
145
 
143
146
  self.name = name
144
147
  self.val_type = val_type
145
- self.description = description
148
+ self.description = ru.rich_text_to_string(description) if isinstance(description, Text) else description
146
149
  self.settable_obj = settable_object
147
150
  self.settable_attrib_name = settable_attrib_name if settable_attrib_name is not None else name
148
151
  self.onchange_cb = onchange_cb
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmd2
3
- Version: 4.1.1
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
@@ -128,7 +128,6 @@ examples/async_call.py
128
128
  examples/async_commands.py
129
129
  examples/async_printing.py
130
130
  examples/basic_completion.py
131
- examples/cmd2_history.dat
132
131
  examples/cmd_as_argument.py
133
132
  examples/color.py
134
133
  examples/command_sets.py
@@ -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