pax_global_header00006660000000000000000000000064151751405250014517gustar00rootroot0000000000000052 comment=06e718ea6c57938ecceef908c405fb376497ae34 rich-argparse-1.8.0/000077500000000000000000000000001517514052500142545ustar00rootroot00000000000000rich-argparse-1.8.0/.github/000077500000000000000000000000001517514052500156145ustar00rootroot00000000000000rich-argparse-1.8.0/.github/dependabot.yml000066400000000000000000000010011517514052500204340ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" rich-argparse-1.8.0/.github/workflows/000077500000000000000000000000001517514052500176515ustar00rootroot00000000000000rich-argparse-1.8.0/.github/workflows/nightly-tests.yml000066400000000000000000000017131517514052500232140ustar00rootroot00000000000000name: nightly-tests on: push: branches: ["main", '*nightly*'] paths-ignore: - ".vscode/**" - "scripts/**" - ".pre-commit-config.yaml" - "*.md" schedule: - cron: '0 8 * * *' workflow_dispatch: pull_request: types: - labeled - synchronize - opened - reopened jobs: build: runs-on: ubuntu-latest name: main if: contains( github.event.pull_request.labels.*.name, 'nightly') || github.event_name != 'pull_request' steps: - uses: actions/checkout@v6 - uses: deadsnakes/action@v3.2.0 with: python-version: "3.15-dev" - run: python --version --version && which python - name: Install uv uses: astral-sh/setup-uv@v7 - name: Install dependencies run: | uv venv --python $(which python) uv pip install . -r tests/requirements.txt - name: Run the test suite run: uv run pytest -vv --color=yes rich-argparse-1.8.0/.github/workflows/tests.yml000066400000000000000000000020551517514052500215400ustar00rootroot00000000000000name: tests on: pull_request: paths-ignore: - ".vscode/**" - "scripts/**" - ".pre-commit-config.yaml" - "*.md" push: branches: [main] paths-ignore: - ".vscode/**" - "scripts/**" - ".pre-commit-config.yaml" - "*.md" jobs: build: strategy: matrix: os: [ubuntu-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.11"] include: - os: windows-latest python-version: "3.13" - os: macos-latest python-version: "3.13" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Set up uv with Python ${{ matrix.python-version }} uses: astral-sh/setup-uv@v7 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | uv venv --python ${{ matrix.python-version }} uv pip install . -r tests/requirements.txt - name: Run the test suite run: uv run pytest -vv --color=yes --cov rich-argparse-1.8.0/.gitignore000066400000000000000000000000561517514052500162450ustar00rootroot00000000000000*.egg-info *.pyc *.swp /.coverage /.tox dist/ rich-argparse-1.8.0/.pre-commit-config.yaml000066400000000000000000000021211517514052500205310ustar00rootroot00000000000000ci: autoupdate_schedule: "quarterly" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-toml - id: check-yaml - id: end-of-file-fixer - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.10 hooks: - id: ruff-check args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.20.1 hooks: - id: mypy additional_dependencies: ["rich", "types-colorama", "django-stubs"] pass_filenames: false args: ["rich_argparse"] - repo: local hooks: - id: bad-gh-link name: bad-gh-link description: Detect PR/Issue GitHub links text that don't match their URL in CHANGELOG.md language: pygrep entry: '(?i)\[(?:PR|GH)-(\d+)\]\(https://github.com/hamdanal/rich-argparse/(?:pull|issues)/(?!\1/?\))\d+/?\)' files: CHANGELOG.md rich-argparse-1.8.0/.vscode/000077500000000000000000000000001517514052500156155ustar00rootroot00000000000000rich-argparse-1.8.0/.vscode/cspell.json000066400000000000000000000020151517514052500177700ustar00rootroot00000000000000{ // Version of the setting file. Always 0.2 "version": "0.2", // language - current active spelling language "language": "en", // words - list of words to be always considered correct "words": [ "capsys", "devenv", "htmlcov", "isort", "kwargs", "kwds", "mbar", "mdescription", "mepilog", "mfile", "mfoo", "mmiddle", "moom", "moptional", "mparser", "mremainder", "mrequired", "mshow", "msuppress", "msyntax", "mypy", "mzom", "positionals", "pypi", "pyproject", "pytest", "pyupgrade", "sdist", "venv", "virtualenv", "yesqa" ], // flagWords - list of words to be always considered incorrect // This is useful for offensive words and common spelling errors. // For example "hte" should be "the" "flagWords": [ "hte", "tge" ] } rich-argparse-1.8.0/.vscode/extensions.json000066400000000000000000000001361517514052500207070ustar00rootroot00000000000000{ "recommendations": [ "charliermarsh.ruff", "ms-python.python", ], } rich-argparse-1.8.0/.vscode/launch.json000066400000000000000000000011341517514052500177610ustar00rootroot00000000000000{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Python: Debug Tests", "type": "debugpy", "request": "launch", "program": "${file}", "purpose": [ "debug-test", "debug-in-terminal", ], "console": "integratedTerminal", "justMyCode": false } ] } rich-argparse-1.8.0/.vscode/settings.json000066400000000000000000000006171517514052500203540ustar00rootroot00000000000000{ "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, "[python]": { "editor.tabSize": 4, "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports.ruff": "explicit", }, }, "[yaml][toml]": { "editor.tabSize": 2 } } rich-argparse-1.8.0/CHANGELOG.md000066400000000000000000000317071517514052500160750ustar00rootroot00000000000000# Change Log ## Unreleased ## 1.8.0 - 2026-05-01 ### Features - [GH-155](https://github.com/hamdanal/rich-argparse/issues/155), [PR-160](https://github.com/hamdanal/rich-argparse/pull/160) Python 3.8 is no longer supported (EOL since 7/10/2024) - [GH-164](https://github.com/hamdanal/rich-argparse/issues/164), [PR-165](https://github.com/hamdanal/rich-argparse/pull/165) Add support for PyPy 3.11 - [GH-180](https://github.com/hamdanal/rich-argparse/issues/180), [PR-182](https://github.com/hamdanal/rich-argparse/pull/182) Add `ExtendedParagraphRichHelpFormatter` to `rich_argparse.contrib`. This is similar to `ParagraphRichHelpFormatter` but with better support for paragraph spacing. ## 1.7.2 - 2025-11-01 ### Fixes - [PR-171](https://github.com/hamdanal/rich-argparse/pull/171) Fix colors overlapping with Python 3.14.0+ which enabled colors by default in the help formatter. ## 1.7.1 - 2025-05-25 ### Fixes - [PR-162](https://github.com/hamdanal/rich-argparse/pull/162) Fix TypeError on nightly builds (Python 3.14.0a7+) due to new `HelpFormatter` arguments. The `console` parameter is now keyword-only. ## 1.7.0 - 2025-02-08 ### Features - [PR-149](https://github.com/hamdanal/rich-argparse/pull/149) Add support for django commands in the new `rich_argparse.django` module. Read more at https://github.com/hamdanal/rich-argparse#django-support - [GH-140](https://github.com/hamdanal/rich-argparse/issues/140), [PR-147](https://github.com/hamdanal/rich-argparse/pull/147) Add `ParagraphRichHelpFormatter`, a formatter that preserves paragraph breaks, in the new `rich_argparse.contrib` module. Read more at https://github.com/hamdanal/rich-argparse#additional-formatters ### Fixes - [GH-152](https://github.com/hamdanal/rich-argparse/issues/152), [PR-153](https://github.com/hamdanal/rich-argparse/pull/153), [PR-154](https://github.com/hamdanal/rich-argparse/pull/154) Fix `ValueError` when using `%(default)s` inside square brackets and `help_markup` is enabled. - [GH-141](https://github.com/hamdanal/rich-argparse/issues/141), [PR-142](https://github.com/hamdanal/rich-argparse/pull/142) Do not highlight --options inside backticks. ## 1.6.0 - 2024-11-02 ### Fixes - [GH-133](https://github.com/hamdanal/rich-argparse/issues/133), [PR-135](https://github.com/hamdanal/rich-argparse/pull/135) Fix help preview generation with newer releases of rich. - [GH-130](https://github.com/hamdanal/rich-argparse/issues/130), [PR-131](https://github.com/hamdanal/rich-argparse/pull/131) Fix a bug that caused long group titles to wrap. - [GH-125](https://github.com/hamdanal/rich-argparse/issues/125), [GH-127](https://github.com/hamdanal/rich-argparse/pull/127), [PR-128](https://github.com/hamdanal/rich-argparse/pull/128) Redesign metavar styling to fix broken colors of usage when some metavars are wrapped to multiple lines. The brackets and spaces of metavars are no longer colored. ## 1.5.2 - 2024-06-15 ### Fixes - [PR-124](https://github.com/hamdanal/rich-argparse/pull/124) Fix a regression in `%(default)s` style that was introduced in #123. ## 1.5.1 - 2024-06-06 ### Fixes - [GH-121](https://github.com/hamdanal/rich-argparse/issues/121), [PR-123](https://github.com/hamdanal/rich-argparse/pull/123) Fix `%(default)s` style when help markup is deactivated. ## 1.5.0 - 2024-06-01 ### Features - [PR-103](https://github.com/hamdanal/rich-argparse/pull/103) Python 3.13 is now officially supported - [GH-95](https://github.com/hamdanal/rich-argparse/issues/95), [PR-103](https://github.com/hamdanal/rich-argparse/pull/103) Python 3.7 is no longer supported (EOL since 27/6/2023) - [GH-120](https://github.com/hamdanal/rich-argparse/issues/120), [GH-121](https://github.com/hamdanal/rich-argparse/issues/121), [PR-122](https://github.com/hamdanal/rich-argparse/pull/122) Add options `help_markup` and `text_markup` to disable console markup in the help text and the description text respectively. ### Fixes - [GH-115](https://github.com/hamdanal/rich-argparse/issues/115), [PR-116](https://github.com/hamdanal/rich-argparse/pull/116) Do not print group names suppressed with `argparse.SUPPRESS` ## 1.4.0 - 2023-10-21 ### Features - [PR-90](https://github.com/hamdanal/rich-argparse/pull/90) Make `RichHelpFormatter` itself a rich renderable with rich console. - [GH-91](https://github.com/hamdanal/rich-argparse/issues/91), [PR-92](https://github.com/hamdanal/rich-argparse/pull/92) Allow passing custom console to `RichHelpFormatter`. - [GH-91](https://github.com/hamdanal/rich-argparse/issues/91), [PR-93](https://github.com/hamdanal/rich-argparse/pull/93) Add `HelpPreviewAction` to generate a preview of the help output in SVG, HTML, or TXT formats. - [PR-97](https://github.com/hamdanal/rich-argparse/pull/97) Avoid importing `typing` to improve startup time by about 35%. - [GH-84](https://github.com/hamdanal/rich-argparse/issues/84), [PR-98](https://github.com/hamdanal/rich-argparse/pull/98) Add a style for default values when using `%(default)s` in the help text. - [PR-99](https://github.com/hamdanal/rich-argparse/pull/99) Allow arbitrary renderables in the descriptions and epilog. ### Fixes - [GH-100](https://github.com/hamdanal/rich-argparse/issues/100), [PR-101](https://github.com/hamdanal/rich-argparse/pull/101) Fix color of brackets surrounding positional arguments in the usage. ## 1.3.0 - 2023-08-19 ### Features - [PR-87](https://github.com/hamdanal/rich-argparse/pull/87) Add `optparse.GENERATE_USAGE` to auto generate a usage similar to argparse. - [PR-87](https://github.com/hamdanal/rich-argparse/pull/87) Add `rich_format_*` methods to optparse formatters. These return a `rich.text.Text` object. ### Fixes - [GH-79](https://github.com/hamdanal/rich-argparse/issues/79), [PR-80](https://github.com/hamdanal/rich-argparse/pull/80), [PR-85](https://github.com/hamdanal/rich-argparse/pull/85) Fix ansi escape codes on legacy Windows console ## 1.2.0 - 2023-07-02 ### Features - [PR-73](https://github.com/hamdanal/rich-argparse/pull/73) Add experimental support for `optparse`. Import optparse formatters from `rich_argparse.optparse`. ### Changes - [PR-72](https://github.com/hamdanal/rich-argparse/pull/72) The project now uses `ruff` for linting and import sorting. - [PR-71](https://github.com/hamdanal/rich-argparse/pull/71) `rich_argparse` is now a package instead of a module. This should not affect users. ### Fixes - [PR-74](https://github.com/hamdanal/rich-argparse/pull/74) Fix crash when a metavar following a long option contains control codes. ## 1.1.1 - 2023-05-30 ### Fixes - [GH-67](https://github.com/hamdanal/rich-argparse/issues/67), [PR-69](https://github.com/hamdanal/rich-argparse/pull/69) Fix `%` not being escaped properly - [PR-68](https://github.com/hamdanal/rich-argparse/pull/68) Restore lazy loading of `rich`. Delay its import until it is needed. ## 1.1.0 - 2023-03-11 ### Features - [GH-55](https://github.com/hamdanal/rich-argparse/issues/55), [PR-56](https://github.com/hamdanal/rich-argparse/pull/56) Add a new style for `%(prog)s` in the usage. The style is applied in argparse-generated usage and in user defined usage whether the user usage is plain text or rich markup. ## 1.0.0 - 2023-01-07 ### Fixes - [GH-49](https://github.com/hamdanal/rich-argparse/issues/49), [PR-50](https://github.com/hamdanal/rich-argparse/pull/50) `RichHelpFormatter` now respects format conversion types in help strings ## 0.7.0 - 2022-12-31 ### Features - [GH-47](https://github.com/hamdanal/rich-argparse/issues/47), [PR-48](https://github.com/hamdanal/rich-argparse/pull/48) The default `group_name_formatter` has changed from `str.upper` to `str.title`. This renders better with long group names and follows the convention of popular CLI tools and programs. Please note that if you test the output of your CLI **verbatim** and rely on the default behavior of rich_argparse, you will have to either set the formatter explicitly or update the tests. ## 0.6.0 - 2022-12-18 ### Features - [PR-43](https://github.com/hamdanal/rich-argparse/pull/43) Support type checking for users. Bundle type information in the wheel and sdist. ### Fixes - [PR-43](https://github.com/hamdanal/rich-argparse/pull/43) Fix annotations of class variables previously typed as instance variables. ## 0.5.0 - 2022-11-05 ### Features - [PR-38](https://github.com/hamdanal/rich-argparse/pull/38) Support console markup in **custom** `usage` messages. Note that this feature is not activated by default. To enable it, set `RichHelpFormatter.usage_markup = True`. ### Fixes - [PR-35](https://github.com/hamdanal/rich-argparse/pull/35) Use `soft_wrap` in `console.print` instead of a large fixed console width for wrapping - [GH-36](https://github.com/hamdanal/rich-argparse/issues/36), [PR-37](https://github.com/hamdanal/rich-argparse/pull/37) Fix a regression in highlight regexes that caused the formatter to crash when using the same style multiple times. ## 0.4.0 - 2022-10-15 ### Features - [PR-31](https://github.com/hamdanal/rich-argparse/pull/31) Add support for all help formatters of argparse. Now there are five formatter classes defined in `rich_argparse`: ``` RichHelpFormatter: the equivalent of argparse.HelpFormatter RawDescriptionRichHelpFormatter: the equivalent of argparse.RawDescriptionHelpFormatter RawTextRichHelpFormatter: the equivalent of argparse.RawTextHelpFormatter ArgumentDefaultsRichHelpFormatter: the equivalent of argparse.ArgumentDefaultsHelpFormatter MetavarTypeRichHelpFormatter: the equivalent of argparse.MetavarTypeHelpFormatter ``` Note that this changes the default behavior of `RichHelpFormatter` to no longer respect line breaks in the description and help text. It now behaves similarly to the original `HelpFormatter`. You have now to use the appropriate subclass for this to happen. ## 0.3.1 - 2022-10-08 ### Fixes - [GH-28](https://github.com/hamdanal/rich-argparse/issues/28), [PR-30](https://github.com/hamdanal/rich-argparse/pull/30) Fix required options not coloured in the usage ## 0.3.0 - 2022-10-01 ### Features - [GH-16](https://github.com/hamdanal/rich-argparse/issues/16), [PR-17](https://github.com/hamdanal/rich-argparse/pull/17) A new custom usage lexer that is consistent with the formatter styles ### Fixes - [GH-16](https://github.com/hamdanal/rich-argparse/issues/16), [PR-17](https://github.com/hamdanal/rich-argparse/pull/17) Fix inconsistent coloring of args in the top usage panel - [GH-12](https://github.com/hamdanal/rich-argparse/issues/12), [PR-20](https://github.com/hamdanal/rich-argparse/pull/20) Fix incorrect line breaks that put metavars on a alone on a new line - [GH-19](https://github.com/hamdanal/rich-argparse/issues/19), [PR-21](https://github.com/hamdanal/rich-argparse/pull/21) Do not print help output, return it instead ### Changes - [PR-17](https://github.com/hamdanal/rich-argparse/pull/17) The default styles have been changed to be more in line with the new usage coloring - [PR-20](https://github.com/hamdanal/rich-argparse/pull/20) The default `max_help_position` is now set to 24 (the default used in argparse) as line breaks are no longer an issue ### Removed - [PR-20](https://github.com/hamdanal/rich-argparse/pull/20) The `RichHelpFormatter.renderables` property has been removed, it was never documented ### Tests - [PR-22](https://github.com/hamdanal/rich-argparse/pull/22) Run windows tests in CI ## 0.2.1 - 2022-09-25 ### Fixes - [GH-13](https://github.com/hamdanal/rich-argparse/issues/13), [PR-14](https://github.com/hamdanal/rich-argparse/pull/14) Fix compatibility with `argparse.ArgumentDefaultsHelpFormatter` ## 0.2.0 - 2022-09-17 ### Features - [GH-4](https://github.com/hamdanal/rich-argparse/issues/4), [PR-9](https://github.com/hamdanal/rich-argparse/pull/9) Metavars now have their own style `argparse.metavar` which defaults to `'bold cyan'` ### Fixes - [GH-4](https://github.com/hamdanal/rich-argparse/issues/4), [PR-10](https://github.com/hamdanal/rich-argparse/pull/10) Add missing ":" after the group name similar to the default HelpFormatter - [PR-11](https://github.com/hamdanal/rich-argparse/pull/11) Fix padding of long options or metavars - [PR-11](https://github.com/hamdanal/rich-argparse/pull/11) Fix overflow of text in help that was truncated - [PR-11](https://github.com/hamdanal/rich-argparse/pull/11) Escape parameters that get substituted with % such as %(prog)s and %(default)s - [PR-11](https://github.com/hamdanal/rich-argparse/pull/11) Fix flaky wrapping of long lines ## 0.1.1 - 2022-09-10 ### Fixes - [GH-5](https://github.com/hamdanal/rich-argparse/issues/5), [PR-6](https://github.com/hamdanal/rich-argparse/pull/6) Fix `RichHelpFormatter` does not replace `%(prog)s` in text - [GH-7](https://github.com/hamdanal/rich-argparse/issues/7), [PR-8](https://github.com/hamdanal/rich-argparse/pull/8) Fix extra newline at the end ## 0.1.0 - 2022-09-03 Initial release ### Features - First upload to PyPI, `pip install rich-argparse` now supported rich-argparse-1.8.0/CONTRIBUTING.md000066400000000000000000000045651517514052500165170ustar00rootroot00000000000000# Contributing to rich-argparse The best way to contribute to this project is by opening an issue in the issue tracker. Issues for reporting bugs or requesting new features are all welcome and appreciated. Also, [Discussions] are open for any discussion related to this project. There you can ask questions, discuss ideas, or simply show your clever snippets or hacks. Code contributions are also welcome in the form of Pull Requests. For these you need to open an issue prior to starting work to discuss it first (with the exception of very clear bug fixes and typo fixes where an issue may not be needed). ## Getting started *python* version 3.9 or higher is required for development. 1. Fork the repository on GitHub. 2. Clone the repository: ```sh git clone git@github.com:/rich-argparse.git rich-argparse cd rich-argparse ``` 3. Create and activate a virtual environment: Linux and macOS: ```sh python3 -m venv .venv . .venv/bin/activate ``` Windows: ```sh py -m venv .venv .venv\Scripts\activate ``` 4. Install the project and its dependencies: ```sh python -m pip install -r requirements-dev.txt ``` ## Testing Running all the tests can be done with `pytest --cov`. This also runs the test coverage to ensure 100% of the code is covered by tests. You can also run individual tests with `pytest -k the_name_of_your_test`. The helper script `scripts/run-tests` runs the tests with coverage on all supported python versions. ### Code quality After staging your work with `git add`, you can run `pre-commit run --all-files` to run all the code quality tools. These include [ruff] for formatting and linting, and [mypy] for type checking. You can also run each tool individually with `pre-commit run --all-files`. ## Creating a Pull Request Once you are happy with your change you can create a pull request. GitHub offers a guide on how to do this [here][PR]. Please ensure that you include a good description of what your change does in your pull request, and link it to any relevant issues or discussions. [Discussions]: https://github.com/hamdanal/rich-argparse/discussions [mypy]: https://mypy.readthedocs.io/en/stable/ [ruff]: https://docs.astral.sh/ruff/ [PR]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork rich-argparse-1.8.0/LICENSE000066400000000000000000000020531517514052500152610ustar00rootroot00000000000000MIT License Copyright (c) 2022 Ali Hamdan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rich-argparse-1.8.0/README.md000066400000000000000000000330451517514052500155400ustar00rootroot00000000000000# rich-argparse ![python -m rich_argparse]( https://github.com/hamdanal/rich-argparse/assets/93259987/5eb719ce-9865-4654-a5c6-04950a86d40d) [![tests](https://github.com/hamdanal/rich-argparse/actions/workflows/tests.yml/badge.svg) ](https://github.com/hamdanal/rich-argparse/actions/workflows/tests.yml) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/hamdanal/rich-argparse/main.svg) ](https://results.pre-commit.ci/latest/github/hamdanal/rich-argparse/main) [![Downloads](https://img.shields.io/pypi/dm/rich-argparse)](https://pypistats.org/packages/rich-argparse) [![Python Version](https://img.shields.io/pypi/pyversions/rich-argparse) ![Release](https://img.shields.io/pypi/v/rich-argparse) ](https://pypi.org/project/rich-argparse/) Format argparse and optparse help using [rich](https://pypi.org/project/rich). *rich-argparse* improves the look and readability of argparse's help while requiring minimal changes to the code. ## Table of contents * [Installation](#installation) * [Usage](#usage) * [Output styles](#output-styles) * [Customizing colors](#customize-the-colors) * [Group name formatting](#customize-the-group-name-format) * [Special text highlighting](#special-text-highlighting) * [Customizing `usage`](#colors-in-the-usage) * [Console markup](#disable-console-markup) * [Colors in `--version`](#colors-in---version) * [Rich renderables](#rich-descriptions-and-epilog) * [Working with subparsers](#working-with-subparsers) * [Documenting your CLI](#generate-help-preview) * [Additional formatters](#additional-formatters) * [Django support](#django-support) * [Optparse support](#optparse-support) (experimental) * [Legacy Windows](#legacy-windows-support) ## Installation Install from PyPI with pip or your favorite tool. ```sh pip install rich-argparse ``` ## Usage Simply pass `formatter_class` to the argument parser ```python import argparse from rich_argparse import RichHelpFormatter parser = argparse.ArgumentParser(..., formatter_class=RichHelpFormatter) ... ``` *rich-argparse* defines equivalents to all [argparse's standard formatters]( https://docs.python.org/3/library/argparse.html#formatter-class): | `rich_argparse` formatter | equivalent in `argparse` | |-------------------------------------|---------------------------------| | `RichHelpFormatter` | `HelpFormatter` | | `RawDescriptionRichHelpFormatter` | `RawDescriptionHelpFormatter` | | `RawTextRichHelpFormatter` | `RawTextHelpFormatter` | | `ArgumentDefaultsRichHelpFormatter` | `ArgumentDefaultsHelpFormatter` | | `MetavarTypeRichHelpFormatter` | `MetavarTypeHelpFormatter` | Additional formatters are available in the `rich_argparse.contrib` [module](#additional-formatters). ## Output styles The default styles used by *rich-argparse* are carefully chosen to work in different light and dark themes. ### Customize the colors You can customize the colors of the output by modifying the `styles` dictionary on the formatter class. You can use any rich style as defined [here](https://rich.readthedocs.io/en/latest/style.html). *rich-argparse* defines and uses the following styles: ```python { 'argparse.args': 'cyan', # for positional-arguments and --options (e.g "--help") 'argparse.groups': 'dark_orange', # for group names (e.g. "positional arguments") 'argparse.help': 'default', # for argument's help text (e.g. "show this help message and exit") 'argparse.metavar': 'dark_cyan', # for metavariables (e.g. "FILE" in "--file FILE") 'argparse.prog': 'grey50', # for %(prog)s in the usage (e.g. "foo" in "Usage: foo [options]") 'argparse.syntax': 'bold', # for highlights of back-tick quoted text (e.g. "`some text`") 'argparse.text': 'default', # for descriptions, epilog, and --version (e.g. "A program to foo") 'argparse.default': 'italic', # for %(default)s in the help (e.g. "Value" in "(default: Value)") } ``` For example, to make the description and epilog *italic*, change the `argparse.text` style: ```python RichHelpFormatter.styles["argparse.text"] = "italic" ``` ### Customize the group name format You can change how the names of the groups (like `'positional arguments'` and `'options'`) are formatted by setting the `RichHelpFormatter.group_name_formatter` which is set to `str.title` by default. Any callable that takes the group name as an input and returns a str works: ```python RichHelpFormatter.group_name_formatter = str.upper # Make group names UPPERCASE ``` ### Special text highlighting You can [highlight patterns](https://rich.readthedocs.io/en/stable/highlighting.html) in the arguments help and the description and epilog using regular expressions. By default, *rich-argparse* highlights patterns of `--options-with-hyphens` using the `argparse.args` style and patterns of `` `back tick quoted text` `` using the `argparse.syntax` style. You can control what patterns are highlighted by modifying the `RichHelpFormatter.highlights` list. To disable all highlights, you can clear this list using `RichHelpFormatter.highlights.clear()`. You can also add custom highlight patterns and styles. The following example highlights all occurrences of `pyproject.toml` in green: ```python # Add a style called `pyproject` which applies a green style (any rich style works) RichHelpFormatter.styles["argparse.pyproject"] = "green" # Add the highlight regex (the regex group name must match an existing style name) RichHelpFormatter.highlights.append(r"\b(?Ppyproject\.toml)\b") # Pass the formatter class to argparse parser = argparse.ArgumentParser(..., formatter_class=RichHelpFormatter) ... ``` ### Colors in the `usage` The usage **generated by the formatter** is colored using the `argparse.args` and `argparse.metavar` styles. If you use a custom `usage` message in the parser, it will be treated as "plain text" and will **not** be colored by default. You can enable colors in user defined usage message through [console markup](https://rich.readthedocs.io/en/stable/markup.html) by setting `RichHelpFormatter.usage_markup = True`. If you enable this option, make sure to [escape]( https://rich.readthedocs.io/en/stable/markup.html#escaping) any square brackets in the usage text. ### Disable console markup The text of the descriptions and epilog is interpreted as [console markup](https://rich.readthedocs.io/en/stable/markup.html) by default. If this conflicts with your usage of square brackets, make sure to [escape]( https://rich.readthedocs.io/en/stable/markup.html#escaping) the square brackets or to disable markup globally with `RichHelpFormatter.text_markup = False`. Similarly the help text of arguments is interpreted as markup by default. It can be disabled using `RichHelpFormatter.help_markup = False`. ### Colors in `--version` If you use the `"version"` action from argparse, you can use console markup in the `version` string: ```python parser.add_argument( "--version", action="version", version="[argparse.prog]%(prog)s[/] version [i]1.0.0[/]" ) ``` Note that the `argparse.text` style is applied to the `version` string similar to the description and epilog. ### Rich descriptions and epilog You can use any rich renderable in the descriptions and epilog. This includes all built-in rich renderables like `Table` and `Markdown` and any custom renderables defined using the [Console Protocol](https://rich.readthedocs.io/en/stable/protocol.html#console-protocol). ```python import argparse from rich.markdown import Markdown from rich_argparse import RichHelpFormatter description = """ # My program This is a markdown description of my program. * It has a list * And a table | Column 1 | Column 2 | | -------- | -------- | | Value 1 | Value 2 | """ parser = argparse.ArgumentParser( description=Markdown(description, style="argparse.text"), formatter_class=RichHelpFormatter, ) ... ``` Certain features are **disabled** for arbitrary renderables other than strings, including: * Syntax highlighting with `RichHelpFormatter.highlights` * Styling with the `"argparse.text"` style defined in `RichHelpFormatter.styles` * Replacement of `%(prog)s` with the program name ## Working with subparsers Subparsers do not inherit the formatter class from the parent parser by default. You have to pass the formatter class explicitly: ```python subparsers = parser.add_subparsers(...) p1 = subparsers.add_parser(..., formatter_class=parser.formatter_class) p2 = subparsers.add_parser(..., formatter_class=parser.formatter_class) ``` ## Generate help preview You can generate a preview of the help message for your CLI in SVG, HTML, or TXT formats using the `HelpPreviewAction` action. This is useful for including the help message in the documentation of your app. The action uses the [rich exporting API](https://rich.readthedocs.io/en/stable/console.html#exporting) internally. ```python import argparse from rich.terminal_theme import DIMMED_MONOKAI from rich_argparse import HelpPreviewAction, RichHelpFormatter parser = argparse.ArgumentParser(..., formatter_class=RichHelpFormatter) ... parser.add_argument( "--generate-help-preview", action=HelpPreviewAction, path="help-preview.svg", # (optional) or "help-preview.html" or "help-preview.txt" export_kwds={"theme": DIMMED_MONOKAI}, # (optional) keywords passed to console.save_... methods ) ``` This action is hidden, it won't show up in the help message or in the parsed arguments namespace. Use it like this: ```sh python my_cli.py --generate-help-preview # generates help-preview.svg (default path specified above) # or python my_cli.py --generate-help-preview my-help.svg # generates my-help.svg # or COLUMNS=120 python my_cli.py --generate-help-preview # force the width of the output to 120 columns ``` ## Additional formatters *rich-argparse* defines additional non-standard argparse formatters for some common use cases in the `rich_argparse.contrib` module. They can be imported with the `from rich_argparse.contrib import` syntax. The following formatters are available: * `ParagraphRichHelpFormatter`: A formatter similar to `RichHelpFormatter` that preserves paragraph breaks. A paragraph break is defined as two consecutive newlines (`\n\n`) in the help or description text. Leading and trailing trailing whitespace are stripped similar to `RichHelpFormatter`. * `ExtendedParagraphRichHelpFormatter`: A formatter that preserves paragraph breaks similar to `ParagraphRichHelpFormatter` but with control over paragraph spacing. A paragraph break with spacing is defined as three consecutive newlines (`\n\n\n`) and a paragraph break without spacing is defined as two consecutive newlines (`\n\n`). _Your use case is not covered by the existing formatters? Please open an issue on GitHub!_ ## Django support *rich-argparse* provides support for django's custom help formatter. You can instruct django to use *rich-argparse* with all built-in, extension libraries, and user defined commands in a django project by adding these two lines to the `manage.py` file: ```diff diff --git a/manage.py b/manage.py def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'my_project.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc + from rich_argparse.django import richify_command_line_help + richify_command_line_help() execute_from_command_line(sys.argv) ``` Alternatively, you can use the `DjangoRichHelpFormatter` class directly in your commands: ```diff diff --git a/my_app/management/commands/my_command.py b/my_app/management/commands/my_command.py from django.core.management.base import BaseCommand +from rich_argparse.django import DjangoRichHelpFormatter class Command(BaseCommand): def add_arguments(self, parser): + parser.formatter_class = DjangoRichHelpFormatter parser.add_argument("--option", action="store_true", help="An option") ... ``` ## Optparse support *rich-argparse* now ships with experimental support for [optparse]( https://docs.python.org/3/library/optparse.html). Import optparse help formatters from `rich_argparse.optparse`: ```python import optparse from rich_argparse.optparse import IndentedRichHelpFormatter # or TitledRichHelpFormatter parser = optparse.OptionParser(formatter=IndentedRichHelpFormatter()) ... ``` You can also generate a more helpful usage message by passing `usage=GENERATE_USAGE` to the parser. This is similar to the default behavior of `argparse`. ```python from rich_argparse.optparse import GENERATE_USAGE, IndentedRichHelpFormatter parser = optparse.OptionParser(usage=GENERATE_USAGE, formatter=IndentedRichHelpFormatter()) ``` Similar to `argparse`, you can customize the styles used by the formatter by modifying the `RichHelpFormatter.styles` dictionary. These are the same styles used by `argparse` but with the `optparse.` prefix instead: ```python RichHelpFormatter.styles["optparse.metavar"] = "bold magenta" ``` Syntax highlighting works the same as with `argparse`. Colors in the `usage` are only supported when using `GENERATE_USAGE`. ## Legacy Windows support When used on legacy Windows versions like *Windows 7*, colors are disabled unless [colorama](https://pypi.org/project/colorama/) is used: ```python import argparse import colorama from rich_argparse import RichHelpFormatter colorama.init() parser = argparse.ArgumentParser(..., formatter_class=RichHelpFormatter) ... ``` rich-argparse-1.8.0/pyproject.toml000066400000000000000000000041551517514052500171750ustar00rootroot00000000000000[build-system] requires = ["hatchling>=1.11.0"] build-backend = "hatchling.build" [project] name = "rich-argparse" version = "1.8.0" description = "Rich help formatters for argparse and optparse" authors = [ {name="Ali Hamdan", email="ali.hamdan.dev@gmail.com"}, ] readme = "README.md" license = "MIT" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Software Development :: User Interfaces", ] keywords = ["argparse", "rich", "help-formatter", "optparse"] dependencies = [ "rich >= 11.0.0", ] requires-python = ">=3.9" [project.urls] Homepage = "https://github.com/hamdanal/rich-argparse" Documentation = "https://github.com/hamdanal/rich-argparse#rich-argparse" Issue-Tracker = "https://github.com/hamdanal/rich-argparse/issues" Changelog = "https://github.com/hamdanal/rich-argparse/blob/main/CHANGELOG.md" [tool.hatch.build.targets.sdist] include = [ "CHANGELOG.md", "CONTRIBUTING.md", "requirements-dev.txt", "rich_argparse", "tests", "LICENSE", "README.md", "pyproject.toml", ] [tool.hatch.build.targets.wheel] packages = ["rich_argparse"] [tool.ruff] line-length = 100 [tool.ruff.lint] extend-select = ["C4", "B", "UP", "RUF100", "TID", "T10"] extend-ignore = ["E501"] unfixable = ["B"] isort.required-imports = ["from __future__ import annotations"] isort.extra-standard-library = ["typing_extensions"] flake8-tidy-imports.ban-relative-imports = "all" [tool.mypy] python_version = "3.9" strict = true local_partial_types = true allow_redefinition_new = true [[tool.mypy.overrides]] module = ["tests.*"] check_untyped_defs = false disallow_untyped_defs = false disallow_incomplete_defs = false [tool.pytest.ini_options] testpaths = ["tests"] [tool.coverage.run] plugins = ["covdefaults"] source = ["rich_argparse", "tests"] rich-argparse-1.8.0/requirements-dev.txt000066400000000000000000000000551517514052500203140ustar00rootroot00000000000000-e . -r tests/requirements.txt pre-commit uv rich-argparse-1.8.0/rich_argparse/000077500000000000000000000000001517514052500170655ustar00rootroot00000000000000rich-argparse-1.8.0/rich_argparse/__init__.py000066400000000000000000000011271517514052500211770ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan from __future__ import annotations from rich_argparse._argparse import ( ArgumentDefaultsRichHelpFormatter, HelpPreviewAction, MetavarTypeRichHelpFormatter, RawDescriptionRichHelpFormatter, RawTextRichHelpFormatter, RichHelpFormatter, ) __all__ = [ "RichHelpFormatter", "RawDescriptionRichHelpFormatter", "RawTextRichHelpFormatter", "ArgumentDefaultsRichHelpFormatter", "MetavarTypeRichHelpFormatter", "HelpPreviewAction", ] rich-argparse-1.8.0/rich_argparse/__main__.py000066400000000000000000000065421517514052500211660ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan from __future__ import annotations if __name__ == "__main__": import argparse import sys from rich.terminal_theme import DIMMED_MONOKAI from rich_argparse import HelpPreviewAction, RichHelpFormatter parser = argparse.ArgumentParser( prog="python -m rich_argparse", formatter_class=RichHelpFormatter, description=( "This is a [link https://pypi.org/project/rich]rich[/]-based formatter for " "[link https://docs.python.org/3/library/argparse.html#formatter-class]" "argparse's help output[/].\n\n" "It enables you to use the powers of rich like markup and highlights in your CLI help. " ), epilog=":link: Read more at https://github.com/hamdanal/rich-argparse#usage.", ) parser.add_argument( "formatter-class", help=( "Simply pass `formatter_class=RichHelpFormatter` to the argument parser to get a " "colorful help like this." ), ) parser.add_argument( "styles", help="Customize your CLI's help with the `RichHelpFormatter.styles` dictionary.", ) parser.add_argument( "--highlights", metavar="REGEXES", help=( "Highlighting the help text is managed by the list of regular expressions " "`RichHelpFormatter.highlights`. Set to empty list to turn off highlighting.\n" "See the next two options for default values." ), ) parser.add_argument( "--syntax", default=RichHelpFormatter.styles["argparse.syntax"], help=( "Text inside backticks is highlighted using the `argparse.syntax` style " "(default: %(default)r)" ), ) parser.add_argument( "-o", "--option", metavar="METAVAR", help="Text that looks like an --option is highlighted using the `argparse.args` style.", ) group = parser.add_argument_group( "more arguments", description=( "This is a custom group. Group names are [italic]*Title Cased*[/] by default. Use the " "`RichHelpFormatter.group_name_formatter` function to change their format." ), ) group.add_argument( "--more", nargs="*", help="This formatter works with subparsers, mutually exclusive groups and hidden arguments.", ) mutex = group.add_mutually_exclusive_group() mutex.add_argument( "--rich", action="store_true", help="Rich and poor are mutually exclusive. Choose either one but not both.", ) mutex.add_argument( "--poor", action="store_false", dest="rich", help="Does poor mean --not-rich ๐Ÿ˜‰?" ) mutex.add_argument("--not-rich", action="store_false", dest="rich", help=argparse.SUPPRESS) parser.add_argument( "--generate-rich-argparse-preview", action=HelpPreviewAction, path="rich-argparse.svg", export_kwds={"theme": DIMMED_MONOKAI}, ) # There is no program to run, always print help (except for the hidden --generate option) # You probably don't want to do this in your own code. if any(arg.startswith("--generate") for arg in sys.argv): parser.parse_args() else: parser.print_help() rich-argparse-1.8.0/rich_argparse/_argparse.py000066400000000000000000000625721517514052500214160ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan # for internal use only from __future__ import annotations import argparse import re import sys import rich_argparse._lazy_rich as r from rich_argparse._common import ( _HIGHLIGHTS, _fix_legacy_win_text, _strip_codes, rich_fill, rich_strip, rich_wrap, ) TYPE_CHECKING = False if TYPE_CHECKING: from argparse import Action, ArgumentParser, Namespace, _MutuallyExclusiveGroup from collections.abc import Callable, Iterable, Iterator, MutableMapping, Sequence from typing import Any, ClassVar from typing_extensions import Self class RichHelpFormatter(argparse.HelpFormatter): """An argparse HelpFormatter class that renders using rich.""" group_name_formatter: ClassVar[Callable[[str], str]] = str.title """A function that formats group names. Defaults to ``str.title``.""" styles: ClassVar[dict[str, r.StyleType]] = { "argparse.args": "cyan", "argparse.groups": "dark_orange", "argparse.help": "default", "argparse.metavar": "dark_cyan", "argparse.syntax": "bold", "argparse.text": "default", "argparse.prog": "grey50", "argparse.default": "italic", } """A dict of rich styles to control the formatter styles. The following styles are used: - ``argparse.args``: for positional-arguments and --options (e.g "--help") - ``argparse.groups``: for group names (e.g. "positional arguments") - ``argparse.help``: for argument's help text (e.g. "show this help message and exit") - ``argparse.metavar``: for meta variables (e.g. "FILE" in "--file FILE") - ``argparse.prog``: for %(prog)s in the usage (e.g. "foo" in "Usage: foo [options]") - ``argparse.syntax``: for highlights of back-tick quoted text (e.g. "``` `some text` ```") - ``argparse.text``: for the descriptions and epilog (e.g. "A foo program") - ``argparse.default``: for %(default)s in the help (e.g. "Value" in "(default: Value)") """ highlights: ClassVar[list[str]] = _HIGHLIGHTS[:] """A list of regex patterns to highlight in the help text. It is used in the description, epilog, groups descriptions, and arguments' help. By default, it highlights ``--words-with-dashes`` with the `argparse.args` style and `` `text in backquotes` `` with the `argparse.syntax` style. To disable highlighting, clear this list (``RichHelpFormatter.highlights.clear()``). """ usage_markup: ClassVar[bool] = False """If True, render the usage string passed to ``ArgumentParser(usage=...)`` as markup. Defaults to ``False`` meaning the text of the usage will be printed verbatim. Note that the auto-generated usage string is always colored. """ help_markup: ClassVar[bool] = True """If True (default), render the help message of arguments as console markup.""" text_markup: ClassVar[bool] = True """If True (default), render the descriptions and epilog as console markup.""" _root_section: _Section _current_section: _Section def __init__( self, prog: str, indent_increment: int = 2, max_help_position: int = 24, width: int | None = None, *, console: r.Console | None = None, **kwargs: Any, ) -> None: super().__init__(prog, indent_increment, max_help_position, width, **kwargs) self._console = console # https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting self._printf_style_pattern = re.compile( r""" % # Percent character (?:\((?P[^)]*)\))? # Mapping key (?P[#0\-+ ])? # Conversion Flags (?P\*|\d+)? # Minimum field width (?P\.(?:\*?|\d*))? # Precision [hlL]? # Length modifier (ignored) (?P[diouxXeEfFgGcrsa%]) # Conversion type """, re.VERBOSE, ) @property def console(self) -> r.Console: if self._console is None: self._console = r.Console() return self._console @console.setter def console(self, console: r.Console) -> None: self._console = console if sys.version_info >= (3, 14): # pragma: >=3.14 cover def _set_color(self, color: bool, *args, **kwargs) -> None: # Override to disable color setting in argparse.HelpFormatter for Python 3.14+ return super()._set_color(False, *args, **kwargs) class _Section(argparse.HelpFormatter._Section): def __init__( self, formatter: RichHelpFormatter, parent: Self | None, heading: str | None = None ) -> None: if heading is not argparse.SUPPRESS and heading is not None: heading = f"{type(formatter).group_name_formatter(heading)}:" super().__init__(formatter, parent, heading) self.formatter: RichHelpFormatter self.rich_items: list[r.RenderableType] = [] self.rich_actions: list[tuple[r.Text, r.Text | None]] = [] if parent is not None: parent.rich_items.append(self) def _render_items(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult: if not self.rich_items: return generated_options = options.update(no_wrap=True, overflow="ignore") new_line = r.Segment.line() for item in self.rich_items: if isinstance(item, RichHelpFormatter._Section): yield from console.render(item, options) elif isinstance(item, r.Padding): # user added rich renderable yield from console.render(item, options) yield new_line else: # argparse generated rich renderable yield from console.render(item, generated_options) def _render_actions(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult: if not self.rich_actions: return options = options.update(no_wrap=True, overflow="ignore") help_pos = min(self.formatter._action_max_length + 2, self.formatter._max_help_position) help_width = max(self.formatter._width - help_pos, 11) indent = r.Text(" " * help_pos) for action_header, action_help in self.rich_actions: if not action_help: # no help, yield the header and finish yield from console.render(action_header, options) continue action_help_lines = self.formatter._rich_split_lines(action_help, help_width) if len(action_header) > help_pos - 2: # the header is too long, put it on its own line yield from console.render(action_header, options) action_header = indent action_header.set_length(help_pos) action_help_lines[0].rstrip() yield from console.render(action_header + action_help_lines[0], options) for line in action_help_lines[1:]: line.rstrip() yield from console.render(indent + line, options) yield "" def __rich_console__(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult: if not self.rich_items and not self.rich_actions: return # empty section if self.heading is not argparse.SUPPRESS and self.heading is not None: yield r.Text(self.heading, style="argparse.groups", overflow="ignore") yield from self._render_items(console, options) yield from self._render_actions(console, options) def __rich_console__(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult: with console.use_theme(r.Theme(self.styles)): root = console.render(self._root_section, options.update_width(self._width)) new_line = r.Segment.line() add_empty_line = False for line_segments in r.Segment.split_lines(root): for i, segment in enumerate(reversed(line_segments), start=1): stripped = segment.text.rstrip() if stripped: if add_empty_line: yield new_line add_empty_line = False yield from line_segments[:-i] yield r.Segment(stripped, style=segment.style, control=segment.control) yield new_line break else: # empty line add_empty_line = True def add_text(self, text: r.RenderableType | None) -> None: if text is argparse.SUPPRESS or text is None: return elif isinstance(text, str): self._current_section.rich_items.append(self._rich_format_text(text)) else: self.add_renderable(text) def add_renderable(self, renderable: r.RenderableType) -> None: padded = r.Padding.indent(renderable, self._current_indent) self._current_section.rich_items.append(padded) def add_usage( self, usage: str | None, actions: Iterable[Action], groups: Iterable[_MutuallyExclusiveGroup], prefix: str | None = None, ) -> None: if usage is argparse.SUPPRESS: return if prefix is None: prefix = self._format_usage(usage="", actions=(), groups=(), prefix=None).rstrip("\n") prefix = _strip_codes(prefix) prefix_end = ": " if prefix.endswith(": ") else "" prefix = prefix[: len(prefix) - len(prefix_end)] prefix = type(self).group_name_formatter(prefix) + prefix_end usage_spans = [r.Span(0, len(prefix.rstrip()), "argparse.groups")] usage_text = _strip_codes(self._format_usage(usage, actions, groups, prefix=prefix)) if usage is None: # get colour spans for generated usage prog = _strip_codes(f"{self._prog}") if actions: prog_start = usage_text.index(prog, len(prefix)) usage_spans.append(r.Span(prog_start, prog_start + len(prog), "argparse.prog")) actions_start = len(prefix) + len(prog) + 1 try: spans = list(self._rich_usage_spans(usage_text, actions_start, actions=actions)) except ValueError: spans = [] usage_spans.extend(spans) rich_usage = r.Text(usage_text) elif self.usage_markup: # treat user provided usage as markup usage_spans.extend(self._rich_prog_spans(prefix + r.Text.from_markup(usage).plain)) rich_usage = r.Text.from_markup(usage_text) usage_spans.extend(rich_usage.spans) rich_usage.spans.clear() else: # treat user provided usage as plain text usage_spans.extend(self._rich_prog_spans(prefix + usage)) rich_usage = r.Text(usage_text) rich_usage.spans.extend(usage_spans) self._root_section.rich_items.append(rich_usage) def add_argument(self, action: Action) -> None: super().add_argument(action) if action.help is not argparse.SUPPRESS: self._current_section.rich_actions.extend(self._rich_format_action(action)) def format_help(self) -> str: with self.console.capture() as capture: self.console.print(self, crop=False) return _fix_legacy_win_text(self.console, capture.get()) # =============== # Utility methods # =============== def _rich_prog_spans(self, usage: str) -> Iterator[r.Span]: if "%(prog)" not in usage: return params = {"prog": self._prog} formatted_usage = "" last = 0 for m in self._printf_style_pattern.finditer(usage): start, end = m.span() formatted_usage += usage[last:start] sub = usage[start:end] % params prog_start = len(formatted_usage) prog_end = prog_start + len(sub) formatted_usage += sub last = end yield r.Span(prog_start, prog_end, "argparse.prog") def _rich_usage_spans( self, text: str, start: int, actions: Iterable[Action] ) -> Iterator[r.Span]: options: list[Action] = [] positionals: list[Action] = [] for action in actions: if action.help is not argparse.SUPPRESS: options.append(action) if action.option_strings else positionals.append(action) pos = start def find_span(_string: str) -> tuple[int, int]: stripped = _strip_codes(_string) _start = text.index(stripped, pos) _end = _start + len(stripped) return _start, _end for action in options: # start with the options usage = action.format_usage() if isinstance(action, argparse.BooleanOptionalAction): for option_string in action.option_strings: start, end = find_span(option_string) yield r.Span(start, end, "argparse.args") pos = end + 1 continue start, end = find_span(usage) yield r.Span(start, end, "argparse.args") pos = end + 1 if action.nargs != 0: default_metavar = self._get_default_metavar_for_optional(action) for metavar_part, colorize in self._rich_metavar_parts(action, default_metavar): start, end = find_span(metavar_part) if colorize: yield r.Span(start, end, "argparse.metavar") pos = end pos = end + 1 for action in positionals: # positionals come at the end default_metavar = self._get_default_metavar_for_positional(action) for metavar_part, colorize in self._rich_metavar_parts(action, default_metavar): start, end = find_span(metavar_part) if colorize: yield r.Span(start, end, "argparse.args") pos = end pos = end + 1 def _rich_metavar_parts( self, action: Action, default_metavar: str ) -> Iterator[tuple[str, bool]]: get_metavar = self._metavar_formatter(action, default_metavar) # similar to self._format_args but yields (part, colorize) of the metavar if action.nargs is None: # '%s' % get_metavar(1) yield "%s" % get_metavar(1), True # noqa: UP031 elif action.nargs == argparse.OPTIONAL: # '[%s]' % get_metavar(1) yield from ( ("[", False), ("%s" % get_metavar(1), True), # noqa: UP031 ("]", False), ) elif action.nargs == argparse.ZERO_OR_MORE: if len(get_metavar(1)) == 2: metavar = get_metavar(2) # '[%s [%s ...]]' % metavar yield from ( ("[", False), ("%s" % metavar[0], True), # noqa: UP031 (" [", False), ("%s" % metavar[1], True), # noqa: UP031 (" ", False), ("...", True), ("]]", False), ) else: # '[%s ...]' % metavar yield from ( ("[", False), ("%s" % get_metavar(1), True), # noqa: UP031 (" ", False), ("...", True), ("]", False), ) elif action.nargs == argparse.ONE_OR_MORE: # '%s [%s ...]' % get_metavar(2) metavar = get_metavar(2) yield from ( ("%s" % metavar[0], True), # noqa: UP031 (" [", False), ("%s" % metavar[1], True), # noqa: UP031 (" ", False), ("...", True), ("]", False), ) elif action.nargs == argparse.REMAINDER: # '...' yield "...", True elif action.nargs == argparse.PARSER: # '%s ...' % get_metavar(1) yield from ( ("%s" % get_metavar(1), True), # noqa: UP031 (" ", False), ("...", True), ) elif action.nargs == argparse.SUPPRESS: # '' yield "", False else: metavar = get_metavar(action.nargs) # type: ignore[arg-type] first = True for met in metavar: if first: first = False else: yield " ", False yield "%s" % met, True # noqa: UP031 def _rich_whitespace_sub(self, text: r.Text) -> r.Text: # do this `self._whitespace_matcher.sub(' ', text).strip()` but text is Text spans = [m.span() for m in self._whitespace_matcher.finditer(text.plain)] for start, end in reversed(spans): if end - start > 1: # slow path space = text[start : start + 1] space.plain = " " text = text[:start] + space + text[end:] else: # performance shortcut text.plain = text.plain[:start] + " " + text.plain[end:] return rich_strip(text) # ===================================== # Rich version of HelpFormatter methods # ===================================== def _rich_expand_help(self, action: Action) -> r.Text: params = dict(vars(action), prog=self._prog) for name in list(params): if params[name] is argparse.SUPPRESS: del params[name] elif hasattr(params[name], "__name__"): params[name] = params[name].__name__ if params.get("choices") is not None: params["choices"] = ", ".join([str(c) for c in params["choices"]]) help_string = self._get_help_string(action) assert help_string is not None # raise ValueError if needed help_string % params # pyright: ignore[reportUnusedExpression] parts = [] defaults: list[str] = [] default_sub_template = "rich-argparse-f3ae8b55df34d5d83a8189d2e4766e68-{}-argparse-rich" default_n = 0 last = 0 for m in self._printf_style_pattern.finditer(help_string): start, end = m.span() parts.append(help_string[last:start]) sub = help_string[start:end] % params if m.group("mapping") == "default": defaults.append(sub) sub = default_sub_template.format(default_n) default_n += 1 else: sub = r.escape(sub) parts.append(sub) last = end parts.append(help_string[last:]) rich_help = ( r.Text.from_markup("".join(parts), style="argparse.help") if self.help_markup else r.Text("".join(parts), style="argparse.help") ) for i, default in reversed(list(enumerate(defaults))): default_sub = default_sub_template.format(i) try: start = rich_help.plain.rindex(default_sub) except ValueError: # This could happen in cases like `[default: %(default)s]` with markup activated import warnings action_id = next(iter(action.option_strings), action.dest) printf_pat = self._printf_style_pattern.pattern repl = next( ( repr(m.group(1))[1:-1] for m in re.finditer(rf"\[([^\]]*{printf_pat}[^\]]*)\]", help_string, re.X) if m.group("mapping") == "default" ), "default: %(default)s", ) msg = ( f"Failed to process default value in help string of argument {action_id!r}." f"\nHint: try disabling rich markup: `RichHelpFormatter.help_markup = False`" f"\n or replace brackets by parenthesis: `[{repl}]` -> `({repl})`" ) warnings.warn(msg, UserWarning, stacklevel=4) continue end = start + len(default_sub) rich_help = ( rich_help[:start].append(default, style="argparse.default").append(rich_help[end:]) ) for highlight in self.highlights: rich_help.highlight_regex(highlight, style_prefix="argparse.") return rich_help def _rich_format_text(self, text: str) -> r.Text: if "%(prog)" in text: text = text % {"prog": r.escape(self._prog)} rich_text = ( r.Text.from_markup(text, style="argparse.text") if self.text_markup else r.Text(text, style="argparse.text") ) for highlight in self.highlights: rich_text.highlight_regex(highlight, style_prefix="argparse.") text_width = max(self._width - self._current_indent * 2, 11) indent = r.Text(" " * self._current_indent) return self._rich_fill_text(rich_text, text_width, indent) def _rich_format_action(self, action: Action) -> Iterator[tuple[r.Text, r.Text | None]]: header = self._rich_format_action_invocation(action) header.pad_left(self._current_indent) help = self._rich_expand_help(action) if action.help and action.help.strip() else None yield header, help for subaction in self._iter_indented_subactions(action): yield from self._rich_format_action(subaction) def _rich_format_action_invocation(self, action: Action) -> r.Text: if not action.option_strings: return r.Text().append(self._format_action_invocation(action), style="argparse.args") else: action_header = r.Text(", ").join( r.Text(o, "argparse.args") for o in action.option_strings ) if action.nargs != 0: default = self._get_default_metavar_for_optional(action) action_header.append(" ") for metavar_part, colorize in self._rich_metavar_parts(action, default): style = "argparse.metavar" if colorize else None action_header.append(metavar_part, style=style) return action_header def _rich_split_lines(self, text: r.Text, width: int) -> r.Lines: return rich_wrap(self.console, self._rich_whitespace_sub(text), width) def _rich_fill_text(self, text: r.Text, width: int, indent: r.Text) -> r.Text: return rich_fill(self.console, self._rich_whitespace_sub(text), width, indent) + "\n\n" class RawDescriptionRichHelpFormatter(RichHelpFormatter): """Rich help message formatter which retains any formatting in descriptions.""" def _rich_fill_text(self, text: r.Text, width: int, indent: r.Text) -> r.Text: return r.Text("\n").join(indent + line for line in text.split()) + "\n\n" class RawTextRichHelpFormatter(RawDescriptionRichHelpFormatter): """Rich help message formatter which retains formatting of all help text.""" def _rich_split_lines(self, text: r.Text, width: int) -> r.Lines: return text.split() class ArgumentDefaultsRichHelpFormatter(argparse.ArgumentDefaultsHelpFormatter, RichHelpFormatter): """Rich help message formatter which adds default values to argument help.""" class MetavarTypeRichHelpFormatter(argparse.MetavarTypeHelpFormatter, RichHelpFormatter): """Rich help message formatter which uses the argument 'type' as the default metavar value (instead of the argument 'dest'). """ class HelpPreviewAction(argparse.Action): """Action that renders the help to SVG, HTML, or text file and exits.""" def __init__( self, option_strings: Sequence[str], dest: str = argparse.SUPPRESS, default: str = argparse.SUPPRESS, help: str = argparse.SUPPRESS, *, path: str | None = None, export_kwds: MutableMapping[str, Any] | None = None, ) -> None: super().__init__(option_strings, dest, nargs="?", const=path, default=default, help=help) self.export_kwds = export_kwds or {} def __call__( self, parser: ArgumentParser, namespace: Namespace, values: str | Sequence[Any] | None, option_string: str | None = None, ) -> None: path = values if path is None: parser.exit(1, "error: help preview path is not provided\n") if not isinstance(path, str): parser.exit(1, "error: help preview path must be a string\n") if not path.endswith((".svg", ".html", ".txt")): parser.exit(1, "error: help preview path must end with .svg, .html, or .txt\n") import io text = r.Text.from_ansi(parser.format_help()) console = r.Console(file=io.StringIO(), record=True) console.print(text, crop=False) if path.endswith(".svg"): self.export_kwds.setdefault("title", "") console.save_svg(path, **self.export_kwds) elif path.endswith(".html"): console.save_html(path, **self.export_kwds) elif path.endswith(".txt"): console.save_text(path, **self.export_kwds) else: raise AssertionError("unreachable") parser.exit(0, f"Help preview saved to {path}\n") rich-argparse-1.8.0/rich_argparse/_common.py000066400000000000000000000061731517514052500210750ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan # for internal use only from __future__ import annotations import sys import rich_argparse._lazy_rich as r # Default highlight patterns: # - highlight `text in backquotes` as "syntax" # - --words-with-dashes outside backticks as "args" _HIGHLIGHTS = [ r"`(?P[^`]*)`|(?:^|\s)(?P-{1,2}[\w]+[\w-]*)", ] _windows_console_fixed: bool | None = None def rich_strip(text: r.Text) -> r.Text: """Strip leading and trailing whitespace from `rich.text.Text`.""" lstrip_at = len(text.plain) - len(text.plain.lstrip()) if lstrip_at: # rich.Text.lstrip() is not available yet!! text = text[lstrip_at:] text.rstrip() return text def rich_wrap(console: r.Console, text: r.Text, width: int) -> r.Lines: """`textwrap.wrap()` equivalent for `rich.text.Text`.""" text = text.copy() text.expand_tabs(8) # textwrap expands tabs first whitespace_trans = dict.fromkeys(map(ord, "\t\n\x0b\x0c\r "), ord(" ")) text.plain = text.plain.translate(whitespace_trans) return text.wrap(console, width) def rich_fill(console: r.Console, text: r.Text, width: int, indent: r.Text) -> r.Text: """`textwrap.fill()` equivalent for `rich.text.Text`.""" lines = rich_wrap(console, text, width) return r.Text("\n").join(indent + line for line in lines) def _strip_codes(text: str) -> str: """Remove ANSI color codes and control codes from a string.""" return r.re_ansi.sub("", r.strip_control_codes(text)) def _initialize_win_colors() -> bool: # pragma: no cover global _windows_console_fixed assert sys.platform == "win32" if _windows_console_fixed is None: winver = sys.getwindowsversion() # type: ignore[attr-defined] if winver.major < 10 or winver.build < 10586: try: import colorama _windows_console_fixed = isinstance(sys.stdout, colorama.ansitowin32.StreamWrapper) except Exception: _windows_console_fixed = False else: import ctypes kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] ENABLE_PROCESSED_OUTPUT = 0x1 ENABLE_WRAP_AT_EOL_OUTPUT = 0x2 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4 STD_OUTPUT_HANDLE = -11 kernel32.SetConsoleMode( kernel32.GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING, ) _windows_console_fixed = True return _windows_console_fixed def _fix_legacy_win_text(console: r.Console, text: str) -> str: # activate legacy Windows console colors if needed (and available) or strip ANSI escape codes if ( text and sys.platform == "win32" and console.legacy_windows and console.color_system is not None and not _initialize_win_colors() ): # pragma: win32 cover text = "\n".join(r.re_ansi.sub("", line) for line in text.split("\n")) return text rich-argparse-1.8.0/rich_argparse/_contrib.py000066400000000000000000000051051517514052500212370ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan # for internal use only from __future__ import annotations import rich_argparse._lazy_rich as r from rich_argparse._argparse import RichHelpFormatter from rich_argparse._common import rich_strip, rich_wrap class ParagraphRichHelpFormatter(RichHelpFormatter): """Rich help message formatter which retains paragraph separation with spacing using `\\n\\n`.""" def _rich_split_lines(self, text: r.Text, width: int) -> r.Lines: text = rich_strip(text) lines = r.Lines() for paragraph in text.split("\n\n"): # Normalize whitespace in the paragraph paragraph = self._rich_whitespace_sub(paragraph) # Wrap the paragraph to the specified width paragraph_lines = rich_wrap(self.console, paragraph, width) # Add the wrapped lines to the output lines.extend(paragraph_lines) # Add a blank line between paragraphs lines.append(r.Text("\n")) if lines: # pragma: no cover lines.pop() # Remove trailing newline return lines def _rich_fill_text(self, text: r.Text, width: int, indent: r.Text) -> r.Text: lines = self._rich_split_lines(text, width) return r.Text("\n").join(indent + line for line in lines) + "\n" class ExtendedParagraphRichHelpFormatter(RichHelpFormatter): """Rich help message formatter which retains paragraph separation without spacing using `\\n\\n` and paragraph spacing using `\\n\\n\\n`.""" def _rich_split_lines(self, text: r.Text, width: int) -> r.Lines: text = rich_strip(text) lines = r.Lines() for paragraph in text.split("\n\n\n"): for subparagraph in paragraph.split("\n\n"): # Normalize whitespace in the subparagraph subparagraph = self._rich_whitespace_sub(subparagraph) # Wrap the subparagraph to the specified width paragraph_lines = rich_wrap(self.console, subparagraph, width) # Add the wrapped lines to the output lines.extend(paragraph_lines) # Add a blank line between paragraphs lines.append(r.Text("\n")) if lines: # pragma: no cover lines.pop() # Remove trailing newline return lines def _rich_fill_text(self, text: r.Text, width: int, indent: r.Text) -> r.Text: lines = self._rich_split_lines(text, width) return r.Text("\n").join(indent + line for line in lines) + "\n" rich-argparse-1.8.0/rich_argparse/_lazy_rich.py000066400000000000000000000043441517514052500215670ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan # for internal use only from __future__ import annotations TYPE_CHECKING = False if TYPE_CHECKING: from typing import Any from rich.ansi import re_ansi as re_ansi from rich.console import Console as Console from rich.console import ConsoleOptions as ConsoleOptions from rich.console import RenderableType as RenderableType from rich.console import RenderResult as RenderResult from rich.containers import Lines as Lines from rich.control import strip_control_codes as strip_control_codes from rich.markup import escape as escape from rich.padding import Padding as Padding from rich.segment import Segment as Segment from rich.style import StyleType as StyleType from rich.text import Span as Span from rich.text import Text as Text from rich.theme import Theme as Theme __all__ = [ "re_ansi", "Console", "ConsoleOptions", "RenderableType", "RenderResult", "Lines", "strip_control_codes", "escape", "Padding", "Segment", "StyleType", "Span", "Text", "Theme", ] def __getattr__(name: str) -> Any: if name not in __all__: raise AttributeError(name) import rich.ansi import rich.console import rich.containers import rich.control import rich.markup import rich.padding import rich.segment import rich.style import rich.text import rich.theme globals().update( { "re_ansi": rich.ansi.re_ansi, "Console": rich.console.Console, "ConsoleOptions": rich.console.ConsoleOptions, "RenderableType": rich.console.RenderableType, "RenderResult": rich.console.RenderResult, "Lines": rich.containers.Lines, "strip_control_codes": rich.control.strip_control_codes, "escape": rich.markup.escape, "Padding": rich.padding.Padding, "Segment": rich.segment.Segment, "StyleType": rich.style.StyleType, "Span": rich.text.Span, "Text": rich.text.Text, "Theme": rich.theme.Theme, } ) return globals()[name] rich-argparse-1.8.0/rich_argparse/_optparse.py000066400000000000000000000272631517514052500214450ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan # for internal use only from __future__ import annotations import optparse import rich_argparse._lazy_rich as r from rich_argparse._common import _HIGHLIGHTS, _fix_legacy_win_text, rich_fill, rich_wrap TYPE_CHECKING = False if TYPE_CHECKING: from typing import Literal GENERATE_USAGE = "==GENERATE_USAGE==" class RichHelpFormatter(optparse.HelpFormatter): """An optparse HelpFormatter class that renders using rich.""" styles: dict[str, r.StyleType] = { "optparse.args": "cyan", "optparse.groups": "dark_orange", "optparse.help": "default", "optparse.metavar": "dark_cyan", "optparse.syntax": "bold", "optparse.text": "default", "optparse.prog": "grey50", } """A dict of rich styles to control the formatter styles. The following styles are used: - ``optparse.args``: for --options (e.g "--help") - ``optparse.groups``: for group names (e.g. "Options") - ``optparse.help``: for options's help text (e.g. "show this help message and exit") - ``optparse.metavar``: for meta variables (e.g. "FILE" in "--file=FILE") - ``argparse.prog``: for %prog in generated usage (e.g. "foo" in "Usage: foo [options]") - ``optparse.syntax``: for highlights of back-tick quoted text (e.g. "``` `some text` ```"), - ``optparse.text``: for the descriptions and epilog (e.g. "A foo program") """ highlights: list[str] = _HIGHLIGHTS[:] """A list of regex patterns to highlight in the help text. It is used in the description, epilog, groups descriptions, and arguments' help. By default, it highlights ``--words-with-dashes`` with the `optparse.args` style and ``` `text in backquotes` ``` with the `optparse.syntax` style. To disable highlighting, clear this list (``RichHelpFormatter.highlights.clear()``). """ def __init__( self, indent_increment: int, max_help_position: int, width: int | None, short_first: bool | Literal[0, 1], ) -> None: super().__init__(indent_increment, max_help_position, width, short_first) self._console: r.Console | None = None self.rich_option_strings: dict[optparse.Option, r.Text] = {} @property def console(self) -> r.Console: if self._console is None: self._console = r.Console(theme=r.Theme(self.styles)) return self._console @console.setter def console(self, console: r.Console) -> None: self._console = console def _stringify(self, text: r.RenderableType) -> str: # Render a rich object to a string with self.console.capture() as capture: self.console.print(text, highlight=False, soft_wrap=True, end="") help = capture.get() help = "\n".join(line.rstrip() for line in help.split("\n")) return _fix_legacy_win_text(self.console, help) def rich_format_usage(self, usage: str) -> r.Text: raise NotImplementedError("subclasses must implement") def rich_format_heading(self, heading: str) -> r.Text: raise NotImplementedError("subclasses must implement") def _rich_format_text(self, text: str) -> r.Text: # HelpFormatter._format_text() equivalent that produces rich.text.Text text_width = max(self.width - 2 * self.current_indent, 11) indent = r.Text(" " * self.current_indent) rich_text = r.Text.from_markup(text, style="optparse.text") for highlight in self.highlights: rich_text.highlight_regex(highlight, style_prefix="optparse.") return rich_fill(self.console, rich_text, text_width, indent) def rich_format_description(self, description: str | None) -> r.Text: if not description: return r.Text() return self._rich_format_text(description) + r.Text("\n") def rich_format_epilog(self, epilog: str | None) -> r.Text: if not epilog: return r.Text() return r.Text("\n") + self._rich_format_text(epilog) + r.Text("\n") def format_usage(self, usage: str) -> str: if usage is GENERATE_USAGE: rich_usage = self._generate_usage() else: rich_usage = self.rich_format_usage(usage) return self._stringify(rich_usage) def format_heading(self, heading: str) -> str: return self._stringify(self.rich_format_heading(heading)) def format_description(self, description: str | None) -> str: return self._stringify(self.rich_format_description(description)) def format_epilog(self, epilog: str | None) -> str: return self._stringify(self.rich_format_epilog(epilog)) def rich_expand_default(self, option: optparse.Option) -> r.Text: assert option.help is not None if self.parser is None or not self.default_tag: help = option.help else: default_value = self.parser.defaults.get(option.dest) # type: ignore[arg-type] if default_value is optparse.NO_DEFAULT or default_value is None: default_value = self.NO_DEFAULT_VALUE help = option.help.replace(self.default_tag, r.escape(str(default_value))) rich_help = r.Text.from_markup(help, style="optparse.help") for highlight in self.highlights: rich_help.highlight_regex(highlight, style_prefix="optparse.") return rich_help def rich_format_option(self, option: optparse.Option) -> r.Text: result: list[r.Text] = [] opts = self.rich_option_strings[option] opt_width = self.help_position - self.current_indent - 2 if len(opts) > opt_width: opts.append("\n") indent_first = self.help_position else: # start help on same line as opts opts.set_length(opt_width + 2) indent_first = 0 opts.pad_left(self.current_indent) result.append(opts) if option.help: help_text = self.rich_expand_default(option) help_lines = rich_wrap(self.console, help_text, self.help_width) result.append(r.Text(" " * indent_first) + help_lines[0] + "\n") indent = r.Text(" " * self.help_position) for line in help_lines[1:]: result.append(indent + line + "\n") elif opts.plain[-1] != "\n": result.append(r.Text("\n")) else: pass # pragma: no cover return r.Text().join(result) def format_option(self, option: optparse.Option) -> str: return self._stringify(self.rich_format_option(option)) def store_option_strings(self, parser: optparse.OptionParser) -> None: self.indent() max_len = 0 for opt in parser.option_list: strings = self.rich_format_option_strings(opt) self.option_strings[opt] = strings.plain self.rich_option_strings[opt] = strings max_len = max(max_len, len(strings) + self.current_indent) self.indent() for group in parser.option_groups: for opt in group.option_list: strings = self.rich_format_option_strings(opt) self.option_strings[opt] = strings.plain self.rich_option_strings[opt] = strings max_len = max(max_len, len(strings) + self.current_indent) self.dedent() self.dedent() self.help_position = min(max_len + 2, self.max_help_position) self.help_width = max(self.width - self.help_position, 11) def rich_format_option_strings(self, option: optparse.Option) -> r.Text: if option.takes_value(): if option.metavar: metavar = option.metavar else: assert option.dest is not None metavar = option.dest.upper() s_delim = self._short_opt_fmt.replace("%s", "") short_opts = [ r.Text(s_delim).join( [r.Text(o, "optparse.args"), r.Text(metavar, "optparse.metavar")] ) for o in option._short_opts ] l_delim = self._long_opt_fmt.replace("%s", "") long_opts = [ r.Text(l_delim).join( [r.Text(o, "optparse.args"), r.Text(metavar, "optparse.metavar")] ) for o in option._long_opts ] else: short_opts = [r.Text(o, style="optparse.args") for o in option._short_opts] long_opts = [r.Text(o, style="optparse.args") for o in option._long_opts] if self.short_first: opts = short_opts + long_opts else: opts = long_opts + short_opts return r.Text(", ").join(opts) def _generate_usage(self) -> r.Text: """Generate usage string from the parser's actions.""" if self.parser is None: raise TypeError("Cannot generate usage if parser is not set") mark = "==GENERATED_USAGE_MARKER==" usage_lines: list[r.Text] = [] prefix = self.rich_format_usage(mark).split(mark)[0] usage_lines.extend(prefix.split("\n")) usage_lines[-1].append(self.parser.get_prog_name(), "optparse.prog") indent = len(usage_lines[-1]) + 1 for option in self.parser.option_list: if option.help == optparse.SUPPRESS_HELP: continue opt_str = option._short_opts[0] if option._short_opts else option.get_opt_string() option_usage = r.Text("[").append(opt_str, "optparse.args") if option.takes_value(): metavar = option.metavar or option.dest.upper() # type: ignore[union-attr] option_usage.append(" ").append(metavar, "optparse.metavar") option_usage.append("]") if len(usage_lines[-1]) + len(option_usage) + 1 > self.width: usage_lines.append(r.Text(" " * indent) + option_usage) else: usage_lines[-1].append(" ").append(option_usage) usage_lines.append(r.Text()) return r.Text("\n").join(usage_lines) class IndentedRichHelpFormatter(RichHelpFormatter): """Format help with indented section bodies.""" def __init__( self, indent_increment: int = 2, max_help_position: int = 24, width: int | None = None, short_first: bool | Literal[0, 1] = 1, ) -> None: super().__init__(indent_increment, max_help_position, width, short_first) def rich_format_usage(self, usage: str) -> r.Text: usage_template = optparse._("Usage: %s\n") # type: ignore[attr-defined] usage = usage_template % usage prefix = (usage_template % "").rstrip() spans = [r.Span(0, len(prefix), "optparse.groups")] return r.Text(usage, spans=spans) def rich_format_heading(self, heading: str) -> r.Text: text = r.Text(" " * self.current_indent).append(f"{heading}:", "optparse.groups") return text + r.Text("\n") class TitledRichHelpFormatter(RichHelpFormatter): """Format help with underlined section headers.""" def __init__( self, indent_increment: int = 0, max_help_position: int = 24, width: int | None = None, short_first: bool | Literal[0, 1] = 0, ) -> None: super().__init__(indent_increment, max_help_position, width, short_first) def rich_format_usage(self, usage: str) -> r.Text: heading = self.rich_format_heading(optparse._("Usage")) # type: ignore[attr-defined] return r.Text.assemble(heading, " ", usage, "\n") def rich_format_heading(self, heading: str) -> r.Text: underline = "=-"[self.level] * len(heading) return r.Text.assemble( (heading, "optparse.groups"), "\n", (underline, "optparse.groups"), "\n" ) rich-argparse-1.8.0/rich_argparse/_patching.py000066400000000000000000000042751517514052500214030ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan # for internal use only from __future__ import annotations from rich_argparse._argparse import RichHelpFormatter def patch_default_formatter_class( cls=None, /, *, formatter_class=RichHelpFormatter, method_name="__init__" ): """Patch the default `formatter_class` parameter of an argument parser constructor. Parameters ---------- cls : (type, optional) The class to patch. If not provided, a decorator is returned. formatter_class : (type, optional) The new formatter class to use. Defaults to ``RichHelpFormatter``. method_name : (str, optional) The method name to patch. Defaults to ``__init__``. Examples -------- Can be used as a normal function to patch an existing class:: # Patch the default formatter class of `argparse.ArgumentParser` patch_default_formatter_class(argparse.ArgumentParser) # Patch the default formatter class of django commands from django.core.management.base import BaseCommand, DjangoHelpFormatter class DjangoRichHelpFormatter(DjangoHelpFormatter, RichHelpFormatter): ... patch_default_formatter_class( BaseCommand, formatter_class=DjangoRichHelpFormatter, method_name="create_parser" ) Or as a decorator to patch a new class:: @patch_default_formatter_class class MyArgumentParser(argparse.ArgumentParser): pass @patch_default_formatter_class(formatter_class=RawDescriptionRichHelpFormatter) class MyOtherArgumentParser(argparse.ArgumentParser): pass """ import functools def decorator(cls, /): method = getattr(cls, method_name) if not callable(method): raise TypeError(f"'{cls.__name__}.{method_name}' is not callable") @functools.wraps(method) def wrapper(*args, **kwargs): kwargs.setdefault("formatter_class", formatter_class) return method(*args, **kwargs) setattr(cls, method_name, wrapper) return cls if cls is None: return decorator return decorator(cls) rich-argparse-1.8.0/rich_argparse/_patching.pyi000066400000000000000000000013311517514052500215420ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan # for internal use only from argparse import _FormatterClass from collections.abc import Callable from typing import TypeVar, overload from rich_argparse._argparse import RichHelpFormatter _T = TypeVar("_T", bound=type) @overload def patch_default_formatter_class( cls: None = None, /, *, formatter_class: _FormatterClass = RichHelpFormatter, method_name: str = "__init__", ) -> Callable[[_T], _T]: ... @overload def patch_default_formatter_class( cls: _T, /, *, formatter_class: _FormatterClass = RichHelpFormatter, method_name: str = "__init__", ) -> _T: ... rich-argparse-1.8.0/rich_argparse/contrib.py000066400000000000000000000012221517514052500210740ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan """Extra formatters for rich help messages. The rich_argparse.contrib module contains optional, standard implementations of common patterns of rich help message formatting. These formatters are not included in the main rich_argparse module because they do not translate directly to argparse formatters. """ from __future__ import annotations from rich_argparse._contrib import ExtendedParagraphRichHelpFormatter, ParagraphRichHelpFormatter __all__ = [ "ExtendedParagraphRichHelpFormatter", "ParagraphRichHelpFormatter", ] rich-argparse-1.8.0/rich_argparse/django.py000066400000000000000000000027141517514052500207050ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan """Django-specific utilities for rich command line help.""" from __future__ import annotations try: from django.core.management.base import DjangoHelpFormatter as _DjangoHelpFormatter except ImportError as e: # pragma: no cover raise ImportError("rich_argparse.django requires django to be installed.") from e from rich_argparse._argparse import RichHelpFormatter as _RichHelpFormatter from rich_argparse._patching import patch_default_formatter_class as _patch_default_formatter_class __all__ = [ "DjangoRichHelpFormatter", "richify_command_line_help", ] class DjangoRichHelpFormatter(_DjangoHelpFormatter, _RichHelpFormatter): """A rich help formatter for django commands.""" def richify_command_line_help( formatter_class: type[_RichHelpFormatter] = DjangoRichHelpFormatter, ) -> None: """Set a rich default formatter class for ``BaseCommand`` project-wide. Calling this function affects all built-in, third-party, and user defined django commands. Note that this function only changes the **default** formatter class of commands. User commands can still override the default by explicitly setting a formatter class. """ from django.core.management.base import BaseCommand _patch_default_formatter_class( BaseCommand, formatter_class=formatter_class, method_name="create_parser" ) rich-argparse-1.8.0/rich_argparse/optparse.py000066400000000000000000000035131517514052500212760ustar00rootroot00000000000000# Source code: https://github.com/hamdanal/rich-argparse # MIT license: Copyright (c) Ali Hamdan from __future__ import annotations from rich_argparse._optparse import ( GENERATE_USAGE, IndentedRichHelpFormatter, RichHelpFormatter, TitledRichHelpFormatter, ) __all__ = [ "RichHelpFormatter", "IndentedRichHelpFormatter", "TitledRichHelpFormatter", "GENERATE_USAGE", ] if __name__ == "__main__": import optparse IndentedRichHelpFormatter.highlights.append(r"(?P\bregexes\b)") parser = optparse.OptionParser( description="I [link https://pypi.org/project/rich]rich[/]ify:trade_mark: optparse help.", formatter=IndentedRichHelpFormatter(), prog="python -m rich_arparse.optparse", epilog=":link: https://github.com/hamdanal/rich-argparse#optparse-support.", usage=GENERATE_USAGE, ) parser.add_option("--formatter", metavar="rich", help="A piece of :cake: isn't it? :wink:") parser.add_option( "--styles", metavar="yours", help="Not your style? No biggie, change it :sunglasses:" ) parser.add_option( "--highlights", action="store_true", help=":clap: --highlight :clap: all :clap: the :clap: regexes :clap:", ) parser.add_option( "--syntax", action="store_true", help="`backquotes` may be bold, but they are :muscle:" ) parser.add_option( "-s", "--long", metavar="METAVAR", help="That's a lot of metavars for an option!" ) group = parser.add_option_group("Magic", description=":sparkles: :sparkles: :sparkles:") group.add_option( "--treasure", action="store_false", help="Mmm, did you find the --hidden :gem:?" ) group.add_option("--hidden", action="store_false", dest="treasure", help=optparse.SUPPRESS_HELP) parser.print_help() rich-argparse-1.8.0/rich_argparse/py.typed000066400000000000000000000000001517514052500205520ustar00rootroot00000000000000rich-argparse-1.8.0/scripts/000077500000000000000000000000001517514052500157435ustar00rootroot00000000000000rich-argparse-1.8.0/scripts/generate-preview000077500000000000000000000001261517514052500211410ustar00rootroot00000000000000#!/bin/env bash COLUMNS=128 python -m rich_argparse --generate-rich-argparse-preview rich-argparse-1.8.0/scripts/release000077500000000000000000000006061517514052500173130ustar00rootroot00000000000000#!/bin/env bash set -euxo pipefail # clear the dist directory rm -rf dist/ # build the sdist and wheel pyproject-build . # check the contents of the sdist and wheel tar -tvf dist/rich_argparse-*.tar.gz unzip -l dist/rich_argparse-*.whl # continue? [[ "$(read -e -p 'Release? [y/N]> '; echo $REPLY)" == [Yy]* ]] || exit 1; # upload the new artifacts to pypi twine upload -r pypi dist/* rich-argparse-1.8.0/scripts/run-tests000077500000000000000000000003271517514052500176370ustar00rootroot00000000000000#!/bin/env bash set -euxo pipefail for python in "3.9" "3.10" "3.11" "3.12" "3.13" "3.14" "pypy3.11"; do uvx --isolated --python=${python} --with=. --with-requirements=tests/requirements.txt pytest --cov done rich-argparse-1.8.0/tests/000077500000000000000000000000001517514052500154165ustar00rootroot00000000000000rich-argparse-1.8.0/tests/__init__.py000066400000000000000000000000001517514052500175150ustar00rootroot00000000000000rich-argparse-1.8.0/tests/conftest.py000066400000000000000000000014431517514052500176170ustar00rootroot00000000000000from __future__ import annotations import os from unittest.mock import patch import pytest from rich_argparse import RichHelpFormatter # Common fixtures # =============== @pytest.fixture(scope="session", autouse=True) def set_terminal_properties(): with patch.dict(os.environ, {"COLUMNS": "100", "TERM": "xterm-256color"}): yield @pytest.fixture(scope="session", autouse=True) def turnoff_legacy_windows(): with patch("rich.console.detect_legacy_windows", return_value=False): yield @pytest.fixture() def force_color(): with patch.dict(os.environ, {"FORCE_COLOR": "1"}): yield # argparse fixtures # ================= @pytest.fixture() def disable_group_name_formatter(): with patch.object(RichHelpFormatter, "group_name_formatter", str): yield rich-argparse-1.8.0/tests/helpers.py000066400000000000000000000152131517514052500174340ustar00rootroot00000000000000from __future__ import annotations import argparse as ap import functools import io import optparse as op import sys import textwrap from collections.abc import Callable from typing import Any, Generic, TypeVar from unittest.mock import patch import pytest if sys.version_info >= (3, 10): # pragma: >=3.10 cover from typing import Concatenate, ParamSpec else: # pragma: <3.10 cover from typing_extensions import Concatenate, ParamSpec R = TypeVar("R") # return type S = TypeVar("S") # self type P = ParamSpec("P") # other parameters type PT = TypeVar("PT", bound="ap.ArgumentParser | op.OptionParser") # parser type GT = TypeVar("GT", bound="ap._ArgumentGroup | op.OptionGroup") # group type def get_cmd_output(parser: ap.ArgumentParser | op.OptionParser, cmd: list[str]) -> str: __tracebackhide__ = True stdout = io.StringIO() with pytest.raises(SystemExit), patch.object(sys, "stdout", stdout): parser.parse_args(cmd) return stdout.getvalue() def copy_signature( func: Callable[Concatenate[Any, P], object], ) -> Callable[[Callable[Concatenate[S, ...], R]], Callable[Concatenate[S, P], R]]: """Copy the signature of the given method except self and return types.""" return functools.wraps(func)(lambda f: f) class BaseGroups(Generic[GT]): """Base class for argument groups and option groups.""" def __init__(self) -> None: self.groups: list[GT] = [] def append(self, group: GT) -> None: self.groups.append(group) class BaseParsers(Generic[PT]): """Base class for argument parsers and option parsers.""" parsers: list[PT] def assert_format_help_equal(self, expected: str | None = None) -> None: assert self.parsers, "No parsers to compare." outputs = [parser.format_help() for parser in self.parsers] if expected is None: # pragma: no cover expected = outputs.pop() assert outputs, "No outputs to compare." for output in outputs: assert output == expected def assert_cmd_output_equal(self, cmd: list[str], expected: str | None = None) -> None: assert self.parsers, "No parsers to compare." outputs = [get_cmd_output(parser, cmd) for parser in self.parsers] if expected is None: # pragma: no cover expected = outputs.pop() assert outputs, "No outputs to compare." for output in outputs: assert output == expected # argparse # ======== class ArgumentGroups(BaseGroups[ap._ArgumentGroup]): @copy_signature(ap._ArgumentGroup.add_argument) # type: ignore[arg-type] def add_argument(self, /, *args, **kwds) -> None: for group in self.groups: group.add_argument(*args, **kwds) class _SubParsersActions: def __init__(self) -> None: self.parents: list[ap.ArgumentParser] = [] self.subparsers: list[ap._SubParsersAction[ap.ArgumentParser]] = [] def append(self, p: ap.ArgumentParser, sp: ap._SubParsersAction[ap.ArgumentParser]) -> None: self.parents.append(p) self.subparsers.append(sp) @copy_signature(ap._SubParsersAction.add_parser) # type: ignore[arg-type] def add_parser(self, /, *args, **kwds) -> ArgumentParsers: parsers = ArgumentParsers() for parent, subparser in zip(self.parents, self.subparsers): sp = subparser.add_parser(*args, **kwds, formatter_class=parent.formatter_class) parsers.parsers.append(sp) return parsers class ArgumentParsers(BaseParsers[ap.ArgumentParser]): def __init__( self, *formatter_classes: type[ap.HelpFormatter], prog: str | None = None, usage: str | None = None, description: str | None = None, epilog: str | None = None, ) -> None: assert len(set(formatter_classes)) == len(formatter_classes), "Duplicate formatter_class" self.parsers = [ ap.ArgumentParser( prog=prog, usage=usage, description=description, epilog=epilog, formatter_class=formatter_class, ) for formatter_class in formatter_classes ] @copy_signature(ap.ArgumentParser.add_argument) # type: ignore[arg-type] def add_argument(self, /, *args, **kwds) -> None: for parser in self.parsers: parser.add_argument(*args, **kwds) @copy_signature(ap.ArgumentParser.add_argument_group) def add_argument_group(self, /, *args, **kwds) -> ArgumentGroups: groups = ArgumentGroups() for parser in self.parsers: groups.append(parser.add_argument_group(*args, **kwds)) return groups @copy_signature(ap.ArgumentParser.add_subparsers) def add_subparsers(self, /, *args, **kwds) -> _SubParsersActions: subparsers = _SubParsersActions() for parser in self.parsers: sp = parser.add_subparsers(*args, **kwds) subparsers.append(parser, sp) return subparsers def clean_argparse(text: str, dedent: bool = True) -> str: """Clean argparse help text.""" # Can be replaced with textwrap.dedent(text) when Python 3.10 is the minimum version if sys.version_info >= (3, 10): # pragma: >=3.10 cover # replace "optional arguments:" with "options:" pos = text.lower().index("optional arguments:") text = text[: pos + 6] + text[pos + 17 :] if dedent: text = textwrap.dedent(text) return text # optparse # ======== class OptionGroups(BaseGroups[op.OptionGroup]): @copy_signature(op.OptionGroup.add_option) def add_option(self, /, *args, **kwds) -> None: for group in self.groups: group.add_option(*args, **kwds) class OptionParsers(BaseParsers[op.OptionParser]): def __init__( self, *formatters: op.HelpFormatter, prog: str | None = None, usage: str | None = None, description: str | None = None, epilog: str | None = None, ) -> None: assert len(set(formatters)) == len(formatters), "Duplicate formatter" self.parsers = [ op.OptionParser( prog=prog, usage=usage, description=description, epilog=epilog, formatter=formatter ) for formatter in formatters ] @copy_signature(op.OptionParser.add_option) def add_option(self, /, *args, **kwds) -> None: for parser in self.parsers: parser.add_option(*args, **kwds) @copy_signature(op.OptionParser.add_option_group) def add_option_group(self, /, *args, **kwds) -> OptionGroups: groups = OptionGroups() for parser in self.parsers: groups.append(parser.add_option_group(*args, **kwds)) return groups rich-argparse-1.8.0/tests/requirements.txt000066400000000000000000000001301517514052500206740ustar00rootroot00000000000000pytest coverage[toml] covdefaults pytest-cov typing-extensions; python_version < "3.10" rich-argparse-1.8.0/tests/test_argparse.py000066400000000000000000001423351517514052500206430ustar00rootroot00000000000000from __future__ import annotations import argparse import re import string import sys import textwrap from argparse import ( SUPPRESS, Action, ArgumentDefaultsHelpFormatter, ArgumentParser, HelpFormatter, MetavarTypeHelpFormatter, RawDescriptionHelpFormatter, RawTextHelpFormatter, ) from contextlib import nullcontext from unittest.mock import Mock, patch import pytest from rich import get_console from rich.console import Group from rich.markdown import Markdown from rich.table import Table from rich.text import Text import rich_argparse._lazy_rich as r from rich_argparse import ( ArgumentDefaultsRichHelpFormatter, HelpPreviewAction, MetavarTypeRichHelpFormatter, RawDescriptionRichHelpFormatter, RawTextRichHelpFormatter, RichHelpFormatter, ) from rich_argparse._common import _fix_legacy_win_text from rich_argparse._patching import patch_default_formatter_class from tests.helpers import ArgumentParsers, clean_argparse, get_cmd_output def test_params_substitution(): # in text (description, epilog, group description) and version: substitute %(prog)s # in help message: substitute %(param)s for all param in vars(action) parser = ArgumentParser( "awesome_program", description="This is the %(prog)s program.", epilog="The epilog of %(prog)s.", formatter_class=RichHelpFormatter, ) parser.add_argument("--version", action="version", version="%(prog)s 1.0.0") parser.add_argument("--option", default="value", help="help of option (default: %(default)s)") expected_help_output = """\ Usage: awesome_program [-h] [--version] [--option OPTION] This is the awesome_program program. Optional Arguments: -h, --help show this help message and exit --version show program's version number and exit --option OPTION help of option (default: value) The epilog of awesome_program. """ assert parser.format_help() == clean_argparse(expected_help_output) assert get_cmd_output(parser, cmd=["--version"]) == "awesome_program 1.0.0\n" @pytest.mark.parametrize("prog", (None, "PROG"), ids=("no_prog", "prog")) @pytest.mark.parametrize("usage", (None, "USAGE"), ids=("no_usage", "usage")) @pytest.mark.parametrize("description", (None, "A description."), ids=("no_desc", "desc")) @pytest.mark.parametrize("epilog", (None, "An epilog."), ids=("no_epilog", "epilog")) @pytest.mark.usefixtures("disable_group_name_formatter") def test_overall_structure(prog, usage, description, epilog): # The output must be consistent with the original HelpFormatter in these cases: # 1. no markup/emoji codes are used # 2. no short and long options with args are used # 3. group_name_formatter is disabled # 4. colors are disabled parsers = ArgumentParsers( HelpFormatter, RichHelpFormatter, prog=prog, usage=usage, description=description, epilog=epilog, ) parsers.add_argument("file", default="-", help="A file (default: %(default)s).") parsers.add_argument("spaces", help="Arg with weird\n\n whitespaces\t\t.") parsers.add_argument("--very-very-very-very-very-very-very-very-long-option-name", help="help!") # all types of empty groups parsers.add_argument_group("empty group name", description="empty_group description") parsers.add_argument_group("no description empty group name") parsers.add_argument_group("", description="empty_name_empty_group description") parsers.add_argument_group(description="no_name_empty_group description") parsers.add_argument_group("spaces group", description=" \tspaces_group description ") parsers.add_argument_group(SUPPRESS, description="suppressed_name_group description") parsers.add_argument_group(SUPPRESS, description=SUPPRESS) # all types of non-empty groups groups = parsers.add_argument_group("group name", description="group description") groups.add_argument("arg", help="help inside group") no_desc_groups = parsers.add_argument_group("no description group name") no_desc_groups.add_argument("arg", help="arg help inside no_desc_group") empty_name_groups = parsers.add_argument_group("", description="empty_name_group description") empty_name_groups.add_argument("arg", help="arg help inside empty_name_group") no_name_groups = parsers.add_argument_group(description="no_name_group description") no_name_groups.add_argument("arg", help="arg help inside no_name_group") no_name_no_desc_groups = parsers.add_argument_group() no_name_no_desc_groups.add_argument("arg", help="arg help inside no_name_no_desc_group") suppressed_name_groups = parsers.add_argument_group( SUPPRESS, description="suppressed_name_group description" ) suppressed_name_groups.add_argument("arg", help="arg help inside suppressed_name_group") suppressed_name_desc_groups = parsers.add_argument_group(SUPPRESS, description=SUPPRESS) suppressed_name_desc_groups.add_argument( "arg", help="arg help inside suppressed_name_desc_group" ) parsers.assert_format_help_equal() @pytest.mark.usefixtures("disable_group_name_formatter") def test_padding_and_wrapping(): parsers = ArgumentParsers( HelpFormatter, RichHelpFormatter, prog="PROG", description="-" * 120, epilog="%" * 120 ) parsers.add_argument("--very-long-option-name", metavar="LONG_METAVAR", help="." * 120) groups_with_description = parsers.add_argument_group("group", description="*" * 120) groups_with_description.add_argument("pos-arg", help="#" * 120) parsers.add_argument_group( "= =" * 40, description="group with a very long name that should not wrap" ) expected_help_output = """\ usage: PROG [-h] [--very-long-option-name LONG_METAVAR] pos-arg -------------------------------------------------------------------------------------------------- ---------------------- optional arguments: -h, --help show this help message and exit --very-long-option-name LONG_METAVAR .......................................................................... .............................................. group: ********************************************************************************************** ************************** pos-arg ########################################################################## ############################################## = == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =: group with a very long name that should not wrap %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% """ parsers.assert_format_help_equal(expected=clean_argparse(expected_help_output)) @pytest.mark.xfail(reason="rich wraps differently") @pytest.mark.usefixtures("disable_group_name_formatter") def test_wrapping_compatible(): # needs fixing rich wrapping to be compatible with textwrap.wrap parsers = ArgumentParsers( HelpFormatter, RichHelpFormatter, prog="PROG", description="some text " + "-" * 120 ) parsers.assert_format_help_equal() @pytest.mark.parametrize("title", (None, "available commands"), ids=("no_title", "title")) @pytest.mark.parametrize("description", (None, "subparsers description"), ids=("no_desc", "desc")) @pytest.mark.parametrize("dest", (None, "command"), ids=("no_dest", "dest")) @pytest.mark.parametrize("metavar", (None, ""), ids=("no_mv", "mv")) @pytest.mark.parametrize("help", (None, "The subcommand to execute"), ids=("no_help", "help")) @pytest.mark.parametrize("required", (False, True), ids=("opt", "req")) @pytest.mark.usefixtures("disable_group_name_formatter") def test_subparsers(title, description, dest, metavar, help, required): subparsers_kwargs = { "title": title, "description": description, "dest": dest, "metavar": metavar, "help": help, "required": required, } subparsers_kwargs = {k: v for k, v in subparsers_kwargs.items() if v is not None} parsers = ArgumentParsers(HelpFormatter, RichHelpFormatter) subparsers_actions = parsers.add_subparsers(**subparsers_kwargs) subparsers = subparsers_actions.add_parser("help", help="help subcommand.") parsers.assert_format_help_equal() subparsers.assert_format_help_equal() @pytest.mark.usefixtures("disable_group_name_formatter") def test_escape_params(): # params such as %(prog)s and %(default)s must be escaped when substituted parsers = ArgumentParsers( HelpFormatter, RichHelpFormatter, prog="[underline]", usage="%(prog)s [%%options] %% [args]\n%%%(prog)s %%(prog)s [%%%%options] %%%% [args]", description="%(prog)s description.", epilog="%(prog)s epilog.", ) class SpecialType(str): ... SpecialType.__name__ = "[link]" parsers.add_argument("--version", action="version", version="%(prog)s %%1.0.0") parsers.add_argument("pos-arg", metavar="[italic]", help="help of pos arg with special metavar") parsers.add_argument( "--default", default="[default]", help="help with special default: %(default)s" ) parsers.add_argument("--type", type=SpecialType, help="help with special type: %(type)s") parsers.add_argument( "--metavar", metavar="[bold]", help="help with special metavar: %(metavar)s" ) parsers.add_argument( "--float", type=float, default=1.5, help="help with float conversion: %(default).5f" ) parsers.add_argument("--repr", type=str, help="help with repr conversion: %(type)r") parsers.add_argument( "--percent", help="help with percent escaping: %%(prog)s %%%(prog)s %% %%%% %%%%prog" ) expected_help_output = """\ usage: [underline] [%options] % [args] %[underline] %(prog)s [%%options] %% [args] [underline] description. positional arguments: [italic] help of pos arg with special metavar optional arguments: -h, --help show this help message and exit --version show program's version number and exit --default DEFAULT help with special default: [default] --type TYPE help with special type: [link] --metavar [bold] help with special metavar: [bold] --float FLOAT help with float conversion: 1.50000 --repr REPR help with repr conversion: 'str' --percent PERCENT help with percent escaping: %(prog)s %[underline] % %% %%prog [underline] epilog. """ parsers.assert_format_help_equal(expected=clean_argparse(expected_help_output)) parsers.assert_cmd_output_equal(cmd=["--version"], expected="[underline] %1.0.0\n") @pytest.mark.usefixtures("force_color") def test_generated_usage(): parser = ArgumentParser("PROG", formatter_class=RichHelpFormatter) parser.add_argument("file") parser.add_argument("hidden", help=SUPPRESS) parser.add_argument("--weird", metavar="y)") hidden_group = parser.add_mutually_exclusive_group() hidden_group.add_argument("--hidden-group-arg1", help=SUPPRESS) hidden_group.add_argument("--hidden-group-arg2", help=SUPPRESS) parser.add_argument("--required", metavar="REQ", required=True) mut_ex = parser.add_mutually_exclusive_group() mut_ex.add_argument("--flag", action="store_true", help="Is flag?") mut_ex.add_argument("--not-flag", action="store_true", help="Is not flag?") req_mut_ex = parser.add_mutually_exclusive_group(required=True) req_mut_ex.add_argument("-y", help="Yes.") req_mut_ex.add_argument("-n", help="No.") usage_text = ( "\x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] " "[\x1b[36m--weird\x1b[0m \x1b[38;5;36my)\x1b[0m] " "\x1b[36m--required\x1b[0m \x1b[38;5;36mREQ\x1b[0m " "[\x1b[36m--flag\x1b[0m | \x1b[36m--not-flag\x1b[0m] " "(\x1b[36m-y\x1b[0m \x1b[38;5;36mY\x1b[0m | \x1b[36m-n\x1b[0m \x1b[38;5;36mN\x1b[0m) " "\x1b[36mfile\x1b[0m" ) if sys.version_info >= (3, 11): # pragma: >=3.11 cover usage_text = usage_text.replace(" ", " ") expected_help_output = f"""\ \x1b[38;5;208mUsage:\x1b[0m {usage_text} \x1b[38;5;208mPositional Arguments:\x1b[0m \x1b[36mfile\x1b[0m \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--weird\x1b[0m \x1b[38;5;36my)\x1b[0m \x1b[36m--required\x1b[0m \x1b[38;5;36mREQ\x1b[0m \x1b[36m--flag\x1b[0m \x1b[39mIs flag?\x1b[0m \x1b[36m--not-flag\x1b[0m \x1b[39mIs not flag?\x1b[0m \x1b[36m-y\x1b[0m \x1b[38;5;36mY\x1b[0m \x1b[39mYes.\x1b[0m \x1b[36m-n\x1b[0m \x1b[38;5;36mN\x1b[0m \x1b[39mNo.\x1b[0m """ assert parser.format_help() == clean_argparse(expected_help_output) @pytest.mark.parametrize( ("usage", "expected", "usage_markup"), ( pytest.param( "%(prog)s [bold] PROG_CMD[/]", "\x1b[38;5;244mPROG\x1b[0m [bold] PROG_CMD[/]", None, id="default", ), pytest.param( "%(prog)s [bold] PROG_CMD[/]", "\x1b[38;5;244mPROG\x1b[0m [bold] PROG_CMD[/]", False, id="no_markup", ), pytest.param( "%(prog)s [bold] PROG_CMD[/]", "\x1b[38;5;244mPROG\x1b[0m \x1b[1m PROG_CMD\x1b[0m", True, id="markup", ), pytest.param( "PROG %(prog)s [bold] %(prog)s [/]\n%(prog)r", ( "PROG " "\x1b[38;5;244mPROG\x1b[0m " "\x1b[1m \x1b[0m\x1b[1;38;5;244mPROG\x1b[0m" # "\x1b[1m \x1b[0m" "\n\x1b[38;5;244m'PROG'\x1b[0m" ), True, id="prog_prog", ), ), ) @pytest.mark.usefixtures("force_color") def test_user_usage(usage, expected, usage_markup): parser = ArgumentParser(prog="PROG", usage=usage, formatter_class=RichHelpFormatter) if usage_markup is not None: ctx = patch.object(RichHelpFormatter, "usage_markup", usage_markup) else: ctx = nullcontext() with ctx: assert parser.format_usage() == f"\x1b[38;5;208mUsage:\x1b[0m {expected}\n" @pytest.mark.usefixtures("force_color") def test_actions_spans_in_usage(): parser = ArgumentParser("PROG", formatter_class=RichHelpFormatter) parser.add_argument("required") parser.add_argument("int", nargs=2) parser.add_argument("optional", nargs=argparse.OPTIONAL) parser.add_argument("zom", nargs=argparse.ZERO_OR_MORE) parser.add_argument("oom", nargs=argparse.ONE_OR_MORE) parser.add_argument("remainder", nargs=argparse.REMAINDER) parser.add_argument("parser", nargs=argparse.PARSER) parser.add_argument("suppress", nargs=argparse.SUPPRESS) mut_ex = parser.add_mutually_exclusive_group() mut_ex.add_argument("--opt", nargs="?") mut_ex.add_argument("--opts", nargs="+") usage_text = ( "\x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] " "[\x1b[36m--opt\x1b[0m [\x1b[38;5;36mOPT\x1b[0m] | " "\x1b[36m--opts\x1b[0m \x1b[38;5;36mOPTS\x1b[0m [\x1b[38;5;36mOPTS\x1b[0m \x1b[38;5;36m...\x1b[0m]]\n " "\x1b[36mrequired\x1b[0m \x1b[36mint\x1b[0m \x1b[36mint\x1b[0m [\x1b[36moptional\x1b[0m] " "[\x1b[36mzom\x1b[0m \x1b[36m...\x1b[0m] \x1b[36moom\x1b[0m [\x1b[36moom\x1b[0m \x1b[36m...\x1b[0m] \x1b[36m...\x1b[0m \x1b[36mparser\x1b[0m \x1b[36m...\x1b[0m" ) expected_help_output = f"""\ {usage_text} \x1b[38;5;208mPositional Arguments:\x1b[0m \x1b[36mrequired\x1b[0m \x1b[36mint\x1b[0m \x1b[36moptional\x1b[0m \x1b[36mzom\x1b[0m \x1b[36moom\x1b[0m \x1b[36mremainder\x1b[0m \x1b[36mparser\x1b[0m \x1b[36msuppress\x1b[0m \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--opt\x1b[0m [\x1b[38;5;36mOPT\x1b[0m] \x1b[36m--opts\x1b[0m \x1b[38;5;36mOPTS\x1b[0m [\x1b[38;5;36mOPTS\x1b[0m \x1b[38;5;36m...\x1b[0m] """ assert parser.format_help() == clean_argparse(expected_help_output) @pytest.mark.usefixtures("force_color") def test_boolean_optional_action_spans(): parser = ArgumentParser("PROG", formatter_class=RichHelpFormatter) parser.add_argument("--bool", action=argparse.BooleanOptionalAction) expected_help_output = """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] [\x1b[36m--bool\x1b[0m | \x1b[36m--no-bool\x1b[0m] \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--bool\x1b[0m, \x1b[36m--no-bool\x1b[0m """ assert parser.format_help() == clean_argparse(expected_help_output) def test_usage_spans_errors(): parser = ArgumentParser() parser._optionals.required = False actions = parser._actions groups = [parser._optionals] formatter = RichHelpFormatter("PROG") if sys.version_info >= (3, 14): # pragma: >=3.14 cover formatter._set_color(False) with patch.object(RichHelpFormatter, "_rich_usage_spans", side_effect=ValueError): formatter.add_usage(usage=None, actions=actions, groups=groups, prefix=None) (usage,) = formatter._root_section.rich_items assert isinstance(usage, Text) assert str(usage).rstrip() == "Usage: PROG [-h]" prefix_span, prog_span = usage.spans assert prefix_span.start == 0 assert prefix_span.end == len("usage:") assert prefix_span.style == "argparse.groups" assert prog_span.start == len("usage: ") assert prog_span.end == len("usage: PROG") assert prog_span.style == "argparse.prog" def test_no_help(): formatter = RichHelpFormatter("prog") formatter.add_usage(usage=SUPPRESS, actions=[], groups=[]) out = formatter.format_help() assert not formatter._root_section.rich_items assert not out @pytest.mark.usefixtures("disable_group_name_formatter") def test_raw_description_rich_help_formatter(): long_text = " ".join(["The quick brown fox jumps over the lazy dog."] * 3) parsers = ArgumentParsers( RawDescriptionHelpFormatter, RawDescriptionRichHelpFormatter, prog="PROG", description=long_text, epilog=long_text, ) groups = parsers.add_argument_group("group", description=long_text) groups.add_argument("--long", help=long_text) expected_help_output = """\ usage: PROG [-h] [--long LONG] The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. optional arguments: -h, --help show this help message and exit group: The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. --long LONG The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. """ parsers.assert_format_help_equal(expected=clean_argparse(expected_help_output)) @pytest.mark.usefixtures("disable_group_name_formatter") def test_raw_text_rich_help_formatter(): long_text = " ".join(["The quick brown fox jumps over the lazy dog."] * 3) parsers = ArgumentParsers( RawTextHelpFormatter, RawTextRichHelpFormatter, prog="PROG", description=long_text, epilog=long_text, ) groups = parsers.add_argument_group("group", description=long_text) groups.add_argument("--long", help=long_text) expected_help_output = """\ usage: PROG [-h] [--long LONG] The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. optional arguments: -h, --help show this help message and exit group: The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. --long LONG The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. """ parsers.assert_format_help_equal(expected=clean_argparse(expected_help_output)) @pytest.mark.usefixtures("disable_group_name_formatter") def test_argument_default_rich_help_formatter(): parsers = ArgumentParsers( ArgumentDefaultsHelpFormatter, ArgumentDefaultsRichHelpFormatter, prog="PROG" ) parsers.add_argument("--option", default="def", help="help of option") expected_help_output = """\ usage: PROG [-h] [--option OPTION] optional arguments: -h, --help show this help message and exit --option OPTION help of option (default: def) """ parsers.assert_format_help_equal(expected=clean_argparse(expected_help_output)) @pytest.mark.usefixtures("disable_group_name_formatter") def test_metavar_type_help_formatter(): parsers = ArgumentParsers(MetavarTypeHelpFormatter, MetavarTypeRichHelpFormatter, prog="PROG") parsers.add_argument("--count", type=int, default=0, help="how many?") expected_help_output = """\ usage: PROG [-h] [--count int] optional arguments: -h, --help show this help message and exit --count int how many? """ parsers.assert_format_help_equal(expected=clean_argparse(expected_help_output)) def test_django_rich_help_formatter(): # https://github.com/django/django/blob/8eed30aec6/django/core/management/base.py#L105-L131 class DjangoHelpFormatter(HelpFormatter): """ Customized formatter so that command-specific arguments appear in the --help output before arguments common to all commands. """ show_last = { "--version", "--verbosity", "--traceback", "--settings", "--pythonpath", "--no-color", "--force-color", "--skip-checks", } def _reordered_actions(self, actions): return sorted(actions, key=lambda a: set(a.option_strings) & self.show_last != set()) def add_usage(self, usage, actions, *args, **kwargs): super().add_usage(usage, self._reordered_actions(actions), *args, **kwargs) def add_arguments(self, actions): super().add_arguments(self._reordered_actions(actions)) class DjangoRichHelpFormatter(DjangoHelpFormatter, RichHelpFormatter): """Rich help message formatter with django's special ordering of arguments.""" parser = ArgumentParser("command", formatter_class=DjangoRichHelpFormatter) parser.add_argument("--version", action="version", version="1.0.0") parser.add_argument("--traceback", action="store_true", help="show traceback") parser.add_argument("my-arg", help="custom argument.") parser.add_argument("--my-option", action="store_true", help="custom option") parser.add_argument("--verbosity", action="count", help="verbosity level") parser.add_argument("-a", "--an-option", action="store_true", help="another custom option") expected_help_output = """\ Usage: command [-h] [--my-option] [-a] [--version] [--traceback] [--verbosity] my-arg Positional Arguments: my-arg custom argument. Optional Arguments: -h, --help show this help message and exit --my-option custom option -a, --an-option another custom option --version show program's version number and exit --traceback show traceback --verbosity verbosity level """ assert parser.format_help() == clean_argparse(expected_help_output) @pytest.mark.parametrize("indent_increment", (1, 3)) @pytest.mark.parametrize("max_help_position", (25, 26, 27)) @pytest.mark.parametrize("width", (None, 70)) @pytest.mark.usefixtures("disable_group_name_formatter") def test_help_formatter_args(indent_increment, max_help_position, width): # Note: the length of the option string is chosen to test edge cases where it is less than, # equal to, and bigger than max_help_position parsers = ArgumentParsers( lambda prog: HelpFormatter(prog, indent_increment, max_help_position, width), lambda prog: RichHelpFormatter(prog, indent_increment, max_help_position, width), prog="program", ) parsers.add_argument("option-of-certain-length", help="This is the help of the said option") parsers.assert_format_help_equal() def test_return_output(): parser = ArgumentParser("prog", formatter_class=RichHelpFormatter) assert parser.format_help() @pytest.mark.usefixtures("force_color") def test_text_highlighter(): parser = ArgumentParser("PROG", formatter_class=RichHelpFormatter) parser.add_argument("arg", help="Did you try `RichHelpFormatter.highlighter`?") expected_help_output = """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] \x1b[36marg\x1b[0m \x1b[38;5;208mPositional Arguments:\x1b[0m \x1b[36marg\x1b[0m \x1b[39mDid you try `\x1b[0m\x1b[1;39mRichHelpFormatter.highlighter\x1b[0m\x1b[39m`?\x1b[0m \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m """ # Make sure we can use a style multiple times in regexes pattern_with_duplicate_style = r"'(?P[^']*)'" RichHelpFormatter.highlights.append(pattern_with_duplicate_style) assert parser.format_help() == clean_argparse(expected_help_output) RichHelpFormatter.highlights.remove(pattern_with_duplicate_style) @pytest.mark.usefixtures("force_color") def test_default_highlights(): parser = ArgumentParser( "PROG", formatter_class=RichHelpFormatter, description="Description with `syntax` and --options.", epilog="Epilog with `syntax` and --options.", ) # syntax highlights parser.add_argument("--syntax-normal", action="store_true", help="Start `middle` end") parser.add_argument("--syntax-start", action="store_true", help="`Start` middle end") parser.add_argument("--syntax-end", action="store_true", help="Start middle `end`") # options highlights parser.add_argument("--option-normal", action="store_true", help="Start --middle end") parser.add_argument("--option-start", action="store_true", help="--Start middle end") parser.add_argument("--option-end", action="store_true", help="Start middle --end") parser.add_argument("--option-comma", action="store_true", help="Start --middle, end") parser.add_argument("--option-multi", action="store_true", help="Start --middle-word end") parser.add_argument("--option-not", action="store_true", help="Start middle-word end") parser.add_argument("--option-short", action="store_true", help="Start -middle end") # options inside backticks should not be highlighted parser.add_argument("--not-option", action="store_true", help="Start `not --option` end") # %(default)s highlights parser.add_argument("--default", default=10, help="The default value is %(default)s.") expected_help_output = """ \x1b[39mDescription with `\x1b[0m\x1b[1;39msyntax\x1b[0m\x1b[39m` and \x1b[0m\x1b[36m--options\x1b[0m\x1b[39m.\x1b[0m \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--syntax-normal\x1b[0m \x1b[39mStart `\x1b[0m\x1b[1;39mmiddle\x1b[0m\x1b[39m` end\x1b[0m \x1b[36m--syntax-start\x1b[0m \x1b[39m`\x1b[0m\x1b[1;39mStart\x1b[0m\x1b[39m` middle end\x1b[0m \x1b[36m--syntax-end\x1b[0m \x1b[39mStart middle `\x1b[0m\x1b[1;39mend\x1b[0m\x1b[39m`\x1b[0m \x1b[36m--option-normal\x1b[0m \x1b[39mStart \x1b[0m\x1b[36m--middle\x1b[0m\x1b[39m end\x1b[0m \x1b[36m--option-start\x1b[0m \x1b[36m--Start\x1b[0m\x1b[39m middle end\x1b[0m \x1b[36m--option-end\x1b[0m \x1b[39mStart middle \x1b[0m\x1b[36m--end\x1b[0m \x1b[36m--option-comma\x1b[0m \x1b[39mStart \x1b[0m\x1b[36m--middle\x1b[0m\x1b[39m, end\x1b[0m \x1b[36m--option-multi\x1b[0m \x1b[39mStart \x1b[0m\x1b[36m--middle-word\x1b[0m\x1b[39m end\x1b[0m \x1b[36m--option-not\x1b[0m \x1b[39mStart middle-word end\x1b[0m \x1b[36m--option-short\x1b[0m \x1b[39mStart \x1b[0m\x1b[36m-middle\x1b[0m\x1b[39m end\x1b[0m \x1b[36m--not-option\x1b[0m \x1b[39mStart `\x1b[0m\x1b[1;39mnot --option\x1b[0m\x1b[39m` end\x1b[0m \x1b[36m--default\x1b[0m \x1b[38;5;36mDEFAULT\x1b[0m \x1b[39mThe default value is \x1b[0m\x1b[3;39m10\x1b[0m\x1b[39m.\x1b[0m \x1b[39mEpilog with `\x1b[0m\x1b[1;39msyntax\x1b[0m\x1b[39m` and \x1b[0m\x1b[36m--options\x1b[0m\x1b[39m.\x1b[0m """ assert parser.format_help().endswith(clean_argparse(expected_help_output)) @pytest.mark.usefixtures("force_color") def test_subparsers_usage(): # Parent uses RichHelpFormatter rich_parent = ArgumentParser("PROG", formatter_class=RichHelpFormatter) rich_subparsers = rich_parent.add_subparsers() rich_child1 = rich_subparsers.add_parser("sp1", formatter_class=RichHelpFormatter) rich_child2 = rich_subparsers.add_parser("sp2") assert rich_parent.format_usage() == ( "\x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] " "\x1b[36m{sp1,sp2}\x1b[0m \x1b[36m...\x1b[0m\n" ) assert rich_child1.format_usage() == ( "\x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG sp1\x1b[0m [\x1b[36m-h\x1b[0m]\n" ) if sys.version_info < (3, 14): # pragma: <3.14 cover assert rich_child2.format_usage() == "usage: PROG sp2 [-h]\n" else: # pragma: >=3.14 cover # Python 3.14 adds ANSI color codes to the default usage message assert ( rich_child2.format_usage() == "\x1b[1;34musage: \x1b[0m\x1b[1;35mPROG sp2\x1b[0m [\x1b[32m-h\x1b[0m]\n" ) # Parent uses original formatter orig_parent = ArgumentParser("PROG") orig_subparsers = orig_parent.add_subparsers() orig_child1 = orig_subparsers.add_parser("sp1", formatter_class=RichHelpFormatter) orig_child2 = orig_subparsers.add_parser("sp2") if sys.version_info < (3, 14): # pragma: <3.14 cover assert orig_parent.format_usage() == ("usage: PROG [-h] {sp1,sp2} ...\n") else: # pragma: >=3.14 cover # Python 3.14 adds ANSI color codes to the default usage message assert orig_parent.format_usage() == ( "\x1b[1;34musage: \x1b[0m\x1b[1;35mPROG\x1b[0m [\x1b[32m-h\x1b[0m] \x1b[32m{sp1,sp2} ...\x1b[0m\n" ) assert orig_child1.format_usage() == ( "\x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG sp1\x1b[0m [\x1b[36m-h\x1b[0m]\n" ) if sys.version_info < (3, 14): # pragma: <3.14 cover assert orig_child2.format_usage() == "usage: PROG sp2 [-h]\n" elif sys.version_info[:3] == (3, 14, 0): # pragma: no cover # Subparsers prog broken in Python 3.14.0 # TODO: remove when Python 3.14.1 becomes available on GitHub Actions assert ( orig_child2.format_usage() == "\x1b[1;34musage: \x1b[0m\x1b[1;35m\x1b[1;34m\x1b[0m\x1b[1;35mPROG\x1b[0m sp2\x1b[0m [\x1b[32m-h\x1b[0m]\n" ) else: # pragma: >=3.15 cover # Python 3.14 adds ANSI color codes to the default usage message # TODO: change to 'pragma: >=3.14 cover' when Python 3.14.1 becomes available on GitHub Actions assert ( orig_child2.format_usage() == "\x1b[1;34musage: \x1b[0m\x1b[1;35mPROG sp2\x1b[0m [\x1b[32m-h\x1b[0m]\n" ) @pytest.mark.parametrize("ct", string.printable) def test_expand_help_format_specifier(ct): prog = 1 if ct in "cdeEfFgGiouxX*" else "PROG" help_formatter = RichHelpFormatter(prog=prog) action = Action(["-t"], dest="test", help=f"%(prog){ct}") try: expected = help_formatter._expand_help(action) except (ValueError, TypeError) as e: with pytest.raises(type(e)) as exc_info: help_formatter._rich_expand_help(action) assert exc_info.value.args == e.args else: assert help_formatter._rich_expand_help(action).plain == expected def test_rich_lazy_import(): sys_modules_no_rich = { mod_name: mod for mod_name, mod in sys.modules.items() if mod_name != "rich" and not mod_name.startswith("rich.") } lazy_rich = {k: v for k, v in r.__dict__.items() if k not in r.__all__} with ( patch.dict(sys.modules, sys_modules_no_rich, clear=True), patch.dict(r.__dict__, lazy_rich, clear=True), ): parser = ArgumentParser(formatter_class=RichHelpFormatter) parser.add_argument("--foo", help="foo help") args = parser.parse_args(["--foo", "bar"]) assert args.foo == "bar" assert sys.modules assert "rich" not in sys.modules # no help formatting, do not import rich for mod_name in sys.modules: assert not mod_name.startswith("rich.") parser.format_help() assert "rich" in sys.modules # format help has been called formatter = RichHelpFormatter("PROG") assert formatter._console is None formatter.console = get_console() assert formatter._console is not None with pytest.raises(AttributeError, match="Foo"): _ = r.Foo def test_help_with_control_codes(): parsers = ArgumentParsers(HelpFormatter, RichHelpFormatter, prog="PROG\r\nRAM") parsers.add_argument( "--long-option-with-control-codes-in-metavar", metavar="META\r\nVAR", help="%(metavar)s" ) orig_parser, rich_parser = parsers.parsers orig_help = orig_parser.format_help().lower() rich_help = rich_parser.format_help().lower() assert rich_help == orig_help.replace("\r", "") # rich strips \r and other control codes expected_help_text = """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m \x1b[38;5;244mRAM\x1b[0m [\x1b[36m-h\x1b[0m] [\x1b[36m--long-option-with-control-codes-in-metavar\x1b[0m \x1b[38;5;36mMETA\x1b[0m \x1b[38;5;36mVAR\x1b[0m] \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--long-option-with-control-codes-in-metavar\x1b[0m \x1b[38;5;36mMETA\x1b[0m \x1b[38;5;36mVAR\x1b[0m \x1b[39mMETA VAR\x1b[0m """ with patch("rich.console.Console.is_terminal", return_value=True): colored_help_text = rich_parser.format_help() # cannot use textwrap.dedent because of the control codes assert colored_help_text == clean_argparse(expected_help_text, dedent=False) @pytest.mark.skipif(sys.platform != "win32", reason="windows-only test") @pytest.mark.usefixtures("force_color") def test_legacy_windows(): # pragma: win32 cover expected_output = """\ Usage: PROG [-h] Optional Arguments: -h, --help show this help message and exit """ expected_colored_output = """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m """ # New windows console => colors: YES, initialization: NO init_win_colors = Mock(return_value=True) parser = ArgumentParser("PROG", formatter_class=RichHelpFormatter) with patch("rich_argparse._common._initialize_win_colors", init_win_colors): help = parser.format_help() assert help == clean_argparse(expected_colored_output) init_win_colors.assert_not_called() # Legacy windows console on new windows => colors: YES, initialization: YES init_win_colors = Mock(return_value=True) parser = ArgumentParser("PROG", formatter_class=RichHelpFormatter) with ( patch("rich.console.detect_legacy_windows", return_value=True), patch("rich_argparse._common._initialize_win_colors", init_win_colors), ): help = parser.format_help() assert help == clean_argparse(expected_colored_output) init_win_colors.assert_called_once_with() # Legacy windows console on old windows => colors: NO, initialization: YES init_win_colors = Mock(return_value=False) parser = ArgumentParser("PROG", formatter_class=RichHelpFormatter) with ( patch("rich.console.detect_legacy_windows", return_value=True), patch("rich_argparse._common._initialize_win_colors", init_win_colors), ): help = parser.format_help() assert help == clean_argparse(expected_output) init_win_colors.assert_called_once_with() # Legacy windows, but colors disabled in formatter => colors: NO, initialization: NO def fmt_no_color(prog): fmt = RichHelpFormatter(prog) fmt.console = r.Console(theme=r.Theme(fmt.styles), color_system=None) return fmt init_win_colors = Mock(return_value=True) no_colors_parser = ArgumentParser("PROG", formatter_class=fmt_no_color) with ( patch("rich.console.detect_legacy_windows", return_value=True), patch("rich_argparse._common._initialize_win_colors", init_win_colors), ): help = no_colors_parser.format_help() assert help == clean_argparse(expected_output) init_win_colors.assert_not_called() @pytest.mark.skipif(sys.platform == "win32", reason="non-windows test") def test_no_win_console_init_on_unix(): # pragma: win32 no cover text = "\x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m]" console = r.Console(legacy_windows=True, force_terminal=True) init_win_colors = Mock(return_value=True) with patch("rich_argparse._common._initialize_win_colors", init_win_colors): out = _fix_legacy_win_text(console, text) assert out == text init_win_colors.assert_not_called() @pytest.mark.usefixtures("force_color") def test_rich_renderables(): table = Table("foo", "bar") table.add_row("1", "2") parser = ArgumentParser( "PROG", formatter_class=RichHelpFormatter, description=Markdown( textwrap.dedent( """\ This is a **description** _________________________ | foo | bar | | --- | --- | | 1 | 2 | """ ) ), epilog=Group(Markdown("This is an *epilog*"), table, Text("The end.", style="red")), ) expected_help = """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] This is a \x1b[1mdescription\x1b[0m \x1b[2m--------------------------------------------------------------------------------------------------\x1b[0m \x1b[36m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m \x1b[0m\x1b[36m \x1b[0m\x1b[36mbar\x1b[0m \x1b[36m โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\x1b[0m \x1b[36m \x1b[0m1 \x1b[36m \x1b[0m2 \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m This is an \x1b[3mepilog\x1b[0m โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”“ โ”ƒ\x1b[1m \x1b[0m\x1b[1mfoo\x1b[0m\x1b[1m \x1b[0mโ”ƒ\x1b[1m \x1b[0m\x1b[1mbar\x1b[0m\x1b[1m \x1b[0mโ”ƒ โ”กโ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”ฉ โ”‚ 1 โ”‚ 2 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”˜ \x1b[31mThe end.\x1b[0m """ assert parser.format_help() == clean_argparse(expected_help) def test_help_preview_generation(tmp_path): parser = ArgumentParser("PROG", formatter_class=RichHelpFormatter) parser.add_argument("--foo", help="foo help") preview_action = parser.add_argument("--generate", action=HelpPreviewAction) default_path = tmp_path / "default-preview.svg" parser.add_argument("--generate-with-default", action=HelpPreviewAction, path=str(default_path)) # No namespace pollution args = parser.parse_args(["--foo", "FOO"]) assert vars(args) == {"foo": "FOO"} # No help pollution assert "--generate" not in parser.format_help() # No file, error with pytest.raises(SystemExit) as exc_info: parser.parse_args(["--generate"]) assert exc_info.value.code == 1 # Default file, ok with pytest.raises(SystemExit) as exc_info: parser.parse_args(["--generate-with-default"]) assert exc_info.value.code == 0 assert default_path.exists() # SVG file svg_file = tmp_path / "preview.svg" with pytest.raises(SystemExit) as exc_info: parser.parse_args(["--generate", str(svg_file)]) assert exc_info.value.code == 0 assert svg_file.exists() svg_out = svg_file.read_text() assert svg_out.startswith("") assert "Usage" in html_out # TXT file preview_action.export_kwds = {} txt_file = tmp_path / "preview.txt" with pytest.raises(SystemExit) as exc_info: parser.parse_args(["--generate", str(txt_file)]) assert exc_info.value.code == 0 assert txt_file.exists() assert txt_file.read_text().startswith("Usage:") # Wrong file extension with pytest.raises(SystemExit) as exc_info: parser.parse_args(["--generate", str(tmp_path / "preview.png")]) assert exc_info.value.code == 1 # Wrong type with pytest.raises(SystemExit) as exc_info: parser.parse_args(["--generate", ("",)]) assert exc_info.value.code == 1 def test_disable_help_markup(): parser = ArgumentParser( prog="PROG", formatter_class=RichHelpFormatter, description="[red]Description text.[/]" ) parser.add_argument("--foo", default="def", help="[red]Help text (default: %(default)s).[/]") with patch.object(RichHelpFormatter, "help_markup", False): help_text = parser.format_help() expected_help_text = """\ Usage: PROG [-h] [--foo FOO] Description text. Optional Arguments: -h, --help show this help message and exit --foo FOO [red]Help text (default: def).[/] """ assert help_text == clean_argparse(expected_help_text) def test_disable_text_markup(): parser = ArgumentParser( prog="PROG", formatter_class=RichHelpFormatter, description="[red]Description text.[/]" ) parser.add_argument("--foo", help="[red]Help text.[/]") with patch.object(RichHelpFormatter, "text_markup", False): help_text = parser.format_help() expected_help_text = """\ Usage: PROG [-h] [--foo FOO] [red]Description text.[/] Optional Arguments: -h, --help show this help message and exit --foo FOO Help text. """ assert help_text == clean_argparse(expected_help_text) @pytest.mark.usefixtures("force_color") def test_arg_default_spans(): parser = ArgumentParser(prog="PROG", formatter_class=RichHelpFormatter) parser.add_argument( "--foo", default="def", help="(default: %(default)r) [red](default: %(default)s)[/] (default: %(default)s)", ) expected_help_text = """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] [\x1b[36m--foo\x1b[0m \x1b[38;5;36mFOO\x1b[0m] \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--foo\x1b[0m \x1b[38;5;36mFOO\x1b[0m \x1b[39m(default: \x1b[0m\x1b[3;39m'def'\x1b[0m\x1b[39m) \x1b[0m\x1b[31m(default: \x1b[0m\x1b[3;39mdef\x1b[0m\x1b[31m)\x1b[0m\x1b[39m (default: \x1b[0m\x1b[3;39mdef\x1b[0m\x1b[39m)\x1b[0m """ help_text = parser.format_help() assert help_text == clean_argparse(expected_help_text) def test_arg_default_in_markup(): parser = ArgumentParser(prog="PROG", formatter_class=RichHelpFormatter) parser.add_argument( "--foo", default="def", help=( "[%(type)r type](good: %(default)r)[bad: %(default)r] text [bad: %(default)s]" "(good: %(default)s) [link bad %(default)s] %(default)s" ), ) expected_help_text = """\ Usage: PROG [-h] [--foo FOO] Optional Arguments: -h, --help show this help message and exit --foo FOO [None type](good: 'def') text (good: def) def """ with pytest.warns( UserWarning, match=re.escape( "Failed to process default value in help string of argument '--foo'.\n" "Hint: try disabling rich markup: `RichHelpFormatter.help_markup = False`\n" " or replace brackets by parenthesis: `[bad: %(default)r]` -> `(bad: %(default)r)`" ), ): help_text = parser.format_help() assert help_text == clean_argparse(expected_help_text) @pytest.mark.usefixtures("force_color") def test_metavar_spans(): # tests exotic metavars (tuples, wrapped, different nargs, etc.) in usage and help text parser = argparse.ArgumentParser( prog="PROG", formatter_class=lambda prog: RichHelpFormatter(prog, width=20) ) meg = parser.add_mutually_exclusive_group() meg.add_argument("--op1", metavar="MET", nargs="?") meg.add_argument("--op2", metavar=("MET1", "MET2"), nargs="*") meg.add_argument("--op3", nargs="*") meg.add_argument("--op4", metavar=("MET1", "MET2"), nargs="+") meg.add_argument("--op5", nargs="+") meg.add_argument("--op6", nargs=3) meg.add_argument("--op7", metavar=("MET1", "MET2", "MET3"), nargs=3) help_text = parser.format_help() if sys.version_info >= (3, 13): # pragma: >=3.13 cover usage_tail = """ | \x1b[36m--op2\x1b[0m [\x1b[38;5;36mMET1\x1b[0m [\x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36m...\x1b[0m]] | \x1b[36m--op3\x1b[0m [\x1b[38;5;36mOP3\x1b[0m \x1b[38;5;36m...\x1b[0m] | \x1b[36m--op4\x1b[0m \x1b[38;5;36mMET1\x1b[0m [\x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36m...\x1b[0m] | \x1b[36m--op5\x1b[0m \x1b[38;5;36mOP5\x1b[0m [\x1b[38;5;36mOP5\x1b[0m \x1b[38;5;36m...\x1b[0m] | \x1b[36m--op6\x1b[0m \x1b[38;5;36mOP6\x1b[0m \x1b[38;5;36mOP6\x1b[0m \x1b[38;5;36mOP6\x1b[0m | \x1b[36m--op7\x1b[0m \x1b[38;5;36mMET1\x1b[0m \x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36mMET3\x1b[0m] """ else: # pragma: <3.13 cover usage_tail = """ | \x1b[36m--op2\x1b[0m [\x1b[38;5;36mMET1\x1b[0m [\x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36m...\x1b[0m]] | \x1b[36m--op3\x1b[0m [\x1b[38;5;36mOP3\x1b[0m \x1b[38;5;36m...\x1b[0m] | \x1b[36m--op4\x1b[0m \x1b[38;5;36mMET1\x1b[0m [\x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36m...\x1b[0m] | \x1b[36m--op5\x1b[0m \x1b[38;5;36mOP5\x1b[0m [\x1b[38;5;36mOP5\x1b[0m \x1b[38;5;36m...\x1b[0m] | \x1b[36m--op6\x1b[0m \x1b[38;5;36mOP6\x1b[0m \x1b[38;5;36mOP6\x1b[0m \x1b[38;5;36mOP6\x1b[0m | \x1b[36m--op7\x1b[0m \x1b[38;5;36mMET1\x1b[0m \x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36mMET3\x1b[0m] """ expected_help_text = f"""\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] [\x1b[36m--op1\x1b[0m [\x1b[38;5;36mMET\x1b[0m]{usage_tail} \x1b[38;5;208mOptional Arguments:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help\x1b[0m \x1b[39mmessage and exit\x1b[0m \x1b[36m--op1\x1b[0m [\x1b[38;5;36mMET\x1b[0m] \x1b[36m--op2\x1b[0m [\x1b[38;5;36mMET1\x1b[0m [\x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36m...\x1b[0m]] \x1b[36m--op3\x1b[0m [\x1b[38;5;36mOP3\x1b[0m \x1b[38;5;36m...\x1b[0m] \x1b[36m--op4\x1b[0m \x1b[38;5;36mMET1\x1b[0m [\x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36m...\x1b[0m] \x1b[36m--op5\x1b[0m \x1b[38;5;36mOP5\x1b[0m [\x1b[38;5;36mOP5\x1b[0m \x1b[38;5;36m...\x1b[0m] \x1b[36m--op6\x1b[0m \x1b[38;5;36mOP6\x1b[0m \x1b[38;5;36mOP6\x1b[0m \x1b[38;5;36mOP6\x1b[0m \x1b[36m--op7\x1b[0m \x1b[38;5;36mMET1\x1b[0m \x1b[38;5;36mMET2\x1b[0m \x1b[38;5;36mMET3\x1b[0m """ assert help_text == clean_argparse(expected_help_text) def test_patching(): class MyArgumentParser(ArgumentParser): not_callable = None # Patch existing class patch_default_formatter_class(MyArgumentParser) assert MyArgumentParser().formatter_class is RichHelpFormatter # Override previous patch patch_default_formatter_class(MyArgumentParser, formatter_class=MetavarTypeRichHelpFormatter) assert MyArgumentParser().formatter_class is MetavarTypeRichHelpFormatter # Patch new class @patch_default_formatter_class(formatter_class=ArgumentDefaultsRichHelpFormatter) class MyArgumentParser2(ArgumentParser): pass assert MyArgumentParser2().formatter_class is ArgumentDefaultsRichHelpFormatter # Errors with pytest.raises(AttributeError, match=r"'MyArgumentParser' has no attribute 'missing'"): patch_default_formatter_class(MyArgumentParser, method_name="missing") with pytest.raises(TypeError, match=r"'MyArgumentParser\.not_callable' is not callable"): patch_default_formatter_class(MyArgumentParser, method_name="not_callable") rich-argparse-1.8.0/tests/test_contrib.py000066400000000000000000000062431517514052500204740ustar00rootroot00000000000000from __future__ import annotations from argparse import ArgumentParser from rich_argparse.contrib import ExtendedParagraphRichHelpFormatter, ParagraphRichHelpFormatter from tests.helpers import clean_argparse def test_paragraph_rich_help_formatter(): long_sentence = "The quick brown fox jumps over the lazy dog. " * 3 long_paragraphs = [long_sentence] * 2 long_text = "\n\n\r\n\t " + "\n\n".join(long_paragraphs) + "\n\n\r\n\t " parser = ArgumentParser( prog="PROG", description=long_text, epilog=long_text, formatter_class=ParagraphRichHelpFormatter, ) group = parser.add_argument_group("group", description=long_text) group.add_argument("--long", help=long_text) expected_help_output = """\ Usage: PROG [-h] [--long LONG] The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. Optional Arguments: -h, --help show this help message and exit Group: The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. --long LONG The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. """ assert parser.format_help() == clean_argparse(expected_help_output) def test_extended_paragraph_rich_help_formatter(): text = "\n\n\r\n\t The quick brown fox jumps over the lazy dog.\n\n\n- Brown fox\n\n- Lazy dog\n\n\r\n\t " parser = ArgumentParser( prog="PROG", description=text, epilog=text, formatter_class=ExtendedParagraphRichHelpFormatter, ) group = parser.add_argument_group("group", description=text) group.add_argument("--long", help=text) expected_help_output = """\ Usage: PROG [-h] [--long LONG] The quick brown fox jumps over the lazy dog. - Brown fox - Lazy dog Optional Arguments: -h, --help show this help message and exit Group: The quick brown fox jumps over the lazy dog. - Brown fox - Lazy dog --long LONG The quick brown fox jumps over the lazy dog. - Brown fox - Lazy dog The quick brown fox jumps over the lazy dog. - Brown fox - Lazy dog """ assert parser.format_help() == clean_argparse(expected_help_output) rich-argparse-1.8.0/tests/test_django.py000066400000000000000000000022251517514052500202720ustar00rootroot00000000000000from __future__ import annotations from argparse import ArgumentParser, HelpFormatter from types import ModuleType from unittest.mock import patch import pytest @pytest.fixture(autouse=True) def patch_django_import(): class DjangoHelpFormatter(HelpFormatter): ... class BaseCommand: def create_parser(self, *args, **kwargs): kwargs.setdefault("formatter_class", DjangoHelpFormatter) return ArgumentParser(*args, **kwargs) module = ModuleType("django.core.management.base") module.DjangoHelpFormatter = DjangoHelpFormatter module.BaseCommand = BaseCommand with patch.dict("sys.modules", {"django.core.management.base": module}, clear=False): yield def test_richify_command_line_help(): from django.core.management.base import BaseCommand, DjangoHelpFormatter from rich_argparse.django import DjangoRichHelpFormatter, richify_command_line_help parser = BaseCommand().create_parser("", "") assert parser.formatter_class is DjangoHelpFormatter richify_command_line_help() parser = BaseCommand().create_parser("", "") assert parser.formatter_class is DjangoRichHelpFormatter rich-argparse-1.8.0/tests/test_optparse.py000066400000000000000000000436261517514052500206770ustar00rootroot00000000000000from __future__ import annotations import sys from optparse import SUPPRESS_HELP, IndentedHelpFormatter, OptionParser, TitledHelpFormatter from textwrap import dedent from unittest.mock import Mock, patch import pytest from rich import get_console import rich_argparse._lazy_rich as r from rich_argparse.optparse import ( GENERATE_USAGE, IndentedRichHelpFormatter, RichHelpFormatter, TitledRichHelpFormatter, ) from tests.helpers import OptionParsers def test_default_substitution(): parser = OptionParser(prog="PROG", formatter=IndentedRichHelpFormatter()) parser.add_option("--option", default="[bold]", help="help of option (default: %default)") expected_help_output = """\ Usage: PROG [options] Options: -h, --help show this help message and exit --option=OPTION help of option (default: [bold]) """ assert parser.format_help() == dedent(expected_help_output) @pytest.mark.parametrize("prog", (None, "PROG"), ids=("no_prog", "prog")) @pytest.mark.parametrize("usage", (None, "USAGE"), ids=("no_usage", "usage")) @pytest.mark.parametrize("description", (None, "A description."), ids=("no_desc", "desc")) @pytest.mark.parametrize("epilog", (None, "An epilog."), ids=("no_epilog", "epilog")) def test_overall_structure(prog, usage, description, epilog): # The output must be consistent with the original HelpFormatter in these cases: # 1. no markup/emoji codes are used # 4. colors are disabled parsers = OptionParsers( IndentedHelpFormatter(), IndentedRichHelpFormatter(), prog=prog, usage=usage, description=description, epilog=epilog, ) parsers.add_option("--file", default="-", help="A file (default: %default).") parsers.add_option("--spaces", help="Arg with weird\n\n whitespaces\t\t.") parsers.add_option("--very-very-very-very-very-very-very-very-long-option-name", help="help!") parsers.add_option("--very-long-option-that-has-no-help-text") # all types of empty groups parsers.add_option_group("empty group name", description="empty_group description") parsers.add_option_group("no description empty group name") parsers.add_option_group("", description="empty_name_empty_group description") parsers.add_option_group("spaces group", description=" \tspaces_group description ") # all types of non-empty groups groups = parsers.add_option_group("title", description="description") groups.add_option("--arg1", help="help inside group") no_desc_groups = parsers.add_option_group("title") no_desc_groups.add_option("--arg2", help="arg help inside no_desc_group") empty_title_group = parsers.add_option_group("", description="description") empty_title_group.add_option("--arg3", help="arg help inside empty_title_group") parsers.assert_format_help_equal() def test_padding_and_wrapping(): parsers = OptionParsers( IndentedHelpFormatter(), IndentedRichHelpFormatter(), prog="PROG", description="-" * 120, epilog="%" * 120, ) parsers.add_option("--very-long-option-name", metavar="LONG_METAVAR", help="." * 120) group_with_descriptions = parsers.add_option_group("Group", description="*" * 120) group_with_descriptions.add_option("--arg", help="#" * 120) expected_help_output = """\ Usage: PROG [options] -------------------------------------------------------------------------------------------------- ---------------------- Options: -h, --help show this help message and exit --very-long-option-name=LONG_METAVAR .......................................................................... .............................................. Group: ****************************************************************************************** ****************************** --arg=ARG ########################################################################## ############################################## %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% """ parsers.assert_format_help_equal(expected=dedent(expected_help_output)) @pytest.mark.xfail(reason="rich wraps differently") def test_wrapping_compatible(): # needs fixing rich wrapping to be compatible with textwrap.wrap parsers = OptionParsers( IndentedHelpFormatter(), IndentedRichHelpFormatter(), prog="PROG", description="some text " + "-" * 120, ) parsers.assert_format_help_equal() @pytest.mark.usefixtures("force_color") def test_with_colors(): parser = OptionParser(prog="PROG", formatter=IndentedRichHelpFormatter()) parser.add_option("--file") parser.add_option("--hidden", help=SUPPRESS_HELP) parser.add_option("--flag", action="store_true", help="Is flag?") parser.add_option("--not-flag", action="store_true", help="Is not flag?") parser.add_option("-y", help="Yes.") parser.add_option("-n", help="No.") expected_help_output = """\ \x1b[38;5;208mUsage:\x1b[0m PROG [options] \x1b[38;5;208mOptions:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--file\x1b[0m=\x1b[38;5;36mFILE\x1b[0m \x1b[36m--flag\x1b[0m \x1b[39mIs flag?\x1b[0m \x1b[36m--not-flag\x1b[0m \x1b[39mIs not flag?\x1b[0m \x1b[36m-y\x1b[0m \x1b[38;5;36mY\x1b[0m \x1b[39mYes.\x1b[0m \x1b[36m-n\x1b[0m \x1b[38;5;36mN\x1b[0m \x1b[39mNo.\x1b[0m """ assert parser.format_help() == dedent(expected_help_output) @pytest.mark.parametrize("indent_increment", (1, 3)) @pytest.mark.parametrize("max_help_position", (25, 26, 27)) @pytest.mark.parametrize("width", (None, 70)) @pytest.mark.parametrize("short_first", (1, 0)) def test_help_formatter_args(indent_increment, max_help_position, width, short_first): parsers = OptionParsers( IndentedHelpFormatter(indent_increment, max_help_position, width, short_first), IndentedRichHelpFormatter(indent_increment, max_help_position, width, short_first), prog="PROG", ) # Note: the length of the option string is chosen to test edge cases where it is less than, # equal to, and bigger than max_help_position parsers.add_option( "--option-of-certain-size", action="store_true", help="This is the help of the said option" ) parsers.assert_format_help_equal() def test_return_output(): parser = OptionParser(prog="prog", formatter=IndentedRichHelpFormatter()) assert parser.format_help() @pytest.mark.usefixtures("force_color") def test_text_highlighter(): parser = OptionParser(prog="PROG", formatter=IndentedRichHelpFormatter()) parser.add_option( "--arg", action="store_true", help="Did you try `RichHelpFormatter.highlighter`?" ) expected_help_output = """\ \x1b[38;5;208mUsage:\x1b[0m PROG [options] \x1b[38;5;208mOptions:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--arg\x1b[0m \x1b[39mDid you try `\x1b[0m\x1b[1;39mRichHelpFormatter.highlighter\x1b[0m\x1b[39m`?\x1b[0m """ # Make sure we can use a style multiple times in regexes pattern_with_duplicate_style = r"'(?P[^']*)'" RichHelpFormatter.highlights.append(pattern_with_duplicate_style) assert parser.format_help() == dedent(expected_help_output) RichHelpFormatter.highlights.remove(pattern_with_duplicate_style) @pytest.mark.usefixtures("force_color") def test_default_highlights(): parser = OptionParser( "PROG", formatter=IndentedRichHelpFormatter(), description="Description with `syntax` and --options.", epilog="Epilog with `syntax` and --options.", ) # syntax highlights parser.add_option("--syntax-normal", action="store_true", help="Start `middle` end") parser.add_option("--syntax-start", action="store_true", help="`Start` middle end") parser.add_option("--syntax-end", action="store_true", help="Start middle `end`") # --options highlights parser.add_option("--option-normal", action="store_true", help="Start --middle end") parser.add_option("--option-start", action="store_true", help="--Start middle end") parser.add_option("--option-end", action="store_true", help="Start middle --end") parser.add_option("--option-comma", action="store_true", help="Start --middle, end") parser.add_option("--option-multi", action="store_true", help="Start --middle-word end") parser.add_option("--option-not", action="store_true", help="Start middle-word end") parser.add_option("--option-short", action="store_true", help="Start -middle end") expected_help_output = """ \x1b[39mDescription with `\x1b[0m\x1b[1;39msyntax\x1b[0m\x1b[39m` and \x1b[0m\x1b[36m--options\x1b[0m\x1b[39m.\x1b[0m \x1b[38;5;208mOptions:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--syntax-normal\x1b[0m \x1b[39mStart `\x1b[0m\x1b[1;39mmiddle\x1b[0m\x1b[39m` end\x1b[0m \x1b[36m--syntax-start\x1b[0m \x1b[39m`\x1b[0m\x1b[1;39mStart\x1b[0m\x1b[39m` middle end\x1b[0m \x1b[36m--syntax-end\x1b[0m \x1b[39mStart middle `\x1b[0m\x1b[1;39mend\x1b[0m\x1b[39m`\x1b[0m \x1b[36m--option-normal\x1b[0m \x1b[39mStart \x1b[0m\x1b[36m--middle\x1b[0m\x1b[39m end\x1b[0m \x1b[36m--option-start\x1b[0m \x1b[36m--Start\x1b[0m\x1b[39m middle end\x1b[0m \x1b[36m--option-end\x1b[0m \x1b[39mStart middle \x1b[0m\x1b[36m--end\x1b[0m \x1b[36m--option-comma\x1b[0m \x1b[39mStart \x1b[0m\x1b[36m--middle\x1b[0m\x1b[39m, end\x1b[0m \x1b[36m--option-multi\x1b[0m \x1b[39mStart \x1b[0m\x1b[36m--middle-word\x1b[0m\x1b[39m end\x1b[0m \x1b[36m--option-not\x1b[0m \x1b[39mStart middle-word end\x1b[0m \x1b[36m--option-short\x1b[0m \x1b[39mStart \x1b[0m\x1b[36m-middle\x1b[0m\x1b[39m end\x1b[0m \x1b[39mEpilog with `\x1b[0m\x1b[1;39msyntax\x1b[0m\x1b[39m` and \x1b[0m\x1b[36m--options\x1b[0m\x1b[39m.\x1b[0m """ assert parser.format_help().endswith(dedent(expected_help_output)) def test_empty_fields(): orig_fmt = IndentedRichHelpFormatter() rich_fmt = IndentedRichHelpFormatter() assert rich_fmt.format_usage("") == orig_fmt.format_usage("") assert rich_fmt.format_heading("") == orig_fmt.format_heading("") assert rich_fmt.format_description("") == orig_fmt.format_description("") assert rich_fmt.format_epilog("") == orig_fmt.format_epilog("") parser = OptionParser() option = parser.add_option("--option") for fmt in (orig_fmt, rich_fmt): fmt.store_option_strings(parser) fmt.set_parser(parser) assert rich_fmt.format_option(option) == orig_fmt.format_option(option) option = parser.add_option("--option2", help="help") for fmt in (orig_fmt, rich_fmt): fmt.store_option_strings(parser) fmt.default_tag = None assert rich_fmt.format_option(option) == orig_fmt.format_option(option) def test_titled_help_formatter(): parsers = OptionParsers( TitledHelpFormatter(), TitledRichHelpFormatter(), prog="PROG", description="Description.", epilog="Epilog.", ) parsers.add_option("--option", help="help") groups = parsers.add_option_group("Group") groups.add_option("-s", "--short", help="help") groups.add_option("-o", "-O", help="help") parsers.assert_format_help_equal() @pytest.mark.usefixtures("force_color") def test_titled_help_formatter_colors(): parser = OptionParser( prog="PROG", description="Description.", epilog="Epilog.", formatter=TitledRichHelpFormatter(), ) parser.add_option("--option", help="help") expected_help_output = """\ \x1b[38;5;208mUsage\x1b[0m \x1b[38;5;208m=====\x1b[0m PROG [options] \x1b[39mDescription.\x1b[0m \x1b[38;5;208mOptions\x1b[0m \x1b[38;5;208m=======\x1b[0m \x1b[36m--help\x1b[0m, \x1b[36m-h\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--option\x1b[0m=\x1b[38;5;36mOPTION\x1b[0m \x1b[39mhelp\x1b[0m \x1b[39mEpilog.\x1b[0m """ assert parser.format_help() == dedent(expected_help_output) def test_rich_lazy_import(): sys_modules_no_rich = { mod_name: mod for mod_name, mod in sys.modules.items() if mod_name != "rich" and not mod_name.startswith("rich.") } lazy_rich = {k: v for k, v in r.__dict__.items() if k not in r.__all__} with ( patch.dict(sys.modules, sys_modules_no_rich, clear=True), patch.dict(r.__dict__, lazy_rich, clear=True), ): parser = OptionParser(formatter=IndentedRichHelpFormatter()) parser.add_option("--foo", help="foo help") values, args = parser.parse_args(["--foo", "bar"]) assert values.foo == "bar" assert not args assert sys.modules assert "rich" not in sys.modules # no help formatting, do not import rich for mod_name in sys.modules: assert not mod_name.startswith("rich.") parser.format_help() assert "rich" in sys.modules # format help has been called formatter = IndentedRichHelpFormatter() assert formatter._console is None formatter.console = get_console() assert formatter._console is not None with pytest.raises(AttributeError, match="Foo"): _ = r.Foo @pytest.mark.skipif(sys.platform != "win32", reason="windows-only test") @pytest.mark.usefixtures("force_color") @pytest.mark.parametrize( ("legacy_console", "old_windows", "colors"), ( pytest.param(True, False, True, id="legacy_console-new_windows"), pytest.param(True, True, False, id="legacy_console-old_windows"), pytest.param(False, None, True, id="new_console"), ), ) def test_legacy_windows(legacy_console, old_windows, colors): # pragma: win32 cover expected_output = { False: """\ Usage: PROG [options] Options: -h, --help show this help message and exit """, True: """\ \x1b[38;5;208mUsage:\x1b[0m PROG [options] \x1b[38;5;208mOptions:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m """, }[colors] init_win_colors = Mock(return_value=not old_windows) parser = OptionParser(prog="PROG", formatter=IndentedRichHelpFormatter()) with ( patch("rich.console.detect_legacy_windows", return_value=legacy_console), patch("rich_argparse._common._initialize_win_colors", init_win_colors), ): assert parser.format_help() == dedent(expected_output) if legacy_console: init_win_colors.assert_called_with() else: init_win_colors.assert_not_called() @pytest.mark.parametrize( ("formatter", "description", "nb_o", "expected"), ( pytest.param( IndentedRichHelpFormatter(), None, 2, """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] [\x1b[36m--foo\x1b[0m \x1b[38;5;36mFOO\x1b[0m] \x1b[38;5;208mOptions:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--foo\x1b[0m=\x1b[38;5;36mFOO\x1b[0m \x1b[39mfoo help\x1b[0m """, id="indented", ), pytest.param( IndentedRichHelpFormatter(), "A description.", 2, """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] [\x1b[36m--foo\x1b[0m \x1b[38;5;36mFOO\x1b[0m] \x1b[39mA description.\x1b[0m \x1b[38;5;208mOptions:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--foo\x1b[0m=\x1b[38;5;36mFOO\x1b[0m \x1b[39mfoo help\x1b[0m """, id="indented-desc", ), pytest.param( IndentedRichHelpFormatter(), None, 30, """\ \x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] [\x1b[36m--foooooooooooooooooooooooooooooo\x1b[0m \x1b[38;5;36mFOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO\x1b[0m] \x1b[38;5;208mOptions:\x1b[0m \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--foooooooooooooooooooooooooooooo\x1b[0m=\x1b[38;5;36mFOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO\x1b[0m \x1b[39mfoo help\x1b[0m """, id="indented-long", ), pytest.param( TitledRichHelpFormatter(), None, 2, """\ \x1b[38;5;208mUsage\x1b[0m \x1b[38;5;208m=====\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m] [\x1b[36m--foo\x1b[0m \x1b[38;5;36mFOO\x1b[0m] \x1b[38;5;208mOptions\x1b[0m \x1b[38;5;208m=======\x1b[0m \x1b[36m--help\x1b[0m, \x1b[36m-h\x1b[0m \x1b[39mshow this help message and exit\x1b[0m \x1b[36m--foo\x1b[0m=\x1b[38;5;36mFOO\x1b[0m \x1b[39mfoo help\x1b[0m """, id="titled", ), ), ) @pytest.mark.usefixtures("force_color") def test_generated_usage(formatter, description, nb_o, expected): parser = OptionParser( prog="PROG", formatter=formatter, usage=GENERATE_USAGE, description=description ) parser.add_option("--f" + "o" * nb_o, help="foo help") parser.add_option("--bar", help=SUPPRESS_HELP) assert parser.format_help() == dedent(expected) def test_generated_usage_no_parser(): formatter = IndentedRichHelpFormatter() with pytest.raises(TypeError) as exc_info: formatter.format_usage(GENERATE_USAGE) assert str(exc_info.value) == "Cannot generate usage if parser is not set"