cmd2 2.5.6__tar.gz → 2.5.7__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 (249) hide show
  1. {cmd2-2.5.6 → cmd2-2.5.7}/CHANGELOG.md +5 -0
  2. {cmd2-2.5.6 → cmd2-2.5.7}/PKG-INFO +1 -1
  3. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/cmd2.py +195 -133
  4. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/decorators.py +3 -1
  5. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2.egg-info/PKG-INFO +1 -1
  6. {cmd2-2.5.6 → cmd2-2.5.7}/pyproject.toml +1 -0
  7. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_argparse.py +1 -3
  8. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_cmd2.py +35 -14
  9. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/from_cmdloop.txt +1 -1
  10. {cmd2-2.5.6 → cmd2-2.5.7}/tests_isolated/test_commandset/test_commandset.py +57 -3
  11. {cmd2-2.5.6 → cmd2-2.5.7}/.readthedocs.yaml +0 -0
  12. {cmd2-2.5.6 → cmd2-2.5.7}/LICENSE +0 -0
  13. {cmd2-2.5.6 → cmd2-2.5.7}/MANIFEST.in +0 -0
  14. {cmd2-2.5.6 → cmd2-2.5.7}/Pipfile +0 -0
  15. {cmd2-2.5.6 → cmd2-2.5.7}/README.md +0 -0
  16. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/__init__.py +0 -0
  17. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/ansi.py +0 -0
  18. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/argparse_completer.py +0 -0
  19. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/argparse_custom.py +0 -0
  20. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/clipboard.py +0 -0
  21. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/command_definition.py +0 -0
  22. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/constants.py +0 -0
  23. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/exceptions.py +0 -0
  24. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/history.py +0 -0
  25. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/parsing.py +0 -0
  26. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/plugin.py +0 -0
  27. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/py.typed +0 -0
  28. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/py_bridge.py +0 -0
  29. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/rl_utils.py +0 -0
  30. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/table_creator.py +0 -0
  31. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/transcript.py +0 -0
  32. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2/utils.py +0 -0
  33. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2.egg-info/SOURCES.txt +0 -0
  34. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2.egg-info/dependency_links.txt +0 -0
  35. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2.egg-info/requires.txt +0 -0
  36. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2.egg-info/top_level.txt +0 -0
  37. {cmd2-2.5.6 → cmd2-2.5.7}/cmd2.png +0 -0
  38. {cmd2-2.5.6 → cmd2-2.5.7}/docs/Makefile +0 -0
  39. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/ansi.rst +0 -0
  40. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/argparse_completer.rst +0 -0
  41. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/argparse_custom.rst +0 -0
  42. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/cmd.rst +0 -0
  43. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/command_definition.rst +0 -0
  44. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/constants.rst +0 -0
  45. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/decorators.rst +0 -0
  46. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/exceptions.rst +0 -0
  47. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/history.rst +0 -0
  48. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/index.rst +0 -0
  49. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/parsing.rst +0 -0
  50. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/plugin.rst +0 -0
  51. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/plugin_external_test.rst +0 -0
  52. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/py_bridge.rst +0 -0
  53. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/table_creator.rst +0 -0
  54. {cmd2-2.5.6 → cmd2-2.5.7}/docs/api/utils.rst +0 -0
  55. {cmd2-2.5.6 → cmd2-2.5.7}/docs/conf.py +0 -0
  56. {cmd2-2.5.6 → cmd2-2.5.7}/docs/doc_conventions.rst +0 -0
  57. {cmd2-2.5.6 → cmd2-2.5.7}/docs/examples/alternate_event_loops.rst +0 -0
  58. {cmd2-2.5.6 → cmd2-2.5.7}/docs/examples/first_app.rst +0 -0
  59. {cmd2-2.5.6 → cmd2-2.5.7}/docs/examples/index.rst +0 -0
  60. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/argument_processing.rst +0 -0
  61. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/builtin_commands.rst +0 -0
  62. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/clipboard.rst +0 -0
  63. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/commands.rst +0 -0
  64. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/completion.rst +0 -0
  65. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/disable_commands.rst +0 -0
  66. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/embedded_python_shells.rst +0 -0
  67. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/generating_output.rst +0 -0
  68. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/help.rst +0 -0
  69. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/history.rst +0 -0
  70. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/hooks.rst +0 -0
  71. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/index.rst +0 -0
  72. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/initialization.rst +0 -0
  73. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/misc.rst +0 -0
  74. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/modular_commands.rst +0 -0
  75. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/multiline_commands.rst +0 -0
  76. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/os.rst +0 -0
  77. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/packaging.rst +0 -0
  78. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/plugins.rst +0 -0
  79. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/prompt.rst +0 -0
  80. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/redirection.rst +0 -0
  81. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/scripting.rst +0 -0
  82. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/settings.rst +0 -0
  83. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/shortcuts_aliases_macros.rst +0 -0
  84. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/startup_commands.rst +0 -0
  85. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/table_creation.rst +0 -0
  86. {cmd2-2.5.6 → cmd2-2.5.7}/docs/features/transcripts.rst +0 -0
  87. {cmd2-2.5.6 → cmd2-2.5.7}/docs/index.rst +0 -0
  88. {cmd2-2.5.6 → cmd2-2.5.7}/docs/make.bat +0 -0
  89. {cmd2-2.5.6 → cmd2-2.5.7}/docs/migrating/incompatibilities.rst +0 -0
  90. {cmd2-2.5.6 → cmd2-2.5.7}/docs/migrating/index.rst +0 -0
  91. {cmd2-2.5.6 → cmd2-2.5.7}/docs/migrating/minimum.rst +0 -0
  92. {cmd2-2.5.6 → cmd2-2.5.7}/docs/migrating/next_steps.rst +0 -0
  93. {cmd2-2.5.6 → cmd2-2.5.7}/docs/migrating/summary.rst +0 -0
  94. {cmd2-2.5.6 → cmd2-2.5.7}/docs/migrating/why.rst +0 -0
  95. {cmd2-2.5.6 → cmd2-2.5.7}/docs/overview/alternatives.rst +0 -0
  96. {cmd2-2.5.6 → cmd2-2.5.7}/docs/overview/index.rst +0 -0
  97. {cmd2-2.5.6 → cmd2-2.5.7}/docs/overview/installation.rst +0 -0
  98. {cmd2-2.5.6 → cmd2-2.5.7}/docs/overview/integrating.rst +0 -0
  99. {cmd2-2.5.6 → cmd2-2.5.7}/docs/overview/resources.rst +0 -0
  100. {cmd2-2.5.6 → cmd2-2.5.7}/docs/overview/summary.rst +0 -0
  101. {cmd2-2.5.6 → cmd2-2.5.7}/docs/plugins/external_test.rst +0 -0
  102. {cmd2-2.5.6 → cmd2-2.5.7}/docs/plugins/index.rst +0 -0
  103. {cmd2-2.5.6 → cmd2-2.5.7}/docs/requirements.txt +0 -0
  104. {cmd2-2.5.6 → cmd2-2.5.7}/docs/testing.rst +0 -0
  105. {cmd2-2.5.6 → cmd2-2.5.7}/examples/.cmd2rc +0 -0
  106. {cmd2-2.5.6 → cmd2-2.5.7}/examples/alias_startup.py +0 -0
  107. {cmd2-2.5.6 → cmd2-2.5.7}/examples/arg_decorators.py +0 -0
  108. {cmd2-2.5.6 → cmd2-2.5.7}/examples/arg_print.py +0 -0
  109. {cmd2-2.5.6 → cmd2-2.5.7}/examples/argparse_completion.py +0 -0
  110. {cmd2-2.5.6 → cmd2-2.5.7}/examples/async_printing.py +0 -0
  111. {cmd2-2.5.6 → cmd2-2.5.7}/examples/basic.py +0 -0
  112. {cmd2-2.5.6 → cmd2-2.5.7}/examples/basic_completion.py +0 -0
  113. {cmd2-2.5.6 → cmd2-2.5.7}/examples/cmd_as_argument.py +0 -0
  114. {cmd2-2.5.6 → cmd2-2.5.7}/examples/colors.py +0 -0
  115. {cmd2-2.5.6 → cmd2-2.5.7}/examples/custom_parser.py +0 -0
  116. {cmd2-2.5.6 → cmd2-2.5.7}/examples/decorator_example.py +0 -0
  117. {cmd2-2.5.6 → cmd2-2.5.7}/examples/default_categories.py +0 -0
  118. {cmd2-2.5.6 → cmd2-2.5.7}/examples/dynamic_commands.py +0 -0
  119. {cmd2-2.5.6 → cmd2-2.5.7}/examples/environment.py +0 -0
  120. {cmd2-2.5.6 → cmd2-2.5.7}/examples/event_loops.py +0 -0
  121. {cmd2-2.5.6 → cmd2-2.5.7}/examples/example.py +0 -0
  122. {cmd2-2.5.6 → cmd2-2.5.7}/examples/exit_code.py +0 -0
  123. {cmd2-2.5.6 → cmd2-2.5.7}/examples/first_app.py +0 -0
  124. {cmd2-2.5.6 → cmd2-2.5.7}/examples/hello_cmd2.py +0 -0
  125. {cmd2-2.5.6 → cmd2-2.5.7}/examples/help_categories.py +0 -0
  126. {cmd2-2.5.6 → cmd2-2.5.7}/examples/hooks.py +0 -0
  127. {cmd2-2.5.6 → cmd2-2.5.7}/examples/initialization.py +0 -0
  128. {cmd2-2.5.6 → cmd2-2.5.7}/examples/migrating.py +0 -0
  129. {cmd2-2.5.6 → cmd2-2.5.7}/examples/modular_commands/__init__.py +0 -0
  130. {cmd2-2.5.6 → cmd2-2.5.7}/examples/modular_commands/commandset_basic.py +0 -0
  131. {cmd2-2.5.6 → cmd2-2.5.7}/examples/modular_commands/commandset_complex.py +0 -0
  132. {cmd2-2.5.6 → cmd2-2.5.7}/examples/modular_commands/commandset_custominit.py +0 -0
  133. {cmd2-2.5.6 → cmd2-2.5.7}/examples/modular_commands_basic.py +0 -0
  134. {cmd2-2.5.6 → cmd2-2.5.7}/examples/modular_commands_dynamic.py +0 -0
  135. {cmd2-2.5.6 → cmd2-2.5.7}/examples/modular_commands_main.py +0 -0
  136. {cmd2-2.5.6 → cmd2-2.5.7}/examples/modular_subcommands.py +0 -0
  137. {cmd2-2.5.6 → cmd2-2.5.7}/examples/override_parser.py +0 -0
  138. {cmd2-2.5.6 → cmd2-2.5.7}/examples/paged_output.py +0 -0
  139. {cmd2-2.5.6 → cmd2-2.5.7}/examples/persistent_history.py +0 -0
  140. {cmd2-2.5.6 → cmd2-2.5.7}/examples/pirate.py +0 -0
  141. {cmd2-2.5.6 → cmd2-2.5.7}/examples/python_scripting.py +0 -0
  142. {cmd2-2.5.6 → cmd2-2.5.7}/examples/read_input.py +0 -0
  143. {cmd2-2.5.6 → cmd2-2.5.7}/examples/remove_builtin_commands.py +0 -0
  144. {cmd2-2.5.6 → cmd2-2.5.7}/examples/remove_settable.py +0 -0
  145. {cmd2-2.5.6 → cmd2-2.5.7}/examples/scripts/arg_printer.py +0 -0
  146. {cmd2-2.5.6 → cmd2-2.5.7}/examples/scripts/conditional.py +0 -0
  147. {cmd2-2.5.6 → cmd2-2.5.7}/examples/scripts/nested.txt +0 -0
  148. {cmd2-2.5.6 → cmd2-2.5.7}/examples/scripts/quit.txt +0 -0
  149. {cmd2-2.5.6 → cmd2-2.5.7}/examples/scripts/save_help_text.py +0 -0
  150. {cmd2-2.5.6 → cmd2-2.5.7}/examples/scripts/script.py +0 -0
  151. {cmd2-2.5.6 → cmd2-2.5.7}/examples/scripts/script.txt +0 -0
  152. {cmd2-2.5.6 → cmd2-2.5.7}/examples/subcommands.py +0 -0
  153. {cmd2-2.5.6 → cmd2-2.5.7}/examples/table_creation.py +0 -0
  154. {cmd2-2.5.6 → cmd2-2.5.7}/examples/transcripts/exampleSession.txt +0 -0
  155. {cmd2-2.5.6 → cmd2-2.5.7}/examples/transcripts/pirate.transcript +0 -0
  156. {cmd2-2.5.6 → cmd2-2.5.7}/examples/transcripts/quit.txt +0 -0
  157. {cmd2-2.5.6 → cmd2-2.5.7}/examples/transcripts/transcript_regex.txt +0 -0
  158. {cmd2-2.5.6 → cmd2-2.5.7}/examples/unicode_commands.py +0 -0
  159. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/README.txt +0 -0
  160. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/CHANGELOG.md +0 -0
  161. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/README.md +0 -0
  162. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/build-pyenvs.sh +0 -0
  163. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/cmd2_ext_test/__init__.py +0 -0
  164. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py +0 -0
  165. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/cmd2_ext_test/py.typed +0 -0
  166. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/cmd2_ext_test/pylintrc +0 -0
  167. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/examples/example.py +0 -0
  168. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/noxfile.py +0 -0
  169. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/pyproject.toml +0 -0
  170. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/setup.py +0 -0
  171. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/tasks.py +0 -0
  172. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/tests/__init__.py +0 -0
  173. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/tests/pylintrc +0 -0
  174. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/ext_test/tests/test_ext_test.py +0 -0
  175. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/tasks.py +0 -0
  176. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/CHANGELOG.md +0 -0
  177. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/LICENSE +0 -0
  178. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/README.md +0 -0
  179. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/build-pyenvs.sh +0 -0
  180. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/cmd2_myplugin/__init__.py +0 -0
  181. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/cmd2_myplugin/myplugin.py +0 -0
  182. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/cmd2_myplugin/pylintrc +0 -0
  183. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/examples/example.py +0 -0
  184. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/noxfile.py +0 -0
  185. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/setup.py +0 -0
  186. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/tasks.py +0 -0
  187. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/tests/__init__.py +0 -0
  188. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/tests/pylintrc +0 -0
  189. {cmd2-2.5.6 → cmd2-2.5.7}/plugins/template/tests/test_myplugin.py +0 -0
  190. {cmd2-2.5.6 → cmd2-2.5.7}/readme_files/shout_out.csv +0 -0
  191. {cmd2-2.5.6 → cmd2-2.5.7}/readme_files/shoutout.txt +0 -0
  192. {cmd2-2.5.6 → cmd2-2.5.7}/setup.cfg +0 -0
  193. {cmd2-2.5.6 → cmd2-2.5.7}/tasks.py +0 -0
  194. {cmd2-2.5.6 → cmd2-2.5.7}/tests/.cmd2rc +0 -0
  195. {cmd2-2.5.6 → cmd2-2.5.7}/tests/__init__.py +0 -0
  196. {cmd2-2.5.6 → cmd2-2.5.7}/tests/conftest.py +0 -0
  197. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/echo.py +0 -0
  198. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/environment.py +0 -0
  199. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/help.py +0 -0
  200. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/py_locals.py +0 -0
  201. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/pyscript_dir.py +0 -0
  202. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/raises_exception.py +0 -0
  203. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/recursive.py +0 -0
  204. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/self_in_py.py +0 -0
  205. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/stdout_capture.py +0 -0
  206. {cmd2-2.5.6 → cmd2-2.5.7}/tests/pyscript/stop.py +0 -0
  207. {cmd2-2.5.6 → cmd2-2.5.7}/tests/relative_multiple.txt +0 -0
  208. {cmd2-2.5.6 → cmd2-2.5.7}/tests/script.py +0 -0
  209. {cmd2-2.5.6 → cmd2-2.5.7}/tests/script.txt +0 -0
  210. {cmd2-2.5.6 → cmd2-2.5.7}/tests/scripts/binary.bin +0 -0
  211. {cmd2-2.5.6 → cmd2-2.5.7}/tests/scripts/empty.txt +0 -0
  212. {cmd2-2.5.6 → cmd2-2.5.7}/tests/scripts/help.txt +0 -0
  213. {cmd2-2.5.6 → cmd2-2.5.7}/tests/scripts/nested.txt +0 -0
  214. {cmd2-2.5.6 → cmd2-2.5.7}/tests/scripts/one_down.txt +0 -0
  215. {cmd2-2.5.6 → cmd2-2.5.7}/tests/scripts/postcmds.txt +0 -0
  216. {cmd2-2.5.6 → cmd2-2.5.7}/tests/scripts/precmds.txt +0 -0
  217. {cmd2-2.5.6 → cmd2-2.5.7}/tests/scripts/utf8.txt +0 -0
  218. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_ansi.py +0 -0
  219. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_argparse_completer.py +0 -0
  220. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_argparse_custom.py +0 -0
  221. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_completion.py +0 -0
  222. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_history.py +0 -0
  223. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_parsing.py +0 -0
  224. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_plugin.py +0 -0
  225. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_run_pyscript.py +0 -0
  226. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_table_creator.py +0 -0
  227. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_transcript.py +0 -0
  228. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_utils.py +0 -0
  229. {cmd2-2.5.6 → cmd2-2.5.7}/tests/test_utils_defining_class.py +0 -0
  230. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/bol_eol.txt +0 -0
  231. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/characterclass.txt +0 -0
  232. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/dotstar.txt +0 -0
  233. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/extension_notation.txt +0 -0
  234. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/failure.txt +0 -0
  235. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/multiline_no_regex.txt +0 -0
  236. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/multiline_regex.txt +0 -0
  237. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/no_output.txt +0 -0
  238. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/no_output_last.txt +0 -0
  239. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/regex_set.txt +0 -0
  240. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/singleslash.txt +0 -0
  241. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/slashes_escaped.txt +0 -0
  242. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/slashslash.txt +0 -0
  243. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/spaces.txt +0 -0
  244. {cmd2-2.5.6 → cmd2-2.5.7}/tests/transcripts/word_boundaries.txt +0 -0
  245. {cmd2-2.5.6 → cmd2-2.5.7}/tests_isolated/__init__.py +0 -0
  246. {cmd2-2.5.6 → cmd2-2.5.7}/tests_isolated/test_commandset/__init__.py +0 -0
  247. {cmd2-2.5.6 → cmd2-2.5.7}/tests_isolated/test_commandset/conftest.py +0 -0
  248. {cmd2-2.5.6 → cmd2-2.5.7}/tests_isolated/test_commandset/test_argparse_subcommands.py +0 -0
  249. {cmd2-2.5.6 → cmd2-2.5.7}/tests_isolated/test_commandset/test_categories.py +0 -0
@@ -1,3 +1,8 @@
1
+ ## 2.5.7 (November 22, 2024)
2
+ * Bug Fixes
3
+ * Fixed issue where argument parsers for overridden commands were not being created.
4
+ * Fixed issue where `Cmd.ppaged()` was not writing to the passed in destination.
5
+
1
6
  ## 2.5.6 (November 14, 2024)
2
7
  * Bug Fixes
3
8
  * Fixed type hint for `with_default_category` decorator which caused type checkers to mistype
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cmd2
3
- Version: 2.5.6
3
+ Version: 2.5.7
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: The MIT License (MIT)
@@ -218,6 +218,81 @@ else:
218
218
  ClassArgParseBuilder = classmethod
219
219
 
220
220
 
221
+ class _CommandParsers:
222
+ """
223
+ Create and store all command method argument parsers for a given Cmd instance.
224
+
225
+ Parser creation and retrieval are accomplished through the get() method.
226
+ """
227
+
228
+ def __init__(self, cmd: 'Cmd') -> None:
229
+ self._cmd = cmd
230
+
231
+ # Keyed by the fully qualified method names. This is more reliable than
232
+ # the methods themselves, since wrapping a method will change its address.
233
+ self._parsers: Dict[str, argparse.ArgumentParser] = {}
234
+
235
+ @staticmethod
236
+ def _fully_qualified_name(command_method: CommandFunc) -> str:
237
+ """Return the fully qualified name of a method or None if a method wasn't passed in."""
238
+ try:
239
+ return f"{command_method.__module__}.{command_method.__qualname__}"
240
+ except AttributeError:
241
+ return ""
242
+
243
+ def __contains__(self, command_method: CommandFunc) -> bool:
244
+ """
245
+ Return whether a given method's parser is in self.
246
+
247
+ If the parser does not yet exist, it will be created if applicable.
248
+ This is basically for checking if a method is argarse-based.
249
+ """
250
+ parser = self.get(command_method)
251
+ return bool(parser)
252
+
253
+ def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]:
254
+ """
255
+ Return a given method's parser or None if the method is not argparse-based.
256
+
257
+ If the parser does not yet exist, it will be created.
258
+ """
259
+ full_method_name = self._fully_qualified_name(command_method)
260
+ if not full_method_name:
261
+ return None
262
+
263
+ if full_method_name not in self._parsers:
264
+ if not command_method.__name__.startswith(COMMAND_FUNC_PREFIX):
265
+ return None
266
+ command = command_method.__name__[len(COMMAND_FUNC_PREFIX) :]
267
+
268
+ parser_builder = getattr(command_method, constants.CMD_ATTR_ARGPARSER, None)
269
+ parent = self._cmd.find_commandset_for_command(command) or self._cmd
270
+ parser = self._cmd._build_parser(parent, parser_builder)
271
+ if parser is None:
272
+ return None
273
+
274
+ # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
275
+ from .decorators import (
276
+ _set_parser_prog,
277
+ )
278
+
279
+ _set_parser_prog(parser, command)
280
+
281
+ # If the description has not been set, then use the method docstring if one exists
282
+ if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__:
283
+ parser.description = strip_doc_annotations(command_method.__wrapped__.__doc__)
284
+
285
+ self._parsers[full_method_name] = parser
286
+
287
+ return self._parsers.get(full_method_name)
288
+
289
+ def remove(self, command_method: CommandFunc) -> None:
290
+ """Remove a given method's parser if it exists."""
291
+ full_method_name = self._fully_qualified_name(command_method)
292
+ if full_method_name in self._parsers:
293
+ del self._parsers[full_method_name]
294
+
295
+
221
296
  class Cmd(cmd.Cmd):
222
297
  """An easy but powerful framework for writing line-oriented command interpreters.
223
298
 
@@ -537,11 +612,7 @@ class Cmd(cmd.Cmd):
537
612
  self.matches_sorted = False
538
613
 
539
614
  # Command parsers for this Cmd instance.
540
- self._command_parsers: Dict[str, argparse.ArgumentParser] = {}
541
-
542
- # Locates the command parser template or factory and creates an instance-specific parser
543
- for command in self.get_all_commands():
544
- self._register_command_parser(command, self.cmd_func(command)) # type: ignore[arg-type]
615
+ self._command_parsers = _CommandParsers(self)
545
616
 
546
617
  # Add functions decorated to be subcommands
547
618
  self._register_subcommands(self)
@@ -657,11 +728,11 @@ class Cmd(cmd.Cmd):
657
728
 
658
729
  installed_attributes = []
659
730
  try:
660
- for method_name, method in methods:
661
- command = method_name[len(COMMAND_FUNC_PREFIX) :]
731
+ for cmd_func_name, command_method in methods:
732
+ command = cmd_func_name[len(COMMAND_FUNC_PREFIX) :]
662
733
 
663
- self._install_command_function(command, method, type(cmdset).__name__)
664
- installed_attributes.append(method_name)
734
+ self._install_command_function(cmd_func_name, command_method, type(cmdset).__name__)
735
+ installed_attributes.append(cmd_func_name)
665
736
 
666
737
  completer_func_name = COMPLETER_FUNC_PREFIX + command
667
738
  cmd_completer = getattr(cmdset, completer_func_name, None)
@@ -677,8 +748,8 @@ class Cmd(cmd.Cmd):
677
748
 
678
749
  self._cmd_to_command_sets[command] = cmdset
679
750
 
680
- if default_category and not hasattr(method, constants.CMD_ATTR_HELP_CATEGORY):
681
- utils.categorize(method, default_category)
751
+ if default_category and not hasattr(command_method, constants.CMD_ATTR_HELP_CATEGORY):
752
+ utils.categorize(command_method, default_category)
682
753
 
683
754
  self._installed_command_sets.add(cmdset)
684
755
 
@@ -718,33 +789,31 @@ class Cmd(cmd.Cmd):
718
789
  parser = copy.deepcopy(parser_builder)
719
790
  return parser
720
791
 
721
- def _register_command_parser(self, command: str, command_method: Callable[..., Any]) -> None:
722
- if command not in self._command_parsers:
723
- parser_builder = getattr(command_method, constants.CMD_ATTR_ARGPARSER, None)
724
- parent = self.find_commandset_for_command(command) or self
725
- parser = self._build_parser(parent, parser_builder)
726
- if parser is None:
727
- return
728
-
729
- # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
730
- from .decorators import (
731
- _set_parser_prog,
732
- )
792
+ def _install_command_function(self, command_func_name: str, command_method: CommandFunc, context: str = '') -> None:
793
+ """
794
+ Install a new command function into the CLI.
733
795
 
734
- _set_parser_prog(parser, command)
796
+ :param command_func_name: name of command function to add
797
+ This points to the command method and may differ from the method's
798
+ name if it's being used as a synonym. (e.g. do_exit = do_quit)
799
+ :param command_method: the actual command method which runs when the command function is called
800
+ :param context: optional info to provide in error message. (e.g. class this function belongs to)
801
+ :raises CommandSetRegistrationError: if the command function fails to install
802
+ """
735
803
 
736
- # If the description has not been set, then use the method docstring if one exists
737
- if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__:
738
- parser.description = strip_doc_annotations(command_method.__wrapped__.__doc__)
804
+ # command_func_name must begin with COMMAND_FUNC_PREFIX to be identified as a command by cmd2.
805
+ if not command_func_name.startswith(COMMAND_FUNC_PREFIX):
806
+ raise CommandSetRegistrationError(f"{command_func_name} does not begin with '{COMMAND_FUNC_PREFIX}'")
739
807
 
740
- self._command_parsers[command] = parser
808
+ # command_method must start with COMMAND_FUNC_PREFIX for use in self._command_parsers.
809
+ if not command_method.__name__.startswith(COMMAND_FUNC_PREFIX):
810
+ raise CommandSetRegistrationError(f"{command_method.__name__} does not begin with '{COMMAND_FUNC_PREFIX}'")
741
811
 
742
- def _install_command_function(self, command: str, command_wrapper: Callable[..., Any], context: str = '') -> None:
743
- cmd_func_name = COMMAND_FUNC_PREFIX + command
812
+ command = command_func_name[len(COMMAND_FUNC_PREFIX) :]
744
813
 
745
814
  # Make sure command function doesn't share name with existing attribute
746
- if hasattr(self, cmd_func_name):
747
- raise CommandSetRegistrationError(f'Attribute already exists: {cmd_func_name} ({context})')
815
+ if hasattr(self, command_func_name):
816
+ raise CommandSetRegistrationError(f'Attribute already exists: {command_func_name} ({context})')
748
817
 
749
818
  # Check if command has an invalid name
750
819
  valid, errmsg = self.statement_parser.is_valid_command(command)
@@ -761,9 +830,7 @@ class Cmd(cmd.Cmd):
761
830
  self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command")
762
831
  del self.macros[command]
763
832
 
764
- self._register_command_parser(command, command_wrapper)
765
-
766
- setattr(self, cmd_func_name, command_wrapper)
833
+ setattr(self, command_func_name, command_method)
767
834
 
768
835
  def _install_completer_function(self, cmd_name: str, cmd_completer: CompleterFunc) -> None:
769
836
  completer_func_name = COMPLETER_FUNC_PREFIX + cmd_name
@@ -790,62 +857,66 @@ class Cmd(cmd.Cmd):
790
857
  cmdset.on_unregister()
791
858
  self._unregister_subcommands(cmdset)
792
859
 
793
- methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers(
860
+ methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers(
794
861
  cmdset,
795
862
  predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
796
863
  and hasattr(meth, '__name__')
797
864
  and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
798
865
  )
799
866
 
800
- for method in methods:
801
- cmd_name = method[0][len(COMMAND_FUNC_PREFIX) :]
867
+ for cmd_func_name, command_method in methods:
868
+ command = cmd_func_name[len(COMMAND_FUNC_PREFIX) :]
802
869
 
803
870
  # Enable the command before uninstalling it to make sure we remove both
804
871
  # the real functions and the ones used by the DisabledCommand object.
805
- if cmd_name in self.disabled_commands:
806
- self.enable_command(cmd_name)
872
+ if command in self.disabled_commands:
873
+ self.enable_command(command)
874
+
875
+ if command in self._cmd_to_command_sets:
876
+ del self._cmd_to_command_sets[command]
807
877
 
808
- if cmd_name in self._cmd_to_command_sets:
809
- del self._cmd_to_command_sets[cmd_name]
878
+ # Only remove the parser if this is the actual
879
+ # command since command synonyms don't own it.
880
+ if cmd_func_name == command_method.__name__:
881
+ self._command_parsers.remove(command_method)
810
882
 
811
- delattr(self, COMMAND_FUNC_PREFIX + cmd_name)
812
- if cmd_name in self._command_parsers:
813
- del self._command_parsers[cmd_name]
883
+ if hasattr(self, COMPLETER_FUNC_PREFIX + command):
884
+ delattr(self, COMPLETER_FUNC_PREFIX + command)
885
+ if hasattr(self, HELP_FUNC_PREFIX + command):
886
+ delattr(self, HELP_FUNC_PREFIX + command)
814
887
 
815
- if hasattr(self, COMPLETER_FUNC_PREFIX + cmd_name):
816
- delattr(self, COMPLETER_FUNC_PREFIX + cmd_name)
817
- if hasattr(self, HELP_FUNC_PREFIX + cmd_name):
818
- delattr(self, HELP_FUNC_PREFIX + cmd_name)
888
+ delattr(self, cmd_func_name)
819
889
 
820
890
  cmdset.on_unregistered()
821
891
  self._installed_command_sets.remove(cmdset)
822
892
 
823
893
  def _check_uninstallable(self, cmdset: CommandSet) -> None:
824
- methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers(
894
+ def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
895
+ for action in parser._actions:
896
+ if isinstance(action, argparse._SubParsersAction):
897
+ for subparser in action.choices.values():
898
+ attached_cmdset = getattr(subparser, constants.PARSER_ATTR_COMMANDSET, None)
899
+ if attached_cmdset is not None and attached_cmdset is not cmdset:
900
+ raise CommandSetRegistrationError(
901
+ 'Cannot uninstall CommandSet when another CommandSet depends on it'
902
+ )
903
+ check_parser_uninstallable(subparser)
904
+ break
905
+
906
+ methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers(
825
907
  cmdset,
826
908
  predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
827
909
  and hasattr(meth, '__name__')
828
910
  and meth.__name__.startswith(COMMAND_FUNC_PREFIX),
829
911
  )
830
912
 
831
- for method in methods:
832
- command_name = method[0][len(COMMAND_FUNC_PREFIX) :]
833
- command_parser = self._command_parsers.get(command_name, None)
834
-
835
- def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
836
- for action in parser._actions:
837
- if isinstance(action, argparse._SubParsersAction):
838
- for subparser in action.choices.values():
839
- attached_cmdset = getattr(subparser, constants.PARSER_ATTR_COMMANDSET, None)
840
- if attached_cmdset is not None and attached_cmdset is not cmdset:
841
- raise CommandSetRegistrationError(
842
- 'Cannot uninstall CommandSet when another CommandSet depends on it'
843
- )
844
- check_parser_uninstallable(subparser)
845
- break
846
-
847
- if command_parser is not None:
848
- check_parser_uninstallable(command_parser)
913
+ for cmd_func_name, command_method in methods:
914
+ # We only need to check if it's safe to remove the parser if this
915
+ # is the actual command since command synonyms don't own it.
916
+ if cmd_func_name == command_method.__name__:
917
+ command_parser = self._command_parsers.get(command_method)
918
+ if command_parser is not None:
919
+ check_parser_uninstallable(command_parser)
849
920
 
850
921
  def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
851
922
  """
@@ -889,7 +960,7 @@ class Cmd(cmd.Cmd):
889
960
  raise CommandSetRegistrationError(
890
961
  f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
891
962
  )
892
- command_parser = self._command_parsers.get(command_name, None)
963
+ command_parser = self._command_parsers.get(command_func)
893
964
  if command_parser is None:
894
965
  raise CommandSetRegistrationError(
895
966
  f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}"
@@ -991,7 +1062,7 @@ class Cmd(cmd.Cmd):
991
1062
  raise CommandSetRegistrationError(
992
1063
  f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
993
1064
  )
994
- command_parser = self._command_parsers.get(command_name, None)
1065
+ command_parser = self._command_parsers.get(command_func)
995
1066
  if command_parser is None: # pragma: no cover
996
1067
  # This really shouldn't be possible since _register_subcommands would prevent this from happening
997
1068
  # but keeping in case it does for some strange reason
@@ -1332,49 +1403,43 @@ class Cmd(cmd.Cmd):
1332
1403
 
1333
1404
  WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
1334
1405
  """
1335
- # msg can be any type, so convert to string before checking if it's blank
1336
- msg_str = str(msg)
1337
1406
  dest = self.stdout if dest is None else dest
1338
1407
 
1339
- # Consider None to be no data to print
1340
- if msg is None or msg_str == '':
1341
- return
1342
-
1343
- try:
1344
- import subprocess
1345
-
1346
- # Attempt to detect if we are not running within a fully functional terminal.
1347
- # Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
1348
- functional_terminal = False
1408
+ # Attempt to detect if we are not running within a fully functional terminal.
1409
+ # Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
1410
+ functional_terminal = False
1349
1411
 
1350
- if self.stdin.isatty() and self.stdout.isatty():
1351
- if sys.platform.startswith('win') or os.environ.get('TERM') is not None:
1352
- functional_terminal = True
1412
+ if self.stdin.isatty() and dest.isatty():
1413
+ if sys.platform.startswith('win') or os.environ.get('TERM') is not None:
1414
+ functional_terminal = True
1353
1415
 
1354
- # Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
1355
- # Also only attempt to use a pager if actually running in a real fully functional terminal
1356
- if functional_terminal and not self._redirecting and not self.in_pyscript() and not self.in_script():
1357
- if ansi.allow_style == ansi.AllowStyle.NEVER:
1358
- msg_str = ansi.strip_style(msg_str)
1359
- msg_str += end
1416
+ # Don't attempt to use a pager that can block if redirecting or running a script (either text or Python).
1417
+ # Also only attempt to use a pager if actually running in a real fully functional terminal.
1418
+ if functional_terminal and not self._redirecting and not self.in_pyscript() and not self.in_script():
1419
+ final_msg = f"{msg}{end}"
1420
+ if ansi.allow_style == ansi.AllowStyle.NEVER:
1421
+ final_msg = ansi.strip_style(final_msg)
1360
1422
 
1361
- pager = self.pager
1362
- if chop:
1363
- pager = self.pager_chop
1423
+ pager = self.pager
1424
+ if chop:
1425
+ pager = self.pager_chop
1364
1426
 
1427
+ try:
1365
1428
  # Prevent KeyboardInterrupts while in the pager. The pager application will
1366
1429
  # still receive the SIGINT since it is in the same process group as us.
1367
1430
  with self.sigint_protection:
1368
- pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE)
1369
- pipe_proc.communicate(msg_str.encode('utf-8', 'replace'))
1370
- else:
1371
- ansi.style_aware_write(dest, f'{msg_str}{end}')
1372
- except BrokenPipeError:
1373
- # This occurs if a command's output is being piped to another process and that process closes before the
1374
- # command is finished. If you would like your application to print a warning message, then set the
1375
- # broken_pipe_warning attribute to the message you want printed.`
1376
- if self.broken_pipe_warning:
1377
- sys.stderr.write(self.broken_pipe_warning)
1431
+ import subprocess
1432
+
1433
+ pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=dest)
1434
+ pipe_proc.communicate(final_msg.encode('utf-8', 'replace'))
1435
+ except BrokenPipeError:
1436
+ # This occurs if a command's output is being piped to another process and that process closes before the
1437
+ # command is finished. If you would like your application to print a warning message, then set the
1438
+ # broken_pipe_warning attribute to the message you want printed.`
1439
+ if self.broken_pipe_warning:
1440
+ sys.stderr.write(self.broken_pipe_warning)
1441
+ else:
1442
+ self.print_to(dest, msg, end=end, paged=False)
1378
1443
 
1379
1444
  # ----- Methods related to tab completion -----
1380
1445
 
@@ -2098,12 +2163,12 @@ class Cmd(cmd.Cmd):
2098
2163
  else:
2099
2164
  # There's no completer function, next see if the command uses argparse
2100
2165
  func = self.cmd_func(command)
2101
- argparser = self._command_parsers.get(command, None)
2166
+ argparser = None if func is None else self._command_parsers.get(func)
2102
2167
 
2103
2168
  if func is not None and argparser is not None:
2104
2169
  # Get arguments for complete()
2105
2170
  preserve_quotes = getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES)
2106
- cmd_set = self._cmd_to_command_sets[command] if command in self._cmd_to_command_sets else None
2171
+ cmd_set = self.find_commandset_for_command(command)
2107
2172
 
2108
2173
  # Create the argparse completer
2109
2174
  completer_type = self._determine_ap_completer_type(argparser)
@@ -3044,19 +3109,9 @@ class Cmd(cmd.Cmd):
3044
3109
 
3045
3110
  helpfunc now contains a reference to the ``do_help`` method
3046
3111
  """
3047
- func_name = self._cmd_func_name(command)
3048
- if func_name:
3049
- return cast(Optional[CommandFunc], getattr(self, func_name))
3050
- return None
3051
-
3052
- def _cmd_func_name(self, command: str) -> str:
3053
- """Get the method name associated with a given command.
3054
-
3055
- :param command: command to look up method name which implements it
3056
- :return: method name which implements the given command
3057
- """
3058
- target = constants.COMMAND_FUNC_PREFIX + command
3059
- return target if callable(getattr(self, target, None)) else ''
3112
+ func_name = constants.COMMAND_FUNC_PREFIX + command
3113
+ func = getattr(self, func_name, None)
3114
+ return cast(CommandFunc, func) if callable(func) else None
3060
3115
 
3061
3116
  def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
3062
3117
  """This executes the actual do_* method for a command.
@@ -3404,7 +3459,7 @@ class Cmd(cmd.Cmd):
3404
3459
  alias_description = "Manage aliases\n" "\n" "An alias is a command that enables replacement of a word by another string."
3405
3460
  alias_epilog = "See also:\n" " macro"
3406
3461
  alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
3407
- alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND', required=True)
3462
+ alias_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
3408
3463
 
3409
3464
  # Preserve quotes since we are passing strings to other commands
3410
3465
  @with_argparser(alias_parser, preserve_quotes=True)
@@ -3572,7 +3627,7 @@ class Cmd(cmd.Cmd):
3572
3627
  macro_description = "Manage macros\n" "\n" "A macro is similar to an alias, but it can contain argument placeholders."
3573
3628
  macro_epilog = "See also:\n" " alias"
3574
3629
  macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
3575
- macro_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND', required=True)
3630
+ macro_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
3576
3631
 
3577
3632
  # Preserve quotes since we are passing strings to other commands
3578
3633
  @with_argparser(macro_parser, preserve_quotes=True)
@@ -3820,9 +3875,7 @@ class Cmd(cmd.Cmd):
3820
3875
  return []
3821
3876
 
3822
3877
  # Check if this command uses argparse
3823
- func = self.cmd_func(command)
3824
- argparser = self._command_parsers.get(command, None)
3825
- if func is None or argparser is None:
3878
+ if (func := self.cmd_func(command)) is None or (argparser := self._command_parsers.get(func)) is None:
3826
3879
  return []
3827
3880
 
3828
3881
  completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
@@ -3857,7 +3910,7 @@ class Cmd(cmd.Cmd):
3857
3910
  # Getting help for a specific command
3858
3911
  func = self.cmd_func(args.command)
3859
3912
  help_func = getattr(self, constants.HELP_FUNC_PREFIX + args.command, None)
3860
- argparser = self._command_parsers.get(args.command, None)
3913
+ argparser = None if func is None else self._command_parsers.get(func)
3861
3914
 
3862
3915
  # If the command function uses argparse, then use argparse's help
3863
3916
  if func is not None and argparser is not None:
@@ -3979,28 +4032,29 @@ class Cmd(cmd.Cmd):
3979
4032
  def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str], List[str]]:
3980
4033
  # Get a sorted list of help topics
3981
4034
  help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
4035
+
3982
4036
  # Get a sorted list of visible command names
3983
4037
  visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key)
3984
4038
  cmds_doc: List[str] = []
3985
4039
  cmds_undoc: List[str] = []
3986
4040
  cmds_cats: Dict[str, List[str]] = {}
3987
4041
  for command in visible_commands:
3988
- func = self.cmd_func(command)
4042
+ func = cast(CommandFunc, self.cmd_func(command))
3989
4043
  has_help_func = False
4044
+ has_parser = func in self._command_parsers
3990
4045
 
3991
4046
  if command in help_topics:
3992
4047
  # Prevent the command from showing as both a command and help topic in the output
3993
4048
  help_topics.remove(command)
3994
4049
 
3995
4050
  # Non-argparse commands can have help_functions for their documentation
3996
- if command not in self._command_parsers:
3997
- has_help_func = True
4051
+ has_help_func = not has_parser
3998
4052
 
3999
4053
  if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY):
4000
4054
  category: str = getattr(func, constants.CMD_ATTR_HELP_CATEGORY)
4001
4055
  cmds_cats.setdefault(category, [])
4002
4056
  cmds_cats[category].append(command)
4003
- elif func.__doc__ or has_help_func:
4057
+ elif func.__doc__ or has_help_func or has_parser:
4004
4058
  cmds_doc.append(command)
4005
4059
  else:
4006
4060
  cmds_undoc.append(command)
@@ -4035,11 +4089,17 @@ class Cmd(cmd.Cmd):
4035
4089
  # Try to get the documentation string for each command
4036
4090
  topics = self.get_help_topics()
4037
4091
  for command in cmds:
4038
- cmd_func = self.cmd_func(command)
4092
+ if (cmd_func := self.cmd_func(command)) is None:
4093
+ continue
4094
+
4039
4095
  doc: Optional[str]
4040
4096
 
4097
+ # If this is an argparse command, use its description.
4098
+ if (cmd_parser := self._command_parsers.get(cmd_func)) is not None:
4099
+ doc = cmd_parser.description
4100
+
4041
4101
  # Non-argparse commands can have help_functions for their documentation
4042
- if command not in self._command_parsers and command in topics:
4102
+ elif command in topics:
4043
4103
  help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
4044
4104
  result = io.StringIO()
4045
4105
 
@@ -5436,12 +5496,13 @@ class Cmd(cmd.Cmd):
5436
5496
  if command not in self.disabled_commands:
5437
5497
  return
5438
5498
 
5499
+ cmd_func_name = constants.COMMAND_FUNC_PREFIX + command
5439
5500
  help_func_name = constants.HELP_FUNC_PREFIX + command
5440
5501
  completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
5441
5502
 
5442
5503
  # Restore the command function to its original value
5443
5504
  dc = self.disabled_commands[command]
5444
- setattr(self, self._cmd_func_name(command), dc.command_function)
5505
+ setattr(self, cmd_func_name, dc.command_function)
5445
5506
 
5446
5507
  # Restore the help function to its original value
5447
5508
  if dc.help_function is None:
@@ -5489,6 +5550,7 @@ class Cmd(cmd.Cmd):
5489
5550
  if command_function is None:
5490
5551
  raise AttributeError(f"'{command}' does not refer to a command")
5491
5552
 
5553
+ cmd_func_name = constants.COMMAND_FUNC_PREFIX + command
5492
5554
  help_func_name = constants.HELP_FUNC_PREFIX + command
5493
5555
  completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
5494
5556
 
@@ -5503,7 +5565,7 @@ class Cmd(cmd.Cmd):
5503
5565
  new_func = functools.partial(
5504
5566
  self._report_disabled_command_usage, message_to_print=message_to_print.replace(constants.COMMAND_NAME, command)
5505
5567
  )
5506
- setattr(self, self._cmd_func_name(command), new_func)
5568
+ setattr(self, cmd_func_name, new_func)
5507
5569
  setattr(self, help_func_name, new_func)
5508
5570
 
5509
5571
  # Set the completer to a function that returns a blank list
@@ -346,7 +346,9 @@ def with_argparser(
346
346
  statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(
347
347
  command_name, statement_arg, preserve_quotes
348
348
  )
349
- arg_parser = cmd2_app._command_parsers.get(command_name, None)
349
+
350
+ # Pass cmd_wrapper instead of func, since it contains the parser info.
351
+ arg_parser = cmd2_app._command_parsers.get(cmd_wrapper)
350
352
  if arg_parser is None:
351
353
  # This shouldn't be possible to reach
352
354
  raise ValueError(f'No argument parser found for {command_name}') # pragma: no cover
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cmd2
3
- Version: 2.5.6
3
+ Version: 2.5.7
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: The MIT License (MIT)
@@ -328,6 +328,7 @@ dev-dependencies = [
328
328
  "pytest",
329
329
  "pytest-cov",
330
330
  "pytest-mock",
331
+ "ruff",
331
332
  "sphinx",
332
333
  "sphinx-autobuild",
333
334
  "sphinx-rtd-theme",
@@ -14,7 +14,6 @@ import pytest
14
14
  import cmd2
15
15
 
16
16
  from .conftest import (
17
- find_subcommand,
18
17
  run_cmd,
19
18
  )
20
19
 
@@ -402,8 +401,7 @@ def test_add_another_subcommand(subcommand_app):
402
401
  This tests makes sure _set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls
403
402
  to add_parser() write the correct prog value to the parser being added.
404
403
  """
405
- base_parser = subcommand_app._command_parsers.get('base')
406
- find_subcommand(subcommand_app._command_parsers.get('base'), [])
404
+ base_parser = subcommand_app._command_parsers.get(subcommand_app.do_base)
407
405
  for sub_action in base_parser._actions:
408
406
  if isinstance(sub_action, argparse._SubParsersAction):
409
407
  new_parser = sub_action.add_parser('new_sub', help='stuff')