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.
- {cmd2-4.2.3 → cmd2-4.2.4}/.pre-commit-config.yaml +1 -1
- {cmd2-4.2.3 → cmd2-4.2.4}/CHANGELOG.md +13 -0
- cmd2-4.2.4/CLAUDE.md +123 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/PKG-INFO +2 -1
- {cmd2-4.2.3 → cmd2-4.2.4}/README.md +1 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/annotated.py +7 -4
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/cmd2.py +42 -5
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/types.py +7 -4
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/PKG-INFO +2 -1
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/SOURCES.txt +2 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/scm_file_list.json +2 -0
- cmd2-4.2.4/cmd2.egg-info/scm_version.json +8 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/pyproject.toml +1 -1
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/conftest.py +17 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_cmd2.py +35 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_run_pyscript.py +1 -1
- cmd2-4.2.4/tests/test_suite_environment.py +59 -0
- cmd2-4.2.3/cmd2.egg-info/scm_version.json +0 -8
- {cmd2-4.2.3 → cmd2-4.2.4}/.prettierignore +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/.prettierrc +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/.typos.toml +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/AGENTS.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/GEMINI.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/LICENSE +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/MANIFEST.in +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/Makefile +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/__init__.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/argparse_completer.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/argparse_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/clipboard.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/colors.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/command_set.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/completion.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/constants.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/decorators.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/exceptions.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/history.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/parsing.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/plugin.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/pt_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/py.typed +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/py_bridge.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/rich_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/string_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/styles.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/theme.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2/utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/dependency_links.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/requires.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/cmd2.egg-info/top_level.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/codecov.yml +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/annotated.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/argparse_completer.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/argparse_utils.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/clipboard.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/cmd.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/colors.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/command_set.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/completion.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/constants.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/decorators.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/exceptions.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/history.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/index.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/parsing.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/plugin.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/pt_utils.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/py_bridge.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/rich_utils.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/string_utils.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/styles.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/theme.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/api/utils.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/assets/cmd2-readme-demo.gif +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/assets/getting-started-demo.gif +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/doc_conventions.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/examples/alternate_event_loops.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/examples/examples.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/examples/getting_started.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/examples/index.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/annotated.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/argument_processing.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/async_commands.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/builtin_commands.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/clipboard.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/commands.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/completion.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/disable_commands.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/embedded_python_shells.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/generating_output.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/help.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/history.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/hooks.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/index.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/initialization.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/misc.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/modular_commands.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/multiline_commands.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/os.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/packaging.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/plugins.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/prompt.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/redirection.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/scripting.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/settings.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/shortcuts_aliases_macros.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/startup_commands.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/table_creation.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/features/theme.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/index.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/javascripts/readthedocs.js +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/incompatibilities.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/index.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/minimum.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/next_steps.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/migrating/why.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/mixins/index.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/mixins/mixin_template.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/overrides/main.html +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/alternatives.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/index.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/installation.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/integrating.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/overview/resources.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/stylesheets/cmd2.css +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/testing.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/docs/upgrades.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/.cmd2rc +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/README.md +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/annotated_example.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/argparse_completion.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/argparse_example.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/async_call.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/async_commands.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/async_printing.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/basic_completion.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/cmd2_history.dat +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/cmd_as_argument.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/color.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/command_sets.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/completion_item_choices.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/custom_parser.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/custom_types.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/default_categories.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/dynamic_commands.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/environment.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/event_loops.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/exit_code.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/getting_started.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/hello_cmd2.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/help_categories.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/hooks.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/migrating.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/mixin.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commands/__init__.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commands/commandset_basic.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commands/commandset_complex.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commands/commandset_custominit.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/modular_commandsets.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/paged_output.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/persistent_history.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/pretty_print.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/python_scripting.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/read_input.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/remove_builtin_commands.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/remove_settable.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/rich_tables.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/rich_theme.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/arg_printer.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/conditional.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/nested.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/quit.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/save_help_text.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/script.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/scripts/script.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/tmux_launch.sh +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/tmux_split.sh +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/examples/unicode_commands.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/mkdocs.yml +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/package.json +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/ruff.toml +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/scripts/validate_tag.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/setup.cfg +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/.cmd2rc +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/__init__.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/echo.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/environment.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/help.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/py_locals.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/pyscript_dir.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/raises_exception.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/recursive.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/self_in_py.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/stop.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/pyscript/test_print.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/relative_multiple.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/script.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/script.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/binary.bin +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/empty.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/help.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/nested.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/one_down.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/postcmds.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/precmds.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/scripts/utf8.txt +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_annotated.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_argparse.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_argparse_completer.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_argparse_subcommands.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_argparse_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_categories.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_commandset.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_completion.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_dynamic_complete_style.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_future_annotations.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_history.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_parsing.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_plugin.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_pt_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_py_completion.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_rich_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_string_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_theme.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_utils.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/tests/test_utils_defining_class.py +0 -0
- {cmd2-4.2.3 → cmd2-4.2.4}/ty.toml +0 -0
|
@@ -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
|
+
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[
|
|
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:
|
|
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[
|
|
2924
|
+
) -> "_AnnotatedCommand[CmdOrSetT_contra, _CommandParams, _CommandReturn_co]": ...
|
|
2924
2925
|
|
|
2925
2926
|
@overload
|
|
2926
|
-
def __get__(
|
|
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
|
-
|
|
3301
|
-
|
|
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
|
-
|
|
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[
|
|
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:
|
|
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[
|
|
104
|
+
def __get__(self, instance: None, owner: Any = ...) -> "UnboundCommandFunc[CmdOrSetT_contra, P]": ...
|
|
102
105
|
|
|
103
106
|
@overload
|
|
104
|
-
def __get__(self, instance:
|
|
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
|
+
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",
|
|
@@ -64,7 +64,7 @@ test = [
|
|
|
64
64
|
"pytest-cov>=5",
|
|
65
65
|
"pytest-mock>=3.14.1",
|
|
66
66
|
]
|
|
67
|
-
validate = ["
|
|
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")
|
|
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
|
|
File without changes
|