cmd2 4.3.1__tar.gz → 4.4.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 (234) hide show
  1. {cmd2-4.3.1 → cmd2-4.4.0}/.pre-commit-config.yaml +1 -1
  2. {cmd2-4.3.1 → cmd2-4.4.0}/CHANGELOG.md +59 -0
  3. {cmd2-4.3.1 → cmd2-4.4.0}/PKG-INFO +1 -1
  4. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/cmd2.py +223 -51
  5. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/history.py +110 -47
  6. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/PKG-INFO +1 -1
  7. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/SOURCES.txt +2 -0
  8. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/scm_file_list.json +3 -0
  9. cmd2-4.4.0/cmd2.egg-info/scm_version.json +8 -0
  10. {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/embedding_in_textual.md +0 -3
  11. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/history.md +36 -6
  12. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/prompt.md +20 -0
  13. {cmd2-4.3.1 → cmd2-4.4.0}/examples/textual_tty_embed.py +0 -4
  14. cmd2-4.4.0/tests/test_history.py +1822 -0
  15. cmd2-4.4.0/tests/test_history_mutations.py +316 -0
  16. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_pipeline_job_control.py +34 -7
  17. cmd2-4.4.0/tests/test_window_title.py +828 -0
  18. cmd2-4.3.1/cmd2.egg-info/scm_version.json +0 -8
  19. cmd2-4.3.1/tests/test_history.py +0 -1002
  20. {cmd2-4.3.1 → cmd2-4.4.0}/.prettierignore +0 -0
  21. {cmd2-4.3.1 → cmd2-4.4.0}/.prettierrc +0 -0
  22. {cmd2-4.3.1 → cmd2-4.4.0}/.typos.toml +0 -0
  23. {cmd2-4.3.1 → cmd2-4.4.0}/AGENTS.md +0 -0
  24. {cmd2-4.3.1 → cmd2-4.4.0}/CLAUDE.md +0 -0
  25. {cmd2-4.3.1 → cmd2-4.4.0}/GEMINI.md +0 -0
  26. {cmd2-4.3.1 → cmd2-4.4.0}/LICENSE +0 -0
  27. {cmd2-4.3.1 → cmd2-4.4.0}/MANIFEST.in +0 -0
  28. {cmd2-4.3.1 → cmd2-4.4.0}/Makefile +0 -0
  29. {cmd2-4.3.1 → cmd2-4.4.0}/README.md +0 -0
  30. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/__init__.py +0 -0
  31. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/annotated.py +0 -0
  32. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/argparse_completer.py +0 -0
  33. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/argparse_utils.py +0 -0
  34. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/clipboard.py +0 -0
  35. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/colors.py +0 -0
  36. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/command_set.py +0 -0
  37. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/completion.py +0 -0
  38. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/constants.py +0 -0
  39. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/decorators.py +0 -0
  40. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/exceptions.py +0 -0
  41. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/parsing.py +0 -0
  42. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/plugin.py +0 -0
  43. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/pt_utils.py +0 -0
  44. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/py.typed +0 -0
  45. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/py_bridge.py +0 -0
  46. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/rich_utils.py +0 -0
  47. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/string_utils.py +0 -0
  48. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/styles.py +0 -0
  49. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/theme.py +0 -0
  50. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/types.py +0 -0
  51. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2/utils.py +0 -0
  52. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/dependency_links.txt +0 -0
  53. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/requires.txt +0 -0
  54. {cmd2-4.3.1 → cmd2-4.4.0}/cmd2.egg-info/top_level.txt +0 -0
  55. {cmd2-4.3.1 → cmd2-4.4.0}/codecov.yml +0 -0
  56. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/annotated.md +0 -0
  57. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/argparse_completer.md +0 -0
  58. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/argparse_utils.md +0 -0
  59. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/clipboard.md +0 -0
  60. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/cmd.md +0 -0
  61. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/colors.md +0 -0
  62. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/command_set.md +0 -0
  63. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/completion.md +0 -0
  64. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/constants.md +0 -0
  65. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/decorators.md +0 -0
  66. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/exceptions.md +0 -0
  67. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/history.md +0 -0
  68. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/index.md +0 -0
  69. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/parsing.md +0 -0
  70. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/plugin.md +0 -0
  71. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/pt_utils.md +0 -0
  72. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/py_bridge.md +0 -0
  73. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/rich_utils.md +0 -0
  74. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/string_utils.md +0 -0
  75. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/styles.md +0 -0
  76. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/theme.md +0 -0
  77. {cmd2-4.3.1 → cmd2-4.4.0}/docs/api/utils.md +0 -0
  78. {cmd2-4.3.1 → cmd2-4.4.0}/docs/assets/cmd2-logo.png +0 -0
  79. {cmd2-4.3.1 → cmd2-4.4.0}/docs/assets/cmd2-readme-demo.gif +0 -0
  80. {cmd2-4.3.1 → cmd2-4.4.0}/docs/assets/getting-started-demo.gif +0 -0
  81. {cmd2-4.3.1 → cmd2-4.4.0}/docs/doc_conventions.md +0 -0
  82. {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/alternate_event_loops.md +0 -0
  83. {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/examples.md +0 -0
  84. {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/getting_started.md +0 -0
  85. {cmd2-4.3.1 → cmd2-4.4.0}/docs/examples/index.md +0 -0
  86. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/annotated.md +0 -0
  87. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/argument_processing.md +0 -0
  88. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/async_commands.md +0 -0
  89. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/builtin_commands.md +0 -0
  90. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/clipboard.md +0 -0
  91. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/commands.md +0 -0
  92. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/completion.md +0 -0
  93. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/disable_commands.md +0 -0
  94. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/embedded_python_shells.md +0 -0
  95. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/generating_output.md +0 -0
  96. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/help.md +0 -0
  97. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/hooks.md +0 -0
  98. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/index.md +0 -0
  99. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/initialization.md +0 -0
  100. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/misc.md +0 -0
  101. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/modular_commands.md +0 -0
  102. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/multiline_commands.md +0 -0
  103. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/os.md +0 -0
  104. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/packaging.md +0 -0
  105. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/plugins.md +0 -0
  106. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/redirection.md +0 -0
  107. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/scripting.md +0 -0
  108. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/settings.md +0 -0
  109. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/shortcuts_aliases_macros.md +0 -0
  110. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/startup_commands.md +0 -0
  111. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/table_creation.md +0 -0
  112. {cmd2-4.3.1 → cmd2-4.4.0}/docs/features/theme.md +0 -0
  113. {cmd2-4.3.1 → cmd2-4.4.0}/docs/index.md +0 -0
  114. {cmd2-4.3.1 → cmd2-4.4.0}/docs/javascripts/readthedocs.js +0 -0
  115. {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/incompatibilities.md +0 -0
  116. {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/index.md +0 -0
  117. {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/minimum.md +0 -0
  118. {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/next_steps.md +0 -0
  119. {cmd2-4.3.1 → cmd2-4.4.0}/docs/migrating/why.md +0 -0
  120. {cmd2-4.3.1 → cmd2-4.4.0}/docs/mixins/index.md +0 -0
  121. {cmd2-4.3.1 → cmd2-4.4.0}/docs/mixins/mixin_template.md +0 -0
  122. {cmd2-4.3.1 → cmd2-4.4.0}/docs/overrides/main.html +0 -0
  123. {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/alternatives.md +0 -0
  124. {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/index.md +0 -0
  125. {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/installation.md +0 -0
  126. {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/integrating.md +0 -0
  127. {cmd2-4.3.1 → cmd2-4.4.0}/docs/overview/resources.md +0 -0
  128. {cmd2-4.3.1 → cmd2-4.4.0}/docs/stylesheets/cmd2.css +0 -0
  129. {cmd2-4.3.1 → cmd2-4.4.0}/docs/testing.md +0 -0
  130. {cmd2-4.3.1 → cmd2-4.4.0}/docs/upgrades.md +0 -0
  131. {cmd2-4.3.1 → cmd2-4.4.0}/examples/.cmd2rc +0 -0
  132. {cmd2-4.3.1 → cmd2-4.4.0}/examples/README.md +0 -0
  133. {cmd2-4.3.1 → cmd2-4.4.0}/examples/annotated_example.py +0 -0
  134. {cmd2-4.3.1 → cmd2-4.4.0}/examples/argparse_completion.py +0 -0
  135. {cmd2-4.3.1 → cmd2-4.4.0}/examples/argparse_example.py +0 -0
  136. {cmd2-4.3.1 → cmd2-4.4.0}/examples/async_call.py +0 -0
  137. {cmd2-4.3.1 → cmd2-4.4.0}/examples/async_commands.py +0 -0
  138. {cmd2-4.3.1 → cmd2-4.4.0}/examples/async_printing.py +0 -0
  139. {cmd2-4.3.1 → cmd2-4.4.0}/examples/basic_completion.py +0 -0
  140. {cmd2-4.3.1 → cmd2-4.4.0}/examples/cmd2_history.dat +0 -0
  141. {cmd2-4.3.1 → cmd2-4.4.0}/examples/cmd_as_argument.py +0 -0
  142. {cmd2-4.3.1 → cmd2-4.4.0}/examples/color.py +0 -0
  143. {cmd2-4.3.1 → cmd2-4.4.0}/examples/command_sets.py +0 -0
  144. {cmd2-4.3.1 → cmd2-4.4.0}/examples/completion_item_choices.py +0 -0
  145. {cmd2-4.3.1 → cmd2-4.4.0}/examples/custom_parser.py +0 -0
  146. {cmd2-4.3.1 → cmd2-4.4.0}/examples/custom_types.py +0 -0
  147. {cmd2-4.3.1 → cmd2-4.4.0}/examples/default_categories.py +0 -0
  148. {cmd2-4.3.1 → cmd2-4.4.0}/examples/dynamic_commands.py +0 -0
  149. {cmd2-4.3.1 → cmd2-4.4.0}/examples/environment.py +0 -0
  150. {cmd2-4.3.1 → cmd2-4.4.0}/examples/event_loops.py +0 -0
  151. {cmd2-4.3.1 → cmd2-4.4.0}/examples/exit_code.py +0 -0
  152. {cmd2-4.3.1 → cmd2-4.4.0}/examples/getting_started.py +0 -0
  153. {cmd2-4.3.1 → cmd2-4.4.0}/examples/hello_cmd2.py +0 -0
  154. {cmd2-4.3.1 → cmd2-4.4.0}/examples/help_categories.py +0 -0
  155. {cmd2-4.3.1 → cmd2-4.4.0}/examples/hooks.py +0 -0
  156. {cmd2-4.3.1 → cmd2-4.4.0}/examples/migrating.py +0 -0
  157. {cmd2-4.3.1 → cmd2-4.4.0}/examples/mixin.py +0 -0
  158. {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commands/__init__.py +0 -0
  159. {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commands/commandset_basic.py +0 -0
  160. {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commands/commandset_complex.py +0 -0
  161. {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commands/commandset_custominit.py +0 -0
  162. {cmd2-4.3.1 → cmd2-4.4.0}/examples/modular_commandsets.py +0 -0
  163. {cmd2-4.3.1 → cmd2-4.4.0}/examples/paged_output.py +0 -0
  164. {cmd2-4.3.1 → cmd2-4.4.0}/examples/persistent_history.py +0 -0
  165. {cmd2-4.3.1 → cmd2-4.4.0}/examples/pretty_print.py +0 -0
  166. {cmd2-4.3.1 → cmd2-4.4.0}/examples/python_scripting.py +0 -0
  167. {cmd2-4.3.1 → cmd2-4.4.0}/examples/read_input.py +0 -0
  168. {cmd2-4.3.1 → cmd2-4.4.0}/examples/remove_builtin_commands.py +0 -0
  169. {cmd2-4.3.1 → cmd2-4.4.0}/examples/remove_settable.py +0 -0
  170. {cmd2-4.3.1 → cmd2-4.4.0}/examples/rich_tables.py +0 -0
  171. {cmd2-4.3.1 → cmd2-4.4.0}/examples/rich_theme.py +0 -0
  172. {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/arg_printer.py +0 -0
  173. {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/conditional.py +0 -0
  174. {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/nested.txt +0 -0
  175. {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/quit.txt +0 -0
  176. {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/save_help_text.py +0 -0
  177. {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/script.py +0 -0
  178. {cmd2-4.3.1 → cmd2-4.4.0}/examples/scripts/script.txt +0 -0
  179. {cmd2-4.3.1 → cmd2-4.4.0}/examples/tmux_launch.sh +0 -0
  180. {cmd2-4.3.1 → cmd2-4.4.0}/examples/tmux_split.sh +0 -0
  181. {cmd2-4.3.1 → cmd2-4.4.0}/examples/unicode_commands.py +0 -0
  182. {cmd2-4.3.1 → cmd2-4.4.0}/mkdocs.yml +0 -0
  183. {cmd2-4.3.1 → cmd2-4.4.0}/package.json +0 -0
  184. {cmd2-4.3.1 → cmd2-4.4.0}/pyproject.toml +0 -0
  185. {cmd2-4.3.1 → cmd2-4.4.0}/ruff.toml +0 -0
  186. {cmd2-4.3.1 → cmd2-4.4.0}/scripts/validate_tag.py +0 -0
  187. {cmd2-4.3.1 → cmd2-4.4.0}/setup.cfg +0 -0
  188. {cmd2-4.3.1 → cmd2-4.4.0}/tests/.cmd2rc +0 -0
  189. {cmd2-4.3.1 → cmd2-4.4.0}/tests/__init__.py +0 -0
  190. {cmd2-4.3.1 → cmd2-4.4.0}/tests/conftest.py +0 -0
  191. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/echo.py +0 -0
  192. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/environment.py +0 -0
  193. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/help.py +0 -0
  194. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/py_locals.py +0 -0
  195. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/pyscript_dir.py +0 -0
  196. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/raises_exception.py +0 -0
  197. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/recursive.py +0 -0
  198. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/self_in_py.py +0 -0
  199. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/stop.py +0 -0
  200. {cmd2-4.3.1 → cmd2-4.4.0}/tests/pyscript/test_print.py +0 -0
  201. {cmd2-4.3.1 → cmd2-4.4.0}/tests/relative_multiple.txt +0 -0
  202. {cmd2-4.3.1 → cmd2-4.4.0}/tests/script.py +0 -0
  203. {cmd2-4.3.1 → cmd2-4.4.0}/tests/script.txt +0 -0
  204. {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/binary.bin +0 -0
  205. {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/empty.txt +0 -0
  206. {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/help.txt +0 -0
  207. {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/nested.txt +0 -0
  208. {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/one_down.txt +0 -0
  209. {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/postcmds.txt +0 -0
  210. {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/precmds.txt +0 -0
  211. {cmd2-4.3.1 → cmd2-4.4.0}/tests/scripts/utf8.txt +0 -0
  212. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_annotated.py +0 -0
  213. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_argparse.py +0 -0
  214. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_argparse_completer.py +0 -0
  215. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_argparse_subcommands.py +0 -0
  216. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_argparse_utils.py +0 -0
  217. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_categories.py +0 -0
  218. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_cmd2.py +0 -0
  219. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_commandset.py +0 -0
  220. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_completion.py +0 -0
  221. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_dynamic_complete_style.py +0 -0
  222. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_future_annotations.py +0 -0
  223. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_parsing.py +0 -0
  224. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_plugin.py +0 -0
  225. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_pt_utils.py +0 -0
  226. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_py_completion.py +0 -0
  227. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_rich_utils.py +0 -0
  228. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_run_pyscript.py +0 -0
  229. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_string_utils.py +0 -0
  230. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_suite_environment.py +0 -0
  231. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_theme.py +0 -0
  232. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_utils.py +0 -0
  233. {cmd2-4.3.1 → cmd2-4.4.0}/tests/test_utils_defining_class.py +0 -0
  234. {cmd2-4.3.1 → cmd2-4.4.0}/ty.toml +0 -0
@@ -15,7 +15,7 @@ repos:
15
15
  - id: trailing-whitespace
16
16
 
17
17
  - repo: https://github.com/astral-sh/ruff-pre-commit
18
- rev: "v0.17.1"
18
+ rev: "v0.17.3"
19
19
  hooks:
20
20
  - id: ruff-format
21
21
  args: [--config=ruff.toml]
@@ -1,3 +1,62 @@
1
+ ## 4.4.0 (October 31, 2026) 🎃
2
+
3
+ - Breaking Changes
4
+ - `set_window_title()` is now an instance method. Call `app.set_window_title(title)` or
5
+ `self.set_window_title(title)` so title changes use that application's terminal.
6
+ - `History.get()` and `History.span()` now raise `HistoryIndexError`, an `IndexError` subclass,
7
+ for invalid indices and bounded ranges selecting no history items. Unbounded ranges (`:` and
8
+ `..`) can still return an empty dictionary. Starts before the beginning of history are
9
+ rejected rather than clamped. Malformed ranges, including zero endpoints, raise
10
+ `HistoryFormatError`, a `ValueError` subclass. The `history` command reports these selection
11
+ errors without a traceback hint, and index zero includes the reminder that the first command
12
+ is command 1.
13
+ - End-only ranges now start at command 1 in the full history, including persisted commands, just
14
+ like explicit-start ranges and single indices. Bare `history` and unbounded ranges still
15
+ select only the current session unless `--all` is supplied.
16
+ - Replaying or editing a selection containing persisted commands now requires `--all`. This
17
+ includes single indices and bounded ranges. Displaying or saving these selections does not
18
+ require `--all`, and command numbers are unchanged.
19
+ - Running, editing, or saving an empty history selection now fails before replay, opening an
20
+ editor, or creating or overwriting a file. This includes `history -o FILE` and `history -e`
21
+ with no selector in an empty session, and actions on searches with no matches. Empty unbounded
22
+ selections report `No history items in range`, while search misses report
23
+ `No matching history items`; displaying them still succeeds without output. The no-argument
24
+ `history --run` safety refusal is unchanged.
25
+ - `History` now records which entries were loaded before the session started instead of keeping
26
+ a plain start index. Entries added after `start_session()` belong to the current session
27
+ wherever they are placed, including entries inserted at the front of the history.
28
+ `session_start_index` is now computed as the index just past the last persisted entry.
29
+ Assigning it marks only the entries already in the list, so assign it or call
30
+ `start_session()` after loading entries. After the list is reordered,
31
+ `history[history.session_start_index:]` can omit current-session entries; use
32
+ `History.is_persisted()` to check an entry instead.
33
+ - Bug Fixes
34
+ - Fixed history ranges starting past the end selecting the last command. Invalid indices and
35
+ bounded ranges selecting no items report `No history items in range` before running, editing,
36
+ or saving commands.
37
+ - Negative single-index history selections now display and return the positive command number.
38
+ - History regular expressions containing range separators are now handled as regex searches.
39
+ Other selectors are treated as numbers or ranges only when they match the supported syntax;
40
+ strings such as URLs, `a:b`, `+1`, and `1_0` remain literal searches.
41
+ - Fixed `History.truncate()` leaving the current session's start index unchanged. Code calling
42
+ it directly would then have retained current-session entries hidden from unbounded ranges and
43
+ searches. Interactive `history` use was unaffected, since `cmd2` only truncates when
44
+ persisting history at exit.
45
+ - Fixed changing a `History` list in place moving commands between the persisted history and the
46
+ current session. Deleting, inserting, assigning, reordering (such as `reverse()` and
47
+ `sort()`), or repeating entries no longer shows persisted commands as current or hides
48
+ current-session commands, and `history --run` and `history --edit` still require `--all` for a
49
+ moved persisted command. Equal commands from different sessions remain separate entries.
50
+ - Fixed `set_window_title()` leaving title changes buffered until the next prompt render. The
51
+ title is now flushed before the method returns when no prompt is running. While a prompt or
52
+ `select()` menu is running, it is written on that prompt's event loop so it doesn't interleave
53
+ with rendering, and output errors are ignored so they can't disrupt the prompt. Titles are
54
+ still written to a terminal stdout when prompts use a dummy output, such as when stdin isn't a
55
+ terminal.
56
+ - Enhancements
57
+ - Added `History.is_persisted()` to check whether a history entry was loaded from a previous
58
+ session.
59
+
1
60
  ## 4.3.1 (October 24, 2026)
2
61
 
3
62
  - Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmd2
3
- Version: 4.3.1
3
+ Version: 4.4.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
@@ -29,6 +29,7 @@ Documentation: https://cmd2.readthedocs.io/
29
29
  # import this module, many of these imports are lazy-loaded
30
30
  # i.e. we only import the module when we use it.
31
31
  import argparse
32
+ import asyncio
32
33
  import contextlib
33
34
  import copy
34
35
  import dataclasses
@@ -71,7 +72,7 @@ from prompt_toolkit import (
71
72
  filters,
72
73
  print_formatted_text,
73
74
  )
74
- from prompt_toolkit.application import create_app_session, get_app
75
+ from prompt_toolkit.application import Application, AppSession, create_app_session, get_app
75
76
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
76
77
  from prompt_toolkit.completion import Completer, DummyCompleter
77
78
  from prompt_toolkit.formatted_text import ANSI, AnyFormattedText
@@ -82,9 +83,9 @@ from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
82
83
  from prompt_toolkit.keys import Keys
83
84
  from prompt_toolkit.layout import Window, walk
84
85
  from prompt_toolkit.layout.containers import ConditionalContainer, FloatContainer
85
- from prompt_toolkit.output import DummyOutput, create_output
86
+ from prompt_toolkit.output import DummyOutput, Output, create_output
86
87
  from prompt_toolkit.patch_stdout import patch_stdout
87
- from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, choice, set_title
88
+ from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, choice
88
89
  from prompt_toolkit.styles import DynamicStyle
89
90
  from rich.console import (
90
91
  Group,
@@ -153,7 +154,11 @@ from .exceptions import (
153
154
  SkipPostcommandHooks,
154
155
  )
155
156
  from .history import (
157
+ NO_HISTORY_ITEMS,
158
+ NO_MATCHING_HISTORY_ITEMS,
156
159
  History,
160
+ HistoryFormatError,
161
+ HistoryIndexError,
157
162
  HistoryItem,
158
163
  )
159
164
  from .parsing import (
@@ -216,6 +221,11 @@ if TYPE_CHECKING: # pragma: no cover
216
221
  from prompt_toolkit.buffer import Buffer
217
222
 
218
223
 
224
+ # Errors raised when prompt-toolkit can't use a terminal. For example, GitHub Actions on Windows reports
225
+ # isatty() as True but has no real console.
226
+ _TERMINAL_INIT_ERRORS = (NoConsoleScreenBufferError, AttributeError, ValueError)
227
+
228
+
219
229
  class _SavedCmd2Env:
220
230
  """cmd2 environment settings that are backed up when entering an interactive Python shell."""
221
231
 
@@ -685,6 +695,26 @@ class Cmd:
685
695
  self._alert_thread: threading.Thread | None = None
686
696
  self._alert_prompt_timestamp: float = 0.0 # Uses time.monotonic()
687
697
 
698
+ # Members related to setting the window title. The lock is reentrant so a signal handler which sets the title
699
+ # while the main thread is writing one doesn't deadlock. Such a nested call only records its title, and the
700
+ # outer call writes it next.
701
+ self._window_title_lock = threading.RLock()
702
+ self._pending_window_title: str | None = None
703
+ self._writing_window_title = False
704
+
705
+ # The prompt loop on which a title callback is queued but hasn't run yet. Later titles set while it's queued
706
+ # only replace the pending title, which that callback writes.
707
+ self._window_title_callback_loop: asyncio.AbstractEventLoop | None = None
708
+
709
+ # The stdout this app was created with. When the main session uses a DummyOutput, titles are written to it if
710
+ # it's a terminal, including while a command's output is redirected. See _get_window_title_fallback_output().
711
+ self._window_title_stdout: TextIO = self.stdout
712
+ self._window_title_fallback_checked = False
713
+ self._window_title_fallback_output: Output | None = None
714
+
715
+ # The AppSession of a select() menu while it is displayed, so window titles can find its event loop.
716
+ self._select_app_session: AppSession | None = None
717
+
688
718
  # Add functions decorated to be subcommands
689
719
  self._register_subcommands(self)
690
720
 
@@ -834,7 +864,7 @@ class Cmd:
834
864
  kwargs["output"] = create_output(stdout=self.stdout)
835
865
  return PromptSession(**kwargs)
836
866
 
837
- except (NoConsoleScreenBufferError, AttributeError, ValueError):
867
+ except _TERMINAL_INIT_ERRORS:
838
868
  # Fallback to dummy input/output if PromptSession initialization fails.
839
869
  # This can happen in some CI environments (like GitHub Actions on Windows)
840
870
  # where isatty() is True but there is no real console.
@@ -3655,14 +3685,19 @@ class Cmd:
3655
3685
  if not callable(prompt):
3656
3686
  prompt = pt_filter_style(prompt)
3657
3687
 
3658
- with patch_stdout():
3659
- try:
3660
- # Set this session as the active one for UI/completion logic.
3661
- self.active_session = session
3662
- return session.prompt(prompt, **prompt_kwargs)
3663
- finally:
3664
- # Revert back to the main session.
3665
- self.active_session = self.main_session
3688
+ try:
3689
+ with patch_stdout():
3690
+ try:
3691
+ # Set this session as the active one for UI/completion logic.
3692
+ self.active_session = session
3693
+ return session.prompt(prompt, **prompt_kwargs)
3694
+ finally:
3695
+ # Revert back to the main session.
3696
+ self.active_session = self.main_session
3697
+ finally:
3698
+ # Write any title whose callback the prompt's event loop didn't run. Do this after patch_stdout()
3699
+ # exits so the write can't race with its writer thread on the same output.
3700
+ self._flush_pending_window_title_quietly()
3666
3701
 
3667
3702
  # We're not at a terminal, so we're likely reading from a file or a pipe.
3668
3703
  prompt_obj = prompt if isinstance(prompt, (ANSI, str)) else prompt()
@@ -4790,8 +4825,13 @@ class Cmd:
4790
4825
  if self._is_tty_session(self.main_session):
4791
4826
  try:
4792
4827
  while True:
4793
- with create_app_session(input=self.main_session.input, output=self.main_session.output):
4794
- result = choice(message=prompt, options=fulloptions)
4828
+ with create_app_session(input=self.main_session.input, output=self.main_session.output) as app_session:
4829
+ self._select_app_session = app_session
4830
+ try:
4831
+ result = choice(message=prompt, options=fulloptions)
4832
+ finally:
4833
+ self._select_app_session = None
4834
+ self._flush_pending_window_title_quietly()
4795
4835
  if result is not None:
4796
4836
  return result
4797
4837
  except KeyboardInterrupt:
@@ -5446,13 +5486,14 @@ class Cmd:
5446
5486
  "-a",
5447
5487
  "--all",
5448
5488
  action="store_true",
5449
- help="display all commands, including ones persisted from previous sessions",
5489
+ help="include persisted commands; required to run or edit commands from previous sessions",
5450
5490
  )
5451
5491
 
5452
5492
  history_arg_help = (
5453
- "empty all history items\n"
5454
- "a one history item by number\n"
5455
- "a..b, a:b, a:, ..b items by indices (inclusive)\n"
5493
+ "empty current session (--all includes persisted commands)\n"
5494
+ "a one item by number in the full history\n"
5495
+ "a..b, a:b, a:, ..b inclusive ranges in the full history\n"
5496
+ ":, .. current session (--all includes persisted commands)\n"
5456
5497
  "string items containing string\n"
5457
5498
  "/regex/ items matching regular expression"
5458
5499
  )
@@ -5498,18 +5539,33 @@ class Cmd:
5498
5539
 
5499
5540
  return None
5500
5541
 
5542
+ if args.run and not args.arg:
5543
+ self.perror("Cowardly refusing to run all previously entered commands.")
5544
+ self.perror("If this is what you want to do, specify '1:' as the range of history.")
5545
+ return None
5546
+
5501
5547
  # If an argument was supplied, then retrieve partial contents of the history, otherwise retrieve it all
5502
- history = self._get_history(args)
5548
+ try:
5549
+ history = self._get_history(args)
5550
+ except (HistoryIndexError, HistoryFormatError) as ex:
5551
+ self.perror(str(ex))
5552
+ return None
5553
+
5554
+ # Selection is independent of the requested action. Empty displays are harmless.
5555
+ if not history and (args.run or args.edit or args.output_file):
5556
+ is_search = args.arg and not self.history.spanpattern.fullmatch(args.arg)
5557
+ self.perror(NO_MATCHING_HISTORY_ITEMS if is_search else NO_HISTORY_ITEMS)
5558
+ return None
5559
+
5560
+ if (args.run or args.edit) and not args.all and any(self.history.is_persisted(item) for item in history.values()):
5561
+ self.perror("Use --all to run or edit commands from previous sessions.")
5562
+ return None
5503
5563
 
5504
5564
  if args.run:
5505
- if not args.arg:
5506
- self.perror("Cowardly refusing to run all previously entered commands.")
5507
- self.perror("If this is what you want to do, specify '1:' as the range of history.")
5508
- else:
5509
- stop = self.runcmds_plus_hooks(list(history.values()))
5510
- self.last_result = True
5511
- return stop
5512
- elif args.edit:
5565
+ stop = self.runcmds_plus_hooks(list(history.values()))
5566
+ self.last_result = True
5567
+ return stop
5568
+ if args.edit:
5513
5569
  fd, fname = tempfile.mkstemp(suffix=".txt", text=True)
5514
5570
  fobj: TextIO
5515
5571
  with os.fdopen(fd, "w") as fobj:
@@ -5550,26 +5606,28 @@ class Cmd:
5550
5606
  def _get_history(self, args: argparse.Namespace) -> dict[int, HistoryItem]:
5551
5607
  """If an argument was supplied, then retrieve partial contents of the history; otherwise retrieve entire history.
5552
5608
 
5553
- This function returns a dictionary with history items keyed by their 1-based index in ascending order.
5609
+ This function returns a dictionary with history items keyed by their positive 1-based index in ascending order.
5610
+ Index and range errors propagate to the caller. Empty selections return an empty dictionary.
5554
5611
  """
5555
5612
  if args.arg:
5556
- try:
5557
- int_arg = int(args.arg)
5558
- return {int_arg: self.history.get(int_arg)}
5559
- except ValueError:
5560
- pass
5561
-
5562
- if ".." in args.arg or ":" in args.arg:
5563
- # Get a slice of history
5564
- history = self.history.span(args.arg, args.all)
5565
- elif args.arg.startswith(r"/") and args.arg.endswith(r"/"):
5566
- history = self.history.regex_search(args.arg, args.all)
5567
- else:
5568
- history = self.history.str_search(args.arg, args.all)
5569
- else:
5570
- # Get a copy of the history so it doesn't get mutated while we are using it
5571
- history = self.history.span(":", args.all)
5572
- return history
5613
+ if args.arg.startswith(r"/") and args.arg.endswith(r"/"):
5614
+ return self.history.regex_search(args.arg, args.all)
5615
+ if re.fullmatch(r"-?[0-9]+", args.arg):
5616
+ try:
5617
+ index = int(args.arg)
5618
+ except ValueError:
5619
+ # Python may reject integer strings exceeding its conversion limit.
5620
+ raise HistoryIndexError(NO_HISTORY_ITEMS) from None
5621
+ if index == 0:
5622
+ # Preserve the dedicated index-zero error and command-one reminder.
5623
+ self.history.get(index)
5624
+ return self.history.span(f"{index}:{index}")
5625
+ if self.history.spanpattern.fullmatch(args.arg):
5626
+ return self.history.span(args.arg, args.all)
5627
+ return self.history.str_search(args.arg, args.all)
5628
+
5629
+ # Get a copy of the history so it doesn't get mutated while we are using it.
5630
+ return self.history.span(":", args.all)
5573
5631
 
5574
5632
  def _initialize_history(self, hist_file: str) -> None:
5575
5633
  """Initialize history using history related attributes.
@@ -5664,8 +5722,10 @@ class Cmd:
5664
5722
  except ModuleNotFoundError: # pragma: no cover
5665
5723
  import bz2 as compress_lib # type: ignore[no-redef]
5666
5724
 
5667
- self.history.truncate(self._persistent_history_length)
5668
- history_json = self.history.to_json()
5725
+ # Truncate a copy so live history numbering and the session boundary are unchanged.
5726
+ history = History(self.history)
5727
+ history.truncate(self._persistent_history_length)
5728
+ history_json = history.to_json()
5669
5729
  compressed_bytes = compress_lib.compress(history_json.encode(encoding="utf-8"))
5670
5730
 
5671
5731
  try:
@@ -5861,13 +5921,125 @@ class Cmd:
5861
5921
  self._alert_queue.append(alert)
5862
5922
  self._alert_condition.notify_all()
5863
5923
 
5864
- @staticmethod
5865
- def set_window_title(title: str) -> None: # pragma: no cover
5866
- """Set the terminal window title.
5924
+ def set_window_title(self, title: str) -> None:
5925
+ """Set the title of this application's terminal.
5926
+
5927
+ The title is written to the output used by the main prompt, custom input prompts, and
5928
+ select() menus. That is a dummy output when stdin or stdout isn't a terminal, or when the
5929
+ terminal couldn't be set up. In that case, the title is written to the stdout this app was
5930
+ created with instead, if that is a terminal. Otherwise, no title is written.
5931
+
5932
+ This can be called from any thread. If no prompt is running, then the title is written and
5933
+ flushed before this returns, and output errors propagate to the caller. Such a direct write
5934
+ from another thread isn't synchronized with other writers to the terminal, such as a prompt
5935
+ which is just starting or finishing.
5936
+
5937
+ While a prompt is running, the title is written on the prompt's event loop, even when this is
5938
+ called on that loop, so it doesn't interleave with rendering. It appears once the loop runs its
5939
+ next callback, which can be delayed while the loop is busy, such as in a slow completer. If
5940
+ several titles are set before then, only the latest is written. Output errors are ignored so
5941
+ they can't disrupt the prompt.
5867
5942
 
5868
5943
  :param title: the new window title
5869
5944
  """
5870
- set_title(title)
5945
+ with self._window_title_lock:
5946
+ self._pending_window_title = title
5947
+ loop = self._get_prompt_loop()
5948
+ if loop is not None:
5949
+ if self._window_title_callback_loop is loop:
5950
+ # A callback queued on this loop hasn't run yet, and it will write this title.
5951
+ return
5952
+
5953
+ try:
5954
+ loop.call_soon_threadsafe(self._flush_pending_window_title_quietly)
5955
+ except RuntimeError:
5956
+ # The loop closed after we found it, so the prompt is no longer rendering.
5957
+ pass
5958
+ else:
5959
+ # If the prompt finishes before the loop runs the callback, then the prompt's
5960
+ # cleanup writes the pending title.
5961
+ self._window_title_callback_loop = loop
5962
+ return
5963
+
5964
+ # Keep ownership until the direct write finishes, so another caller or prompt cleanup cannot consume
5965
+ # this request and receive or suppress its output error.
5966
+ self._flush_pending_window_title()
5967
+
5968
+ def _get_prompt_loop(self) -> asyncio.AbstractEventLoop | None:
5969
+ """Return the event loop of the prompt-toolkit application drawing on this terminal, if one is running.
5970
+
5971
+ An application's loop is set for its whole run, including startup and teardown, when its is_running flag
5972
+ is already cleared but the loop can still write to the terminal.
5973
+ """
5974
+ apps: list[Application[Any]] = [self.active_session.app]
5975
+
5976
+ # The main thread clears these when select() finishes, so read each one only once.
5977
+ select_app_session = self._select_app_session
5978
+ if select_app_session is not None:
5979
+ select_app = select_app_session.app
5980
+ if select_app is not None:
5981
+ apps.append(select_app)
5982
+
5983
+ for app in apps:
5984
+ loop = app.loop
5985
+ if loop is not None:
5986
+ return loop
5987
+ return None
5988
+
5989
+ def _get_window_title_fallback_output(self) -> Output | None:
5990
+ """Return an output for the stdout this app was created with, or None if that isn't a terminal.
5991
+
5992
+ Window titles use it when the main session has a DummyOutput. It's created the first time a title needs it.
5993
+ This isn't a cached_property because inspect.getmembers() evaluates properties while this app is constructed.
5994
+ """
5995
+ if not self._window_title_fallback_checked:
5996
+ self._window_title_fallback_checked = True
5997
+
5998
+ # A stream without isatty(), or a closed one, raises AttributeError or ValueError here.
5999
+ with contextlib.suppress(*_TERMINAL_INIT_ERRORS):
6000
+ if self._window_title_stdout.isatty():
6001
+ self._window_title_fallback_output = create_output(stdout=self._window_title_stdout)
6002
+
6003
+ return self._window_title_fallback_output
6004
+
6005
+ def _get_window_title_output(self) -> Output:
6006
+ """Return the output which window titles are written to."""
6007
+ output = self.main_session.output
6008
+ if isinstance(output, DummyOutput) and (fallback := self._get_window_title_fallback_output()) is not None:
6009
+ return fallback
6010
+ return output
6011
+
6012
+ def _flush_pending_window_title(self) -> None:
6013
+ """Write and flush the most recent title passed to set_window_title(), if it hasn't been written yet."""
6014
+ with self._window_title_lock:
6015
+ if self._writing_window_title:
6016
+ # This thread is already writing a title, such as when a signal handler calls set_window_title().
6017
+ # Writing now would nest inside that write, so leave the title pending for the outer call.
6018
+ return
6019
+
6020
+ self._writing_window_title = True
6021
+ try:
6022
+ # Keep going until no newer title arrived during the last write, so the latest title always wins.
6023
+ while (title := self._pending_window_title) is not None:
6024
+ self._pending_window_title = None
6025
+ output = self._get_window_title_output()
6026
+ output.set_title(title)
6027
+ output.flush()
6028
+ finally:
6029
+ self._writing_window_title = False
6030
+
6031
+ def _flush_pending_window_title_quietly(self) -> None:
6032
+ """Flush a pending window title on a prompt's event loop or during its cleanup, ignoring output errors.
6033
+
6034
+ An error here would otherwise disrupt the prompt or replace its result. Writing to a closed stream raises
6035
+ ValueError rather than OSError.
6036
+ """
6037
+ with self._window_title_lock:
6038
+ # Either this is the queued callback, or the prompt finished and its loop won't run that callback.
6039
+ # Either way, the next title set during a prompt needs a new callback.
6040
+ self._window_title_callback_loop = None
6041
+ with contextlib.suppress(OSError, ValueError):
6042
+ self._flush_pending_window_title()
5871
6043
 
5872
6044
  def enable_command(self, command: str) -> None:
5873
6045
  """Enable a command by restoring its functions.