cmd2 4.2.3__tar.gz → 4.2.4__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 (227) hide show
  1. {cmd2-4.2.3 → cmd2-4.2.4}/.pre-commit-config.yaml +1 -1
  2. {cmd2-4.2.3 → cmd2-4.2.4}/CHANGELOG.md +13 -0
  3. cmd2-4.2.4/CLAUDE.md +123 -0
  4. {cmd2-4.2.3 → cmd2-4.2.4}/PKG-INFO +2 -1
  5. {cmd2-4.2.3 → cmd2-4.2.4}/README.md +1 -0
  6. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/annotated.py +7 -4
  7. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/cmd2.py +42 -5
  8. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/types.py +7 -4
  9. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/PKG-INFO +2 -1
  10. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/SOURCES.txt +2 -0
  11. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/scm_file_list.json +2 -0
  12. cmd2-4.2.4/cmd2.egg-info/scm_version.json +8 -0
  13. {cmd2-4.2.3 → cmd2-4.2.4}/pyproject.toml +1 -1
  14. {cmd2-4.2.3 → cmd2-4.2.4}/tests/conftest.py +17 -0
  15. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_cmd2.py +35 -0
  16. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_run_pyscript.py +1 -1
  17. cmd2-4.2.4/tests/test_suite_environment.py +59 -0
  18. cmd2-4.2.3/cmd2.egg-info/scm_version.json +0 -8
  19. {cmd2-4.2.3 → cmd2-4.2.4}/.prettierignore +0 -0
  20. {cmd2-4.2.3 → cmd2-4.2.4}/.prettierrc +0 -0
  21. {cmd2-4.2.3 → cmd2-4.2.4}/.typos.toml +0 -0
  22. {cmd2-4.2.3 → cmd2-4.2.4}/AGENTS.md +0 -0
  23. {cmd2-4.2.3 → cmd2-4.2.4}/GEMINI.md +0 -0
  24. {cmd2-4.2.3 → cmd2-4.2.4}/LICENSE +0 -0
  25. {cmd2-4.2.3 → cmd2-4.2.4}/MANIFEST.in +0 -0
  26. {cmd2-4.2.3 → cmd2-4.2.4}/Makefile +0 -0
  27. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/__init__.py +0 -0
  28. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/argparse_completer.py +0 -0
  29. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/argparse_utils.py +0 -0
  30. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/clipboard.py +0 -0
  31. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/colors.py +0 -0
  32. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/command_set.py +0 -0
  33. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/completion.py +0 -0
  34. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/constants.py +0 -0
  35. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/decorators.py +0 -0
  36. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/exceptions.py +0 -0
  37. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/history.py +0 -0
  38. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/parsing.py +0 -0
  39. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/plugin.py +0 -0
  40. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/pt_utils.py +0 -0
  41. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/py.typed +0 -0
  42. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/py_bridge.py +0 -0
  43. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/rich_utils.py +0 -0
  44. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/string_utils.py +0 -0
  45. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/styles.py +0 -0
  46. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/theme.py +0 -0
  47. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/utils.py +0 -0
  48. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/dependency_links.txt +0 -0
  49. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/requires.txt +0 -0
  50. {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/top_level.txt +0 -0
  51. {cmd2-4.2.3 → cmd2-4.2.4}/codecov.yml +0 -0
  52. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/annotated.md +0 -0
  53. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/argparse_completer.md +0 -0
  54. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/argparse_utils.md +0 -0
  55. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/clipboard.md +0 -0
  56. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/cmd.md +0 -0
  57. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/colors.md +0 -0
  58. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/command_set.md +0 -0
  59. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/completion.md +0 -0
  60. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/constants.md +0 -0
  61. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/decorators.md +0 -0
  62. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/exceptions.md +0 -0
  63. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/history.md +0 -0
  64. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/index.md +0 -0
  65. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/parsing.md +0 -0
  66. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/plugin.md +0 -0
  67. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/pt_utils.md +0 -0
  68. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/py_bridge.md +0 -0
  69. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/rich_utils.md +0 -0
  70. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/string_utils.md +0 -0
  71. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/styles.md +0 -0
  72. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/theme.md +0 -0
  73. {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/utils.md +0 -0
  74. {cmd2-4.2.3 → cmd2-4.2.4}/docs/assets/cmd2-readme-demo.gif +0 -0
  75. {cmd2-4.2.3 → cmd2-4.2.4}/docs/assets/getting-started-demo.gif +0 -0
  76. {cmd2-4.2.3 → cmd2-4.2.4}/docs/doc_conventions.md +0 -0
  77. {cmd2-4.2.3 → cmd2-4.2.4}/docs/examples/alternate_event_loops.md +0 -0
  78. {cmd2-4.2.3 → cmd2-4.2.4}/docs/examples/examples.md +0 -0
  79. {cmd2-4.2.3 → cmd2-4.2.4}/docs/examples/getting_started.md +0 -0
  80. {cmd2-4.2.3 → cmd2-4.2.4}/docs/examples/index.md +0 -0
  81. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/annotated.md +0 -0
  82. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/argument_processing.md +0 -0
  83. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/async_commands.md +0 -0
  84. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/builtin_commands.md +0 -0
  85. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/clipboard.md +0 -0
  86. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/commands.md +0 -0
  87. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/completion.md +0 -0
  88. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/disable_commands.md +0 -0
  89. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/embedded_python_shells.md +0 -0
  90. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/generating_output.md +0 -0
  91. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/help.md +0 -0
  92. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/history.md +0 -0
  93. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/hooks.md +0 -0
  94. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/index.md +0 -0
  95. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/initialization.md +0 -0
  96. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/misc.md +0 -0
  97. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/modular_commands.md +0 -0
  98. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/multiline_commands.md +0 -0
  99. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/os.md +0 -0
  100. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/packaging.md +0 -0
  101. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/plugins.md +0 -0
  102. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/prompt.md +0 -0
  103. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/redirection.md +0 -0
  104. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/scripting.md +0 -0
  105. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/settings.md +0 -0
  106. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/shortcuts_aliases_macros.md +0 -0
  107. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/startup_commands.md +0 -0
  108. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/table_creation.md +0 -0
  109. {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/theme.md +0 -0
  110. {cmd2-4.2.3 → cmd2-4.2.4}/docs/index.md +0 -0
  111. {cmd2-4.2.3 → cmd2-4.2.4}/docs/javascripts/readthedocs.js +0 -0
  112. {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/incompatibilities.md +0 -0
  113. {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/index.md +0 -0
  114. {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/minimum.md +0 -0
  115. {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/next_steps.md +0 -0
  116. {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/why.md +0 -0
  117. {cmd2-4.2.3 → cmd2-4.2.4}/docs/mixins/index.md +0 -0
  118. {cmd2-4.2.3 → cmd2-4.2.4}/docs/mixins/mixin_template.md +0 -0
  119. {cmd2-4.2.3 → cmd2-4.2.4}/docs/overrides/main.html +0 -0
  120. {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/alternatives.md +0 -0
  121. {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/index.md +0 -0
  122. {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/installation.md +0 -0
  123. {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/integrating.md +0 -0
  124. {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/resources.md +0 -0
  125. {cmd2-4.2.3 → cmd2-4.2.4}/docs/stylesheets/cmd2.css +0 -0
  126. {cmd2-4.2.3 → cmd2-4.2.4}/docs/testing.md +0 -0
  127. {cmd2-4.2.3 → cmd2-4.2.4}/docs/upgrades.md +0 -0
  128. {cmd2-4.2.3 → cmd2-4.2.4}/examples/.cmd2rc +0 -0
  129. {cmd2-4.2.3 → cmd2-4.2.4}/examples/README.md +0 -0
  130. {cmd2-4.2.3 → cmd2-4.2.4}/examples/annotated_example.py +0 -0
  131. {cmd2-4.2.3 → cmd2-4.2.4}/examples/argparse_completion.py +0 -0
  132. {cmd2-4.2.3 → cmd2-4.2.4}/examples/argparse_example.py +0 -0
  133. {cmd2-4.2.3 → cmd2-4.2.4}/examples/async_call.py +0 -0
  134. {cmd2-4.2.3 → cmd2-4.2.4}/examples/async_commands.py +0 -0
  135. {cmd2-4.2.3 → cmd2-4.2.4}/examples/async_printing.py +0 -0
  136. {cmd2-4.2.3 → cmd2-4.2.4}/examples/basic_completion.py +0 -0
  137. {cmd2-4.2.3 → cmd2-4.2.4}/examples/cmd2_history.dat +0 -0
  138. {cmd2-4.2.3 → cmd2-4.2.4}/examples/cmd_as_argument.py +0 -0
  139. {cmd2-4.2.3 → cmd2-4.2.4}/examples/color.py +0 -0
  140. {cmd2-4.2.3 → cmd2-4.2.4}/examples/command_sets.py +0 -0
  141. {cmd2-4.2.3 → cmd2-4.2.4}/examples/completion_item_choices.py +0 -0
  142. {cmd2-4.2.3 → cmd2-4.2.4}/examples/custom_parser.py +0 -0
  143. {cmd2-4.2.3 → cmd2-4.2.4}/examples/custom_types.py +0 -0
  144. {cmd2-4.2.3 → cmd2-4.2.4}/examples/default_categories.py +0 -0
  145. {cmd2-4.2.3 → cmd2-4.2.4}/examples/dynamic_commands.py +0 -0
  146. {cmd2-4.2.3 → cmd2-4.2.4}/examples/environment.py +0 -0
  147. {cmd2-4.2.3 → cmd2-4.2.4}/examples/event_loops.py +0 -0
  148. {cmd2-4.2.3 → cmd2-4.2.4}/examples/exit_code.py +0 -0
  149. {cmd2-4.2.3 → cmd2-4.2.4}/examples/getting_started.py +0 -0
  150. {cmd2-4.2.3 → cmd2-4.2.4}/examples/hello_cmd2.py +0 -0
  151. {cmd2-4.2.3 → cmd2-4.2.4}/examples/help_categories.py +0 -0
  152. {cmd2-4.2.3 → cmd2-4.2.4}/examples/hooks.py +0 -0
  153. {cmd2-4.2.3 → cmd2-4.2.4}/examples/migrating.py +0 -0
  154. {cmd2-4.2.3 → cmd2-4.2.4}/examples/mixin.py +0 -0
  155. {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commands/__init__.py +0 -0
  156. {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commands/commandset_basic.py +0 -0
  157. {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commands/commandset_complex.py +0 -0
  158. {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commands/commandset_custominit.py +0 -0
  159. {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commandsets.py +0 -0
  160. {cmd2-4.2.3 → cmd2-4.2.4}/examples/paged_output.py +0 -0
  161. {cmd2-4.2.3 → cmd2-4.2.4}/examples/persistent_history.py +0 -0
  162. {cmd2-4.2.3 → cmd2-4.2.4}/examples/pretty_print.py +0 -0
  163. {cmd2-4.2.3 → cmd2-4.2.4}/examples/python_scripting.py +0 -0
  164. {cmd2-4.2.3 → cmd2-4.2.4}/examples/read_input.py +0 -0
  165. {cmd2-4.2.3 → cmd2-4.2.4}/examples/remove_builtin_commands.py +0 -0
  166. {cmd2-4.2.3 → cmd2-4.2.4}/examples/remove_settable.py +0 -0
  167. {cmd2-4.2.3 → cmd2-4.2.4}/examples/rich_tables.py +0 -0
  168. {cmd2-4.2.3 → cmd2-4.2.4}/examples/rich_theme.py +0 -0
  169. {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/arg_printer.py +0 -0
  170. {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/conditional.py +0 -0
  171. {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/nested.txt +0 -0
  172. {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/quit.txt +0 -0
  173. {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/save_help_text.py +0 -0
  174. {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/script.py +0 -0
  175. {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/script.txt +0 -0
  176. {cmd2-4.2.3 → cmd2-4.2.4}/examples/tmux_launch.sh +0 -0
  177. {cmd2-4.2.3 → cmd2-4.2.4}/examples/tmux_split.sh +0 -0
  178. {cmd2-4.2.3 → cmd2-4.2.4}/examples/unicode_commands.py +0 -0
  179. {cmd2-4.2.3 → cmd2-4.2.4}/mkdocs.yml +0 -0
  180. {cmd2-4.2.3 → cmd2-4.2.4}/package.json +0 -0
  181. {cmd2-4.2.3 → cmd2-4.2.4}/ruff.toml +0 -0
  182. {cmd2-4.2.3 → cmd2-4.2.4}/scripts/validate_tag.py +0 -0
  183. {cmd2-4.2.3 → cmd2-4.2.4}/setup.cfg +0 -0
  184. {cmd2-4.2.3 → cmd2-4.2.4}/tests/.cmd2rc +0 -0
  185. {cmd2-4.2.3 → cmd2-4.2.4}/tests/__init__.py +0 -0
  186. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/echo.py +0 -0
  187. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/environment.py +0 -0
  188. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/help.py +0 -0
  189. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/py_locals.py +0 -0
  190. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/pyscript_dir.py +0 -0
  191. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/raises_exception.py +0 -0
  192. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/recursive.py +0 -0
  193. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/self_in_py.py +0 -0
  194. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/stop.py +0 -0
  195. {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/test_print.py +0 -0
  196. {cmd2-4.2.3 → cmd2-4.2.4}/tests/relative_multiple.txt +0 -0
  197. {cmd2-4.2.3 → cmd2-4.2.4}/tests/script.py +0 -0
  198. {cmd2-4.2.3 → cmd2-4.2.4}/tests/script.txt +0 -0
  199. {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/binary.bin +0 -0
  200. {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/empty.txt +0 -0
  201. {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/help.txt +0 -0
  202. {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/nested.txt +0 -0
  203. {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/one_down.txt +0 -0
  204. {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/postcmds.txt +0 -0
  205. {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/precmds.txt +0 -0
  206. {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/utf8.txt +0 -0
  207. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_annotated.py +0 -0
  208. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_argparse.py +0 -0
  209. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_argparse_completer.py +0 -0
  210. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_argparse_subcommands.py +0 -0
  211. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_argparse_utils.py +0 -0
  212. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_categories.py +0 -0
  213. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_commandset.py +0 -0
  214. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_completion.py +0 -0
  215. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_dynamic_complete_style.py +0 -0
  216. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_future_annotations.py +0 -0
  217. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_history.py +0 -0
  218. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_parsing.py +0 -0
  219. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_plugin.py +0 -0
  220. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_pt_utils.py +0 -0
  221. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_py_completion.py +0 -0
  222. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_rich_utils.py +0 -0
  223. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_string_utils.py +0 -0
  224. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_theme.py +0 -0
  225. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_utils.py +0 -0
  226. {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_utils_defining_class.py +0 -0
  227. {cmd2-4.2.3 → cmd2-4.2.4}/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.16.5"
18
+ rev: "v0.16.6"
19
19
  hooks:
20
20
  - id: ruff-format
21
21
  args: [--config=ruff.toml]
@@ -1,3 +1,16 @@
1
+ ## 4.2.4 (September 8, 2026)
2
+
3
+ - Bug Fixes
4
+ - Fixed output redirection and piping raising `UnicodeEncodeError` and leaving an empty file
5
+ behind. Command output is rendered by Rich and contains box-drawing characters, which no
6
+ `cp125x` Windows ANSI code page can represent, so redirecting or piping it failed. This
7
+ affects current Windows 11 with default settings on Python 3.11 through 3.14, not only older
8
+ systems. Redirection targets and pipes now use UTF-8 explicitly
9
+ - Fixed the right prompt being redrawn beside every accepted command line in the scrollback.
10
+ prompt-toolkit includes it in the final frame of each prompt, which is the frame left on the
11
+ terminal; it is now hidden there, as the bottom toolbar already was, and stays on the live
12
+ prompt line only
13
+
1
14
  ## 4.2.3 (September 2, 2026)
2
15
 
3
16
  - Bug Fixes
cmd2-4.2.4/CLAUDE.md ADDED
@@ -0,0 +1,123 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this
4
+ repository.
5
+
6
+ ## Project
7
+
8
+ `cmd2` is a framework for building interactive command-line applications (REPLs) in Python. It
9
+ extends the stdlib `cmd` module and is built on **prompt-toolkit** (interactive input, key bindings,
10
+ completion, toolbars) and **Rich** / **rich-argparse** (all output rendering, help formatting,
11
+ theming). Requires Python >= 3.11. Environment/package management is **uv**.
12
+
13
+ ## Commands
14
+
15
+ All work happens inside the uv-managed venv — run Python via `uv run ...`, and `uvx ...` for one-off
16
+ tools. Never `pip install`; if a new dependency is needed, state why before adding it.
17
+
18
+ ```bash
19
+ make install # create the venv, install prek git hooks + prettier (one time)
20
+ make check # lock check + prek (ruff format/lint, prettier, typos) + ty + mypy
21
+ make test # pytest with coverage over tests/
22
+ make docs-test # verify the docs build cleanly (zensical)
23
+ make docs # build + serve docs with live reload
24
+ make help # list all targets
25
+ ```
26
+
27
+ `make check`, `make test`, and `make docs-test` must all pass before committing. Run `make check`
28
+ before creating or modifying any `.py` file as well.
29
+
30
+ Narrower targets: `make format`, `make lint`, `make mypy`, `make ty`, `make typecheck`.
31
+
32
+ Running a single test or file (coverage flags come from `pyproject.toml` `addopts`, so pass
33
+ `--no-cov` when you just want a fast run):
34
+
35
+ ```bash
36
+ uv run pytest tests/test_cmd2.py --no-cov
37
+ uv run pytest tests/test_cmd2.py::test_base_help -x --no-cov
38
+ uv run pytest -k "toolbar" --no-cov
39
+ ```
40
+
41
+ On Windows-sensitive code, note `make test` runs pytest under `python -Xutf8`.
42
+
43
+ ## Architecture
44
+
45
+ ### Command dispatch
46
+
47
+ `cmd2/cmd2.py` (the `Cmd` class) is the core and by far the largest module. The flow for one line of
48
+ input is:
49
+
50
+ 1. `cmdloop()` → `_cmdloop()` drives a prompt-toolkit `PromptSession` (created in
51
+ `_create_prompt_session`), not readline. Completion, history, lexing, and key bindings are all
52
+ supplied by prompt-toolkit adapters in `cmd2/pt_utils.py` (`Cmd2Completer`, `Cmd2History`,
53
+ `Cmd2Lexer`).
54
+ 2. `onecmd_plus_hooks()` parses the line into a `Statement` (`cmd2/parsing.py` — handles shortcuts,
55
+ aliases, macros, terminators, multiline commands, redirection and pipe tokens), then runs the
56
+ plugin hook chain.
57
+ 3. `_redirect_output()` swaps `self.stdout` for a file or subprocess pipe when the statement has
58
+ redirection, and restores it in the `finally` path.
59
+ 4. `onecmd()` looks up and calls the `do_*` method.
60
+
61
+ Hooks (`cmd2/plugin.py` dataclasses, registered via `register_postparsing_hook`,
62
+ `register_precmd_hook`, `register_postcmd_hook`, `register_cmdfinalization_hook`,
63
+ `register_preloop_hook`, `register_postloop_hook`) are the supported extension points; the legacy
64
+ `precmd`/`postcmd` overrides still exist but are weaker.
65
+
66
+ ### Argument parsing and completion
67
+
68
+ Three layers, all producing a `Cmd2ArgumentParser`:
69
+
70
+ - `cmd2/argparse_utils.py` — `Cmd2ArgumentParser` plus monkey-patching that extends argparse with
71
+ range `nargs` tuples, per-argument completion metadata, and subcommand records.
72
+ - `cmd2/decorators.py` — `@with_argparser`, `@with_argument_list`, `@with_category`,
73
+ `@as_subcommand_to` attach parsers/metadata to `do_*` methods.
74
+ - `cmd2/annotated.py` — the newer, still-experimental Typer-style path: `@with_annotated` builds a
75
+ parser from a function's type hints, with `Argument`/`Option` metadata via `typing.Annotated`.
76
+
77
+ Parsers are built lazily and cached by the `CommandParsers` class in `cmd2.py` (`_build_parser`), so
78
+ that `CommandSet`-supplied subcommands can be attached and detached at runtime.
79
+ `cmd2/argparse_completer.py` walks a parser to produce tab completions; completion results are
80
+ `Completions`/`CompletionItem` objects from `cmd2/completion.py`.
81
+
82
+ ### Output and theming
83
+
84
+ Never print directly — go through `Cmd.print_to` / `poutput` / `perror` / `pwarning` / `pfeedback` /
85
+ `ppaged`, which route to Rich consoles defined in `cmd2/rich_utils.py` (`Cmd2GeneralConsole`,
86
+ `Cmd2RichArgparseConsole`, `Cmd2ExceptionConsole`). Styling flows one way: `cmd2/styles.py`
87
+ (style-name StrEnum + defaults) → `cmd2/theme.py` (single global theme, updated in-place for Rich
88
+ and exposed to prompt-toolkit through a `DynamicStyle`) → `cmd2/pt_utils.py` (`rich_to_pt_style`
89
+ converts Rich styles into prompt-toolkit ones). Adding a color or style means touching
90
+ `styles.py`/`colors.py`/`theme.py`, not hardcoding ANSI.
91
+
92
+ `cmd2/command_toolbar.py` and `cmd2/pager.py` are the trickiest parts of the codebase: they keep a
93
+ prompt-toolkit application alive _during_ synchronous command execution (bottom toolbar refresh,
94
+ paging) while stdout is proxied and redirection may be active. Changes there need care around
95
+ threads, signals, and Windows behavior.
96
+
97
+ ### Modularity
98
+
99
+ `cmd2/command_set.py` (`CommandSet`) lets commands live in separate classes that are registered and
100
+ unregistered at runtime via `Cmd.register_command_set`; `cmd2/py_bridge.py` exposes the app to
101
+ embedded Python/IPython shells and `run_pyscript` while keeping isolation.
102
+
103
+ ## Conventions
104
+
105
+ - Ruff is authoritative for format and lint (`ruff.toml`, line length 127, double quotes). Do not
106
+ suppress lint errors in code you write; broad ignores already exist for `examples/` and `tests/`.
107
+ - Both `mypy --strict` and `ty` must pass on the `cmd2` package. Full type annotations are required
108
+ on all library code (excluded: `tests/`, `examples/`, `docs/`).
109
+ - Docstrings are enforced by pydocstyle rules; public API items are documented in `docs/api/*.md`
110
+ and rendered by mkdocstrings, so keep docstrings accurate when changing signatures.
111
+ - Anything not documented under `docs/api/` is not public API (`cmd2/constants.py` says so
112
+ explicitly).
113
+ - Add user-visible changes to `CHANGELOG.md` under the current in-progress version heading.
114
+ - `main` is the branch for the next PATCH release; MAJOR/MINOR work happens on a branch named for
115
+ the target version. Releases are tagged and published from `main`.
116
+ - Do not commit spec, plan, or markdown documents without asking first. Save plans to
117
+ `~/.superpowers/plans/` rather than the project directory.
118
+
119
+ ## Commit conventions
120
+
121
+ Never add "Co-Authored-By" lines to commits. Do not include Claude attribution in commit messages,
122
+ PR descriptions, or any git metadata. Do not include Claude session URL in commit messages, PR
123
+ descriptions, or any git metadata.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmd2
3
- Version: 4.2.3
3
+ Version: 4.2.4
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
@@ -251,6 +251,7 @@ reproduce the bug. At a minimum, please state the following:
251
251
  | [Falcon Toolkit](https://github.com/CrowdStrike/Falcon-Toolkit) | Unleash the power of the CrowdStrike Falcon Platform at the CLI | [CrowdStrike](https://github.com/CrowdStrike) |
252
252
  | [EXPLIoT](https://gitlab.com/expliot_framework/expliot) | Internet of Things Security Testing and Exploitation framework | [expliot_framework](https://gitlab.com/expliot_framework/) |
253
253
  | [Pobshell](https://github.com/pdalloz/pobshell) | A Bash‑like shell for live Python objects: `cd`, `ls`, `cat`, `find` and _CLI piping_ for object code, str values & more | [Peter Dalloz](https://www.linkedin.com/in/pdalloz) |
254
+ | [Wtsh](https://gitee.com/wazzge/wtsh) | WTSH is a shell built with Python cmd2(v4), which uses third-party plugins to construct a universal shell installed via Pip | [wazzge](https://gitee.com/wazzge) |
254
255
 
255
256
  Possibly defunct but still good examples
256
257
 
@@ -222,6 +222,7 @@ reproduce the bug. At a minimum, please state the following:
222
222
  | [Falcon Toolkit](https://github.com/CrowdStrike/Falcon-Toolkit) | Unleash the power of the CrowdStrike Falcon Platform at the CLI | [CrowdStrike](https://github.com/CrowdStrike) |
223
223
  | [EXPLIoT](https://gitlab.com/expliot_framework/expliot) | Internet of Things Security Testing and Exploitation framework | [expliot_framework](https://gitlab.com/expliot_framework/) |
224
224
  | [Pobshell](https://github.com/pdalloz/pobshell) | A Bash‑like shell for live Python objects: `cd`, `ls`, `cat`, `find` and _CLI piping_ for object code, str values & more | [Peter Dalloz](https://www.linkedin.com/in/pdalloz) |
225
+ | [Wtsh](https://gitee.com/wazzge/wtsh) | WTSH is a shell built with Python cmd2(v4), which uses third-party plugins to construct a universal shell installed via Pip | [wazzge](https://gitee.com/wazzge) |
225
226
 
226
227
  Possibly defunct but still good examples
227
228
 
@@ -313,6 +313,7 @@ from .rich_utils import Cmd2HelpFormatter, HelpContent
313
313
  from .types import (
314
314
  BoundCommandFunc,
315
315
  CmdOrSetT,
316
+ CmdOrSetT_contra,
316
317
  UnboundChoicesProvider,
317
318
  UnboundCompleter,
318
319
  )
@@ -2907,23 +2908,25 @@ class _BoundAnnotatedCommand(Protocol[_CommandParams, _CommandReturn_co]):
2907
2908
  def __call__(self, *args: _CommandParams.args, **kwargs: _CommandParams.kwargs) -> _CommandReturn_co: ...
2908
2909
 
2909
2910
 
2910
- class _AnnotatedCommand(Protocol[CmdOrSetT, _CommandParams, _CommandReturn_co]):
2911
+ class _AnnotatedCommand(Protocol[CmdOrSetT_contra, _CommandParams, _CommandReturn_co]):
2911
2912
  """A callable command method with the metadata used by ``with_annotated``."""
2912
2913
 
2913
2914
  __name__: str
2914
2915
  __qualname__: str
2915
2916
 
2916
2917
  def __call__(
2917
- self, __self: CmdOrSetT, /, *args: _CommandParams.args, **kwargs: _CommandParams.kwargs
2918
+ self, __self: CmdOrSetT_contra, /, *args: _CommandParams.args, **kwargs: _CommandParams.kwargs
2918
2919
  ) -> _CommandReturn_co: ...
2919
2920
 
2920
2921
  @overload
2921
2922
  def __get__(
2922
2923
  self, instance: None, owner: Any = ...
2923
- ) -> "_AnnotatedCommand[CmdOrSetT, _CommandParams, _CommandReturn_co]": ...
2924
+ ) -> "_AnnotatedCommand[CmdOrSetT_contra, _CommandParams, _CommandReturn_co]": ...
2924
2925
 
2925
2926
  @overload
2926
- def __get__(self, instance: CmdOrSetT, owner: Any = ...) -> _BoundAnnotatedCommand[_CommandParams, _CommandReturn_co]: ...
2927
+ def __get__(
2928
+ self, instance: CmdOrSetT_contra, owner: Any = ...
2929
+ ) -> _BoundAnnotatedCommand[_CommandParams, _CommandReturn_co]: ...
2927
2930
 
2928
2931
 
2929
2932
  class _WithAnnotatedDecorator(Protocol):
@@ -80,6 +80,8 @@ from prompt_toolkit.input import DummyInput, create_input
80
80
  from prompt_toolkit.key_binding import KeyBindings
81
81
  from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
82
82
  from prompt_toolkit.keys import Keys
83
+ from prompt_toolkit.layout import Window, walk
84
+ from prompt_toolkit.layout.containers import ConditionalContainer, FloatContainer
83
85
  from prompt_toolkit.output import DummyOutput, create_output
84
86
  from prompt_toolkit.patch_stdout import patch_stdout
85
87
  from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, choice, set_title
@@ -552,6 +554,8 @@ class Cmd:
552
554
  enable_rprompt=enable_rprompt,
553
555
  refresh_interval=refresh_interval,
554
556
  )
557
+ if enable_rprompt:
558
+ self._hide_rprompt_when_done(self.main_session)
555
559
 
556
560
  # The session currently holding focus (either the main REPL or a command's
557
561
  # custom prompt). Completion and UI logic should reference this variable
@@ -845,6 +849,30 @@ class Cmd:
845
849
  )
846
850
  return PromptSession(**kwargs)
847
851
 
852
+ @staticmethod
853
+ def _hide_rprompt_when_done(session: PromptSession[str]) -> None:
854
+ """Keep the right prompt out of the frame that is committed to the scrollback.
855
+
856
+ prompt-toolkit ends every ``Application.run()`` with a final render, and that
857
+ frame is what stays on the terminal. Its bottom toolbar container is filtered
858
+ with ``~is_done`` so it is excluded, but the right prompt is a plain ``Float``
859
+ with no such filter, which left a copy of the right prompt beside every
860
+ accepted command line. This applies the same rule the toolbar already gets.
861
+
862
+ The right prompt is cosmetic, so an upstream layout change must not stop the
863
+ application from starting. If the float cannot be found, nothing happens and
864
+ the previous behavior remains.
865
+
866
+ :param session: the PromptSession whose right prompt should be hidden when done
867
+ """
868
+ for container in walk(session.layout.container):
869
+ if not isinstance(container, FloatContainer):
870
+ continue
871
+ for float_ in container.floats:
872
+ content = float_.content
873
+ if isinstance(content, Window) and content.style == "class:rprompt":
874
+ float_.content = ConditionalContainer(content, filter=~filters.is_done)
875
+
848
876
  def find_commandsets(
849
877
  self, commandset_type: type[CommandSet[Any]], *, subclass_match: bool = False
850
878
  ) -> list[CommandSet[Any]]:
@@ -3296,9 +3324,11 @@ class Cmd:
3296
3324
  # Create a pipe with read and write sides
3297
3325
  read_fd, write_fd = os.pipe()
3298
3326
 
3299
- # Open each side of the pipe
3300
- subproc_stdin = open(read_fd) # noqa: SIM115
3301
- new_stdout: TextIO = cast(TextIO, open(write_fd, "w")) # noqa: SIM115
3327
+ # Open each side of the pipe. Both ends are given an explicit encoding:
3328
+ # command output is rendered by Rich and routinely contains non-ASCII, which
3329
+ # the locale encoding cannot always represent.
3330
+ subproc_stdin = open(read_fd, encoding="utf-8") # noqa: SIM115
3331
+ new_stdout: TextIO = cast(TextIO, open(write_fd, "w", encoding="utf-8")) # noqa: SIM115
3302
3332
 
3303
3333
  # Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs,
3304
3334
  # our sigint handler will forward it only to the most recent pipe process. This makes sure pipe
@@ -3347,8 +3377,15 @@ class Cmd:
3347
3377
  # statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT
3348
3378
  mode = "a" if statement.redirector == constants.REDIRECTION_APPEND else "w"
3349
3379
  try:
3350
- # Use line buffering
3351
- new_stdout = cast(TextIO, open(su.strip_quotes(statement.redirect_to), mode=mode, buffering=1)) # noqa: SIM115
3380
+ # Use line buffering. The encoding is explicit rather than the
3381
+ # locale's: command output is rendered by Rich and routinely contains
3382
+ # non-ASCII, so on a non-UTF-8 system -- a default Windows console,
3383
+ # for instance -- redirection would otherwise fail and leave an empty
3384
+ # file behind.
3385
+ new_stdout = cast(
3386
+ TextIO,
3387
+ open(su.strip_quotes(statement.redirect_to), mode=mode, buffering=1, encoding="utf-8"), # noqa: SIM115
3388
+ )
3352
3389
  except OSError as ex:
3353
3390
  raise RedirectionError("Failed to redirect output") from ex
3354
3391
 
@@ -58,6 +58,9 @@ CommandSetT = TypeVar("CommandSetT", bound="CommandSet[Any]")
58
58
  # Tracks the specific subclass instance (either a Cmd or CommandSet)
59
59
  CmdOrSetT = TypeVar("CmdOrSetT", bound=CmdOrSet)
60
60
 
61
+ # For protocols that accept a Cmd or CommandSet instance
62
+ CmdOrSetT_contra = TypeVar("CmdOrSetT_contra", bound=CmdOrSet, contravariant=True)
63
+
61
64
  # Tracks the specific class itself (either a Cmd or CommandSet class)
62
65
  CmdOrSetClassT = TypeVar("CmdOrSetClassT", bound=CmdOrSetClass)
63
66
 
@@ -88,20 +91,20 @@ class BoundCommandFunc(Protocol[P]):
88
91
 
89
92
  # An unbound cmd2 command function (e.g. the class method do_command).
90
93
  # The 'self' argument can be either a Cmd or CommandSet instance.
91
- class UnboundCommandFunc(Protocol[CmdOrSetT, P]):
94
+ class UnboundCommandFunc(Protocol[CmdOrSetT_contra, P]):
92
95
  """Protocol for an unbound command function."""
93
96
 
94
97
  __name__: str
95
98
  __qualname__: str
96
99
 
97
- def __call__(self, __self: CmdOrSetT, /, *args: P.args, **kwargs: P.kwargs) -> bool | None:
100
+ def __call__(self, __self: CmdOrSetT_contra, /, *args: P.args, **kwargs: P.kwargs) -> bool | None:
98
101
  """Invoke the unbound command function with its command instance."""
99
102
 
100
103
  @overload
101
- def __get__(self, instance: None, owner: Any = ...) -> "UnboundCommandFunc[CmdOrSetT, P]": ...
104
+ def __get__(self, instance: None, owner: Any = ...) -> "UnboundCommandFunc[CmdOrSetT_contra, P]": ...
102
105
 
103
106
  @overload
104
- def __get__(self, instance: CmdOrSetT, owner: Any = ...) -> BoundCommandFunc[P]: ...
107
+ def __get__(self, instance: CmdOrSetT_contra, owner: Any = ...) -> BoundCommandFunc[P]: ...
105
108
 
106
109
 
107
110
  ##################################################################################################
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmd2
3
- Version: 4.2.3
3
+ Version: 4.2.4
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
@@ -251,6 +251,7 @@ reproduce the bug. At a minimum, please state the following:
251
251
  | [Falcon Toolkit](https://github.com/CrowdStrike/Falcon-Toolkit) | Unleash the power of the CrowdStrike Falcon Platform at the CLI | [CrowdStrike](https://github.com/CrowdStrike) |
252
252
  | [EXPLIoT](https://gitlab.com/expliot_framework/expliot) | Internet of Things Security Testing and Exploitation framework | [expliot_framework](https://gitlab.com/expliot_framework/) |
253
253
  | [Pobshell](https://github.com/pdalloz/pobshell) | A Bash‑like shell for live Python objects: `cd`, `ls`, `cat`, `find` and _CLI piping_ for object code, str values & more | [Peter Dalloz](https://www.linkedin.com/in/pdalloz) |
254
+ | [Wtsh](https://gitee.com/wazzge/wtsh) | WTSH is a shell built with Python cmd2(v4), which uses third-party plugins to construct a universal shell installed via Pip | [wazzge](https://gitee.com/wazzge) |
254
255
 
255
256
  Possibly defunct but still good examples
256
257
 
@@ -4,6 +4,7 @@
4
4
  .typos.toml
5
5
  AGENTS.md
6
6
  CHANGELOG.md
7
+ CLAUDE.md
7
8
  GEMINI.md
8
9
  LICENSE
9
10
  MANIFEST.in
@@ -199,6 +200,7 @@ tests/test_py_completion.py
199
200
  tests/test_rich_utils.py
200
201
  tests/test_run_pyscript.py
201
202
  tests/test_string_utils.py
203
+ tests/test_suite_environment.py
202
204
  tests/test_theme.py
203
205
  tests/test_utils.py
204
206
  tests/test_utils_defining_class.py
@@ -18,6 +18,7 @@
18
18
  ".typos.toml",
19
19
  "AGENTS.md",
20
20
  "CHANGELOG.md",
21
+ "CLAUDE.md",
21
22
  "GEMINI.md",
22
23
  "LICENSE",
23
24
  "MANIFEST.in",
@@ -222,6 +223,7 @@
222
223
  "tests/test_rich_utils.py",
223
224
  "tests/test_run_pyscript.py",
224
225
  "tests/test_string_utils.py",
226
+ "tests/test_suite_environment.py",
225
227
  "tests/test_theme.py",
226
228
  "tests/test_utils.py",
227
229
  "tests/test_utils_defining_class.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "4.2.4",
3
+ "distance": 0,
4
+ "node": "g3aa75c80675c0b0edc510aec302029a3fe62bf74",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-09-08"
8
+ }
@@ -64,7 +64,7 @@ test = [
64
64
  "pytest-cov>=5",
65
65
  "pytest-mock>=3.14.1",
66
66
  ]
67
- validate = ["mypy>=2.3.1", "ruff>=0.16.0", "ty>=0.0.75", "types-setuptools>=80.8.0"]
67
+ validate = ["ipython>=8.23", "mypy>=2.3.1", "ty>=0.0.75", "types-setuptools>=80.8.0"]
68
68
 
69
69
  [tool.mypy]
70
70
  disallow_incomplete_defs = true
@@ -80,6 +80,23 @@ def run_cmd(app: cmd2.Cmd, cmd: str) -> tuple[list[str], list[str]]:
80
80
  return normalize(out), normalize(err)
81
81
 
82
82
 
83
+ #: Environment variables that change how Rich and cmd2 render output. Left inherited,
84
+ #: they make unrelated tests fail depending on who runs the suite: NO_COLOR fails 15
85
+ #: tests, FORCE_COLOR and TTY_COMPATIBLE 53 each.
86
+ COLOR_ENVIRONMENT = ("NO_COLOR", "FORCE_COLOR", "TTY_COMPATIBLE", "TTY_INTERACTIVE")
87
+
88
+
89
+ @pytest.fixture(autouse=True)
90
+ def neutral_color_environment(monkeypatch: pytest.MonkeyPatch) -> None:
91
+ """Render output the same way regardless of the caller's environment.
92
+
93
+ Tests that exercise these variables set them explicitly, which still works because
94
+ a test's own monkeypatching runs after this fixture.
95
+ """
96
+ for name in COLOR_ENVIRONMENT:
97
+ monkeypatch.delenv(name, raising=False)
98
+
99
+
83
100
  @pytest.fixture
84
101
  def base_app() -> cmd2.Cmd:
85
102
  return cmd2.Cmd(include_py=True, include_ipy=True)
@@ -13,6 +13,7 @@ from typing import (
13
13
  from unittest import mock
14
14
 
15
15
  import pytest
16
+ from prompt_toolkit.application import create_app_session, get_app
16
17
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
17
18
  from prompt_toolkit.completion import DummyCompleter
18
19
  from prompt_toolkit.input import DummyInput, create_pipe_input
@@ -4444,6 +4445,40 @@ def test_enable_rprompt() -> None:
4444
4445
  assert custom_app.main_session.rprompt is None
4445
4446
 
4446
4447
 
4448
+ def test_rprompt_is_not_drawn_into_the_committed_line() -> None:
4449
+ """The right prompt belongs to the live prompt line, not to the scrollback.
4450
+
4451
+ prompt-toolkit renders the right prompt as a Float with no ``~is_done`` filter,
4452
+ unlike its bottom toolbar container, so by default it is part of the final frame
4453
+ that gets committed to the terminal. That left a copy of the right prompt beside
4454
+ every accepted command line in the scrollback.
4455
+ """
4456
+ drawn_while_done = []
4457
+
4458
+ class RPromptApp(cmd2.Cmd):
4459
+ def get_rprompt(self):
4460
+ drawn_while_done.append(get_app().is_done)
4461
+ return [("", "RPROMPT")]
4462
+
4463
+ output = DummyOutput()
4464
+ # Bind the ambient app session to this input and output. Without it, prompt-toolkit
4465
+ # builds a real one on demand for calls such as patch_stdout() in _read_raw_input(),
4466
+ # which needs a Windows console that CI does not provide.
4467
+ with create_pipe_input() as pipe_input, create_app_session(input=pipe_input, output=output):
4468
+ app = RPromptApp(allow_cli_args=False, enable_rprompt=True)
4469
+ app.main_session = PromptSession(
4470
+ input=pipe_input,
4471
+ output=output,
4472
+ rprompt=app.get_rprompt,
4473
+ )
4474
+ cmd2.cmd2.Cmd._hide_rprompt_when_done(app.main_session)
4475
+ pipe_input.send_text("hello\n")
4476
+ assert app._read_raw_input("(Cmd) ", app.main_session) == "hello"
4477
+
4478
+ assert drawn_while_done, "the right prompt was never rendered at all"
4479
+ assert True not in drawn_while_done, "the right prompt was drawn into the committed final frame"
4480
+
4481
+
4447
4482
  def test_get_bottom_toolbar(base_app: cmd2.Cmd) -> None:
4448
4483
  assert base_app.get_bottom_toolbar() is None
4449
4484
 
@@ -246,7 +246,7 @@ def test_run_pyscript_print_redirection(base_app, request, tmp_path, capsys) ->
246
246
  out, err = capsys.readouterr()
247
247
 
248
248
  # Verify the output file contains what we expect from print()
249
- content = pathlib.Path(out_file).read_text()
249
+ content = pathlib.Path(out_file).read_text(encoding="utf-8")
250
250
 
251
251
  # Look for everything written to self.stdout
252
252
  assert len(content.splitlines()) == 4
@@ -0,0 +1,59 @@
1
+ """Guards that the suite renders output independently of the developer's environment.
2
+
3
+ Rich and cmd2 both consult environment variables when deciding whether to emit styling.
4
+ Inheriting them makes large numbers of unrelated tests fail depending on who runs them,
5
+ which is expensive to diagnose because the failures look like product regressions.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+
11
+ import pytest
12
+
13
+ import cmd2
14
+
15
+ #: Variables that change how output is rendered. Rich reads all of these; cmd2 reads
16
+ #: NO_COLOR directly. Tests that exercise them set them explicitly instead.
17
+ COLOR_ENVIRONMENT = ("NO_COLOR", "FORCE_COLOR", "TTY_COMPATIBLE", "TTY_INTERACTIVE")
18
+
19
+
20
+ @pytest.mark.parametrize("name", COLOR_ENVIRONMENT)
21
+ def test_color_environment_does_not_leak_into_tests(name: str) -> None:
22
+ """A developer exporting any of these must not change the suite's results."""
23
+ assert name not in os.environ, (
24
+ f"{name} leaked into the test environment; output-rendering assertions would depend on who is running the suite"
25
+ )
26
+
27
+
28
+ class EncodingProbe(cmd2.Cmd):
29
+ """Reports the encoding of whatever stream output is currently going to."""
30
+
31
+ def do_show_encoding(self, _: str) -> None:
32
+ """Print the current output stream's encoding."""
33
+ self.poutput(f"ENCODING={getattr(self.stdout, 'encoding', None)}")
34
+
35
+
36
+ def test_redirection_to_a_file_uses_utf8(tmp_path) -> None:
37
+ """cmd2 renders non-ASCII, so a redirect target must not use the locale encoding.
38
+
39
+ Opened with the locale encoding, redirecting styled output raises UnicodeEncodeError
40
+ on any non-UTF-8 system -- which includes a default Windows console -- leaving the
41
+ user an empty file and an error.
42
+ """
43
+ app = EncodingProbe(allow_cli_args=False)
44
+ target = tmp_path / "out.txt"
45
+ app.onecmd_plus_hooks(f'show_encoding > "{target}"')
46
+ assert "ENCODING=utf-8" in target.read_text(encoding="utf-8")
47
+
48
+
49
+ #: A pass-through filter, run with this interpreter so the test does not depend on Unix
50
+ #: utilities being installed. `cmd.exe` has no `cat`, and this fix exists for Windows.
51
+ PASS_THROUGH = "import sys; sys.stdin.reconfigure(encoding='utf-8'); sys.stdout.write(sys.stdin.read())"
52
+
53
+
54
+ def test_piping_uses_utf8(tmp_path) -> None:
55
+ """The same applies to the pipe the subprocess reads from."""
56
+ app = EncodingProbe(allow_cli_args=False)
57
+ target = tmp_path / "piped.txt"
58
+ app.onecmd_plus_hooks(f'show_encoding | "{sys.executable}" -c "{PASS_THROUGH}" > "{target}"')
59
+ assert "ENCODING=utf-8" in target.read_text(encoding="utf-8")
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "4.2.3",
3
- "distance": 0,
4
- "node": "g0e90fd16f425411f2a5850b8b0373aafb1d1122a",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-09-02"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes