├── .github ├── FUNDING.yml ├── SECURITY.md ├── dependabot.yml ├── release.yml └── workflows │ ├── check.yaml │ └── release.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── README.md ├── pyproject.toml ├── roots ├── test-basic │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-complex │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-default-handling │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-description-empty │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-description-multiline │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-description-set │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-epilog-empty │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-epilog-multiline │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-epilog-set │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-force-refs-lower │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-group-title-empty-prefixes │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-group-title-prefix-custom-subcommands │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-group-title-prefix-custom │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-group-title-prefix-default │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-group-title-prefix-empty-subcommands │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-group-title-prefix-empty │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-group-title-prefix-prog-replacement │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-group-title-prefix-subcommand-replacement │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-hook-fail │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-hook │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-lower-upper-refs │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-nested │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-prog │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-ref-duplicate-label │ ├── cli.rst │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-ref-prefix-doc │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-ref │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-store-true-false │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-subparsers │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-suppressed-action │ ├── conf.py │ ├── index.rst │ └── parser.py ├── test-title-empty │ ├── conf.py │ ├── index.rst │ └── parser.py └── test-title-set │ ├── conf.py │ ├── index.rst │ └── parser.py ├── src └── sphinx_argparse_cli │ ├── __init__.py │ ├── _logic.py │ └── py.typed ├── tests ├── complex.txt ├── complex_pre_310.txt ├── conftest.py ├── test_logic.py └── test_sphinx_argparse_cli.py └── tox.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "pypi/sphinx-argparse-cli" 2 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.10 + | :white_check_mark: | 8 | | < 1.10 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift 13 | will coordinate the fix and disclosure. 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | - pre-commit-ci 6 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: ["main"] 6 | tags-ignore: ["**"] 7 | pull_request: 8 | schedule: 9 | - cron: "0 8 * * *" 10 | 11 | concurrency: 12 | group: check-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | env: 22 | - "3.13" 23 | - "3.12" 24 | - "3.11" 25 | - "3.10" 26 | - type 27 | - dev 28 | - pkg_meta 29 | steps: 30 | - uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | - name: Install the latest version of uv 34 | uses: astral-sh/setup-uv@v6 35 | with: 36 | enable-cache: true 37 | cache-dependency-glob: "pyproject.toml" 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | - name: Install tox 40 | run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv 41 | - name: Install Python 42 | if: startsWith(matrix.env, '3.') && matrix.env != '3.13' 43 | run: uv python install --python-preference only-managed ${{ matrix.env }} 44 | - name: Setup test suite 45 | run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} 46 | - name: Run test suite 47 | run: tox run --skip-pkg-install -e ${{ matrix.env }} 48 | env: 49 | PYTEST_ADDOPTS: "-vv --durations=20" 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | on: 3 | push: 4 | tags: ["*"] 5 | 6 | env: 7 | dists-artifact-name: python-package-distributions 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Install the latest version of uv 17 | uses: astral-sh/setup-uv@v6 18 | with: 19 | enable-cache: true 20 | cache-dependency-glob: "pyproject.toml" 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Build package 23 | run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: ${{ env.dists-artifact-name }} 28 | path: dist/* 29 | 30 | release: 31 | needs: 32 | - build 33 | runs-on: ubuntu-latest 34 | environment: 35 | name: release 36 | url: https://pypi.org/project/sphinx-argparse-cli/${{ github.ref_name }} 37 | permissions: 38 | id-token: write 39 | steps: 40 | - name: Download all the dists 41 | uses: actions/download-artifact@v4 42 | with: 43 | name: ${{ env.dists-artifact-name }} 44 | path: dist/ 45 | - name: Publish to PyPI 46 | uses: pypa/gh-action-pypi-publish@v1.12.4 47 | with: 48 | attestations: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.egg-info/ 3 | .tox/ 4 | .coverage* 5 | coverage.xml 6 | .*_cache 7 | __pycache__ 8 | **.pyc 9 | build 10 | dist 11 | src/sphinx_argparse_cli/version.py 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: 0.33.0 9 | hooks: 10 | - id: check-github-workflows 11 | args: ["--verbose"] 12 | - repo: https://github.com/codespell-project/codespell 13 | rev: v2.4.1 14 | hooks: 15 | - id: codespell 16 | additional_dependencies: ["tomli>=2.0.1"] 17 | - repo: https://github.com/tox-dev/tox-ini-fmt 18 | rev: "1.5.0" 19 | hooks: 20 | - id: tox-ini-fmt 21 | args: ["-p", "fix"] 22 | - repo: https://github.com/tox-dev/pyproject-fmt 23 | rev: "v2.6.0" 24 | hooks: 25 | - id: pyproject-fmt 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: "v0.11.11" 28 | hooks: 29 | - id: ruff-format 30 | - id: ruff 31 | args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] 32 | - repo: https://github.com/rbubley/mirrors-prettier 33 | rev: "v3.5.3" 34 | hooks: 35 | - id: prettier 36 | args: ["--print-width=120", "--prose-wrap=always"] 37 | - repo: meta 38 | hooks: 39 | - id: check-hooks-apply 40 | - id: check-useless-excludes 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## Unreleased 6 | 7 | - Allow to add content to directive. 8 | - Fix Sphinx warnings about parallel reads. 9 | - Add `force_args_lower` to enable `:ref:` links with mixed-case program names and arguments. 10 | 11 | ## 1.13.1 12 | 13 | - Fix multiline handling of group descriptions. 14 | 15 | ## 1.13.0 16 | 17 | - Don't render arguments with `help=argparse.SUPPRESS`. 18 | - Show epilogue if present in the command description. 19 | - Add option to show usage before description. 20 | 21 | ## 1.12.0 22 | 23 | - Support rendering with `argparse.RawDescriptionHelpFormatter`. 24 | 25 | ## 1.11.0 26 | 27 | - Hatchling as backend and exclude sphinx 6.1 support. 28 | 29 | ## 1.10.0 30 | 31 | - Add the option to override the generated `default` part of the CLI parameter description. 32 | 33 | ## 1.9.0 34 | 35 | - Add the option to override the description using the `description` attribute of the directive 36 | - Add the option to retrieve the arguments by hooking argparse, in cases where `func` consumes the arguments and does 37 | not return them 38 | 39 | ## 1.8.2 40 | 41 | - Don't raise label clashing warnings for options which only differ between upper and lower case 42 | 43 | ## 1.8.1 44 | 45 | - Fix reference clashing for options which only differ between upper and lower case 46 | - Do not render default values for `store_true` and `store_false` actions 47 | 48 | ## 1.8.0 49 | 50 | - Support Python 3.10 51 | 52 | ## 1.7.0 53 | 54 | - Support for adding custom subsection group title prefix (`group_title_prefix` and `group_sub_title_prefix` directive 55 | arguments) 56 | 57 | ## 1.6.0 (2021-04-15) 58 | 59 | - Support for using the `ref` sphinx role to refer to all anchor-able objects generated by the tool 60 | - Flags now have their reference title set (for the HTML builder this is shown when hover over the reference) 61 | - Anchors generated no longer collapse multiple subsequent `-` characters (to avoid clash when there's a flag and a 62 | positional argument with the same name) 63 | - Added a sphinx flag `sphinx_argparse_cli_prefix_document` (by default `False`) to avoid reference clashes when 64 | multiple documents generate the same reference labels 65 | - The root `prog` name now is prefixed for the root level optional/positional arguments' header (to avoid multiple 66 | anchors with the same id when multiple commands are documented in the same document) 67 | 68 | ## 1.5.1 (2021-04-15) 69 | 70 | - For sub-commands use the parser description first as description and only then fallback to the help message 71 | 72 | ## 1.5.0 (2021-02-13) 73 | 74 | - Display the metavar (fallback to dest) if the action has more than one argument (this is inline with how usage is 75 | displayed). 76 | 77 | ## 1.4.0 (2021-02-13) 78 | 79 | - Command line arguments are now bold to highlight them even further from the help text 80 | 81 | ## 1.3.0 (2021-02-13) 82 | 83 | - Add support for changing the usage width via the `usage_width` option on the directive 84 | - Mark document as always needs update (as the underlying content is coming from outside the sphinx documents) 85 | - Help messages is now interpreted as reStructuredText 86 | - Matching curly braces, single and double quotes in help text will be marked as string literals 87 | - Help messages containing the `default(s)` word do not show the default value (as often this indicates the default is 88 | already documented in the help text) 89 | 90 | ## 1.2.0 (2021-02-05) 91 | 92 | - Add support for changing (removing) the title via the `title` attribute of the directive. 93 | 94 | ## 1.1.0 (2021-02-05) 95 | 96 | - Add support for setting the `prog` of the parser via the a `prog` attribute of the directive. 97 | 98 | ## 1.0.0 (2021-02-05) 99 | 100 | - First version. 101 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, 8 | religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | gaborbernat@python.org. The project team will review and investigate all complaints, and will respond in a way that it 48 | deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the 49 | reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 57 | [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] 58 | 59 | [homepage]: https://www.contributor-covenant.org/ 60 | [version]: https://www.contributor-covenant.org/version/1/4/ 61 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a 2 | copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included 10 | in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 13 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 15 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 16 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 17 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 18 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sphinx-argparse-cli 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) 4 | [![PyPI - Implementation](https://img.shields.io/pypi/implementation/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) 5 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) 6 | [![Downloads](https://static.pepy.tech/badge/sphinx-argparse-cli/month)](https://pepy.tech/project/sphinx-argparse-cli) 7 | [![PyPI - License](https://img.shields.io/pypi/l/sphinx-argparse-cli?style=flat-square)](https://opensource.org/licenses/MIT) 8 | [![check](https://github.com/tox-dev/sphinx-argparse-cli/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/sphinx-argparse-cli/actions/workflows/check.yaml) 9 | 10 | Render CLI arguments (sub-commands friendly) defined by the argparse module. For live demo checkout the documentation of 11 | [tox](https://tox.wiki/en/latest/cli_interface.html), 12 | [pypa-build](https://pypa-build.readthedocs.io/en/latest/#python-m-build) and 13 | [mdpo](https://mondeja.github.io/mdpo/latest/cli.html). 14 | 15 | ## Installation 16 | 17 | ```bash 18 | python -m pip install sphinx-argparse-cli 19 | ``` 20 | 21 | ## Enable in `conf.py` 22 | 23 | ```python 24 | # just add it to your list of extensions to load within conf.py 25 | extensions = ["sphinx_argparse_cli"] 26 | ``` 27 | 28 | ## use 29 | 30 | Within the reStructuredText files use the `sphinx_argparse_cli` directive that takes, at least, two arguments: 31 | 32 | | Name | Description | 33 | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 34 | | module | the module path to where the parser is defined | 35 | | func | the name of the function that once called with no arguments constructs the parser | 36 | | prog | (optional) when provided, overwrites the `` name. | 37 | | hook | (optional) hook `argparse` to retrieve the parser if `func` uses a parser instead of returning it. | 38 | | title | (optional) when provided, overwrites the ` - CLI interface` title added by default and when empty, will not be included | 39 | | description | (optional) when provided, overwrites the description and when empty, will not be included | 40 | | epilog | (optional) when provided, overwrites the epilog and when empty, will not be included | 41 | | usage_width | (optional) how large should usage examples be - defaults to 100 character | 42 | | usage_first | (optional) show usage before description | 43 | | group_title_prefix | (optional) groups subsections title prefixes, accepts the string `{prog}` as a replacement for the program name - defaults to `{prog}` | 44 | | group_sub_title_prefix | (optional) subcommands groups subsections title prefixes, accepts replacement of `{prog}` and `{subcommand}` for program and subcommand name - defaults to `{prog} {subcommand}` | 45 | | no_default_values | (optional) suppresses generation of `default` entries | 46 | | force_refs_lower | (optional) Sphinx `:ref:` only supports lower-case references. With this, any capital letter in generated reference anchors are lowered and given an `_` prefix (i.e. `A` becomes `_a`) | 47 | 48 | For example: 49 | 50 | ```rst 51 | .. sphinx_argparse_cli:: 52 | :module: a_project.cli 53 | :func: build_parser 54 | :prog: my-cli-program 55 | ``` 56 | 57 | If you have code that creates and uses a parser but does not return it, you can specify the `:hook:` flag: 58 | 59 | ```rst 60 | .. sphinx_argparse_cli:: 61 | :module: a_project.cli 62 | :func: main 63 | :hook: 64 | :prog: my-cli-program 65 | ``` 66 | 67 | ### Refer to generated content 68 | 69 | The tool will register reference links to all anchors. This means that you can use the sphinx `ref` role to refer to 70 | both the (sub)command title/groups and every flag/argument. The tool offers a configuration flag 71 | `sphinx_argparse_cli_prefix_document` (change by setting this variable in `conf.py` - by default `False`). This option 72 | influences the reference ids generated. If it's false the reference will be the anchor id (the text appearing after the 73 | `'#` in the URI once you click on it). If it's true the anchor id will be prefixed by the document name (this is useful 74 | to avoid reference label clash when the same anchors are generated in multiple documents). 75 | 76 | For example in case of a `tox` command, and `sphinx_argparse_cli_prefix_document=False` (default): 77 | 78 | - to refer to the optional arguments group use ``:ref:`tox-optional-arguments` ``, 79 | - to refer to the run subcommand use ``:ref:`tox-run` ``, 80 | - to refer to flag `--magic` of the `run` sub-command use ``:ref:`tox-run---magic` ``. 81 | 82 | For example in case of a `tox` command, and `sphinx_argparse_cli_prefix_document=True`, and the current document name 83 | being `cli`: 84 | 85 | - to refer to the optional arguments group use ``:ref:`cli:tox-optional-arguments` ``, 86 | - to refer to the run subcommand use ``:ref:`cli:tox-run` ``, 87 | - to refer to flag `--magic` of the `run` sub-command use ``:ref:`cli:tox-run---magic` ``. 88 | 89 | Due to Sphinx's `:ref:` only supporting lower-case values, if you need to distinguish mixed case program names or 90 | arguments, set the `:force_refs_lower:` argument. With this flag, captial-letters in references will be converted to 91 | their lower-case counterpart and prefixed with an `_`. For example: 92 | 93 | - A `prog` name `SampleProgram` will be referenced as ``:ref:`_sample_program...` ``. 94 | - To distinguish between mixed case flags `-a` and `-A` use ``:ref:`_sample_program--a` `` and 95 | ``:ref:`_sample_program--_a` `` respectively 96 | 97 | Note that if you are _not_ concerned about using internal Sphinx `:ref:` cross-references, you may choose to leave this 98 | off to maintain mixed-case anchors in your output HTML; but be aware that later enabling it will change your anchors in 99 | the output HTML. 100 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatch-vcs>=0.4", 5 | "hatchling>=1.25", 6 | ] 7 | 8 | [project] 9 | name = "sphinx-argparse-cli" 10 | description = "render CLI arguments (sub-commands friendly) defined by argparse module" 11 | readme = "README.md" 12 | keywords = [ 13 | "argparse", 14 | "sphinx", 15 | ] 16 | license = "MIT" 17 | maintainers = [ 18 | { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }, 19 | ] # noqa: E999 20 | requires-python = ">=3.10" 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Environment :: Console", 24 | "Framework :: Sphinx", 25 | "Framework :: Sphinx :: Extension", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: MIT License", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python :: 3 :: Only", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: 3.13", 34 | "Programming Language :: Python :: Implementation :: CPython", 35 | "Topic :: Documentation", 36 | "Topic :: Documentation :: Sphinx", 37 | ] 38 | dynamic = [ 39 | "version", 40 | ] 41 | dependencies = [ 42 | "sphinx>=8.0.2", 43 | ] 44 | optional-dependencies.testing = [ 45 | "covdefaults>=2.3", 46 | "defusedxml>=0.7.1", # needed for sphinx.testing 47 | "pytest>=8.3.2", 48 | "pytest-cov>=5", 49 | ] 50 | urls.Documentation = "https://github.com/tox-dev/sphinx-argparse-cli#sphinx-argparse-cli" 51 | urls.Homepage = "https://github.com/tox-dev/sphinx-argparse-cli" 52 | urls.Source = "https://github.com/tox-dev/sphinx-argparse-cli" 53 | urls.Tracker = "https://github.com/tox-dev/sphinx-argparse-cli/issues" 54 | 55 | [tool.hatch] 56 | build.hooks.vcs.version-file = "src/sphinx_argparse_cli/version.py" 57 | build.targets.sdist.include = [ 58 | "/src", 59 | "/tests", 60 | ] 61 | version.source = "vcs" 62 | 63 | [tool.black] 64 | line-length = 120 65 | 66 | [tool.ruff] 67 | target-version = "py310" 68 | line-length = 120 69 | format.preview = true 70 | format.docstring-code-line-length = 100 71 | format.docstring-code-format = true 72 | lint.select = [ 73 | "ALL", 74 | ] 75 | lint.ignore = [ 76 | "ANN101", # no type annotation for self 77 | "ANN401", # allow Any as type annotation 78 | "COM812", # Conflict with formatter 79 | "CPY", # No copyright statements 80 | "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible 81 | "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible 82 | "DOC", # not yet supported 83 | "ISC001", # Conflict with formatter 84 | "S104", # Possible binding to all interface 85 | ] 86 | 87 | lint.per-file-ignores."roots/**/*.py" = [ 88 | "D", # no docs 89 | "INP001", # no namespace 90 | ] 91 | lint.per-file-ignores."tests/**/*.py" = [ 92 | "D", # don't care about documentation in tests 93 | "FBT", # don't care about booleans as positional arguments in tests 94 | "INP001", # no implicit namespace 95 | "PLC2701", # private import 96 | "PLR0913", # any number of arguments in tests 97 | "PLR0917", # any number of arguments in tests 98 | "PLR2004", # Magic value used in comparison, consider replacing with a constant variable 99 | "S101", # asserts allowed in tests 100 | "S603", # `subprocess` call: check for execution of untrusted input 101 | ] 102 | lint.isort = { known-first-party = [ 103 | "sphinx_argparse_cli", 104 | ], required-imports = [ 105 | "from __future__ import annotations", 106 | ] } 107 | 108 | [tool.codespell] 109 | builtin = "clear,usage,en-GB_to_en-US" 110 | count = true 111 | 112 | [tool.pyproject-fmt] 113 | max_supported_python = "3.13" 114 | 115 | [tool.coverage] 116 | html.show_contexts = true 117 | html.skip_covered = false 118 | paths.source = [ 119 | "src", 120 | "**/site-packages", 121 | ] 122 | report.fail_under = 76 123 | run.dynamic_context = "test_function" 124 | run.parallel = true 125 | run.plugins = [ 126 | "covdefaults", 127 | ] 128 | run.relative_files = true 129 | 130 | [tool.mypy] 131 | python_version = "3.12" 132 | show_error_codes = true 133 | strict = true 134 | -------------------------------------------------------------------------------- /roots/test-basic/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-basic/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-basic/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser(prog="basic") 8 | -------------------------------------------------------------------------------- /roots/test-complex/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-complex/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-complex/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="complex", epilog="test epilog") 8 | parser.add_argument("--root", action="store_true", help="root flag") 9 | parser.add_argument("--no-help", action="store_true") 10 | parser.add_argument("--outdir", "-o", type=str, help="output directory", metavar="out_dir") 11 | parser.add_argument("--in-dir", "-i", type=str, help="input directory", dest="in_dir") 12 | 13 | group = parser.add_argument_group("Exclusive", description="this is an exclusive group") 14 | exclusive = group.add_mutually_exclusive_group() 15 | exclusive.add_argument("--foo", action="store_true", help="foo") 16 | exclusive.add_argument("--bar", action="store_true", help="bar") 17 | 18 | parser.add_argument_group("empty") 19 | 20 | sub_parsers_a = parser.add_subparsers(title="sub-parser-a", description="sub parsers A", dest="command") 21 | sub_parsers_a.required = False 22 | sub_parsers_a.default = "first" 23 | 24 | a_parser_first = sub_parsers_a.add_parser("first", aliases=["f"], help="a-first-help", description="a-first-desc") 25 | a_parser_first.add_argument("--flag", dest="a_par_first_flag", action="store_true", help="a parser first flag") 26 | a_parser_first.add_argument("--root", action="store_true", help="root flag") 27 | a_parser_first.add_argument("pos_one", help="first positional argument", metavar="one") 28 | a_parser_first.add_argument("pos_two", help="second positional argument", default=1) 29 | 30 | a_parser_second = sub_parsers_a.add_parser("second") 31 | a_parser_second.add_argument("--flag", dest="a_par_second_flag", action="store_true", help="a parser second flag") 32 | a_parser_second.add_argument("--root", action="store_true", help="root flag") 33 | a_parser_second.add_argument("pos_one", help="first positional argument", metavar="one") 34 | a_parser_second.add_argument("pos_two", help="second positional argument", default="green") 35 | 36 | sub_parsers_a.add_parser("third") # empty sub-command 37 | return parser 38 | -------------------------------------------------------------------------------- /roots/test-default-handling/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-default-handling/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: main 4 | :no_default_values: 5 | :hook: 6 | -------------------------------------------------------------------------------- /roots/test-default-handling/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def main() -> None: 7 | parser = ArgumentParser(prog="foo", add_help=False) 8 | parser.add_argument("x", default=1, help="arg (default: True)") 9 | args = parser.parse_args() 10 | print(args) # noqa: T201 11 | -------------------------------------------------------------------------------- /roots/test-description-empty/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-description-empty/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :description: 5 | -------------------------------------------------------------------------------- /roots/test-description-empty/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser(prog="foo", description="desc", add_help=False) 8 | -------------------------------------------------------------------------------- /roots/test-description-multiline/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-description-multiline/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-description-multiline/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser( 8 | prog="foo", 9 | description="""This description 10 | spans multiple lines. 11 | 12 | this line is indented. 13 | and also this. 14 | 15 | Now this should be a separate paragraph. 16 | """, 17 | formatter_class=RawDescriptionHelpFormatter, 18 | add_help=False, 19 | ) 20 | group = parser.add_argument_group( 21 | description="""This group description 22 | 23 | spans multiple lines. 24 | """ 25 | ) 26 | group.add_argument("--dummy") 27 | return parser 28 | -------------------------------------------------------------------------------- /roots/test-description-set/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-description-set/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :description: My own description 5 | -------------------------------------------------------------------------------- /roots/test-description-set/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser(prog="foo", description="desc", add_help=False) 8 | -------------------------------------------------------------------------------- /roots/test-epilog-empty/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-epilog-empty/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :epilog: 5 | -------------------------------------------------------------------------------- /roots/test-epilog-empty/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser(prog="foo", epilog="epi", add_help=False) 8 | -------------------------------------------------------------------------------- /roots/test-epilog-multiline/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-epilog-multiline/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-epilog-multiline/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser( 8 | prog="foo", 9 | epilog="""This epilog 10 | spans multiple lines. 11 | 12 | this line is indented. 13 | and also this. 14 | 15 | Now this should be a separate paragraph. 16 | """, 17 | formatter_class=RawDescriptionHelpFormatter, 18 | add_help=False, 19 | ) 20 | -------------------------------------------------------------------------------- /roots/test-epilog-set/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-epilog-set/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :epilog: My own epilog 5 | -------------------------------------------------------------------------------- /roots/test-epilog-set/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser(prog="foo", epilog="epi", add_help=False) 8 | -------------------------------------------------------------------------------- /roots/test-force-refs-lower/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-force-refs-lower/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :force_refs_lower: 5 | 6 | Reference test 7 | -------------- 8 | Flag :ref:`_prog--_b` and :ref:`_prog--b` and positional :ref:`_prog-root`. 9 | -------------------------------------------------------------------------------- /roots/test-force-refs-lower/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="Prog") 8 | parser.add_argument("root") 9 | parser.add_argument("--build", "-B", action="store_true", help="build flag") 10 | parser.add_argument("--binary", "-b", action="store_true", help="binary flag") 11 | return parser 12 | -------------------------------------------------------------------------------- /roots/test-group-title-empty-prefixes/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-group-title-empty-prefixes/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :group_title_prefix: 5 | :group_sub_title_prefix: 6 | -------------------------------------------------------------------------------- /roots/test-group-title-empty-prefixes/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="complex") 8 | parser.add_argument("--root", action="store_true", help="root flag") 9 | parser.add_argument("--no-help", action="store_true") 10 | parser.add_argument("--outdir", "-o", type=str, help="output directory", metavar="out_dir") 11 | parser.add_argument("--in-dir", "-i", type=str, help="input directory", dest="in_dir") 12 | 13 | group = parser.add_argument_group("Exclusive", description="this is an exclusive group") 14 | exclusive = group.add_mutually_exclusive_group() 15 | exclusive.add_argument("--foo", action="store_true", help="foo") 16 | exclusive.add_argument("--bar", action="store_true", help="bar") 17 | 18 | parser.add_argument_group("empty") 19 | 20 | sub_parsers_a = parser.add_subparsers(title="sub-parser-a", description="sub parsers A", dest="command") 21 | sub_parsers_a.required = False 22 | sub_parsers_a.default = "first" 23 | 24 | a_parser_first = sub_parsers_a.add_parser("first", aliases=["f"], help="a-first-help", description="a-first-desc") 25 | a_parser_first.add_argument("--flag", dest="a_par_first_flag", action="store_true", help="a parser first flag") 26 | a_parser_first.add_argument("--root", action="store_true", help="root flag") 27 | a_parser_first.add_argument("pos_one", help="first positional argument", metavar="one") 28 | a_parser_first.add_argument("pos_two", help="second positional argument", default=1) 29 | 30 | a_parser_second = sub_parsers_a.add_parser("second") 31 | a_parser_second.add_argument("--flag", dest="a_par_second_flag", action="store_true", help="a parser second flag") 32 | a_parser_second.add_argument("--root", action="store_true", help="root flag") 33 | a_parser_second.add_argument("pos_one", help="first positional argument", metavar="one") 34 | a_parser_second.add_argument("pos_two", help="second positional argument", default="green") 35 | 36 | sub_parsers_a.add_parser("third") # empty sub-command 37 | return parser 38 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-custom-subcommands/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-custom-subcommands/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :group_sub_title_prefix: custom 5 | 6 | .. sphinx_argparse_cli:: 7 | :module: parser 8 | :func: make 9 | :group_title_prefix: 10 | :group_sub_title_prefix: custom-2 11 | 12 | 13 | .. sphinx_argparse_cli:: 14 | :module: parser 15 | :func: make 16 | :group_title_prefix: myprog 17 | :group_sub_title_prefix: custom-3 18 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-custom-subcommands/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="complex") 8 | parser.add_argument("--root", action="store_true", help="root flag") 9 | parser.add_argument("--no-help", action="store_true") 10 | parser.add_argument("--outdir", "-o", type=str, help="output directory", metavar="out_dir") 11 | parser.add_argument("--in-dir", "-i", type=str, help="input directory", dest="in_dir") 12 | 13 | group = parser.add_argument_group("Exclusive", description="this is an exclusive group") 14 | exclusive = group.add_mutually_exclusive_group() 15 | exclusive.add_argument("--foo", action="store_true", help="foo") 16 | exclusive.add_argument("--bar", action="store_true", help="bar") 17 | 18 | parser.add_argument_group("empty") 19 | 20 | sub_parsers_a = parser.add_subparsers(title="sub-parser-a", description="sub parsers A", dest="command") 21 | sub_parsers_a.required = False 22 | sub_parsers_a.default = "first" 23 | 24 | a_parser_first = sub_parsers_a.add_parser("first", aliases=["f"], help="a-first-help", description="a-first-desc") 25 | a_parser_first.add_argument("--flag", dest="a_par_first_flag", action="store_true", help="a parser first flag") 26 | a_parser_first.add_argument("--root", action="store_true", help="root flag") 27 | a_parser_first.add_argument("pos_one", help="first positional argument", metavar="one") 28 | a_parser_first.add_argument("pos_two", help="second positional argument", default=1) 29 | 30 | a_parser_second = sub_parsers_a.add_parser("second") 31 | a_parser_second.add_argument("--flag", dest="a_par_second_flag", action="store_true", help="a parser second flag") 32 | a_parser_second.add_argument("--root", action="store_true", help="root flag") 33 | a_parser_second.add_argument("pos_one", help="first positional argument", metavar="one") 34 | a_parser_second.add_argument("pos_two", help="second positional argument", default="green") 35 | 36 | sub_parsers_a.add_parser("third") # empty sub-command 37 | return parser 38 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-custom/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-custom/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :group_title_prefix: custom 5 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-custom/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="prog") 8 | parser.add_argument("root") 9 | parser.add_argument("--root", action="store_true", help="root flag") 10 | 11 | group = parser.add_argument_group("Exclusive", description="this is an exclusive group") 12 | exclusive = group.add_mutually_exclusive_group() 13 | exclusive.add_argument("--foo", action="store_true", help="foo") 14 | exclusive.add_argument("--bar", action="store_true", help="bar") 15 | 16 | parser.add_argument_group("empty") 17 | 18 | sub_parsers_a = parser.add_subparsers(title="sub-parser-a", description="sub parsers A", dest="command") 19 | sub_parsers_a.required = False 20 | sub_parsers_a.default = "first" 21 | 22 | a_parser_first = sub_parsers_a.add_parser("first", aliases=["f"], help="a-first-help", description="a-first-desc") 23 | a_parser_first.add_argument("--flag", dest="a_par_first_flag", action="store_true", help="a parser first flag") 24 | a_parser_first.add_argument("--root", action="store_true", help="root flag") 25 | a_parser_first.add_argument("pos_one", help="first positional argument", metavar="one") 26 | a_parser_first.add_argument("pos_two", help="second positional argument", default=1) 27 | 28 | a_parser_second = sub_parsers_a.add_parser("second") 29 | a_parser_second.add_argument("--flag", dest="a_par_second_flag", action="store_true", help="a parser second flag") 30 | a_parser_second.add_argument("--root", action="store_true", help="root flag") 31 | a_parser_second.add_argument("pos_one", help="first positional argument", metavar="one") 32 | a_parser_second.add_argument("pos_two", help="second positional argument", default="green") 33 | return parser 34 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-default/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-default/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-default/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="prog") 8 | parser.add_argument("root") 9 | parser.add_argument("--root", action="store_true", help="root flag") 10 | return parser 11 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-empty-subcommands/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-empty-subcommands/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :group_sub_title_prefix: 5 | 6 | 7 | .. sphinx_argparse_cli:: 8 | :module: parser 9 | :func: make 10 | :group_title_prefix: myprog 11 | :group_sub_title_prefix: 12 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-empty-subcommands/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="complex") 8 | parser.add_argument("--root", action="store_true", help="root flag") 9 | parser.add_argument("--no-help", action="store_true") 10 | parser.add_argument("--outdir", "-o", type=str, help="output directory", metavar="out_dir") 11 | parser.add_argument("--in-dir", "-i", type=str, help="input directory", dest="in_dir") 12 | 13 | group = parser.add_argument_group("Exclusive", description="this is an exclusive group") 14 | exclusive = group.add_mutually_exclusive_group() 15 | exclusive.add_argument("--foo", action="store_true", help="foo") 16 | exclusive.add_argument("--bar", action="store_true", help="bar") 17 | 18 | parser.add_argument_group("empty") 19 | 20 | sub_parsers_a = parser.add_subparsers(title="sub-parser-a", description="sub parsers A", dest="command") 21 | sub_parsers_a.required = False 22 | sub_parsers_a.default = "first" 23 | 24 | a_parser_first = sub_parsers_a.add_parser("first", aliases=["f"], help="a-first-help", description="a-first-desc") 25 | a_parser_first.add_argument("--flag", dest="a_par_first_flag", action="store_true", help="a parser first flag") 26 | a_parser_first.add_argument("--root", action="store_true", help="root flag") 27 | a_parser_first.add_argument("pos_one", help="first positional argument", metavar="one") 28 | a_parser_first.add_argument("pos_two", help="second positional argument", default=1) 29 | 30 | a_parser_second = sub_parsers_a.add_parser("second") 31 | a_parser_second.add_argument("--flag", dest="a_par_second_flag", action="store_true", help="a parser second flag") 32 | a_parser_second.add_argument("--root", action="store_true", help="root flag") 33 | a_parser_second.add_argument("pos_one", help="first positional argument", metavar="one") 34 | a_parser_second.add_argument("pos_two", help="second positional argument", default="green") 35 | 36 | sub_parsers_a.add_parser("third") # empty sub-command 37 | return parser 38 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-empty/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-empty/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :group_title_prefix: 5 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-empty/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="prog") 8 | parser.add_argument("root") 9 | parser.add_argument("--root", action="store_true", help="root flag") 10 | return parser 11 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-prog-replacement/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-prog-replacement/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :group_title_prefix: {prog}foo 5 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-prog-replacement/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="bar") 8 | parser.add_argument("root") 9 | parser.add_argument("--root", action="store_true", help="root flag") 10 | 11 | group = parser.add_argument_group("Exclusive", description="this is an exclusive group") 12 | exclusive = group.add_mutually_exclusive_group() 13 | exclusive.add_argument("--foo", action="store_true", help="foo") 14 | exclusive.add_argument("--bar", action="store_true", help="bar") 15 | 16 | parser.add_argument_group("empty") 17 | 18 | sub_parsers_a = parser.add_subparsers(title="sub-parser-a", description="sub parsers A", dest="command") 19 | sub_parsers_a.required = False 20 | sub_parsers_a.default = "first" 21 | 22 | a_parser_first = sub_parsers_a.add_parser("first", aliases=["f"], help="a-first-help", description="a-first-desc") 23 | a_parser_first.add_argument("--flag", dest="a_par_first_flag", action="store_true", help="a parser first flag") 24 | a_parser_first.add_argument("--root", action="store_true", help="root flag") 25 | a_parser_first.add_argument("pos_one", help="first positional argument", metavar="one") 26 | a_parser_first.add_argument("pos_two", help="second positional argument", default=1) 27 | 28 | a_parser_second = sub_parsers_a.add_parser("second") 29 | a_parser_second.add_argument("--flag", dest="a_par_second_flag", action="store_true", help="a parser second flag") 30 | a_parser_second.add_argument("--root", action="store_true", help="root flag") 31 | a_parser_second.add_argument("pos_one", help="first positional argument", metavar="one") 32 | a_parser_second.add_argument("pos_two", help="second positional argument", default="green") 33 | return parser 34 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-subcommand-replacement/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-subcommand-replacement/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :group_sub_title_prefix: {prog}only{subcommand} 5 | -------------------------------------------------------------------------------- /roots/test-group-title-prefix-subcommand-replacement/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="bar") 8 | parser.add_argument("root") 9 | parser.add_argument("--root", action="store_true", help="root flag") 10 | 11 | group = parser.add_argument_group("Exclusive", description="this is an exclusive group") 12 | exclusive = group.add_mutually_exclusive_group() 13 | exclusive.add_argument("--foo", action="store_true", help="foo") 14 | exclusive.add_argument("--bar", action="store_true", help="bar") 15 | 16 | parser.add_argument_group("empty") 17 | 18 | sub_parsers_a = parser.add_subparsers(title="sub-parser-a", description="sub parsers A", dest="command") 19 | sub_parsers_a.required = False 20 | sub_parsers_a.default = "first" 21 | 22 | a_parser_first = sub_parsers_a.add_parser("first", aliases=["f"], help="a-first-help", description="a-first-desc") 23 | a_parser_first.add_argument("--flag", dest="a_par_first_flag", action="store_true", help="a parser first flag") 24 | a_parser_first.add_argument("--root", action="store_true", help="root flag") 25 | a_parser_first.add_argument("pos_one", help="first positional argument", metavar="one") 26 | a_parser_first.add_argument("pos_two", help="second positional argument", default=1) 27 | 28 | a_parser_second = sub_parsers_a.add_parser("second") 29 | a_parser_second.add_argument("--flag", dest="a_par_second_flag", action="store_true", help="a parser second flag") 30 | a_parser_second.add_argument("--root", action="store_true", help="root flag") 31 | a_parser_second.add_argument("pos_one", help="first positional argument", metavar="one") 32 | a_parser_second.add_argument("pos_two", help="second positional argument", default="green") 33 | return parser 34 | -------------------------------------------------------------------------------- /roots/test-hook-fail/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-hook-fail/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: main 4 | :hook: 5 | -------------------------------------------------------------------------------- /roots/test-hook-fail/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def main() -> None: 7 | parser = ArgumentParser(prog="foo", add_help=False) 8 | print(parser) # noqa: T201 9 | -------------------------------------------------------------------------------- /roots/test-hook/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-hook/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: main 4 | :hook: 5 | -------------------------------------------------------------------------------- /roots/test-hook/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def main() -> None: 7 | parser = ArgumentParser(prog="foo", add_help=False) 8 | args = parser.parse_args() 9 | print(args) # noqa: T201 10 | -------------------------------------------------------------------------------- /roots/test-lower-upper-refs/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-lower-upper-refs/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-lower-upper-refs/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(prog="basic") 8 | parser.add_argument("-d") 9 | parser.add_argument("-D") 10 | return parser 11 | -------------------------------------------------------------------------------- /roots/test-nested/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-nested/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make_1 4 | 5 | Some text inside first directive. 6 | 7 | .. sphinx_argparse_cli:: 8 | :module: parser 9 | :func: make_2 10 | 11 | Some text inside second directive. 12 | 13 | Some text after directives. 14 | -------------------------------------------------------------------------------- /roots/test-nested/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make_1() -> ArgumentParser: 7 | return ArgumentParser(prog="basic-1") 8 | 9 | 10 | def make_2() -> ArgumentParser: 11 | return ArgumentParser(prog="basic-2") 12 | -------------------------------------------------------------------------------- /roots/test-prog/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-prog/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :prog: magic 5 | -------------------------------------------------------------------------------- /roots/test-prog/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser(add_help=False) 8 | -------------------------------------------------------------------------------- /roots/test-ref-duplicate-label/cli.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | 5 | .. sphinx_argparse_cli:: 6 | :module: parser 7 | :func: make 8 | -------------------------------------------------------------------------------- /roots/test-ref-duplicate-label/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-ref-duplicate-label/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | 5 | .. sphinx_argparse_cli:: 6 | :module: parser 7 | :func: make 8 | -------------------------------------------------------------------------------- /roots/test-ref-duplicate-label/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="prog") 8 | parser.add_argument("root") 9 | parser.add_argument("--root", action="store_true", help="root flag") 10 | return parser 11 | -------------------------------------------------------------------------------- /roots/test-ref-prefix-doc/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | sphinx_argparse_cli_prefix_document = True 10 | -------------------------------------------------------------------------------- /roots/test-ref-prefix-doc/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | 5 | Reference test 6 | -------------- 7 | Flag :ref:`index:prog---root` and positional :ref:`index:prog-root`. 8 | -------------------------------------------------------------------------------- /roots/test-ref-prefix-doc/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="prog") 8 | parser.add_argument("root") 9 | parser.add_argument("--root", action="store_true", help="root flag") 10 | return parser 11 | -------------------------------------------------------------------------------- /roots/test-ref/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-ref/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | 5 | Reference test 6 | -------------- 7 | Flag :ref:`prog---root` and positional :ref:`prog-root`. 8 | -------------------------------------------------------------------------------- /roots/test-ref/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(description="argparse tester", prog="prog") 8 | parser.add_argument("root") 9 | parser.add_argument("--root", action="store_true", help="root flag") 10 | return parser 11 | -------------------------------------------------------------------------------- /roots/test-store-true-false/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-store-true-false/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-store-true-false/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(prog="basic") 8 | parser.add_argument("--no-foo", dest="foo", action="store_false") 9 | parser.add_argument("--bar", dest="bar", action="store_true") 10 | return parser 11 | -------------------------------------------------------------------------------- /roots/test-subparsers/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-subparsers/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-subparsers/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(prog="test") 8 | 9 | sub_parsers = parser.add_subparsers() 10 | sub_parser = sub_parsers.add_parser("subparser") 11 | sub_parser.add_argument("--foo") 12 | 13 | sub_parser_no_child = sub_parsers.add_parser("no_child") 14 | sub_parser_no_child.add_argument("argument_one", help="no_child argument") 15 | 16 | sub_sub_parsers = sub_parser.add_subparsers() 17 | sub_sub_parser = sub_sub_parsers.add_parser("child_two") 18 | 19 | sub_sub_sub_parsers = sub_sub_parser.add_subparsers() 20 | sub_sub_sub_parser = sub_sub_sub_parsers.add_parser("child_three") 21 | sub_sub_sub_parser.add_argument("argument", help="sub sub sub child pos argument") 22 | sub_sub_sub_parser.add_argument("--flag", help="sub sub sub child argument") 23 | 24 | return parser 25 | -------------------------------------------------------------------------------- /roots/test-suppressed-action/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-suppressed-action/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | -------------------------------------------------------------------------------- /roots/test-suppressed-action/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import SUPPRESS, ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | parser = ArgumentParser(prog="foo", description="desc", add_help=False) 8 | parser.add_argument( 9 | "--activities-since", 10 | metavar="TIMESTAMP", 11 | help=SUPPRESS, 12 | ) 13 | return parser 14 | -------------------------------------------------------------------------------- /roots/test-title-empty/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-title-empty/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :title: 5 | -------------------------------------------------------------------------------- /roots/test-title-empty/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser(prog="foo", add_help=False) 8 | -------------------------------------------------------------------------------- /roots/test-title-set/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.insert(0, str(Path(__file__).parent)) 7 | extensions = ["sphinx_argparse_cli"] 8 | nitpicky = True 9 | -------------------------------------------------------------------------------- /roots/test-title-set/index.rst: -------------------------------------------------------------------------------- 1 | .. sphinx_argparse_cli:: 2 | :module: parser 3 | :func: make 4 | :title: My own title 5 | -------------------------------------------------------------------------------- /roots/test-title-set/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | def make() -> ArgumentParser: 7 | return ArgumentParser(prog="foo", add_help=False) 8 | -------------------------------------------------------------------------------- /src/sphinx_argparse_cli/__init__.py: -------------------------------------------------------------------------------- 1 | """Sphinx generator for argparse.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING, Any 6 | 7 | from .version import __version__ 8 | 9 | if TYPE_CHECKING: 10 | from sphinx.application import Sphinx 11 | 12 | 13 | def setup(app: Sphinx) -> dict[str, Any]: 14 | app.add_css_file("custom.css") 15 | 16 | from ._logic import SphinxArgparseCli 17 | 18 | app.add_directive(SphinxArgparseCli.name, SphinxArgparseCli) 19 | app.add_config_value("sphinx_argparse_cli_prefix_document", False, "env") # noqa: FBT003 20 | 21 | return {"parallel_read_safe": True} 22 | 23 | 24 | __all__ = [ 25 | "__version__", 26 | ] 27 | -------------------------------------------------------------------------------- /src/sphinx_argparse_cli/_logic.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import sys 5 | from argparse import ( 6 | SUPPRESS, 7 | Action, 8 | ArgumentParser, 9 | HelpFormatter, 10 | RawDescriptionHelpFormatter, 11 | _ArgumentGroup, 12 | _StoreFalseAction, 13 | _StoreTrueAction, 14 | _SubParsersAction, 15 | ) 16 | from collections import defaultdict 17 | from pathlib import Path 18 | from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, cast 19 | 20 | from docutils.nodes import ( 21 | Element, 22 | Node, 23 | Text, 24 | bullet_list, 25 | fully_normalize_name, 26 | list_item, 27 | literal, 28 | literal_block, 29 | paragraph, 30 | reference, 31 | section, 32 | strong, 33 | title, 34 | whitespace_normalize_name, 35 | ) 36 | from docutils.parsers.rst.directives import flag, positive_int, unchanged, unchanged_required 37 | from docutils.statemachine import StringList 38 | from sphinx.locale import __ 39 | from sphinx.util.docutils import SphinxDirective 40 | from sphinx.util.logging import getLogger 41 | 42 | if TYPE_CHECKING: 43 | from collections.abc import Iterator 44 | 45 | from docutils.parsers.rst.states import RSTState, RSTStateMachine 46 | from sphinx.domains.std import StandardDomain 47 | 48 | 49 | class TextAsDefault(NamedTuple): 50 | text: str 51 | 52 | 53 | def make_id(key: str) -> str: 54 | return "-".join(key.split()).rstrip("-") 55 | 56 | 57 | def make_id_lower(key: str) -> str: 58 | # replace all capital letters "X" with "_lower(X)" 59 | return re.sub("[A-Z]", lambda m: "_" + m.group(0).lower(), make_id(key)) 60 | 61 | 62 | logger = getLogger(__name__) 63 | 64 | 65 | class SphinxArgparseCli(SphinxDirective): 66 | name = "sphinx_argparse_cli" 67 | has_content = True 68 | option_spec: ClassVar[dict[str, Any]] = { 69 | "module": unchanged_required, 70 | "func": unchanged_required, 71 | "hook": flag, 72 | "prog": unchanged, 73 | "title": unchanged, 74 | "description": unchanged, 75 | "epilog": unchanged, 76 | "usage_width": positive_int, 77 | "usage_first": flag, 78 | "group_title_prefix": unchanged, 79 | "group_sub_title_prefix": unchanged, 80 | "no_default_values": unchanged, 81 | # :ref: only supports lower-case. If this is set, any 82 | # would-be-upper-case chars will be prefixed with _. Since 83 | # this is backwards incompatible for URL's, this is opt-in. 84 | "force_refs_lower": flag, 85 | } 86 | 87 | def __init__( # noqa: PLR0913 88 | self, 89 | name: str, 90 | arguments: list[str], 91 | options: dict[str, str | None], 92 | content: StringList, 93 | lineno: int, 94 | content_offset: int, 95 | block_text: str, 96 | state: RSTState, 97 | state_machine: RSTStateMachine, 98 | ) -> None: 99 | options.setdefault("group_title_prefix", None) 100 | options.setdefault("group_sub_title_prefix", None) 101 | super().__init__(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine) 102 | self._parser: ArgumentParser | None = None 103 | self._std_domain: StandardDomain = cast("StandardDomain", self.env.get_domain("std")) 104 | self._raw_format: bool = False 105 | self.make_id = make_id_lower if "force_refs_lower" in self.options else make_id 106 | 107 | @property 108 | def parser(self) -> ArgumentParser: 109 | if self._parser is None: 110 | module_name, attr_name = self.options["module"], self.options["func"] 111 | parser_creator = getattr(__import__(module_name, fromlist=[attr_name]), attr_name) 112 | if "hook" in self.options: 113 | original_parse_known_args = ArgumentParser.parse_known_args 114 | ArgumentParser.parse_known_args = _parse_known_args_hook # type: ignore[method-assign,assignment] 115 | try: 116 | parser_creator() 117 | except HookError as hooked: 118 | self._parser = hooked.parser 119 | finally: 120 | ArgumentParser.parse_known_args = original_parse_known_args # type: ignore[method-assign] 121 | else: 122 | self._parser = parser_creator() 123 | 124 | del sys.modules[module_name] # no longer needed cleanup 125 | if self._parser is None: 126 | msg = "Failed to hook argparse to get ArgumentParser" 127 | raise self.error(msg) 128 | 129 | if "prog" in self.options: 130 | self._parser.prog = self.options["prog"] 131 | 132 | self._raw_format = self._parser.formatter_class == RawDescriptionHelpFormatter 133 | return self._parser 134 | 135 | def _load_sub_parsers( 136 | self, sub_parser: _SubParsersAction[ArgumentParser] 137 | ) -> Iterator[tuple[list[str], str, ArgumentParser]]: 138 | parser_to_args: dict[int, list[str]] = defaultdict(list) 139 | str_to_parser: dict[str, ArgumentParser] = {} 140 | for key, parser in sub_parser._name_parser_map.items(): # noqa: SLF001 141 | parser_to_args[id(parser)].append(key) 142 | str_to_parser[key] = parser 143 | done_parser: set[int] = set() 144 | 145 | for name, parser in sub_parser.choices.items(): 146 | parser_id = id(parser) 147 | if parser_id in done_parser: 148 | continue 149 | done_parser.add(parser_id) 150 | aliases = parser_to_args[id(parser)] 151 | aliases.remove(name) 152 | # help is stored in a pseudo action 153 | help_msg = next((a.help for a in sub_parser._choices_actions if a.dest == name), None) or "" # noqa: SLF001 154 | yield aliases, help_msg, parser 155 | 156 | # If this parser has a subparser, recurse into it 157 | if parser._subparsers: # noqa: SLF001 158 | sub_sub_parser: _SubParsersAction[ArgumentParser] 159 | sub_sub_parser = parser._subparsers._group_actions[0] # type: ignore[assignment] # noqa: SLF001 160 | yield from self._load_sub_parsers(sub_sub_parser) 161 | 162 | def load_sub_parsers(self) -> Iterator[tuple[list[str], str, ArgumentParser]]: 163 | top_sub_parser = self.parser._subparsers # noqa: SLF001 164 | if not top_sub_parser: 165 | return 166 | sub_parser: _SubParsersAction[ArgumentParser] 167 | sub_parser = top_sub_parser._group_actions[0] # type: ignore[assignment] # noqa: SLF001 168 | 169 | yield from self._load_sub_parsers(sub_parser) 170 | 171 | def run(self) -> list[Node]: 172 | # construct headers 173 | self.env.note_reread() # this document needs to be always updated 174 | title_text = self.options.get("title", f"{self.parser.prog} - CLI interface").strip() 175 | if not title_text.strip(): 176 | home_section: Element = paragraph() 177 | else: 178 | home_section = section("", title("", Text(title_text)), ids=[self.make_id(title_text)], names=[title_text]) 179 | 180 | if "usage_first" in self.options: 181 | home_section += self._mk_usage(self.parser) 182 | 183 | if description := self._pre_format(self.options.get("description", self.parser.description)): 184 | home_section += description 185 | 186 | if "usage_first" not in self.options: 187 | home_section += self._mk_usage(self.parser) 188 | 189 | # construct groups excluding sub-parsers 190 | for group in self.parser._action_groups: # noqa: SLF001 191 | if not group._group_actions or group is self.parser._subparsers: # noqa: SLF001 192 | continue 193 | home_section += self._mk_option_group(group, prefix=self.parser.prog.split("/")[-1]) 194 | # construct sub-parser 195 | for aliases, help_msg, parser in self.load_sub_parsers(): 196 | home_section += self._mk_sub_command(aliases, help_msg, parser) 197 | 198 | if epilog := self._pre_format(self.options.get("epilog", self.parser.epilog)): 199 | home_section += epilog 200 | 201 | if self.content: 202 | self.state.nested_parse(self.content, self.content_offset, home_section) 203 | 204 | return [home_section] 205 | 206 | def _pre_format(self, block: None | str) -> None | paragraph | literal_block: 207 | if block is None: 208 | return None 209 | if self._raw_format and "\n" in block: 210 | lit = literal_block("", Text(block)) 211 | lit["language"] = "none" 212 | return lit 213 | return paragraph("", Text(block)) 214 | 215 | def _mk_option_group(self, group: _ArgumentGroup, prefix: str) -> section: 216 | sub_title_prefix: str = self.options["group_sub_title_prefix"] 217 | title_prefix = self.options["group_title_prefix"] 218 | title_text = self._build_opt_grp_title(group, prefix, sub_title_prefix, title_prefix) 219 | title_ref: str = f"{prefix}{' ' if prefix else ''}{group.title}" 220 | ref_id = self.make_id(title_ref) 221 | # the text sadly needs to be prefixed, because otherwise the autosectionlabel will conflict 222 | header = title("", Text(title_text)) 223 | group_section = section("", header, ids=[ref_id], names=[ref_id]) 224 | if description := self._pre_format(group.description): 225 | group_section += description 226 | self._register_ref(ref_id, title_text, group_section) 227 | opt_group = bullet_list() 228 | for action in group._group_actions: # noqa: SLF001 229 | if action.help == SUPPRESS: 230 | continue 231 | point = self._mk_option_line(action, prefix) 232 | opt_group += point 233 | group_section += opt_group 234 | return group_section 235 | 236 | def _build_opt_grp_title(self, group: _ArgumentGroup, prefix: str, sub_title_prefix: str, title_prefix: str) -> str: 237 | title_text, elements = "", prefix.split(" ") 238 | if title_prefix is not None: 239 | title_prefix = title_prefix.replace("{prog}", elements[0]) 240 | if title_prefix: 241 | title_text += f"{title_prefix} " 242 | if " " in prefix: 243 | if sub_title_prefix is not None: 244 | title_text = self._append_title(title_text, sub_title_prefix, elements[0], " ".join(elements[1:])) 245 | else: 246 | title_text += f"{' '.join(prefix.split(' ')[1:])} " 247 | elif " " in prefix: 248 | if sub_title_prefix is not None: 249 | title_text += f"{elements[0]} " 250 | title_text = self._append_title(title_text, sub_title_prefix, elements[0], " ".join(elements[1:])) 251 | else: 252 | title_text += f"{' '.join(elements)} " 253 | else: 254 | title_text += f"{prefix} " 255 | title_text += group.title or "" 256 | return title_text 257 | 258 | def _mk_option_line(self, action: Action, prefix: str) -> list_item: 259 | line = paragraph() 260 | as_key = action.dest 261 | if action.metavar: 262 | as_key = action.metavar if isinstance(action.metavar, str) else action.metavar[0] 263 | if action.option_strings: 264 | first = True 265 | is_flag = action.nargs == 0 266 | for opt in action.option_strings: 267 | if first: 268 | first = False 269 | else: 270 | line += Text(", ") 271 | self._mk_option_name(line, prefix, opt) 272 | if not is_flag: 273 | line += Text(" ") 274 | line += literal(text=as_key.upper()) 275 | else: 276 | self._mk_option_name(line, prefix, as_key) 277 | 278 | point = list_item("", line, ids=[]) 279 | if action.help: 280 | help_text = load_help_text(action.help) 281 | temp = paragraph() 282 | self.state.nested_parse(StringList(help_text.split("\n")), 0, temp) 283 | line += Text(" - ") 284 | for content in cast("paragraph", temp.children[0]).children: 285 | line += content 286 | if ( 287 | "no_default_values" not in self.options 288 | and action.default != SUPPRESS 289 | and not re.match(r".*[ (]default[s]? .*", (action.help or "")) 290 | and not isinstance(action, _StoreTrueAction | _StoreFalseAction) 291 | ): 292 | line += Text(" (default: ") 293 | line += literal(text=str(action.default).replace(str(Path.cwd()), "{cwd}")) 294 | line += Text(")") 295 | return point 296 | 297 | def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None: 298 | ref_id = self.make_id(f"{prefix}-{opt}") 299 | ref_title = f"{prefix} {opt}" 300 | ref = reference("", refid=ref_id, reftitle=ref_title) 301 | line.attributes["ids"].append(ref_id) 302 | st = strong() 303 | st += literal(text=opt) 304 | ref += st 305 | self._register_ref(ref_id, ref_title, ref, is_cli_option=True) 306 | line += ref 307 | 308 | def _register_ref( 309 | self, 310 | ref_name: str, 311 | ref_title: str, 312 | node: Element, 313 | is_cli_option: bool = False, # noqa: FBT001, FBT002 314 | ) -> None: 315 | doc_name = self.env.docname 316 | normalize_name = whitespace_normalize_name if is_cli_option else fully_normalize_name 317 | if self.env.config.sphinx_argparse_cli_prefix_document: 318 | name = normalize_name(f"{doc_name}:{ref_name}") 319 | else: 320 | name = normalize_name(ref_name) 321 | if name in self._std_domain.labels: 322 | logger.warning( 323 | __("duplicate label %s, other instance in %s"), 324 | name, 325 | self.env.doc2path(self._std_domain.labels[name][0]), 326 | location=node, 327 | type="sphinx-argparse-cli", 328 | subtype=self.env.docname, 329 | ) 330 | self._std_domain.anonlabels[name] = doc_name, ref_name 331 | self._std_domain.labels[name] = doc_name, ref_name, ref_title 332 | 333 | def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentParser) -> section: 334 | sub_title_prefix: str = self.options["group_sub_title_prefix"] 335 | title_prefix: str = self.options["group_title_prefix"] 336 | 337 | title_text = self._build_sub_cmd_title(parser, sub_title_prefix, title_prefix) 338 | title_ref: str = parser.prog 339 | if aliases: 340 | aliases_text: str = f" ({', '.join(aliases)})" 341 | title_text += aliases_text 342 | title_ref += aliases_text 343 | title_text = title_text.strip() 344 | ref_id = self.make_id(title_ref) 345 | group_section = section("", title("", Text(title_text)), ids=[ref_id], names=[title_ref]) 346 | self._register_ref(ref_id, title_ref, group_section) 347 | 348 | if "usage_first" in self.options: 349 | group_section += self._mk_usage(parser) 350 | 351 | command_desc = (parser.description or help_msg or "").strip() 352 | if command_desc: 353 | desc_paragraph = paragraph("", Text(command_desc)) 354 | group_section += desc_paragraph 355 | 356 | if "usage_first" not in self.options: 357 | group_section += self._mk_usage(parser) 358 | 359 | for group in parser._action_groups: # noqa: SLF001 360 | if not group._group_actions: # do not show empty groups # noqa: SLF001 361 | continue 362 | if isinstance(group._group_actions[0], _SubParsersAction): # noqa: SLF001 363 | # If this is a subparser, ignore it 364 | continue 365 | group_section += self._mk_option_group(group, prefix=parser.prog) 366 | return group_section 367 | 368 | def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, title_prefix: str) -> str: 369 | title_text, elements = "", parser.prog.split(" ") 370 | if title_prefix is not None: 371 | title_prefix = title_prefix.replace("{prog}", elements[0]) 372 | if title_prefix: 373 | title_text += f"{title_prefix} " 374 | if sub_title_prefix is not None: 375 | title_text = self._append_title(title_text, sub_title_prefix, elements[0], elements[1]) 376 | else: 377 | title_text += elements[1] 378 | elif sub_title_prefix is not None: 379 | title_text += f"{elements[0]} " 380 | title_text = self._append_title(title_text, sub_title_prefix, elements[0], elements[1]) 381 | else: 382 | title_text += parser.prog 383 | return title_text.rstrip() 384 | 385 | @staticmethod 386 | def _append_title(title_text: str, sub_title_prefix: str, prog: str, sub_cmd: str) -> str: 387 | if sub_title_prefix: 388 | sub_title_prefix = sub_title_prefix.replace("{prog}", prog) 389 | sub_title_prefix = sub_title_prefix.replace("{subcommand}", sub_cmd) 390 | title_text += f"{sub_title_prefix} " 391 | return title_text 392 | 393 | def _mk_usage(self, parser: ArgumentParser) -> literal_block: 394 | parser.formatter_class = lambda prog: HelpFormatter(prog, width=self.options.get("usage_width", 100)) 395 | texts = parser.format_usage()[len("usage: ") :].splitlines() 396 | texts = [line if at == 0 else f"{' ' * (len(parser.prog) + 1)}{line.lstrip()}" for at, line in enumerate(texts)] 397 | return literal_block("", Text("\n".join(texts))) 398 | 399 | 400 | SINGLE_QUOTE = re.compile(r"[']+(.+?)[']+") 401 | DOUBLE_QUOTE = re.compile(r'["]+(.+?)["]+') 402 | CURLY_BRACES = re.compile(r"[{](.+?)[}]") 403 | 404 | 405 | def load_help_text(help_text: str) -> str: 406 | single_quote = SINGLE_QUOTE.sub("``'\\1'``", help_text) 407 | double_quote = DOUBLE_QUOTE.sub('``"\\1"``', single_quote) 408 | return CURLY_BRACES.sub("``{\\1}``", double_quote) 409 | 410 | 411 | class HookError(Exception): 412 | def __init__(self, parser: ArgumentParser) -> None: 413 | self.parser = parser 414 | 415 | 416 | def _parse_known_args_hook(self: ArgumentParser, *args: Any, **kwargs: Any) -> None: # noqa: ARG001 417 | raise HookError(self) 418 | 419 | 420 | __all__ = [ 421 | "SphinxArgparseCli", 422 | ] 423 | -------------------------------------------------------------------------------- /src/sphinx_argparse_cli/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tox-dev/sphinx-argparse-cli/a3a25a503f36c81cb22877d7564b8be26274df58/src/sphinx_argparse_cli/py.typed -------------------------------------------------------------------------------- /tests/complex.txt: -------------------------------------------------------------------------------- 1 | complex - CLI interface 2 | *********************** 3 | 4 | argparse tester 5 | 6 | complex [-h] [--root] [--no-help] [--outdir out_dir] [--in-dir IN_DIR] [--foo | --bar] 7 | {first,f,second,third} ... 8 | 9 | 10 | complex options 11 | =============== 12 | 13 | * **"-h"**, **"--help"** - show this help message and exit 14 | 15 | * **"--root"** - root flag 16 | 17 | * **"--no-help"** 18 | 19 | * **"--outdir"** "OUT_DIR", **"-o"** "OUT_DIR" - output directory 20 | (default: "None") 21 | 22 | * **"--in-dir"** "IN_DIR", **"-i"** "IN_DIR" - input directory 23 | (default: "None") 24 | 25 | 26 | complex Exclusive 27 | ================= 28 | 29 | this is an exclusive group 30 | 31 | * **"--foo"** - foo 32 | 33 | * **"--bar"** - bar 34 | 35 | 36 | complex first (f) 37 | ================= 38 | 39 | a-first-desc 40 | 41 | complex first [-h] [--flag] [--root] one pos_two 42 | 43 | 44 | complex first positional arguments 45 | ---------------------------------- 46 | 47 | * **"one"** - first positional argument (default: "None") 48 | 49 | * **"pos_two"** - second positional argument (default: "1") 50 | 51 | 52 | complex first options 53 | --------------------- 54 | 55 | * **"-h"**, **"--help"** - show this help message and exit 56 | 57 | * **"--flag"** - a parser first flag 58 | 59 | * **"--root"** - root flag 60 | 61 | 62 | complex second 63 | ============== 64 | 65 | complex second [-h] [--flag] [--root] one pos_two 66 | 67 | 68 | complex second positional arguments 69 | ----------------------------------- 70 | 71 | * **"one"** - first positional argument (default: "None") 72 | 73 | * **"pos_two"** - second positional argument (default: "green") 74 | 75 | 76 | complex second options 77 | ---------------------- 78 | 79 | * **"-h"**, **"--help"** - show this help message and exit 80 | 81 | * **"--flag"** - a parser second flag 82 | 83 | * **"--root"** - root flag 84 | 85 | 86 | complex third 87 | ============= 88 | 89 | complex third [-h] 90 | 91 | 92 | complex third options 93 | --------------------- 94 | 95 | * **"-h"**, **"--help"** - show this help message and exit 96 | 97 | test epilog 98 | -------------------------------------------------------------------------------- /tests/complex_pre_310.txt: -------------------------------------------------------------------------------- 1 | complex - CLI interface 2 | *********************** 3 | 4 | argparse tester 5 | 6 | complex [-h] [--root] [--no-help] [--outdir out_dir] [--in-dir IN_DIR] [--foo | --bar] 7 | {first,f,second,third} ... 8 | 9 | 10 | complex optional arguments 11 | ========================== 12 | 13 | * **"-h"**, **"--help"** - show this help message and exit 14 | 15 | * **"--root"** - root flag 16 | 17 | * **"--no-help"** 18 | 19 | * **"--outdir"** "OUT_DIR", **"-o"** "OUT_DIR" - output directory 20 | (default: "None") 21 | 22 | * **"--in-dir"** "IN_DIR", **"-i"** "IN_DIR" - input directory 23 | (default: "None") 24 | 25 | 26 | complex Exclusive 27 | ================= 28 | 29 | this is an exclusive group 30 | 31 | * **"--foo"** - foo 32 | 33 | * **"--bar"** - bar 34 | 35 | 36 | complex first (f) 37 | ================= 38 | 39 | a-first-desc 40 | 41 | complex first [-h] [--flag] [--root] one pos_two 42 | 43 | 44 | complex first positional arguments 45 | ---------------------------------- 46 | 47 | * **"one"** - first positional argument (default: "None") 48 | 49 | * **"pos_two"** - second positional argument (default: "1") 50 | 51 | 52 | complex first optional arguments 53 | -------------------------------- 54 | 55 | * **"-h"**, **"--help"** - show this help message and exit 56 | 57 | * **"--flag"** - a parser first flag 58 | 59 | * **"--root"** - root flag 60 | 61 | 62 | complex second 63 | ============== 64 | 65 | complex second [-h] [--flag] [--root] one pos_two 66 | 67 | 68 | complex second positional arguments 69 | ----------------------------------- 70 | 71 | * **"one"** - first positional argument (default: "None") 72 | 73 | * **"pos_two"** - second positional argument (default: "green") 74 | 75 | 76 | complex second optional arguments 77 | --------------------------------- 78 | 79 | * **"-h"**, **"--help"** - show this help message and exit 80 | 81 | * **"--flag"** - a parser second flag 82 | 83 | * **"--root"** - root flag 84 | 85 | 86 | complex third 87 | ============= 88 | 89 | complex third [-h] 90 | 91 | 92 | complex third optional arguments 93 | -------------------------------- 94 | 95 | * **"-h"**, **"--help"** - show this help message and exit 96 | 97 | test epilog 98 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from typing import TYPE_CHECKING 5 | 6 | import pytest 7 | from docutils import __version__ as docutils_version 8 | from sphinx import __display_version__ as sphinx_version 9 | 10 | if TYPE_CHECKING: 11 | from _pytest.config import Config 12 | 13 | pytest_plugins = "sphinx.testing.fixtures" 14 | collect_ignore = ["roots"] 15 | 16 | 17 | def pytest_report_header(config: Config) -> str: # noqa: ARG001 18 | return f"libraries: Sphinx-{sphinx_version}, docutils-{docutils_version}" 19 | 20 | 21 | @pytest.fixture(scope="session", name="rootdir") 22 | def root_dir() -> Path: 23 | return Path(__file__).parents[1].absolute() / "roots" 24 | 25 | 26 | def pytest_configure(config: Config) -> None: 27 | config.addinivalue_line("markers", "prepare") 28 | -------------------------------------------------------------------------------- /tests/test_logic.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING 7 | 8 | import pytest 9 | 10 | from sphinx_argparse_cli._logic import make_id, make_id_lower 11 | 12 | if TYPE_CHECKING: 13 | from io import StringIO 14 | 15 | from _pytest.fixtures import SubRequest 16 | from sphinx.testing.util import SphinxTestApp 17 | 18 | 19 | @pytest.fixture(scope="session") 20 | def opt_grp_name() -> tuple[str, str]: 21 | return "options", "options" # pragma: no cover 22 | return "optional arguments", "optional-arguments" # pragma: no cover 23 | 24 | 25 | @pytest.fixture 26 | def build_outcome(app: SphinxTestApp, request: SubRequest) -> str: 27 | prepare_marker = request.node.get_closest_marker("prepare") 28 | if prepare_marker: 29 | directive_args: list[str] | None = prepare_marker.kwargs.get("directive_args") 30 | if directive_args: # pragma: no branch 31 | index = Path(app.confdir) / "index.rst" 32 | if not any(i for i in directive_args if i.startswith(":module:")): # pragma: no branch 33 | directive_args.append(":module: parser") 34 | if not any(i for i in directive_args if i.startswith(":func:")): # pragma: no branch 35 | directive_args.append(":func: make") 36 | args = [f" {i}" for i in directive_args] 37 | index.write_text(os.linesep.join([".. sphinx_argparse_cli::", *args])) 38 | 39 | ext_mapping = {"html": "html", "text": "txt"} 40 | sphinx_marker = request.node.get_closest_marker("sphinx") 41 | assert sphinx_marker is not None 42 | ext = ext_mapping[sphinx_marker.kwargs.get("buildername")] 43 | 44 | app.build() 45 | return (Path(app.outdir) / f"index.{ext}").read_text() 46 | 47 | 48 | @pytest.mark.sphinx(buildername="html", testroot="basic") 49 | def test_basic_as_html(build_outcome: str) -> None: 50 | assert build_outcome 51 | 52 | 53 | @pytest.mark.sphinx(buildername="text", testroot="complex") 54 | def test_complex_as_text(build_outcome: str) -> None: 55 | name = "complex.txt" if sys.version_info >= (3, 10) else "complex_pre_310.txt" 56 | expected = (Path(__file__).parent / name).read_text() 57 | assert build_outcome == expected 58 | 59 | 60 | @pytest.mark.sphinx(buildername="html", testroot="complex") 61 | def test_complex_as_html(build_outcome: str) -> None: 62 | assert build_outcome 63 | 64 | 65 | @pytest.mark.sphinx(buildername="html", testroot="hook") 66 | def test_hook(build_outcome: str) -> None: 67 | assert build_outcome 68 | 69 | 70 | @pytest.mark.sphinx(buildername="text", testroot="hook-fail") 71 | def test_hook_fail(app: SphinxTestApp, warning: StringIO) -> None: 72 | app.build() 73 | text = (Path(app.outdir) / "index.txt").read_text() 74 | assert "Failed to hook argparse to get ArgumentParser" in warning.getvalue() 75 | assert not text 76 | 77 | 78 | @pytest.mark.sphinx(buildername="text", testroot="prog") 79 | def test_prog_as_text(build_outcome: str) -> None: 80 | assert build_outcome == "magic - CLI interface\n*********************\n\n magic\n" 81 | 82 | 83 | @pytest.mark.sphinx(buildername="text", testroot="title-set") 84 | def test_set_title_as_text(build_outcome: str) -> None: 85 | assert build_outcome == "My own title\n************\n\n foo\n" 86 | 87 | 88 | @pytest.mark.sphinx(buildername="text", testroot="title-empty") 89 | def test_empty_title_as_text(build_outcome: str) -> None: 90 | assert build_outcome == " foo\n" 91 | 92 | 93 | @pytest.mark.sphinx(buildername="text", testroot="description-set") 94 | def test_set_description_as_text(build_outcome: str) -> None: 95 | assert build_outcome == "foo - CLI interface\n*******************\n\nMy own description\n\n foo\n" 96 | 97 | 98 | @pytest.mark.sphinx(buildername="text", testroot="description-empty") 99 | def test_empty_description_as_text(build_outcome: str) -> None: 100 | assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n" 101 | 102 | 103 | @pytest.mark.sphinx(buildername="html", testroot="description-multiline") 104 | def test_multiline_description_as_html(build_outcome: str) -> None: 105 | ref = ( 106 | "This description\nspans multiple lines.\n\n this line is indented.\n and also this.\n\nNow this should be" 107 | " a separate paragraph.\n" 108 | ) 109 | assert ref in build_outcome 110 | 111 | ref = "This group description\n\nspans multiple lines.\n" 112 | assert ref in build_outcome 113 | 114 | 115 | @pytest.mark.sphinx(buildername="text", testroot="epilog-set") 116 | def test_set_epilog_as_text(build_outcome: str) -> None: 117 | assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n\nMy own epilog\n" 118 | 119 | 120 | @pytest.mark.sphinx(buildername="text", testroot="epilog-empty") 121 | def test_empty_epilog_as_text(build_outcome: str) -> None: 122 | assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n" 123 | 124 | 125 | @pytest.mark.sphinx(buildername="html", testroot="epilog-multiline") 126 | def test_multiline_epilog_as_html(build_outcome: str) -> None: 127 | ref = ( 128 | "This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\nNow this should be" 129 | " a separate paragraph.\n" 130 | ) 131 | assert ref in build_outcome 132 | 133 | 134 | @pytest.mark.sphinx(buildername="text", testroot="complex") 135 | @pytest.mark.prepare(directive_args=[":usage_width: 100"]) 136 | def test_usage_width_default(build_outcome: str) -> None: 137 | assert "complex second [-h] [--flag] [--root] one pos_two\n" in build_outcome 138 | 139 | 140 | @pytest.mark.sphinx(buildername="text", testroot="complex") 141 | @pytest.mark.prepare(directive_args=[":usage_width: 50"]) 142 | def test_usage_width_custom(build_outcome: str) -> None: 143 | assert "complex second [-h] [--flag] [--root]\n" in build_outcome 144 | 145 | 146 | @pytest.mark.sphinx(buildername="text", testroot="complex") 147 | @pytest.mark.prepare(directive_args=[":usage_first:"]) 148 | def test_set_usage_first(build_outcome: str) -> None: 149 | assert "complex [-h]" in build_outcome.split("argparse tester")[0] 150 | assert "complex first [-h]" in build_outcome.split("a-first-desc")[0] 151 | 152 | 153 | @pytest.mark.sphinx(buildername="text", testroot="suppressed-action") 154 | def test_suppressed_action(build_outcome: str) -> None: 155 | assert "--activities-since" not in build_outcome 156 | 157 | 158 | @pytest.mark.parametrize( 159 | ("example", "output"), 160 | [ 161 | ("", ""), 162 | ("{", "{"), 163 | ('"', '"'), 164 | ("'", "'"), 165 | ("{a}", "``{a}``"), 166 | ('"a"', '``"a"``'), 167 | ("'a'", "``'a'``"), 168 | ], 169 | ) 170 | def test_help_loader(example: str, output: str) -> None: 171 | from sphinx_argparse_cli._logic import load_help_text 172 | 173 | result = load_help_text(example) 174 | assert result == output 175 | 176 | 177 | @pytest.mark.sphinx(buildername="html", testroot="ref") 178 | def test_ref_as_html(build_outcome: str) -> None: 179 | ref = ( 180 | '

Flag prog --root and' 181 | ' positional prog root.' 182 | "

" 183 | ) 184 | assert ref in build_outcome 185 | 186 | 187 | @pytest.mark.sphinx(buildername="html", testroot="ref-prefix-doc") 188 | def test_ref_prefix_doc(build_outcome: str) -> None: 189 | ref = ( 190 | '

Flag prog --root and' 191 | ' positional prog root.' 192 | "

" 193 | ) 194 | assert ref in build_outcome 195 | 196 | 197 | @pytest.mark.sphinx(buildername="text", testroot="ref-duplicate-label") 198 | def test_ref_duplicate_label(build_outcome: tuple[str, str], warning: StringIO) -> None: 199 | assert build_outcome 200 | assert "duplicate label prog---help" in warning.getvalue() 201 | 202 | 203 | @pytest.mark.sphinx(buildername="html", testroot="group-title-prefix-default") 204 | def test_group_title_prefix_default(build_outcome: str) -> None: 205 | assert '

prog positional arguments None: 210 | assert '

positional arguments None: 215 | assert '

custom positional arguments None: 220 | assert '

barfoo positional arguments None: 225 | grp, anchor = opt_grp_name 226 | assert '

complex Exclusivecomplex custom (f)complex custom {grp}complex customcustom-2 {grp}myprog custom-3 {grp} None: 241 | grp, anchor = opt_grp_name 242 | assert '

complex Exclusivecomplex (f)complex {grp}complexmyprog {grp} None: 255 | grp, anchor = opt_grp_name 256 | assert '

Exclusive(f)positional arguments{grp} None: 265 | grp, anchor = opt_grp_name 266 | assert f'

bar {grp}bar Exclusivebar baronlyroot (f)bar baronlyroot first positional arguments None: 274 | assert "False" not in build_outcome 275 | assert "True" not in build_outcome 276 | 277 | 278 | @pytest.mark.sphinx(buildername="html", testroot="lower-upper-refs") 279 | def test_lower_upper_refs(build_outcome: str, warning: StringIO) -> None: 280 | assert '

' in build_outcome 281 | assert '

' in build_outcome 282 | assert not warning.getvalue() 283 | 284 | 285 | @pytest.mark.parametrize( 286 | ("key", "mixed", "lower"), 287 | [ 288 | ("ProgramName", "ProgramName", "_program_name"), 289 | ("ProgramName -A", "ProgramName--A", "_program_name--_a"), 290 | ("ProgramName -a", "ProgramName--a", "_program_name--a"), 291 | ], 292 | ) 293 | def test_make_id(key: str, mixed: str, lower: str) -> None: 294 | assert make_id(key) == mixed 295 | assert make_id_lower(key) == lower 296 | 297 | 298 | @pytest.mark.sphinx(buildername="html", testroot="force-refs-lower") 299 | def test_ref_cases(build_outcome: str, warning: StringIO) -> None: 300 | assert '' in build_outcome 301 | assert '' in build_outcome 302 | assert not warning.getvalue() 303 | 304 | 305 | @pytest.mark.sphinx(buildername="text", testroot="default-handling") 306 | def test_with_default(build_outcome: str) -> None: 307 | assert ( 308 | build_outcome 309 | == """foo - CLI interface 310 | ******************* 311 | 312 | foo x 313 | 314 | 315 | foo positional arguments 316 | ======================== 317 | 318 | * **"x"** - arg (default: True) 319 | """ 320 | ) 321 | 322 | 323 | @pytest.mark.sphinx(buildername="html", testroot="nested") 324 | def test_nested_content(build_outcome: str) -> None: 325 | assert '

' in build_outcome 326 | assert "

basic-1 - CLI interface" in build_outcome 327 | assert "

basic-1 opt" in build_outcome 328 | assert "

Some text inside first directive.

" in build_outcome 329 | assert '
' in build_outcome 330 | assert "

basic-2 - CLI interface" in build_outcome 331 | assert "

basic-2 opt" in build_outcome 332 | assert "

Some text inside second directive.

" in build_outcome 333 | assert "

Some text after directives.

" in build_outcome 334 | 335 | 336 | @pytest.mark.sphinx(buildername="html", testroot="subparsers") 337 | def test_subparsers(build_outcome: str) -> None: 338 | assert '
' in build_outcome 339 | assert '
' in build_outcome 340 | assert '
' in build_outcome 341 | assert '
' in build_outcome 342 | assert '
' in build_outcome 343 | assert '
' in build_outcome 344 | assert '
' in build_outcome 345 | assert '
' in build_outcome 346 | assert '
' in build_outcome 347 | assert '
' in build_outcome 348 | assert '
' in build_outcome 349 | -------------------------------------------------------------------------------- /tests/test_sphinx_argparse_cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def test_version() -> None: 5 | import sphinx_argparse_cli 6 | 7 | assert sphinx_argparse_cli.__version__ 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | tox-uv>=1.11.3 5 | env_list = 6 | fix 7 | 3.13 8 | 3.12 9 | 3.11 10 | 3.10 11 | type 12 | pkg_meta 13 | skip_missing_interpreters = true 14 | 15 | [testenv] 16 | description = run the unit tests with pytest under {base_python} 17 | package = wheel 18 | wheel_build_env = .pkg 19 | extras = 20 | testing 21 | pass_env = 22 | PYTEST_* 23 | set_env = 24 | COVERAGE_FILE = {work_dir}/.coverage.{env_name} 25 | commands = 26 | python -m pytest {tty:--color=yes} {posargs: \ 27 | --cov {env_site_packages_dir}{/}sphinx_argparse_cli --cov {tox_root}{/}tests \ 28 | --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ 29 | --cov-report html:{env_tmp_dir}{/}htmlcov --cov-report xml:{work_dir}{/}coverage.{env_name}.xml \ 30 | --junitxml {work_dir}{/}junit.{env_name}.xml \ 31 | tests} 32 | 33 | [testenv:fix] 34 | description = format the code base to adhere to our styles, and complain about what we cannot do automatically 35 | skip_install = true 36 | deps = 37 | pre-commit-uv>=4.1.1 38 | commands = 39 | pre-commit run --all-files --show-diff-on-failure 40 | 41 | [testenv:type] 42 | description = run type check on code base 43 | deps = 44 | mypy==1.11.2 45 | types-docutils>=0.21.0.20240907 46 | commands = 47 | mypy src tests {posargs} 48 | 49 | [testenv:pkg_meta] 50 | description = check that the long description is valid 51 | skip_install = true 52 | deps = 53 | check-wheel-contents>=0.6 54 | twine>=5.1.1 55 | uv>=0.4.10 56 | commands = 57 | uv build --sdist --wheel --out-dir {env_tmp_dir} . 58 | twine check {env_tmp_dir}{/}* 59 | check-wheel-contents --no-config {env_tmp_dir} 60 | 61 | [testenv:dev] 62 | description = generate a DEV environment 63 | package = editable 64 | commands = 65 | uv pip tree 66 | python -c 'import sys; print(sys.executable)' 67 | --------------------------------------------------------------------------------