pax_global_header00006660000000000000000000000064152417777760014541gustar00rootroot0000000000000052 comment=89c7a5751babd15350caad6abe327ba93d415a7a sphinx-doc-sphinx-argparse-89c7a57/000077500000000000000000000000001524177777600172775ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/.github/000077500000000000000000000000001524177777600206375ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/.github/workflows/000077500000000000000000000000001524177777600226745ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/.github/workflows/ci.yml000066400000000000000000000024161524177777600240150ustar00rootroot00000000000000name: CI on: push: paths: - ".github/workflows/ci.yml" - "sphinxarg/**" - "test/**" pull_request: paths: - ".github/workflows/ci.yml" - "sphinxarg/**" - "test/**" workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true env: FORCE_COLOR: "1" jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Check Python version run: python --version --version - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install .[test] - name: Test with pytest run: > python -m pytest -vv --cov . --cov-append --cov-config pyproject.toml - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: flags: unittests sphinx-doc-sphinx-argparse-89c7a57/.github/workflows/create-release.yml000066400000000000000000000053551524177777600263100ustar00rootroot00000000000000name: Create release on: push: tags: - "*" workflow_dispatch: permissions: contents: read jobs: publish-pypi: runs-on: ubuntu-latest name: PyPI Release environment: release permissions: id-token: write # for PyPI trusted publishing steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3" cache: pip cache-dependency-path: pyproject.toml - name: Install build dependencies (pypa/build, twine) run: | pip install -U pip pip install build twine - name: Build distribution run: python -m build - name: Mint PyPI API token id: mint-token uses: actions/github-script@v7 with: # language=JavaScript script: | // retrieve the ambient OIDC token const oidc_request_token = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; const oidc_request_url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; const oidc_resp = await fetch(`${oidc_request_url}&audience=pypi`, { headers: {Authorization: `bearer ${oidc_request_token}`}, }); const oidc_token = (await oidc_resp.json()).value; // exchange the OIDC token for an API token const mint_resp = await fetch('https://pypi.org/_/oidc/github/mint-token', { method: 'post', body: `{"token": "${oidc_token}"}` , headers: {'Content-Type': 'application/json'}, }); const api_token = (await mint_resp.json()).token; // mask the newly minted API token, so that we don't accidentally leak it core.setSecret(api_token) core.setOutput('api-token', api_token) - name: Upload to PyPI env: TWINE_NON_INTERACTIVE: "true" TWINE_USERNAME: "__token__" TWINE_PASSWORD: "${{ steps.mint-token.outputs.api-token }}" run: | twine check dist/* twine upload dist/* github-release: runs-on: ubuntu-latest name: GitHub release environment: release permissions: contents: write # for softprops/action-gh-release to create GitHub release steps: - uses: actions/checkout@v4 - name: Get release version id: get_version uses: actions/github-script@v7 with: script: core.setOutput('version', context.ref.replace("refs/tags/", "")) - name: Create GitHub release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: name: "Release ${{ steps.get_version.outputs.version }}" generate_release_notes: true sphinx-doc-sphinx-argparse-89c7a57/.github/workflows/lint.yml000066400000000000000000000024531524177777600243710ustar00rootroot00000000000000name: Lint source code on: push: pull_request: workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true env: FORCE_COLOR: "1" jobs: lint: name: "Ruff & MyPy" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install .[lint] pytest # Also install pytest here, otherwise mypy will complain about our test files - name: Lint with Ruff run: ruff check . --output-format github - name: Format with Ruff run: ruff format . --diff - name: Type check with MyPy run: mypy twine: name: "Check packing with Twine" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade twine build - name: Check with twine run: | python -m build . twine check dist/* sphinx-doc-sphinx-argparse-89c7a57/.gitignore000066400000000000000000000005211524177777600212650ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .idea .pydevproject .env/ docs/_build/ .cache/ sphinx-doc-sphinx-argparse-89c7a57/.pre-commit-config.yaml000066400000000000000000000016461524177777600235670ustar00rootroot00000000000000# To install the git pre-commit hook run: # pre-commit install # To update the pre-commit hooks run: # pre-commit install-hooks minimum_pre_commit_version: "3" repos: - repo: meta hooks: - id: identity - id: check-hooks-apply - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-builtin-literals - id: check-executables-have-shebangs - id: check-merge-conflict - id: check-toml - id: check-yaml - id: debug-statements - id: detect-private-key - id: end-of-file-fixer - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.2 hooks: - id: ruff - id: ruff-format - repo: local hooks: - id: mypy name: mypy language: system entry: mypy types: [python] exclude: ^(docs|test/roots)/ sphinx-doc-sphinx-argparse-89c7a57/.readthedocs.yaml000066400000000000000000000002401524177777600225220ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py build: os: ubuntu-22.04 tools: python: "3" python: install: - method: pip path: .[docs] sphinx-doc-sphinx-argparse-89c7a57/.ruff.toml000066400000000000000000000017221524177777600212160ustar00rootroot00000000000000target-version = "py310" # Pin Ruff to Python 3.10 line-length = 95 output-format = "full" [lint] preview = true select = [ "C4", "B", # flake8-bugbear "E", # pycodestyle "EM", # flake8-errmsg "F", # pyflakes "FA", # flake8-future-annotations "FLY", # flynt "FURB",# refurb "G", # flake8-logging-format "I", # isort "LOG", # flake8-logging "N", # pep8-naming "PERF",# perflint "PGH", # pygrep-hooks "PT", # flake8-pytest-style "TCH", # flake8-type-checking "UP", # pyupgrade "W", # pycodestyle ] ignore = [ "E722", # do not use bare 'except' "E741", # ambiguous variable name ] [lint.per-file-ignores] "test/sample-default-supressed.py" = [ "N999", # invalid module name ] "test/sample-directive-opts.py" = [ "N999", # invalid module name ] "test/sample-directive-special.py" = [ "N999", # invalid module name ] [format] preview = true quote-style = "single" sphinx-doc-sphinx-argparse-89c7a57/LICENSE.rst000066400000000000000000000020671524177777600211200ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Alex Rudakov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sphinx-doc-sphinx-argparse-89c7a57/README.rst000066400000000000000000000027461524177777600207770ustar00rootroot00000000000000================= sphinx-argparse ================= .. image:: https://img.shields.io/pypi/v/sphinx-argparse.svg :target: https://pypi.org/project/sphinx-argparse/ :alt: PyPI version .. image:: https://img.shields.io/conda/vn/conda-forge/sphinx-argparse.svg :target: https://github.com/conda-forge/sphinx-argparse-feedstock/ :alt: Conda version .. image:: https://github.com/sphinx-doc/sphinx-argparse/actions/workflows/ci.yml/badge.svg :target: https://github.com/sphinx-doc/sphinx-argparse/actions/workflows/ci.yml :alt: Build Status .. image:: https://readthedocs.org/projects/sphinx-argparse/badge/?version=stable :target: https://sphinx-argparse.readthedocs.org/ :alt: Documentation Status .. image:: https://img.shields.io/badge/License-MIT-blue.svg :target: https://opensource.org/licenses/MIT :alt: MIT A Sphinx extension to automatically document argparse_ commands and options. For installation and usage details see the documentation_. The changelog is also `found there`_. .. _argparse: https://docs.python.org/3/library/argparse.html .. _documentation: https://sphinx-argparse.readthedocs.org/ .. _found there: https://sphinx-argparse.readthedocs.org/en/latest/changelog.html .. note:: This project used to live at `alex-rudakov/sphinx-argparse`_, and has been revived and moved to this `new home`_. .. _alex-rudakov/sphinx-argparse: https://github.com/alex-rudakov/sphinx-argparse/ .. _new home: https://github.com/sphinx-doc/sphinx-argparse/ sphinx-doc-sphinx-argparse-89c7a57/docs/000077500000000000000000000000001524177777600202275ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/docs/Makefile000066400000000000000000000127041524177777600216730ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pywizard.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pywizard.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pywizard" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pywizard" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." sphinx-doc-sphinx-argparse-89c7a57/docs/changelog.rst000066400000000000000000000202501524177777600227070ustar00rootroot00000000000000********** Change log ********** 0.6.1 ##### * `#98 `__ Fixed issue where a parallel sphinx build would fail (mostly relevant in CI) 0.6.0 ##### The following enhancements to the HTML output are described on the :doc:`usage` page. * Optional command index. * Optional ``:index-groups:`` field to the directive for an command-by-group index. * A ``sphinxarg_full_subcommand_name`` option to print fully-qualified sub-command headings. This option helps when more than one sub-command offers a ``create`` or ``list`` or other repeated sub-command. * Each command heading is a domain-specific link target. You can link to commands and sub-commands with the ``:ref:`` role, but this release adds support for the domain-specific role like ``:commands:command:`sample-directive-opts A` ``. The ``:commands:command:`` role supports linking from other projects through the intersphinx extension. Changes * Previously, common headings such as **Positional Arguments** were subject to a process that made them unique by adding a ``_repeatX`` suffix to the HREF target. This release continues to support those HREF targets as secondary targets so that bookmarks continue to work. However, this release prefers using fully-qualified HREF targets like ``sample-directive-opts-positional-arguments`` as the primary HREF so that customers are less likely to witness the ``_repeatX`` link in URLs. * `#32 `__ Argparse commands are now part of a Sphinx domain * `#72 `__ Coloring in argparse output can be disabled (default) * `#87 `__ The ``:filename:`` input is now resolved only to the ``conf.py`` directory (``srcdir`` in Sphinx) * `#89 `__ Changed the way `mock` from `autodoc` is important to work around a persistent Sphinx bug 0.5.2 ##### * Fix formatting of empty-string default values. Patch by Adam Turner. 0.5.1 ##### * Fix ``autodoc_mock_imports`` support. Patch by Adam Turner. * Properly declare supported Sphinx and Docutils versions. Patch by Adam Turner. * Fix tests and ensure tests run in CI. Patch by Adam Turner. 0.5.0 ##### * Add HTML tests to avoid regressions. Patch by Mike McKiernan in https://github.com/sphinx-doc/sphinx-argparse/pull/33 * Escape reStructuredText special characters in the default value. Patch by Robert Roos. * Fix the options formatter for manpages. Patch by Felix Moessbauer in https://github.com/sphinx-doc/sphinx-argparse/pull/50 * Handle ``None`` return from :py:func:`shutil.which`. Patch by Om Vats in https://github.com/sphinx-doc/sphinx-argparse/pull/52 * Drop support for Python 3.7, 3.8, and 3.9. Patch by Adam Turner. * Adopt `Flit `_ for packaging. Patch by Adam Turner. * Make argument and option section IDs more unique. Patch by David Hoese in https://github.com/sphinx-doc/sphinx-argparse/pull/44 * Fix cases where a suppressed default was visible. Patch by Michele Riva in https://github.com/sphinx-doc/sphinx-argparse/pull/53 * Support ``autodoc_mock_imports``. Patch by Adam Turner and Prajeesh Ag in https://github.com/sphinx-doc/sphinx-argparse/pull/35 0.4.0 ##### * Minimum python version is now 3.7 by @ashb in https://github.com/sphinx-doc/sphinx-argparse/pull/25 * Fix anchor for toc by @Blaok in https://github.com/sphinx-doc/sphinx-argparse/pull/2 * feat: find executable filename to address #16 by @tsutterley in https://github.com/sphinx-doc/sphinx-argparse/pull/17 * Test against python 3.11 too by @ashb in https://github.com/sphinx-doc/sphinx-argparse/pull/22 0.3.1 ##### * Include tests in sdist 0.3.0 ##### * First release from ashb/sphinx-argparse * Declare that parallel builds are supported (`issue #105`_). .. _issue #105: https://github.com/alex-rudakov/sphinx-argparse/pull/105 0.2.5 ##### * A more verbose error message is now printed if there's an issue during importing a script (issue #102). 0.2.4 ##### * Various bug fixes and documentation updates. 0.2.3 ##### * Fixed a variety of issues, such as with ``@replace`` (issue #99). Thanks to @evgeni * You can now skip sections with ``@skip``. Thanks to @evgeni * Fixed handling of the epilog 0.2.2 ##### * CommonMark is now only imported if absolutely required. This should fix failures on read the docs. Thanks to @Chilipp for fixing this! 0.2.1 ##### * Stopped importing ``sphinx.util.compat``, which was causing issues like that seen in `#65 `_ 0.2.0 ##### * Section titles can now be used in tables of contents and linked to. The title itself is also used as the anchor. In the case of repeated names ``_replicateX``, where ``X`` is a number, is prepended to ensure that all titles are uniquely linkable. This was bug `#46 `_. * The positional (aka required) and named (aka optional) option sections are now named "Positional Arguments" and "Named Arguments", for the sake of clarity (e.g., named arguments can be required). This was issue `#58 `_. * Fixed quoting of default strings (issue `#59 `_). * Added the ``:noepilog:`` and ``:nodescription:`` options, thanks to @arewm. * Added the ``:nosubcommand:`` option, thanks to @arewm. 0.1.17 ###### * Fixed handling of argument groups (this was bug `#49 `_). Thanks to @croth1 for reporting this bug. Note that now position arguments (also known as required arguments) within argument groups are now also handled correctly. 0.1.16 ###### * Added a ``:nodefaultconst:`` directive, which is similar to the ``:nodefault:`` directive, but applies only to ``store_true``, ``store_false``, and ``store_const`` (e.g., it will hide the "=True" part in the output, since that can be misleading to users). * Fixed various typos (thanks to users mikeantonacci, brondsem, and tony) * Format specifiers (e.g., ``%(prog)s`` and ``%(default)s``) are now filled in (if possible) in help sections. If there's a missing keyword, then nothing will be filled in. This was issue #27. * The package is now a bit more robust to incorrectly spelling module names (#39, courtesy of Gabriel Falcão) * Added support for argparse groups (thanks to Fidel Ramirez) 0.1.15 ###### * Fixed malformed docutils DOM in manpages (Matt Boyer) 0.1.14 ###### * Support for aliasing arguments #22 (Campbell Barton) * Support for nested arguments #23 (Campbell Barton) * Support for subcommand descriptions #24 (Campbell Barton) * Improved parsing of content of ``epilog`` and ``description`` #25 (Louis - https://github.com/paternal) * Added 'passparser' option (David Hoese) 0.1.13 ###### * Bugfix: Choices are not always strings (Robert Langlois) * Polished small mistakes in usage documentation (Dean Malmgren) * Started to improve man-pages support (Zygmunt Krynicki) 0.1.12 ###### * Improved error reporting (James Anderson) 0.1.11 ###### * Fixed stupid bug, prevented things working on py3 (Alex Rudakov) * added tox configuration for tests 0.1.10 ###### * Remove the ugly new line in the end of usage string (Vadim Markovtsev) * Issue #9 Display argument choises (Proposed by Felix-neko, done by Alex Rudakov) * :ref: syntax for specifying path to parser instance. Issue #7 (Proposed by David Cottrell, Implemented by Alex Rudakov) * Updated docs to read the docs theme 0.1.9 ###### Fix problem with python version comparison, when python reports it as "2.7.5+" (Alex Rudakov) 0.1.8 ##### Argparse is not required anymore separate module as of python 2.7 (Mike Gleen) 0.1.7 ##### -- Nothing -- Created by accident. 0.1.6 ##### Adding :nodefault: directive that skips default values for options (Stephen Tridgell) 0.1.5 ##### Fix issue: epilog is ignored (James Anderson - https://github.com/jamesra) 0.1.4 ##### Fix issue #3: ==SUPPRESS== in option list with no default value 0.1.2 ##### Fix issue with subcommands (by Tony Narlock - https://github.com/tony) 0.1.1 ##### Initial version sphinx-doc-sphinx-argparse-89c7a57/docs/conf.py000066400000000000000000000032141524177777600215260ustar00rootroot00000000000000# Sphinx documentation build configuration file extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'sphinxarg.ext', ] exclude_patterns = ['_build'] project = 'sphinx-argparse' copyright = '2017, Alex Rudakov, Devon Ryan, and contributors' release = version = '0.6.1' nitpicky = True # -- Options for intersphinx --------------------------------------------------- intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), 'sphinx': ('https://www.sphinx-doc.org/', None), } # -- Options for HTML output --------------------------------------------------- html_theme = 'furo' pygments_style = 'sphinx' htmlhelp_basename = 'sphinxargparsedoc' # -- Options for LaTeX output -------------------------------------------------- latex_documents = [ ( 'index', 'sphinx-argparse.tex', 'sphinx-argparse Documentation', 'Alex Rudakov, Devon Ryan, and contributors', 'manual', ), ] # -- Options for manual page output -------------------------------------------- man_pages = [ ( 'index', 'sphinx-argparse', 'sphinx-argparse Documentation', ['Alex Rudakov', 'Devon Ryan'], True, ) ] # -- Options for Texinfo output ------------------------------------------------ texinfo_documents = [ ( 'index', 'sphinx-argparse', 'sphinx-argparse Documentation', 'Alex Rudakov, Devon Ryan, and contributors', 'sphinx-argparse', 'A sphinx extension that automatically documents argparse commands and options.', 'Miscellaneous', ), ] sphinx-doc-sphinx-argparse-89c7a57/docs/extend.rst000066400000000000000000000057741524177777600222650ustar00rootroot00000000000000Extending results of ``argparse`` directives ============================================ You can add extra content or even replace some parts of the documentation generated by the ``argparse`` directive. For example, any content you put inside directives (you must follow ReStructuredText identation rules) will be inserted just before the argument and option list: .. code:: rst .. argparse:: :module: my.module :func: my_func_that_return_parser :prog: fancytool My content here that will be inserted right before the argument list. Also any valid markup... ************************* ... may ``be`` *applied* here including:: any directives you usually use. Also, there is an option to insert custom content into a specific argument/option/subcommand/argument-group description. Just create a name:definition pair, where the name is an argument/option/subcommand/argument-group name and the definition is any reStructured markup. Changes to options/arguments appearing in multiple action groups can either be targeted (i.e., only one instance of the argument is changed) or general (i.e., all instances are modified). .. code:: rst .. argparse:: :module: my.module :func: my_func_that_return_parser :prog: fancytool My content here that will be inserted right before the argument list. foo This text will go right after the "foo" positional argument help. install This text will go right after the "install" subcommand help and before its arguments. --upgrade -u This text will go after the upgrade option of the install subcommand. Nesting is unlimited. Note the space between --upgrade and -u, which differs from the comma that would normally be used. --output -o Content appended to the --output option, regardless of the argument group. You can also add classifiers, which will change how these definitions are incorporated: .. code:: rst .. argparse:: :module: my.module :func: my_func_that_return_parser :prog: fancytool My content that will be inserted right before the argument list. foo : @before This text will go before the "foo" positional argument help. install : @replace This text will replace the "install" subcommand help/description. --upgrade : @after The after directive is the default, so you needn't specify it. advanced arguments : @skip skip advanced arguments @before Insert content before the parsed help/description message of the argument/option/subcommand/argument-group. @after Insert content after the parsed help/description message of argument/option/subcommand/argument-group. This is the default. @replace Replace content of help/description message of argument/option/subcommand/argument-group. @skip Skip the content of help/description message of subcommand/argument-group. sphinx-doc-sphinx-argparse-89c7a57/docs/index.rst000066400000000000000000000027561524177777600221020ustar00rootroot00000000000000``sphinx-argparse`` =================== `sphinx-argparse` is an extension for Sphinx_ that allows for easy generation of documentation for command line tools using Python's argparse_ library. .. _Sphinx: https://www.sphinx-doc.org/ .. _argparse: https://docs.python.org/3/library/argparse.html .. toctree:: :hidden: :maxdepth: 1 usage extend sample misc markdown changelog Installation ------------ This extension works with Python 3.10 or later and Sphinx 5.1 or later. The package is available in the `Python Package Index`_: .. code:: shell pip install sphinx-argparse And also in `conda-forge`_: .. code:: shell mamba -c conda-forge install sphinx-argparse Enable the extension in your sphinx config: .. code:: python extensions = [ ..., 'sphinxarg.ext', ] .. _Python Package Index: https://pypi.org/project/sphinx-argparse/ .. _conda-forge: https:://github.com/conda-forge/sphinx-argparse-feedstock/ Contribute ---------- Any help is welcome! Most wanted: * Additional features * Bug fixes * Examples Contributions are gratefully accepted through `GitHub pull requests`_. Please report any bugs as issues on GitHub. .. _GitHub pull requests: https://github.com/sphinx-doc/sphinx-argparse/ Don't forget to run tests before committing: .. code:: shell pytest Similar projects ------------------- * https://pypi.org/project/sphinxcontrib-autoprogram/ (See a comparison: https://github.com/sphinx-doc/sphinx-argparse/issues/16) sphinx-doc-sphinx-argparse-89c7a57/docs/make.bat000066400000000000000000000117541524177777600216440ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pywizard.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pywizard.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end sphinx-doc-sphinx-argparse-89c7a57/docs/markdown.rst000066400000000000000000000054511524177777600226100ustar00rootroot00000000000000Markdown ======== As of version 0.2.0, markdown (rather than only reStructuredText) can be included inside directives as nested content. While markdown is much easier to write, please note that it is also less powerful. An example is below: .. code:: rst .. argparse:: :filename: ../test/sample.py :func: parser :prog: sample :markdown: Header 1 ======== [I'm a link to google](http://www.google.com) ## Sub-heading ``` This is a fenced code block ``` The above example renders as follows: .. argparse:: :filename: ../test/sample.py :func: parser :prog: sample :markdown: A random paragraph Heading 1 ========= [I'm a link to google](http://www.google.com) ## Sub heading ``` This is a fenced code block ``` The `CommonMark-py `__ is used internally to parse Markdown. Consequently, only Markdown supported by CommonMark-py will be rendered. You must explicitly use the ``:markdown:`` flag, otherwise all content inside directives will be parsed as reStructuredText. A note on headers ----------------- If the Markdown you nest includes headings, then the first one **MUST** be level 1. Subsequent headings can be at `lower levels `__ and then rendered correctly. Hard line breaks ---------------- Sphinx strips white-space from the end of lines prior to handing it to this package. Because of that, hard line breaks can not currently be rendered. Replacing/appending/prepending content -------------------------------------- When markdown is used as nested content, it's not possible to create dictionary entries like in reStructuredText to `modify program option descriptions `__. This is because CommonMark-py does not support dictionary entries. MarkDown in program descriptions and option help ------------------------------------------------ In addition to using MarkDown in nested content, one can also use MarkDown directly in program descriptions and option help messages. For example: .. code:: python import argparse def blah(): parser = argparse.ArgumentParser(description=""" ### Example of MarkDown inside programs [I'm a link](http://www.google.com) """) parser.add_argument('cmd', help='execute a `command`') return parser To render this as MarkDown rather than reStructuredText, use the ``:markdownhelp:`` option: .. code:: rst .. argparse:: :filename: ../test/sample2.py :func: blah :prog: sample :markdownhelp: This will then be rendered as: .. argparse:: :filename: ../test/sample2.py :func: blah :prog: sample :markdownhelp: sphinx-doc-sphinx-argparse-89c7a57/docs/misc.rst000066400000000000000000000041101524177777600217100ustar00rootroot00000000000000Miscellaneous ============= Text wrapping in argument tables -------------------------------- A common issue with the default html output is that the table within which options are displayed is designed such that the option descriptions are each held on one line. Any even remotely lengthy description then causes the viewer to need to scroll left/right to view the entire text. This is typically undesirable and the fix is described fully `here `_. The short synopsis is below: 1. Create a new CSS file (likely under ``_static``) and point to it in ``html_static_path`` and ``html_context`` (or a template in the ``templates_path``) in :file:`conf.py`. 2. In that CSS file, add the following code: .. code:: CSS .wy-table-responsive table td { white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } Alternatively, you can create a :file:`docutils.conf` file with the following contents: .. code:: ini [writers] option-limit=1 Linking to action groups ------------------------ As of version 0.2.0, action groups (e.g., "Optional arguments", "Required arguments", and subcommands) can be included in tables of contents and external links. The anchor name is the same as the title name (e.g., "Optional arguments"). In cases where titles are duplicated, as is often the case when subcommands are used, ``_repeatX``, where ``X`` is a number, is prepended to duplicate anchor names to ensure that they can all be uniquely linked. Argparse color -------------- Since Python 3.14, ``argparse`` uses `colors `__ in the output by default. These ANSI color sequences won't show right at all in the Sphinx output, so this extension will disable colors first in your ``ArgumentParser`` instance before producing the output. You can override this default behaviour by passing the ``:color:`` flag to your ``.. argparse`` directive, in this case the color setting of the argument parser is enabled. sphinx-doc-sphinx-argparse-89c7a57/docs/sample.rst000066400000000000000000000035101524177777600222410ustar00rootroot00000000000000Examples ======== Example documentation structure ------------------------------- Here is an example structure for the documentation of a complex command with many subcommands. You are free to use any structure, but this may be a good starting point. File "index.rst": .. code:: rst .. toctree:: :maxdepth: 2 cmd File "cmd.rst": .. code:: rst Command line utilities ********************** .. toctree:: :maxdepth: 1 cmd_main cmd_subcommand File "cmd_main.rst": .. code:: rst Fancytool command *********************** .. argparse:: :module: my.module :func: my_func_that_returns_a_parser :prog: fancytool subcommand Here we add a reference to subcommand, to simplify navigation. See :doc:`cmd_subcommand` File "cmd_subcommand.rst": .. code:: rst Subcommand command *********************** .. argparse:: :module: my.module :func: my_func_that_return_parser :prog: fancytool :path: subcommand Source of example file ---------------------- This file will be used in all generated examples. .. literalinclude:: ../test/sample.py :language: python Generated sample 1 - command with subcommands --------------------------------------------- Directive ~~~~~~~~~ Source: .. code:: rst .. argparse:: :filename: ../test/sample.py :func: parser :prog: sample Output ~~~~~~ .. argparse:: :filename: ../test/sample.py :func: parser :prog: sample Generated sample 2 - subcommand ------------------------------- Directive ~~~~~~~~~ Source: .. code:: rst .. argparse:: :filename: ../test/sample.py :func: parser :prog: sample :path: game Output ~~~~~~ .. argparse:: :filename: ../test/sample.py :func: parser :prog: sample :path: game sphinx-doc-sphinx-argparse-89c7a57/docs/usage.rst000066400000000000000000000214721524177777600220730ustar00rootroot00000000000000=========== Basic usage =========== This extension adds the "argparse" directive: .. code:: rst .. argparse:: :module: my.module :func: my_func_that_returns_a_parser :prog: fancytool The ``module``, ``func`` and ``prog`` options are required. ``func`` is a function that returns an instance of the :py:class:`argparse.ArgumentParser` class. Alternatively, one can use :ref: like this: .. code:: rst .. argparse:: :ref: my.module.my_func_that_returns_a_parser :prog: fancytool In this case :ref: points directly to argument parser instance. For this directive to work, you should point it to the function that will return a pre-filled ``ArgumentParser``. Something like: .. code:: python def my_func_that_return_parser(): parser = argparse.ArgumentParser() parser.add_argument('foo', default=False, help='foo help') parser.add_argument('bar', default=False) subparsers = parser.add_subparsers() subparser = subparsers.add_parser('install', help='install help') subparser.add_argument('ref', type=str, help='foo1 help') subparser.add_argument('--upgrade', action='store_true', default=False, help='foo2 help') return parser .. note:: We will use this example as a reference for every example in this document. To document a file that is not part of a module, use :filename: .. code:: rst .. argparse:: :filename: script.py :func: my_func_that_returns_a_parser :prog: script.py The 'filename' option can be an absolute path or a relative one. When a relative path is provided, the Sphinx ``srcdir`` will be taken as the base, which is the directory housing the active ``conf.py`` file. \:module\: Module name, where the function is located \:func\: Function name \:ref\: A combination of :module: and :func: \:filename\: A file name, in cases where the file to be documented is not part of a module. \:prog\: The name of your tool (or how it should appear in the documentation). For example, if you run your script as ``./boo --some args`` then \:prog\: will be "boo" That's it. Directives will render positional arguments, options and sub-commands. Config ====== The following ``conf.py`` config options are available, with their default values. The next sections will describe the options in more detail. .. code:: py sphinxarg_full_subcommand_name = False sphinxarg_build_commands_index = False sphinxarg_commands_index_in_toctree = False sphinxarg_build_commands_by_group_index = False sphinxarg_commands_by_group_index_in_toctree = False sphinxarg_commands_by_group_index_file_suffix = "by-group" sphinxarg_commands_by_group_index_title = "Commands by Group" .. _about-subcommands: About Sub-Commands ================== Sub-commands are limited to one level. But, you can always output help for subcommands separately: .. code:: rst .. argparse:: :module: my.module :func: my_func_that_return_parser :prog: fancytool :path: install This will render same doc for "install" subcommand. Nesting level is unlimited: .. code:: rst .. argparse:: :module: my.module :func: my_func_that_return_parser :prog: fancytool :path: install subcomand1 subcommand2 subcommand3 Other useful directives ======================= :nodefault: Do not show any default values. :nodefaultconst: Like nodefault:, except it applies only to arguments of types ``store_const``, ``store_true`` and ``store_false``. :nosubcommands: Do not show subcommands. :noepilog: Do not parse the epilogue, which can be useful if it contains text that could be incorrectly parse as reStructuredText. :nodescription: Do not parse the description, which can be useful if it contains text that could be incorrectly parse as reStructuredText. :passparser: This can be used if you don't have a function that returns an argument parser, but rather adds commands to it (``:func:`` is then that function). :index-groups: This option is related to grouping related commands in an index. Printing Fully Qualified Sub-Command Headings ============================================= By default, when a command has sub-commands, such as ``fancytool install`` shown in the :ref:`about-subcommands` section, the heading for the sub-command does not include the command name. For instance, the the heading is **install** rather than **fancytool install**. If you prefer to show the full command, **fancytool install**, then you can enable the option in the ``conf.py`` for your project: .. code-block:: python sphinxarg_full_subcommand_name = True Indices ======= The extension supports two types of optional indices. The first type of index is a simple index that provides a list of all the commands in the project by fully qualified name and a link to each command. The second type of index enables you to group related commands into groups and then provide a list of the commands and a link to each command. By default, no index is created. Simple Command Index -------------------- To enable the simple command index, add the following to the project ``conf.py`` file: .. code-block:: python sphinxarg_build_commands_index = True sphinxarg_commands_index_in_toctree = True The first option, ``build_commands_index``, instructs the extension to create the index. For an HTML build, the index is created with the file name ``commands-index.html`` in the output directory. You can reference the index from other files with the ``:ref:`commands-index``` markup. The second option, ``commands_index_in_toctree``, enables you to reference the the index in a ``toctree`` directive. By default, you cannot reference indices generated by extensions in a ``toctree``. When you enable this option, the extension creates a temporary file that is named ``commands-index.rst`` in the source directory of your project. Sphinx locates the temporary file and that makes it possible to reference the file in the ``toctree``. When the Sphinx build finishes, the extension removes the temporary file from the source directory. Commands by Group Index ----------------------- To enable the more complex index, add the following to the project ``conf.py`` file: .. code-block:: python sphinxarg_build_commands_by_group_index = True sphinxarg_commands_by_group_index_in_toctree = True Add the ``:index-groups:`` option to the ``argparse`` directive in your documentation files. Specify one or more groups that the command belongs to (comma-separated). .. code-block:: reStructuredText .. argparse:: :filename: ../test/sample.py :func: parser :prog: sample :index-groups: Basic Commands For an HTML build, the index is created with the file name ``commands-by-group.html`` in the output directory. You can cross reference the index from other files with the ``:ref:`commands-by-group``` role. Like the simple index, the ``sphinxarg_commands_by_group_index_in_toctree`` option enables you to reference the index in ``toctree`` directives. This index has two more options. .. code-block:: python sphinxarg_commands_by_group_index_in_toctree = True sphinxarg_commands_by_group_index_file_suffix = "by-service" sphinxarg_commands_by_group_index_title = "Commands by Service" The ``sphinxarg_commands_by_group_index_file_suffix`` option overrides the default index name of ``commands-by-group.html``. The value ``commands-`` is concatenated with the value you specify. In the preceding sample, the index file name is created as ``commands-by-service.html``. If you specify this option, the default reference of ``:ref:`commands-by-group``` is overridden with the value that you create. The ``sphinxarg_commands_by_group_index_title`` option overrides the default first-level heading for the file. The default heading is "Commands by Group". The value you specify replaces the default value. Customizing the Indices ----------------------- By default, indices are created with the ``domainindex.html`` template. If you want to customize the appearance of an index, copy the default ``domainindex.html`` file for your theme to the ``_templates`` directory in your project and modify it. If you want to customize both indices, but one template cannot accommodate both of them, you can create an additional index template, such as ``customindex.html``. You can configure Sphinx to use the additional template for an index by modifying the ``conf.py`` for the project like the following example. .. code-block:: python def page_template(app: "Sphinx", pagename, templatename, context, doctree) -> str: if pagename == "commands-by-group": return "customindex.html" else: return templatename def setup(app: "Sphinx"): app.connect('html-page-context', page_template) For more information, refer to the Sphinx documentation for :ref:`sphinx:templating` and the :doc:`sphinx:extdev/appapi`. sphinx-doc-sphinx-argparse-89c7a57/pyproject.toml000066400000000000000000000054661524177777600222260ustar00rootroot00000000000000[build-system] requires = ["flit_core >= 3.10, <4"] build-backend = "flit_core.buildapi" # project metadata [project] name = "sphinx-argparse" description = "A sphinx extension that automatically documents argparse commands and options" readme = "README.rst" urls.Changelog = "https://sphinx-argparse.readthedocs.io/en/latest/changelog.html" urls.Code = "https://github.com/sphinx-doc/sphinx-argparse/" urls.Download = "https://pypi.org/project/sphinx-argparse/" urls.Homepage = "https://sphinx-argparse.readthedocs.io/" urls."Issue tracker" = "https://github.com/sphinx-doc/sphinx-argparse/issues" license = "MIT" license-files = ["LICENSE.rst"] requires-python = ">=3.10" # Classifiers list: https://pypi.org/classifiers/ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Sphinx", "Framework :: Sphinx :: Extension", "Topic :: Documentation", "Topic :: Documentation :: Sphinx", ] dependencies = [ "sphinx>=5.1.0", "docutils>=0.19", # for get_default_settings() ] dynamic = ["version"] [project.optional-dependencies] markdown = [ "CommonMark>=0.5.6" ] lint = [ "ruff>=0.5", "mypy>=1.10", "types-docutils>=0.21", "lxml-stubs>=0.4", ] test = [ "pytest>=8.0", "pytest-cov", "lxml>=4.9", "setuptools>=70.0", # for Cython compilation "typing_extensions>=4.9", # for typing_extensions.Unpack ] docs = [ "furo>=2024", "CommonMark>=0.5.6", ] [[project.authors]] name = "Ash Berlin-Taylor" email = "ash_github@firemirror.com" [tool.flit.module] name = "sphinxarg" [tool.flit.sdist] include = [ "LICENSE.rst", # Documentation "docs/", # Tests "test/", ] exclude = [ "docs/_build", ] [tool.black] line-length = 160 skip-string-normalization = true [tool.isort] balanced_wrapping = true default_section = "THIRDPARTY" include_trailing_comma = true known_first_party = [ "sphinxarg", "test" ] line_length = 160 multi_line_output = 3 [tool.mypy] files = ["sphinxarg", "test"] python_version = "3.13" exclude = '''(?x)( ^test/roots | ^docs )''' [[tool.mypy.overrides]] module = [ "commonmark.*", "CommonMark.*", "docutils.parsers.rst.directives", ] ignore_missing_imports = true [tool.pytest.ini_options] addopts = "--strict-markers --tb=short" [tool.coverage.run] omit = ["tests/*","**/__main__.py"] branch = true sphinx-doc-sphinx-argparse-89c7a57/sphinxarg/000077500000000000000000000000001524177777600213025ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/sphinxarg/__init__.py000066400000000000000000000000571524177777600234150ustar00rootroot00000000000000__version__ = '0.6.1' version_info = (0, 6, 1) sphinx-doc-sphinx-argparse-89c7a57/sphinxarg/ext.py000066400000000000000000001230201524177777600224520ustar00rootroot00000000000000from __future__ import annotations import importlib import operator import os import sys from argparse import ArgumentParser from typing import TYPE_CHECKING, cast from docutils import nodes from docutils.frontend import get_default_settings from docutils.parsers.rst import Parser from docutils.parsers.rst.directives import flag, unchanged from docutils.statemachine import StringList from sphinx.domains import Domain, Index, IndexEntry from sphinx.errors import ExtensionError from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.docutils import SphinxDirective, new_document from sphinx.util.nodes import make_id, make_refnode, nested_parse_with_titles try: from sphinx.ext.autodoc.mock import mock except ImportError: from sphinx.ext.autodoc import mock # A persistent bug in sphinx / autodoc causes problems during importing (#82) from sphinxarg import __version__ from sphinxarg.parser import parse_parser, parser_navigate from sphinxarg.utils import command_pos_args, target_to_anchor_id if TYPE_CHECKING: from collections.abc import Iterable, Sequence from pathlib import Path from docutils.nodes import Element from sphinx.addnodes import pending_xref from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.environment import BuildEnvironment _ObjectDescriptionTuple = tuple[str, str, str, str, str, int] _ObjectDescriptionList = list[_ObjectDescriptionTuple] logger = logging.getLogger(__name__) def map_nested_definitions(nested_content): if nested_content is None: msg = 'Nested content should be iterable, not null' raise Exception(msg) # build definition dictionary definitions = {} for item in nested_content: if not isinstance(item, nodes.definition_list): continue for subitem in item: if not isinstance(subitem, nodes.definition_list_item): continue if not len(subitem.children) > 0: continue classifier = '@after' idx = subitem.first_child_matching_class(nodes.classifier) if idx is not None: ci = subitem[idx] if len(ci.children) > 0: classifier = ci.children[0].astext() if classifier is not None and classifier not in { '@replace', '@before', '@after', '@skip', }: msg = f'Unknown classifier: {classifier}' raise Exception(msg) idx = subitem.first_child_matching_class(nodes.term) if idx is not None: term = subitem[idx] if len(term.children) > 0: term = term.children[0].astext() idx = subitem.first_child_matching_class(nodes.definition) if idx is not None: subcontent = [ _ for _ in subitem[idx] if isinstance(_, nodes.definition_list) ] definitions[term] = (classifier, subitem[idx], subcontent) return definitions def render_list(l, markdown_help, settings=None): """ Given a list of reStructuredText or MarkDown sections, return a docutils node list """ if len(l) == 0: return [] if markdown_help: from sphinxarg.markdown import parse_markdown_block return parse_markdown_block('\n\n'.join(l) + '\n') else: if settings is None: settings = get_default_settings(Parser) all_children = [] for element in l: if isinstance(element, str): document = new_document('', settings) Parser().parse(element + '\n', document) all_children += document.children elif isinstance(element, nodes.definition): all_children += element return all_children def _is_suppressed(item: str | None) -> bool: """Return whether item should not be printed.""" if item is None: return True item = str(item).replace('"', '').replace("'", '') return item == '==SUPPRESS==' def print_action_groups( data, nested_content, markdown_help=False, settings=None, id_prefix='', ): """ Process all 'action groups', which are also include 'Options' and 'Required arguments'. A list of nodes is returned. """ definitions = map_nested_definitions(nested_content) nodes_list = [] if 'action_groups' in data: for action_group in data['action_groups']: # Every action group is composed of a section, holding # a title, the description, and the option group (members) title_as_id = action_group['title'].replace(' ', '-').lower() if id_prefix: title_as_id = f'{id_prefix}-{title_as_id}' section = nodes.section(ids=[title_as_id]) section += nodes.title(action_group['title'], action_group['title']) desc = [] if action_group['description']: desc.append(action_group['description']) # Replace/append/prepend content to the description according to nested content subcontent = [] if action_group['title'] in definitions: classifier, s, subcontent = definitions[action_group['title']] if classifier == '@replace': desc = [s] elif classifier == '@after': desc.append(s) elif classifier == '@before': desc.insert(0, s) elif classifier == '@skip': continue if len(subcontent) > 0: for k, v in map_nested_definitions(subcontent).items(): definitions[k] = v # Render appropriately for element in render_list(desc, markdown_help): section += element local_definitions = definitions if len(subcontent) > 0: local_definitions = dict(definitions.items()) for k, v in map_nested_definitions(subcontent).items(): local_definitions[k] = v items = [] # Iterate over action group members for entry in action_group['options']: # Members will include: # default The default value. This may be ==SUPPRESS== # name A list of option names (e.g., ['-h', '--help'] # help The help message string # There may also be a 'choices' member. # Build the help text arg = [] if 'choices' in entry: arg.append( f'Possible choices: {", ".join(str(c) for c in entry["choices"])}\n' ) if 'help' in entry: arg.append(entry['help']) if not _is_suppressed(entry['default']): # Put the default value in a literal block, # but escape backticks already in the string default_str = str(entry['default']).replace('`', r'\`') arg.append(f'Default: ``{default_str}``') # Handle nested content, the term used in the dict # has the comma removed for simplicity desc = arg term = ' '.join(entry['name']) if term in local_definitions: classifier, s, subcontent = local_definitions[term] if classifier == '@replace': desc = [s] elif classifier == '@after': desc.append(s) elif classifier == '@before': desc.insert(0, s) term = ', '.join(entry['name']) n = nodes.option_list_item( '', nodes.option_group('', nodes.option_string(text=term)), nodes.description('', *render_list(desc, markdown_help, settings)), ) items.append(n) section += nodes.option_list('', *items) nodes_list.append(section) return nodes_list def print_subcommands(data, nested_content, markdown_help=False, settings=None): # noqa: N803 """ Each subcommand is a dictionary with the following keys: ['usage', 'action_groups', 'bare_usage', 'name', 'help'] In essence, this is all tossed in a new section with the title 'name'. Apparently there can also be a 'description' entry. """ definitions = map_nested_definitions(nested_content) items = [] if 'children' in data: subcommands = nodes.section(ids=['Sub-commands']) subcommands += nodes.title('Sub-commands', 'Sub-commands') for child in data['children']: sec = nodes.section(ids=[child['name']]) sec += nodes.title(child['name'], child['name']) if 'description' in child and child['description']: desc = [child['description']] elif child['help']: desc = [child['help']] else: desc = ['Undocumented'] # Handle nested content subcontent = [] if child['name'] in definitions: classifier, s, subcontent = definitions[child['name']] if classifier == '@replace': desc = [s] elif classifier == '@after': desc.append(s) elif classifier == '@before': desc.insert(0, s) for element in render_list(desc, markdown_help): sec += element sec += nodes.literal_block(text=child['bare_usage']) for x in print_action_groups( child, nested_content + subcontent, markdown_help, settings=settings ): sec += x for x in print_subcommands( child, nested_content + subcontent, markdown_help, settings=settings ): sec += x if 'epilog' in child and child['epilog']: for element in render_list([child['epilog']], markdown_help): sec += element subcommands += sec items.append(subcommands) return items def ensure_unique_ids(items): """ If action groups are repeated, then links in the table of contents will just go to the first of the repeats. This may not be desirable, particularly in the case of subcommands where the option groups have different members. This function updates the title IDs by adding _repeatX, where X is a number so that the links are then unique. """ s = set() for item in items: for n in item.findall(descend=True, siblings=True, ascend=False): if isinstance(n, nodes.section): ids = n['ids'] for idx, id in enumerate(ids): if id not in s: s.add(id) else: i = 1 while f'{id}_repeat{i}' in s: i += 1 ids[idx] = f'{id}_repeat{i}' s.add(ids[idx]) n['ids'] = ids class ArgParseDirective(SphinxDirective): has_content = True required_arguments = 0 option_spec = { 'module': unchanged, 'func': unchanged, 'ref': unchanged, 'prog': unchanged, 'path': unchanged, 'nodefault': flag, 'nodefaultconst': flag, 'filename': unchanged, 'manpage': unchanged, 'nosubcommands': unchanged, 'passparser': flag, 'noepilog': unchanged, 'nodescription': unchanged, 'markdown': flag, 'markdownhelp': flag, 'color': flag, 'index-groups': unchanged, } index_groups: Sequence[str] = () def _construct_manpage_specific_structure(self, parser_info): """ Construct a typical man page consisting of the following elements: NAME (automatically generated, out of our control) SYNOPSIS DESCRIPTION OPTIONS FILES SEE ALSO BUGS """ items = [] # SYNOPSIS section synopsis_section = nodes.section( '', nodes.title(text='Synopsis'), nodes.literal_block(text=parser_info['bare_usage']), ids=['synopsis-section'], ) items.append(synopsis_section) # DESCRIPTION section if 'nodescription' not in self.options: description_section = nodes.section( '', nodes.title(text='Description'), nodes.paragraph( text=parser_info.get( 'description', parser_info.get('help', 'undocumented').capitalize(), ) ), ids=['description-section'], ) nested_parse_with_titles(self.state, self.content, description_section) items.append(description_section) if parser_info.get('epilog') and 'noepilog' not in self.options: # TODO: do whatever sphinx does to understand ReST inside # docstrings magically imported from other places. The nested # parse method invoked above seem to be able to do this but # I haven't found a way to do it for arbitrary text if description_section: description_section += nodes.paragraph(text=parser_info['epilog']) else: description_section = nodes.paragraph(text=parser_info['epilog']) items.append(description_section) # OPTIONS section options_section = nodes.section( '', nodes.title(text='Options'), ids=['options-section'] ) if 'args' in parser_info: options_section += nodes.paragraph() options_section += nodes.subtitle(text='Positional arguments:') options_section += self._format_positional_arguments(parser_info) for action_group in parser_info['action_groups']: if 'options' in action_group: options_section += nodes.paragraph() options_section += nodes.subtitle(text=action_group['title']) options_section += self._format_optional_arguments(action_group) # NOTE: we cannot generate NAME ourselves. It is generated by # docutils.writers.manpage # TODO: items.append(files) # TODO: items.append(see also) # TODO: items.append(bugs) if len(options_section.children) > 1: items.append(options_section) if 'nosubcommands' not in self.options: # SUBCOMMANDS section (non-standard) subcommands_section = nodes.section( '', nodes.title(text='Sub-Commands'), ids=['subcommands-section'] ) if 'children' in parser_info: subcommands_section += self._format_subcommands(parser_info) if len(subcommands_section) > 1: items.append(subcommands_section) if os.getenv('INCLUDE_DEBUG_SECTION'): import json # DEBUG section (non-standard) debug_section = nodes.section( '', nodes.title(text='Argparse + Sphinx Debugging'), nodes.literal_block(text=json.dumps(parser_info, indent=' ')), ids=['debug-section'], ) items.append(debug_section) return items def _format_positional_arguments(self, parser_info): assert 'args' in parser_info items = [] for arg in parser_info['args']: arg_items = [] if arg['help']: arg_items.append(nodes.paragraph(text=arg['help'])) elif 'choices' not in arg: arg_items.append(nodes.paragraph(text='Undocumented')) if 'choices' in arg: arg_items.append( nodes.paragraph(text='Possible choices: ' + ', '.join(arg['choices'])) ) items.append( nodes.option_list_item( '', nodes.option_group( '', nodes.option('', nodes.option_string(text=arg['metavar'])) ), nodes.description('', *arg_items), ) ) return nodes.option_list('', *items) def _format_optional_arguments(self, parser_info): assert 'options' in parser_info items = [] for opt in parser_info['options']: names = [] opt_items = [] for name in opt['name']: option_declaration = [nodes.option_string(text=name)] if not self._is_suppressed(opt['default']): option_declaration += nodes.option_argument( '', text='=' + str(opt['default']) ) names.append(nodes.option('', *option_declaration)) if opt['help']: opt_items.append(nodes.paragraph(text=opt['help'])) elif 'choices' not in opt: opt_items.append(nodes.paragraph(text='Undocumented')) if 'choices' in opt: opt_items.append( nodes.paragraph(text='Possible choices: ' + ', '.join(opt['choices'])) ) items.append( nodes.option_list_item( '', nodes.option_group('', *names), nodes.description('', *opt_items), ) ) return nodes.option_list('', *items) def _format_subcommands(self, parser_info): assert 'children' in parser_info items = [] for subcmd in parser_info['children']: subcmd_items = [] if subcmd['help']: subcmd_items.append(nodes.paragraph(text=subcmd['help'])) else: subcmd_items.append(nodes.paragraph(text='Undocumented')) items.append( nodes.definition_list_item( '', nodes.term('', '', nodes.strong(text=subcmd['bare_usage'])), nodes.definition('', *subcmd_items), ) ) return nodes.definition_list('', *items) def _nested_parse_paragraph(self, text): content = nodes.paragraph() self.state.nested_parse(StringList(text.split('\n')), 0, content) return content @property def _srcdir(self): """Internal property for source root-dir. This is a property such that we can easily mock it during tests. """ return self.env.srcdir def _open_filename(self): file = self.options['filename'] # If the provided path is not absolute, we consider it relative to the docs # conf dir: if not os.path.isabs(file): file = os.path.realpath( os.path.join(self._srcdir, file) ) # Resolve things like ".." to make clear where we're searching # try open with given path try: return open(file) except OSError: msg = ( f'Failed to find provided source file `{self.options["filename"]}` ' f'(resolved to `{file}`)' ) raise FileNotFoundError(msg) from None def _print_subcommands(self, data, nested_content, markdown_help=False, settings=None): """ Each subcommand is a dictionary with the following keys: ['usage', 'action_groups', 'bare_usage', 'name', 'help'] In essence, this is all tossed in a new section with the title 'name'. Apparently there can also be a 'description' entry. """ definitions = map_nested_definitions(nested_content) items = [] full_subcommand_name_true = self.config.sphinxarg_full_subcommand_name domain = cast('ArgParseDomain', self.env.domains[ArgParseDomain.name]) if 'children' in data: full_command = command_pos_args(data) node_id = make_id( self.env, self.state.document, '', full_command + '-sub-commands' ) target = nodes.target('', '', ids=[node_id]) self.set_source_info(target) self.state.document.note_explicit_target(target) subcommands = nodes.section(ids=['Sub-commands']) subcommands += nodes.title('Sub-commands', 'Sub-commands') for child in data['children']: full_command = command_pos_args(child) node_id = make_id(self.env, self.state.document, '', full_command) target = nodes.target('', '', ids=[node_id]) self.set_source_info(target) self.state.document.note_explicit_target(target) sec = nodes.section(ids=[node_id, child['name']]) if full_subcommand_name_true: title = nodes.title(full_command, full_command) else: title = nodes.title(child['name'], child['name']) sec += title domain.add_argparse_command(child, node_id, self.index_groups) if 'description' in child and child['description']: desc = [child['description']] elif child['help']: desc = [child['help']] else: desc = ['Undocumented'] # Handle nested content subcontent = [] if child['name'] in definitions: classifier, s, subcontent = definitions[child['name']] if classifier == '@replace': desc = [s] elif classifier == '@after': desc.append(s) elif classifier == '@before': desc.insert(0, s) for element in render_list(desc, markdown_help): sec += element sec += nodes.literal_block(text=child['bare_usage']) for x in self._print_action_groups( child, nested_content + subcontent, markdown_help, settings=settings ): sec += x for x in self._print_subcommands( child, nested_content + subcontent, markdown_help, settings=settings ): sec += x if 'epilog' in child and child['epilog']: for element in render_list([child['epilog']], markdown_help): sec += element subcommands += sec items.append(subcommands) return items def _print_action_groups( self, data, nested_content, markdown_help=False, settings=None, id_prefix='', ): """ Process all 'action groups', which are also include 'Options' and 'Required arguments'. A list of nodes is returned. """ definitions = map_nested_definitions(nested_content) nodes_list = [] if 'action_groups' in data: for action_group in data['action_groups']: # Every action group is composed of a section, holding # a title, the description, and the option group (members) title_as_id = action_group['title'].replace(' ', '-').lower() full_command = command_pos_args(data) node_id = make_id( self.env, self.state.document, '', full_command + '-' + title_as_id, ) target = nodes.target('', '', ids=[node_id]) self.set_source_info(target) self.state.document.note_explicit_target(target) section = nodes.section(ids=[node_id, f'{id_prefix}-{title_as_id}']) section += nodes.title(action_group['title'], action_group['title']) desc = [] if action_group['description']: desc.append(action_group['description']) # Replace/append/prepend content to the description according to nested content subcontent = [] if action_group['title'] in definitions: classifier, s, subcontent = definitions[action_group['title']] if classifier == '@replace': desc = [s] elif classifier == '@after': desc.append(s) elif classifier == '@before': desc.insert(0, s) elif classifier == '@skip': continue if len(subcontent) > 0: for k, v in map_nested_definitions(subcontent).items(): definitions[k] = v # Render appropriately for element in render_list(desc, markdown_help): section += element local_definitions = definitions if len(subcontent) > 0: local_definitions = dict(definitions.items()) for k, v in map_nested_definitions(subcontent).items(): local_definitions[k] = v items = [] # Iterate over action group members for entry in action_group['options']: # Members will include: # default The default value. This may be ==SUPPRESS== # name A list of option names (e.g., ['-h', '--help'] # help The help message string # There may also be a 'choices' member. # Build the help text arg = [] if 'choices' in entry: arg.append( f'Possible choices: {", ".join(map(str, entry["choices"]))}\n' ) if 'help' in entry: arg.append(entry['help']) if not self._is_suppressed(entry['default']): # Put the default value in a literal block, # but escape backticks already in the string default_str = str(entry['default']).replace('`', r'\`') arg.append(f'Default: ``{default_str}``') # Handle nested content, the term used in the dict # has the comma removed for simplicity desc = arg term = ' '.join(entry['name']) if term in local_definitions: classifier, s, subcontent = local_definitions[term] if classifier == '@replace': desc = [s] elif classifier == '@after': desc.append(s) elif classifier == '@before': desc.insert(0, s) term = ', '.join(entry['name']) n = nodes.option_list_item( '', nodes.option_group('', nodes.option_string(text=term)), nodes.description('', *render_list(desc, markdown_help, settings)), ) items.append(n) section += nodes.option_list('', *items) nodes_list.append(section) return nodes_list @staticmethod def _is_suppressed(item: str | None) -> bool: """Return whether item should not be printed.""" if item is None: return True item = str(item).replace('"', '').replace("'", '') return item == '==SUPPRESS==' def run(self): if 'module' in self.options and 'func' in self.options: module_name = self.options['module'] attr_name = self.options['func'] elif 'ref' in self.options: _parts = self.options['ref'].split('.') module_name = '.'.join(_parts[0:-1]) attr_name = _parts[-1] elif 'filename' in self.options and 'func' in self.options: mod = {} f = self._open_filename() code = compile(f.read(), self.options['filename'], 'exec') exec(code, mod) module_name = None attr_name = self.options['func'] func = mod[attr_name] else: msg = ':module: and :func: should be specified, or :ref:, or :filename: and :func:' raise self.error(msg) # Skip this if we're dealing with a local file, since it obviously can't be imported if 'filename' not in self.options: with mock(self.config.autodoc_mock_imports): try: mod = importlib.import_module(module_name) except ImportError as exc: msg = ( f'Failed to import "{attr_name}" from "{module_name}".\n' f'{sys.exc_info()[1]}' ) raise self.error(msg) from exc if not hasattr(mod, attr_name): msg = ( f'Module "{module_name}" has no attribute "{attr_name}"\n' f'Incorrect argparse :module: or :func: values?' ) raise self.error(msg) func = getattr(mod, attr_name) if isinstance(func, ArgumentParser): parser = func elif 'passparser' in self.options: parser = ArgumentParser() func(parser) else: parser = func() if 'path' not in self.options: self.options['path'] = '' path = str(self.options['path']) if 'prog' in self.options: parser.prog = self.options['prog'] result = parse_parser( parser, skip_default_values='nodefault' in self.options, skip_default_const_values='nodefaultconst' in self.options, color='color' in self.options, ) result = parser_navigate(result, path) if 'manpage' in self.options: return self._construct_manpage_specific_structure(result) # Handle nested content, where markdown needs to be preprocessed items = [] nested_content = nodes.paragraph() if 'markdown' in self.options: from sphinxarg.markdown import parse_markdown_block items.extend(parse_markdown_block('\n'.join(self.content) + '\n')) else: self.state.nested_parse(self.content, self.content_offset, nested_content) nested_content = nested_content.children # add common content between items += [ item for item in nested_content if not isinstance(item, nodes.definition_list) ] markdown_help = False if 'markdownhelp' in self.options: markdown_help = True if 'description' in result and 'nodescription' not in self.options: if markdown_help: items.extend(render_list([result['description']], True)) else: items.append(self._nested_parse_paragraph(result['description'])) if 'index-groups' in self.options: self.index_groups = list(map(str.strip, self.options['index-groups'].split(', '))) else: self.index_groups = [] full_command = command_pos_args(result) node_id = make_id(self.env, self.state.document, '', full_command) target = nodes.target('', '', ids=[node_id]) items.append(target) self.set_source_info(target) self.state.document.note_explicit_target(target) domain = cast('ArgParseDomain', self.env.get_domain(ArgParseDomain.name)) domain.add_argparse_command(result, node_id, self.index_groups) items.append(nodes.literal_block(text=result['usage'])) items.extend( self._print_action_groups( result, nested_content, markdown_help, settings=self.state.document.settings, id_prefix=f'{module_name}-{attr_name}' if module_name else attr_name, ) ) if 'nosubcommands' not in self.options: items.extend( self._print_subcommands( result, nested_content, markdown_help, settings=self.state.document.settings, ) ) if 'epilog' in result and 'noepilog' not in self.options: items.append(self._nested_parse_paragraph(result['epilog'])) # Traverse the returned nodes, modifying the title IDs as necessary to avoid repeats ensure_unique_ids(items) return items class CommandsIndex(Index): name = 'index' localname = 'Commands Index' def generate( self, docnames: Iterable[str] | None = None ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: content: dict[str, list[IndexEntry]] = {} commands: list[_ObjectDescriptionTuple] commands = sorted(self.domain.get_objects(), key=operator.itemgetter(0)) for cmd, dispname, _typ, docname, anchor, priority in commands: inx_entry = IndexEntry(cmd, priority, docname, anchor, docname, '', dispname) content.setdefault(cmd[0].lower(), []).append(inx_entry) return sorted(content.items()), True class CommandsByGroupIndex(Index): # Defaults (can be overridden through `conf.py`): name = 'by-group' localname = 'Commands by Group' def generate( self, docnames: Iterable[str] | None = None ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: content: dict[str, list[IndexEntry]] = {} commands_by_group: dict[str, list[_ObjectDescriptionTuple]] commands_by_group = self.domain.data['commands-by-group'] for group in sorted(commands_by_group): commands = sorted(commands_by_group[group], key=operator.itemgetter(0)) for cmd, dispname, _typ, docname, anchor, priority in commands: idx_entry = IndexEntry(cmd, priority, docname, anchor, docname, '', dispname) content.setdefault(group, []).append(idx_entry) return sorted(content.items()), True class ArgParseDomain(Domain): name = 'commands' label = 'commands-label' roles = { 'command': XRefRole(), } indices = [] initial_data = { 'commands': [], 'commands-by-group': {}, } # Keep a list of the temporary index files that are created in the # source directory. The files are created if the command_xxx_in_toctree # option is set to True. temporary_index_files: list[Path] = [] def get_objects(self) -> Iterable[_ObjectDescriptionTuple]: yield from self.data['commands'] def clear_doc(self, docname: str) -> None: """Remove traces of a document in the domain-specific inventories. :param docname: Name of the document for which we need to clear cache. """ def keep_by_docname(entries_: _ObjectDescriptionList) -> _ObjectDescriptionList: """Remove entries for this particular docname from an array.""" return [entry for entry in entries_ if entry[3] != docname] self.data['commands'] = keep_by_docname(self.data['commands']) commands_by_group: dict[str, _ObjectDescriptionList] = self.data['commands-by-group'] for group in list(commands_by_group): # Iterate over keys list as we modify the dict entries = keep_by_docname(commands_by_group[group]) if entries: commands_by_group[group] = entries else: del commands_by_group[group] def merge_domaindata(self, docnames: Iterable[str], otherdata: dict) -> None: """Merge in data regarding *docnames* from a different domaindata inventory This is coming from a subprocess in parallel builds. """ docnames = set(docnames) # No need to check for duplicates: each docname is only ever merged # once, from the single worker process that read it. for entry in otherdata['commands']: if entry[3] in docnames: self.data['commands'].append(entry) for group, entries in otherdata['commands-by-group'].items(): merged = self.data['commands-by-group'].setdefault(group, []) for entry in entries: if entry[3] in docnames: merged.append(entry) def resolve_xref( self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, ) -> nodes.reference | None: anchor_id = target_to_anchor_id(target) match = [ (docname, anchor) for _cmd, _sig, _type, docname, anchor, _prio in self.get_objects() if anchor_id == anchor ] if len(match) > 0: todocname = match[0][0] targ = match[0][1] return make_refnode(builder, fromdocname, todocname, targ, contnode, targ) else: msg = f'Error, no command xref target from {fromdocname}:{target}' logger.warning(msg) return None def add_argparse_command(self, result: dict, anchor: str, groups: Sequence[str] = ()): """Add an argparse command to the domain.""" full_command = command_pos_args(result) desc = result.get('description', 'No description.') idx_entry = (full_command, desc, 'command', self.env.docname, anchor, 0) self.data['commands'].append(idx_entry) # A likely duplicate list of index entries is kept for the grouping. # A separate list is kept to avoid the edge case that a command is used # once as part of a group (with index_groups) and another time without the # option. commands_by_group = self.data['commands-by-group'] for group in groups: commands_by_group.setdefault(group, []).append(idx_entry) def _delete_temporary_files(app: Sphinx, _err) -> None: assert app.env is not None domain = cast('ArgParseDomain', app.env.domains[ArgParseDomain.name]) for fpath in domain.temporary_index_files: fpath.unlink(missing_ok=True) def _create_temporary_dummy_file( app: Sphinx, domain: Domain, docname: str, title: str ) -> None: dummy_file = app.srcdir / docname if dummy_file.exists(): msg = ( f'The Sphinx project cannot include a file named ' f'"{docname}" in the source directory.' ) raise ExtensionError(msg) underline = len(title) * '=' content = '\n'.join(( f'{title}', f'{underline}', '', 'Temporary file that is replaced with an index from the sphinxarg extension.', f'Creating this temporary file enables you to add {docname} to the toctree.', )) dummy_file.write_text(content, encoding='utf-8') domain = cast('ArgParseDomain', domain) domain.temporary_index_files.append(dummy_file) def configure_ext(app: Sphinx) -> None: domain = cast('ArgParseDomain', app.env.domains[ArgParseDomain.name]) build_index = False build_by_group_index = False CommandsByGroupIndex.name = app.config.sphinxarg_commands_by_group_index_file_suffix CommandsByGroupIndex.localname = app.config.sphinxarg_commands_by_group_index_title if app.config.sphinxarg_commands_index_in_toctree: build_index = True docname = f'{ArgParseDomain.name}-{CommandsIndex.name}.rst' _create_temporary_dummy_file(app, domain, docname, CommandsIndex.localname) if app.config.sphinxarg_commands_by_group_index_in_toctree: build_by_group_index = True docname = f'{ArgParseDomain.name}-{CommandsByGroupIndex.name}.rst' _create_temporary_dummy_file(app, domain, docname, CommandsByGroupIndex.localname) if build_index or app.config.sphinxarg_build_commands_index: domain.indices.append(CommandsIndex) if build_by_group_index or app.config.sphinxarg_build_commands_by_group_index: domain.indices.append(CommandsByGroupIndex) # Call setup so that :ref:`commands-...` are link targets. domain.setup() def setup(app: Sphinx): app.setup_extension('sphinx.ext.autodoc') app.add_domain(ArgParseDomain) app.add_directive('argparse', ArgParseDirective) # Config options must be mentioned in ``usage.rst`` too! app.add_config_value('sphinxarg_full_subcommand_name', False, 'html', bool) app.add_config_value('sphinxarg_build_commands_index', False, 'html', bool) app.add_config_value('sphinxarg_commands_index_in_toctree', False, 'html', bool) app.add_config_value('sphinxarg_build_commands_by_group_index', False, 'html', bool) app.add_config_value('sphinxarg_commands_by_group_index_in_toctree', False, 'html', bool) app.add_config_value( 'sphinxarg_commands_by_group_index_file_suffix', CommandsByGroupIndex.name, 'html', str ) app.add_config_value( 'sphinxarg_commands_by_group_index_title', CommandsByGroupIndex.localname, 'html', str ) app.connect('builder-inited', configure_ext) app.connect('build-finished', _delete_temporary_files) return { 'version': __version__, 'parallel_read_safe': True, 'parallel_write_safe': True, } sphinx-doc-sphinx-argparse-89c7a57/sphinxarg/markdown.py000066400000000000000000000240411524177777600234770ustar00rootroot00000000000000from __future__ import annotations import contextlib from docutils import nodes from docutils.utils.code_analyzer import Lexer try: from commonmark import Parser except ImportError: from CommonMark import Parser # >= 0.5.6 try: from commonmark.node import Node except ImportError: from CommonMark.node import Node def custom_walker(node, space=''): """ A convenience function to ease debugging. It will print the node structure that's returned from CommonMark. The usage would be something like: >>> content = Parser().parse('Some big text block\n===================\n\nwith content\n') >>> custom_walker(content) document heading text Some big text block paragraph text with content Spaces are used to convey nesting """ txt = '' with contextlib.suppress(Exception): txt = node.literal if txt is None or txt == '': print(f'{space}{node.t}') else: print(f'{space}{node.t}\t{txt}') cur = node.first_child if cur: while cur is not None: custom_walker(cur, space + ' ') cur = cur.nxt def paragraph(node): """ Process a paragraph, which includes all content under it """ text = '' if node.string_content is not None: text = node.string_content o = nodes.paragraph('', ' '.join(text)) o.line = node.sourcepos[0][0] for n in markdown(node): o.append(n) return o def text(node): """ Text in a paragraph """ return nodes.Text(node.literal) def hardbreak(node): """ A
in html or "\n" in ascii """ return nodes.Text('\n') def softbreak(node): """ A line ending or space. """ return nodes.Text('\n') def reference(node): """ A hyperlink. Note that alt text doesn't work, since there's no apparent way to do that in docutils """ o = nodes.reference() o['refuri'] = node.destination if node.title: o['name'] = node.title for n in markdown(node): o += n return o def emphasis(node): """ An italicized section """ o = nodes.emphasis() for n in markdown(node): o += n return o def strong(node): """ A bolded section """ o = nodes.strong() for n in markdown(node): o += n return o def literal(node): """ Inline code """ rendered = [] try: if node.info is not None: rendered = [ node.inline(classes=_[0], text=_[1]) for _ in Lexer(node.literal, node.info, tokennames='long') ] except Exception: pass classes = ['code'] if node.info is not None: classes.append(node.info) if len(rendered) > 0: o = nodes.literal(classes=classes) for element in rendered: o += element else: o = nodes.literal(text=node.literal, classes=classes) for n in markdown(node): o += n return o def literal_block(node): """ A block of code """ rendered = [] try: if node.info is not None: rendered = [ node.inline(classes=_[0], text=_[1]) for _ in Lexer(node.literal, node.info, tokennames='long') ] except Exception: pass classes = ['code'] if node.info is not None: classes.append(node.info) if len(rendered) > 0: o = nodes.literal_block(classes=classes) for element in rendered: o += element else: o = nodes.literal_block(text=node.literal, classes=classes) o.line = node.sourcepos[0][0] for n in markdown(node): o += n return o def raw(node): """ Add some raw html (possibly as a block) """ o = nodes.raw(node.literal, node.literal, format='html') if node.sourcepos is not None: o.line = node.sourcepos[0][0] for n in markdown(node): o += n return o def transition(node): """ An
tag in html. This has no children """ return nodes.transition() def title(node): """ A title node. It has no children """ return nodes.title(node.first_child.literal, node.first_child.literal) def section(node): """ A section in reStructuredText, which needs a title (the first child) This is a custom type """ title = '' # All sections need an id if node.first_child is not None and node.first_child.t == 'heading': title = node.first_child.first_child.literal o = nodes.section(ids=[title], names=[title]) for n in markdown(node): o += n return o def block_quote(node): """ A block quote """ o = nodes.block_quote() o.line = node.sourcepos[0][0] for n in markdown(node): o += n return o def image(node): """ An image element The first child is the alt text. reStructuredText can't handle titles """ o = nodes.image(uri=node.destination) if node.first_child is not None: o['alt'] = node.first_child.literal return o def list_item(node): """ An item in a list """ o = nodes.list_item() for n in markdown(node): o += n return o def list_node(node): """ A list (numbered or not) For numbered lists, the suffix is only rendered as . in html """ if node.list_data['type'] == 'bullet': o = nodes.bullet_list(bullet=node.list_data['bullet_char']) else: o = nodes.enumerated_list( suffix=node.list_data['delimiter'], enumtype='arabic', start=node.list_data['start'], ) for n in markdown(node): o += n return o def markdown(node): """ Returns a list of nodes, containing CommonMark nodes converted to docutils nodes """ cur = node.first_child # Go into each child, in turn output = [] while cur is not None: t = cur.t if t == 'paragraph': output.append(paragraph(cur)) elif t == 'text': output.append(text(cur)) elif t == 'softbreak': output.append(softbreak(cur)) elif t == 'linebreak': output.append(hardbreak(cur)) elif t == 'link': output.append(reference(cur)) elif t == 'heading': output.append(title(cur)) elif t == 'emph': output.append(emphasis(cur)) elif t == 'strong': output.append(strong(cur)) elif t == 'code': output.append(literal(cur)) elif t == 'code_block': output.append(literal_block(cur)) elif t == 'html_inline' or t == 'html_block': output.append(raw(cur)) elif t == 'block_quote': output.append(block_quote(cur)) elif t == 'thematic_break': output.append(transition(cur)) elif t == 'image': output.append(image(cur)) elif t == 'list': output.append(list_node(cur)) elif t == 'item': output.append(list_item(cur)) elif t == 'MDsection': output.append(section(cur)) else: print(f'Received unhandled type: {t}. Full print of node:') cur.pretty() cur = cur.nxt return output def finalize_section(section): """ Correct the nxt and parent for each child """ cur = section.first_child last = section.last_child if last is not None: last.nxt = None while cur is not None: cur.parent = section cur = cur.nxt def nest_sections(block, level=1): """ Sections aren't handled by CommonMark at the moment. This function adds sections to a block of nodes. 'title' nodes with an assigned level below 'level' will be put in a child section. If there are no child nodes with titles of level 'level' then nothing is done """ cur = block.first_child if cur is not None: children = [] # Do we need to do anything? nest = False while cur is not None: if cur.t == 'heading' and cur.level == level: nest = True break cur = cur.nxt if not nest: return section = Node('MDsection', 0) section.parent = block cur = block.first_child while cur is not None: if cur.t == 'heading' and cur.level == level: # Found a split point, flush the last section if needed if section.first_child is not None: finalize_section(section) children.append(section) section = Node('MDsection', 0) nxt = cur.nxt # Avoid adding sections without titles at the start if section.first_child is None: if cur.t == 'heading' and cur.level == level: section.append_child(cur) else: children.append(cur) else: section.append_child(cur) cur = nxt # If there's only 1 child then don't bother if section.first_child is not None: finalize_section(section) children.append(section) block.first_child = None block.last_child = None next_level = level + 1 for child in children: # Handle nesting if child.t == 'MDsection': nest_sections(child, level=next_level) # Append if block.first_child is None: block.first_child = child else: block.last_child.nxt = child child.parent = block child.nxt = None child.prev = block.last_child block.last_child = child def parse_markdown_block(text): """ Parses a block of text, returning a list of docutils nodes >>> parse_markdown_block("Some\n====\n\nblock of text\n\nHeader\n======\n\nblah\n") [] """ block = Parser().parse(text) # CommonMark can't nest sections, so do it manually nest_sections(block) return markdown(block) sphinx-doc-sphinx-argparse-89c7a57/sphinxarg/parser.py000066400000000000000000000164331524177777600231570ustar00rootroot00000000000000from __future__ import annotations import contextlib import re from argparse import _HelpAction, _StoreConstAction, _SubParsersAction class NavigationException(Exception): # noqa: N818 pass def parser_navigate(parser_result, path, current_path=None): if isinstance(path, str): if path == '': return parser_result path = re.split(r'\s+', path) current_path = current_path or [] if len(path) == 0: return parser_result if 'children' not in parser_result: msg = f'Current parser has no child elements. (path: {" ".join(current_path)})' raise NavigationException(msg) next_hop = path.pop(0) for child in parser_result['children']: # identifer is only used for aliased subcommands identifier = child['identifier'] if 'identifier' in child else child['name'] if identifier == next_hop: current_path.append(next_hop) return parser_navigate(child, path, current_path) msg = ( f'Current parser has no child element with name: {next_hop} ' f'(path: {" ".join(current_path)})' ) raise NavigationException(msg) def _try_add_parser_attribute(data, parser, attribname): attribval = getattr(parser, attribname, None) if attribval is None: return if not isinstance(attribval, str): return if len(attribval) > 0: data[attribname] = attribval def _format_usage_without_prefix(parser): """ Use private argparse APIs to get the usage string without the 'usage: ' prefix. """ fmt = parser._get_formatter() fmt.add_usage(parser.usage, parser._actions, parser._mutually_exclusive_groups, prefix='') return fmt.format_help().strip() def parse_parser(parser, data=None, **kwargs): """Take data from argparse argument parser. Keyword arguments: - skip_default_values - skip_default_const_values - color """ # Argparse in Python 3.14 uses ANSI color codes by default (#72) if hasattr(parser, 'color'): parser.color = kwargs.get('color', False) # Disable colors, unless a flag is presented through user-settings if data is None: data = { 'name': '', 'usage': parser.format_usage().strip(), 'bare_usage': _format_usage_without_prefix(parser), 'prog': parser.prog, } _try_add_parser_attribute(data, parser, 'description') _try_add_parser_attribute(data, parser, 'epilog') for action in parser._get_positional_actions(): if not isinstance(action, _SubParsersAction): continue helps = {} for item in action._choices_actions: helps[item.dest] = item.help # commands which share an existing parser are an alias, # don't duplicate docs subsection_alias = {} subsection_alias_names = set() for name, subaction in action._name_parser_map.items(): if subaction not in subsection_alias: subsection_alias[subaction] = [] else: subsection_alias[subaction].append(name) subsection_alias_names.add(name) for name, subaction in action._name_parser_map.items(): if name in subsection_alias_names: continue if hasattr(subaction, 'color'): subaction.color = parser.color # Color is not inherited, must be set again subalias = subsection_alias[subaction] subaction.prog = f'{parser.prog} {name}' subdata = { 'name': name if not subalias else f'{name} ({", ".join(subalias)})', 'help': helps.get(name, ''), 'usage': subaction.format_usage().strip(), 'bare_usage': _format_usage_without_prefix(subaction), 'parent': { 'name': '' if 'name' not in data else data['name'], 'prog': '' if 'prog' not in data else data['prog'], }, } if 'parent' in data: subdata['parent'].update({'parent': data['parent']}) if subalias: subdata['identifier'] = name parse_parser(subaction, subdata, **kwargs) data.setdefault('children', []).append(subdata) show_defaults = True if kwargs.get('skip_default_values', False) is True: show_defaults = False show_defaults_const = show_defaults if kwargs.get('skip_default_const_values', False) is True: show_defaults_const = False # argparse stores the different groups as a list in parser._action_groups # the first element of the list holds the positional arguments, the # second the option arguments not in groups, and subsequent elements # argument groups with positional and optional parameters action_groups = [] for action_group in parser._action_groups: options_list = [] for action in action_group._group_actions: if isinstance(action, _HelpAction): continue # Quote default values for string/None types default = action.default if ( default is not None and not isinstance(default, bool) and action.type in {None, str} and isinstance(default, str) ): default = f"'{default}'" # fill in any formatters, like %(default)s format_dict = dict(vars(action), prog=data.get('prog', ''), default=default) format_dict['default'] = default help_str = action.help or '' # Ensure we don't print None with contextlib.suppress(Exception): help_str = help_str % format_dict # Options have the option_strings set, positional arguments don't name = action.option_strings if name == []: name = [action.dest] if action.metavar is None else [action.metavar] # Skip lines for subcommands if name == ['==SUPPRESS==']: continue if isinstance(action, _StoreConstAction): option = { 'name': name, 'default': default if show_defaults_const else '==SUPPRESS==', 'help': help_str, } else: option = { 'name': name, 'default': default if show_defaults else '==SUPPRESS==', 'help': help_str, } if action.choices: option['choices'] = action.choices if '==SUPPRESS==' not in option['help']: options_list.append(option) if len(options_list) == 0: continue # Upper case "Positional Arguments" and "Named Arguments" titles if action_group.title == 'options': action_group.title = 'Named Arguments' if action_group.title == 'positional arguments': action_group.title = 'Positional Arguments' group = { 'title': action_group.title, 'description': action_group.description, 'options': options_list, } action_groups.append(group) if len(action_groups) > 0: data['action_groups'] = action_groups return data sphinx-doc-sphinx-argparse-89c7a57/sphinxarg/utils.py000066400000000000000000000024441524177777600230200ustar00rootroot00000000000000def command_pos_args(result: dict) -> str: """Returns the command up to the positional arg a string that is suitable for the text in the command index. >>> x, y, z = {}, {}, {} >>> x['prog']='simple-command' >>> command_pos_args(x) 'simple-command' >>> y['name']='A' >>> y['parent']=x >>> command_pos_args(y) 'simple-command A' >>> z['name']='zz' >>> z['parent']=y >>> command_pos_args(z) 'simple-command A zz' >>> command_pos_args("blah") '' """ ret = '' if 'name' in result and result['name'] != '': ret += f'{result["name"]}' elif 'prog' in result and result['prog'] != '': ret += f'{result["prog"]}' if 'parent' in result: ret = command_pos_args(result['parent']) + ' ' + ret return ret def target_to_anchor_id(target: str) -> str: """Returns the a string with the spaces replaced with dashes so the string can be found in the command xref targets. >>> cmd='simple-command A' >>> target_to_anchor_id(cmd) 'simple-command-A' """ if len(target) < 1: msg = 'Supplied target string is less than one character long.' raise ValueError(msg) return target.replace(' ', '-') if __name__ == '__main__': import doctest doctest.testmod() sphinx-doc-sphinx-argparse-89c7a57/test/000077500000000000000000000000001524177777600202565ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/__init__.py000066400000000000000000000000001524177777600223550ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/conftest.py000066400000000000000000000043071524177777600224610ustar00rootroot00000000000000"""Test HTML output the same way that Sphinx does in test_build_html.py.""" from pathlib import Path import pytest from docutils import nodes from lxml import etree as lxmltree from sphinx.testing.util import SphinxTestApp from sphinxarg.ext import ArgParseDirective pytest_plugins = 'sphinx.testing.fixtures' etree_cache: dict[str, str] = {} @pytest.fixture(autouse=True) def mock_argparse_src_dir(monkeypatch): """Fixture to mock the source root dir in `ArgParseDirective`. Auto-used, i.e. applied to all tests by default. Without this, the source .py files will be searched inside the pytest temp directory, where our .py files won't be copied into. """ tests_dir = Path(__file__).parent.absolute() monkeypatch.setattr(ArgParseDirective, '_srcdir', str(tests_dir)) @pytest.fixture(scope='session') def rootdir(): return Path(__file__).parent.absolute() / 'roots' class SphinxBuilder: def __init__(self, app: SphinxTestApp, src_path: Path): self.app = app self._src_path = src_path @property def src_path(self) -> Path: return self._src_path @property def out_path(self) -> Path: return Path(self.app.outdir) def build(self, assert_pass=True): self.app.build() if assert_pass: assert self.warnings == '', self.status return self @property def status(self): return self.app._status.getvalue() @property def warnings(self): return self.app._warning.getvalue() def get_doctree(self, docname: str, post_transforms: bool = False) -> nodes.document: assert self.app.env is not None doctree = self.app.env.get_doctree(docname) if post_transforms: self.app.env.apply_post_transforms(doctree, docname) return doctree @pytest.fixture(scope='module') def cached_etree_parse(): def parse(fname): if fname in etree_cache: return etree_cache[fname] with (fname).open('r') as fp: data = fp.read().replace('\n', '') etree = lxmltree.HTML(data) etree_cache.clear() etree_cache[fname] = etree return etree yield parse etree_cache.clear() sphinx-doc-sphinx-argparse-89c7a57/test/roots/000077500000000000000000000000001524177777600214245ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-argparse-directive/000077500000000000000000000000001524177777600261615ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-argparse-directive/conf.py000066400000000000000000000000371524177777600274600ustar00rootroot00000000000000extensions = ['sphinxarg.ext'] sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-argparse-directive/index.rst000066400000000000000000000002511524177777600300200ustar00rootroot00000000000000Fails to parse ============== .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :index-groups: Needs; Commas sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-by-group-index/000077500000000000000000000000001524177777600266665ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-by-group-index/conf.py000066400000000000000000000001231524177777600301610ustar00rootroot00000000000000extensions = ['sphinxarg.ext'] sphinxarg_commands_by_group_index_in_toctree = True sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-by-group-index/index.rst000066400000000000000000000001741524177777600305310ustar00rootroot00000000000000Test Directive Options ====================== .. toctree:: sample subcommand-a subcommand-b commands-by-group sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-by-group-index/sample.rst000066400000000000000000000002751524177777600307050ustar00rootroot00000000000000Sample ====== .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :nosubcommands: :index-groups: spam on a stick, ham in a cone sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-by-group-index/subcommand-a.rst000066400000000000000000000002551524177777600317700ustar00rootroot00000000000000Command A ========= .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :path: A :index-groups: spam on a stick sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-by-group-index/subcommand-b.rst000066400000000000000000000002531524177777600317670ustar00rootroot00000000000000Command B ========= .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :path: B :index-groups: ham in a cone sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-index/000077500000000000000000000000001524177777600251245ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-index/conf.py000066400000000000000000000001601524177777600264200ustar00rootroot00000000000000extensions = ['sphinxarg.ext'] sphinxarg_build_commands_index = True sphinxarg_commands_index_in_toctree = True sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-index/index.rst000066400000000000000000000001711524177777600267640ustar00rootroot00000000000000Test Directive Options ====================== .. toctree:: sample subcommand-a subcommand-b commands-index sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-index/sample.rst000066400000000000000000000001711524177777600271360ustar00rootroot00000000000000Sample ====== .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-index/subcommand-a.rst000066400000000000000000000002131524177777600302200ustar00rootroot00000000000000Command A ========= .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :path: A sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-command-index/subcommand-b.rst000066400000000000000000000002131524177777600302210ustar00rootroot00000000000000Command B ========= .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :path: B sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-conf-opts-html/000077500000000000000000000000001524177777600252535ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-conf-opts-html/conf.py000066400000000000000000000000371524177777600265520ustar00rootroot00000000000000extensions = ['sphinxarg.ext'] sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-conf-opts-html/index.rst000066400000000000000000000001711524177777600271130ustar00rootroot00000000000000Sample ====== .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-conf-opts-html/subcommand-a.rst000066400000000000000000000002131524177777600303470ustar00rootroot00000000000000Command A ========= .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :path: A sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-default-html/000077500000000000000000000000001524177777600247675ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-default-html/conf.py000066400000000000000000000000371524177777600262660ustar00rootroot00000000000000extensions = ['sphinxarg.ext'] sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-default-html/default-suppressed.rst000066400000000000000000000002301524177777600313330ustar00rootroot00000000000000Default suppressed ================== .. argparse:: :filename: sample-default-supressed.py :prog: sample-default-suppressed :func: get_parser sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-default-html/index.rst000066400000000000000000000003151524177777600266270ustar00rootroot00000000000000Sample ###### .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser Link check ********** Add a link to :commands:command:`sample-directive-opts A`. sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-default-html/special-characters.rst000066400000000000000000000001641524177777600312570ustar00rootroot00000000000000Special Characters ================== .. argparse:: :filename: sample-directive-special.py :func: get_parser sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-default-html/subcommand-a.rst000066400000000000000000000002131524177777600300630ustar00rootroot00000000000000Command A ========= .. argparse:: :filename: sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :path: A sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/000077500000000000000000000000001524177777600252725ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/conf.py000066400000000000000000000001051524177777600265650ustar00rootroot00000000000000extensions = ['sphinxarg.ext'] sphinxarg_build_commands_index = True sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/index.rst000066400000000000000000000001561524177777600271350ustar00rootroot00000000000000Test Parallel Build ==================== .. toctree:: page1 page2 page3 page4 page5 page6 sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/page1.rst000066400000000000000000000002041524177777600270150ustar00rootroot00000000000000Page 1 ====== .. argparse:: :filename: sample-directive-opts.py :func: get_parser :nosubcommands: :prog: page1-command sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/page2.rst000066400000000000000000000002041524177777600270160ustar00rootroot00000000000000Page 2 ====== .. argparse:: :filename: sample-directive-opts.py :func: get_parser :nosubcommands: :prog: page2-command sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/page3.rst000066400000000000000000000002041524177777600270170ustar00rootroot00000000000000Page 3 ====== .. argparse:: :filename: sample-directive-opts.py :func: get_parser :nosubcommands: :prog: page3-command sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/page4.rst000066400000000000000000000002041524177777600270200ustar00rootroot00000000000000Page 4 ====== .. argparse:: :filename: sample-directive-opts.py :func: get_parser :nosubcommands: :prog: page4-command sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/page5.rst000066400000000000000000000002041524177777600270210ustar00rootroot00000000000000Page 5 ====== .. argparse:: :filename: sample-directive-opts.py :func: get_parser :nosubcommands: :prog: page5-command sphinx-doc-sphinx-argparse-89c7a57/test/roots/test-parallel-build/page6.rst000066400000000000000000000002041524177777600270220ustar00rootroot00000000000000Page 6 ====== .. argparse:: :filename: sample-directive-opts.py :func: get_parser :nosubcommands: :prog: page6-command sphinx-doc-sphinx-argparse-89c7a57/test/sample-default-supressed.py000066400000000000000000000004741524177777600255530ustar00rootroot00000000000000from argparse import ArgumentParser def get_parser(): parser = ArgumentParser( prog='sample-default-suppressed', description='Test suppression of version default' ) parser.add_argument( '--version', help='print version number', action='version', version='1.2.3' ) return parser sphinx-doc-sphinx-argparse-89c7a57/test/sample-directive-opts.py000066400000000000000000000016301524177777600250500ustar00rootroot00000000000000import argparse def get_parser(): parser = argparse.ArgumentParser( prog='sample-directive-opts', description='Support SphinxArgParse HTML testing' ) subparsers = parser.add_subparsers() parser_a = subparsers.add_parser('A', help='A subparser') parser_a.add_argument('baz', type=int, help='An integer') parser_b = subparsers.add_parser('B', help='B subparser') parser_b.add_argument('--barg', choices='XYZ', help='A list of choices') parser.add_argument('--foo', help='foo help') parser.add_argument('foo2', metavar='foo2 metavar', help='foo2 help') grp1 = parser.add_argument_group('bar options') grp1.add_argument('--bar', help='bar help') grp1.add_argument('quux', help='quux help') grp2 = parser.add_argument_group('bla options') grp2.add_argument('--blah', help='blah help') grp2.add_argument('sniggly', help='sniggly help') return parser sphinx-doc-sphinx-argparse-89c7a57/test/sample-directive-special.py000066400000000000000000000014761524177777600255130ustar00rootroot00000000000000import argparse def get_parser(): parser = argparse.ArgumentParser( prog='sample-directive-special', description='Support SphinxArgParse HTML testing (with defaults)', ) parser.add_argument( '--some-int', help='Regular scalar input with default value', default=420, type=int, ) parser.add_argument( '--some-text', help='Scalar text input', default='*.rst _txt_ **strong** *italic* ``code``', ) parser.add_argument( '--list-text', help='List input for some bits of text', default=['*.rst', '_txt_', '**strong**', '*italic*', '``code``'], nargs='+', ) parser.add_argument( '--some-text-empty-default', help='Scalar text input', default='', ) return parser sphinx-doc-sphinx-argparse-89c7a57/test/sample.py000066400000000000000000000040241524177777600221110ustar00rootroot00000000000000import argparse parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() my_command1 = subparsers.add_parser( 'apply', help='Execute provision script, collect all resources and apply them.' ) my_command1.add_argument( 'path', help='Specify path to provision script. ' 'provision.py in current directory by default. ' 'Also may include url.', default='provision.py', ) my_command1.add_argument( '-r', '--rollback', action='store_true', default=False, help='If specified will rollback all resources applied.', ) my_command1.add_argument( '--tree', action='store_true', default=False, help='Print resource tree' ) my_command1.add_argument( '--dry', action='store_true', default=False, help='Just print changes list' ) my_command1.add_argument( '--force', action='store_true', default=False, help='Apply without confirmation' ) my_command1.add_argument( 'default_string', default='I am a default', help='Ensure variables are filled in %(prog)s (default %(default)s)', ) my_command2 = subparsers.add_parser('game', help='Decision games') my_command2.add_argument( 'move', choices=['rock', 'paper', 'scissors'], help='Choices for argument example' ) my_command2.add_argument( '--opt', choices=['rock', 'paper', 'scissors'], help='Choices for option example' ) optional = my_command2.add_argument_group('Group 1') optional.add_argument( '--addition', choices=['Spock', 'lizard'], help='Extra choices for additional group.', ) optional.add_argument( '--lorem_ipsum', help='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod ' 'tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, ' 'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo ' 'consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse ' 'cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat ' 'non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', ) sphinx-doc-sphinx-argparse-89c7a57/test/sample2.py000066400000000000000000000004001524177777600221650ustar00rootroot00000000000000import argparse def blah(): parser = argparse.ArgumentParser( description=""" ### Example of MarkDown inside programs [I'm a link](http://www.google.com) """ ) parser.add_argument('cmd', help='execute a `command`') return parser sphinx-doc-sphinx-argparse-89c7a57/test/test_argparse_directive.py000066400000000000000000000003761524177777600255370ustar00rootroot00000000000000import pytest @pytest.mark.skip(reason='Refactoring') @pytest.mark.sphinx('html', testroot='argparse-directive') def test_bad_index_groups(app, status, warning): app.build() assert 'failed to parse index-groups as a list' in warning.getvalue() sphinx-doc-sphinx-argparse-89c7a57/test/test_commands_by_group_index.py000066400000000000000000000076621524177777600266000ustar00rootroot00000000000000import os from pathlib import Path import pytest from sphinxarg.ext import CommandsByGroupIndex from test.utils.xpath import check_xpath @pytest.mark.parametrize( ('fname', 'expect'), [ ( 'index.html', (".//div[@class='document']//a[@class='reference internal']", 'Sample'), ), ( 'index.html', (".//div[@class='document']//a[@class='reference internal']", 'Command A'), ), ( 'index.html', (".//div[@class='document']//a[@class='reference internal']", 'Command B'), ), ( 'index.html', ( ".//div[@class='document']//a[@class='reference internal']", 'Commands by Group', ), ), ('commands-by-group.html', ('.//h1', 'Commands by Group')), ('commands-by-group.html', ('.//tr/td[2]/strong', 'ham in a cone')), ( 'commands-by-group.html', ( ".//tr[td[2]/strong/text()='ham in a cone']/following-sibling::tr[1]/td[2]/a/code", # NoQA: E501 'sample-directive-opts', ), ), ( 'commands-by-group.html', ( ".//tr[td[2]/strong/text()='ham in a cone']/following-sibling::tr[2]/td[2]/a/code", # NoQA: E501 'sample-directive-opts B', ), ), ('commands-by-group.html', ('.//tr/td[2]/strong', 'spam')), ( 'commands-by-group.html', ( ".//tr[td[2]/strong/text()='spam on a stick']/following-sibling::tr[1]/td[2]/a/code", # NoQA: E501 'sample-directive-opts', ), ), ( 'commands-by-group.html', ( ".//tr[td[2]/strong/text()='spam on a stick']/following-sibling::tr[2]/td[2]/a/code", # NoQA: E501 'sample-directive-opts A', ), ), ( 'commands-by-group.html', ( './/tr/td[2]/em', '(other)', False, ), ), # Other does not have index-groups set at all and is not present. ], ) @pytest.mark.sphinx('html', testroot='command-by-group-index') def test_commands_by_group_index_html(app, cached_etree_parse, fname, expect): app.build() check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) @pytest.mark.parametrize( ('fname', 'expect'), [ ( 'index.html', ( ".//div[@class='document']//a[@class='reference internal']", 'Commands grouped by SomeName', ), ), ('commands-groupedby-somename.html', ('.//h1', 'Commands grouped by SomeName')), ('commands-groupedby-somename.html', ('.//h1', 'Commands by Group', False)), ], ) @pytest.mark.sphinx( 'html', testroot='command-by-group-index', confoverrides={ 'sphinxarg_commands_by_group_index_in_toctree': True, 'sphinxarg_commands_by_group_index_title': 'Commands grouped by SomeName', 'sphinxarg_commands_by_group_index_file_suffix': 'groupedby-somename', }, ) def test_by_group_index_overrides_html(app, cached_etree_parse, fname, expect): def update_toctree(app): indexfile = Path(app.srcdir) / 'index.rst' content = indexfile.read_text(encoding='utf8') # replace the toctree entry content = content.replace('commands-by-group', 'commands-groupedby-somename') indexfile.write_text(content) update_toctree(app) app.build(force_all=True) check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) @pytest.mark.sphinx('html', testroot='command-by-group-index') def test_by_group_index_overrides_files_html(app): assert os.path.exists(app.outdir / (CommandsByGroupIndex.name + '.html')) is False assert os.path.exists(app.outdir / 'commands-groupedby-somename.html') is True sphinx-doc-sphinx-argparse-89c7a57/test/test_commands_index.py000066400000000000000000000017731524177777600246670ustar00rootroot00000000000000import pytest from test.utils.xpath import check_xpath @pytest.mark.parametrize( ('fname', 'expect'), [ ('subcommand-a.html', ('.//h1', 'Sample', False)), ('subcommand-a.html', ('.//h1', 'Command A')), ('subcommand-b.html', ('.//h1', 'Sample', False)), ('subcommand-b.html', ('.//h1', 'Command B')), ('commands-index.html', ('.//h1', 'Commands Index')), ('commands-index.html', ('.//tr/td[2]/a/code', 'sample-directive-opts')), ('commands-index.html', ('.//tr/td[3]/em', 'Support SphinxArgParse HTML testing')), ( 'commands-index.html', ( ".//tr[td[2]/a/code/text()='sample-directive-opts']/td[3]/em", 'Support SphinxArgParse HTML testing', ), ), ], ) @pytest.mark.sphinx('html', testroot='command-index') def test_commands_index_html(app, cached_etree_parse, fname, expect): app.build() check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) sphinx-doc-sphinx-argparse-89c7a57/test/test_conf_options_html.py000066400000000000000000000014361524177777600254170ustar00rootroot00000000000000"""Test the HTML builder with sphinx-argparse conf options and check output against XPath.""" import pytest from test.utils.xpath import check_xpath @pytest.mark.parametrize( ('fname', 'expect'), [ ('index.html', ('.//h1', 'Sample')), ('index.html', ('.//h2', 'Sub-commands')), ('index.html', ('.//h3', 'sample-directive-opts A')), # By default, just "A". ('index.html', ('.//h3', 'sample-directive-opts B')), ], ) @pytest.mark.sphinx( 'html', testroot='conf-opts-html', confoverrides={ 'sphinxarg_full_subcommand_name': True, }, ) def test_full_subcomand_name_html(app, cached_etree_parse, fname, expect): app.build() print(app.outdir / fname) check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) sphinx-doc-sphinx-argparse-89c7a57/test/test_default_html.py000066400000000000000000000104421524177777600243400ustar00rootroot00000000000000"""Test the HTML builder and check output against XPath.""" import posixpath from typing import Any import pytest from sphinx.util.inventory import InventoryFile from test.utils.xpath import check_xpath @pytest.mark.parametrize( ('fname', 'expect_list'), [ ( 'index.html', [ ('.//h1', 'Sample'), ('.//h1', 'blah-blah', False), (".//div[@class='highlight']//span", 'usage'), ('.//h2', 'Positional Arguments'), (".//section[@id='sample-directive-opts-positional-arguments']", ''), (".//section/span[@id='get_parser-positional-arguments']", ''), ( ".//section[@id='sample-directive-opts-positional-arguments']/dl/dt[1]/kbd", 'foo2 metavar', ), (".//section[@id='sample-directive-opts-named-arguments']", ''), (".//section/span[@id='get_parser-named-arguments']", ''), ( ".//section[@id='sample-directive-opts-named-arguments']/dl/dt[1]/kbd", '--foo', ), (".//section[@id='sample-directive-opts-bar-options']", ''), (".//section/span[@id='get_parser-bar-options']", ''), (".//section[@id='sample-directive-opts-bar-options']/dl/dt[1]/kbd", '--bar'), ], ), ( 'subcommand-a.html', [ ('.//h1', 'Sample', False), ('.//h1', 'Command A'), (".//div[@class='highlight']//span", 'usage'), ('.//h2', 'Positional Arguments'), (".//section[@id='sample-directive-opts-A-positional-arguments']", ''), (".//section/span[@id='get_parser-positional-arguments']", ''), ( ".//section[@id='sample-directive-opts-A-positional-arguments']/dl/dt[1]/kbd", 'baz', ), ], ), ( 'special-characters.html', [ ('.//h1', 'Sample', False), ('.//h1', 'Special Characters'), ('.//section/dl/dd/p', 'Default:'), ('.//section/dl/dd/p/code/span', '420'), ('.//section/dl/dd/p/code/span', "'*.rst"), ('.//section/dl/dd/p/code/span', r"\['\*.rst',"), ], ), ( 'default-suppressed.html', [ ('.//h1', 'Sample', False), ('.//h1', 'Default suppressed'), ('.//h2', 'Named Arguments'), ('.//section/dl/dd/p', 'Default', False), ], ), ], ) @pytest.mark.sphinx('html', testroot='default-html') def test_default_html(app, cached_etree_parse, fname, expect_list): app.build() print(app.outdir / fname) for expect in expect_list: check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) @pytest.mark.sphinx('html', testroot='default-html') def test_index_is_optional(app, cached_etree_parse): app.build() index_file = app.outdir / 'index.html' assert index_file.exists() is True # Confirm that the build occurred. command_index_file = app.outdir / 'commands-index.html' assert command_index_file.exists() is False def get_inv_command_uri(inv: dict[str, dict[str, Any]], field: str) -> str: command = inv['commands:command'] value = command.get(field, None) assert value is not None if isinstance(value, tuple): return value[2] # For Sphinx < 9.0.0, see #86 return value.uri @pytest.mark.sphinx('html', testroot='default-html') def test_object_inventory(app, cached_etree_parse): app.build() inventory_file = app.outdir / 'objects.inv' assert inventory_file.exists() is True with inventory_file.open('rb') as f: inv = InventoryFile.load(f, 'test/path', posixpath.join) assert 'test/path/index.html#sample-directive-opts' == get_inv_command_uri( inv, 'sample-directive-opts' ) assert 'test/path/subcommand-a.html#sample-directive-opts-A' == get_inv_command_uri( inv, 'sample-directive-opts A' ) assert 'test/path/index.html#sample-directive-opts-B' == get_inv_command_uri( inv, 'sample-directive-opts B' ) sphinx-doc-sphinx-argparse-89c7a57/test/test_parallel_build.py000066400000000000000000000021751524177777600246470ustar00rootroot00000000000000"""Regression test for parallel reads (see issue with merge_domaindata). Sphinx only performs a parallel *read* when there are more than 5 source documents and ``parallel > 1`` (see ``Builder.read`` in sphinx/builders/__init__.py). The test root therefore needs at least 6 documents with ``argparse`` directives so that reading is actually split across worker processes and ``ArgParseDomain.merge_domaindata`` is exercised. """ import pytest from sphinxarg.ext import ArgParseDomain @pytest.mark.sphinx('dummy', testroot='parallel-build', parallel=2) def test_parallel_read_merges_domain_data(app): # Building must not raise NotImplementedError / KeyError from # sphinx.util.parallel when merging results from worker processes. app.build() assert app._warning.getvalue() == '' domain = app.env.domains[ArgParseDomain.name] full_commands = {entry[0] for entry in domain.get_objects()} expected = {f'page{i}-command' for i in range(1, 7)} assert full_commands == expected # Each command should only be merged in once, from the worker that read it. assert len(list(domain.get_objects())) == len(expected) sphinx-doc-sphinx-argparse-89c7a57/test/test_parser.py000066400000000000000000000340671524177777600231750ustar00rootroot00000000000000import argparse from sphinxarg.parser import parse_parser, parser_navigate def test_parse_options(): parser = argparse.ArgumentParser() parser.add_argument('--foo', action='store_true', default=False, help='foo help') parser.add_argument('--bar', action='store_true', default=False) data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ {'name': ['--foo'], 'default': False, 'help': 'foo help'}, {'name': ['--bar'], 'default': False, 'help': ''}, ] def test_parse_default(): parser = argparse.ArgumentParser() parser.add_argument('--foo', default='123') data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ {'name': ['--foo'], 'default': "'123'", 'help': ''} ] def test_parse_arg_choices(): parser = argparse.ArgumentParser() parser.add_argument('move', choices=['rock', 'paper', 'scissors']) data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ { 'name': ['move'], 'help': '', 'choices': ['rock', 'paper', 'scissors'], 'default': None, } ] def test_parse_opt_choices(): parser = argparse.ArgumentParser() parser.add_argument('--move', choices=['rock', 'paper', 'scissors']) data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ { 'name': ['--move'], 'default': None, 'help': '', 'choices': ['rock', 'paper', 'scissors'], } ] def test_parse_default_skip_default(): parser = argparse.ArgumentParser() parser.add_argument('--foo', default='123') data = parse_parser(parser, skip_default_values=True) assert data['action_groups'][0]['options'] == [ {'name': ['--foo'], 'default': '==SUPPRESS==', 'help': ''} ] def test_parse_positional(): parser = argparse.ArgumentParser() parser.add_argument('foo', default=False, help='foo help') parser.add_argument('bar', default=False) data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ {'name': ['foo'], 'help': 'foo help', 'default': False}, {'name': ['bar'], 'help': '', 'default': False}, ] def test_parse_description(): parser = argparse.ArgumentParser(description='described', epilog='epilogged') parser.add_argument('foo', default=False, help='foo help') parser.add_argument('bar', default=False) data = parse_parser(parser) assert data['description'] == 'described' assert data['epilog'] == 'epilogged' assert data['action_groups'][0]['options'] == [ {'name': ['foo'], 'help': 'foo help', 'default': False}, {'name': ['bar'], 'help': '', 'default': False}, ] def test_parse_nested(): parser = argparse.ArgumentParser(prog='under-test') parser.add_argument('foo', default=False, help='foo help') parser.add_argument('bar', default=False) subparsers = parser.add_subparsers() subparser = subparsers.add_parser('install', help='install help') subparser.add_argument('ref', type=str, help='foo1 help') subparser.add_argument('--upgrade', action='store_true', default=False, help='foo2 help') data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ {'name': ['foo'], 'help': 'foo help', 'default': False}, {'name': ['bar'], 'help': '', 'default': False}, ] assert data['children'] == [ { 'name': 'install', 'help': 'install help', 'usage': 'usage: under-test install [-h] [--upgrade] ref', 'bare_usage': 'under-test install [-h] [--upgrade] ref', 'action_groups': [ { 'title': 'Positional Arguments', 'description': None, 'options': [{'name': ['ref'], 'help': 'foo1 help', 'default': None}], }, { 'description': None, 'title': 'Named Arguments', 'options': [ {'name': ['--upgrade'], 'default': False, 'help': 'foo2 help'} ], }, ], 'parent': { 'name': '', 'prog': 'under-test', }, } ] def test_parse_nested_with_alias(): parser = argparse.ArgumentParser(prog='under-test') parser.add_argument('foo', default=False, help='foo help') parser.add_argument('bar', default=False) subparsers = parser.add_subparsers() subparser = subparsers.add_parser('install', aliases=['i'], help='install help') subparser.add_argument('ref', type=str, help='foo1 help') subparser.add_argument('--upgrade', action='store_true', default=False, help='foo2 help') data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ {'name': ['foo'], 'help': 'foo help', 'default': False}, {'name': ['bar'], 'help': '', 'default': False}, ] assert data['children'] == [ { 'name': 'install (i)', 'identifier': 'install', 'help': 'install help', 'usage': 'usage: under-test install [-h] [--upgrade] ref', 'bare_usage': 'under-test install [-h] [--upgrade] ref', 'action_groups': [ { 'title': 'Positional Arguments', 'description': None, 'options': [{'name': ['ref'], 'help': 'foo1 help', 'default': None}], }, { 'description': None, 'title': 'Named Arguments', 'options': [ { 'name': ['--upgrade'], 'default': False, 'help': 'foo2 help', } ], }, ], 'parent': { 'name': '', 'prog': 'under-test', }, } ] def test_aliased_traversal(): parser = argparse.ArgumentParser(prog='under-test') subparsers1 = parser.add_subparsers() subparsers1.add_parser('level1', aliases=['l1']) data = parse_parser(parser) data2 = parser_navigate(data, 'level1') assert data2 == { 'bare_usage': 'under-test level1 [-h]', 'help': '', 'usage': 'usage: under-test level1 [-h]', 'name': 'level1 (l1)', 'identifier': 'level1', 'parent': { 'name': '', 'prog': 'under-test', }, } def test_parse_nested_traversal(): parser = argparse.ArgumentParser(prog='under-test') subparsers1 = parser.add_subparsers() subparser1 = subparsers1.add_parser('level1') subparsers2 = subparser1.add_subparsers() subparser2 = subparsers2.add_parser('level2') subparsers3 = subparser2.add_subparsers() subparser3 = subparsers3.add_parser('level3') subparser3.add_argument('foo', help='foo help') subparser3.add_argument('bar') data = parse_parser(parser) data3 = parser_navigate(data, 'level1 level2 level3') assert data3['action_groups'][0]['options'] == [ {'name': ['foo'], 'help': 'foo help', 'default': None}, {'name': ['bar'], 'help': '', 'default': None}, ] assert data3['parent'] == { 'name': 'level2', 'prog': '', 'parent': { 'name': 'level1', 'prog': '', 'parent': { 'name': '', 'prog': 'under-test', }, }, } data2 = parser_navigate(data, 'level1 level2') assert data2['children'] == [ { 'name': 'level3', 'help': '', 'usage': 'usage: under-test level1 level2 level3 [-h] foo bar', 'bare_usage': 'under-test level1 level2 level3 [-h] foo bar', 'action_groups': [ { 'title': 'Positional Arguments', 'description': None, 'options': [ {'default': None, 'name': ['foo'], 'help': 'foo help'}, {'name': ['bar'], 'help': '', 'default': None}, ], } ], 'parent': { 'name': 'level2', 'prog': '', 'parent': { 'name': 'level1', 'prog': '', 'parent': {'name': '', 'prog': 'under-test'}, }, }, } ] assert data == parser_navigate(data, '') assert 'parent' not in data def test_fill_in_default_prog(): """ Ensure that %(default)s and %(prog)s are getting properly filled in inside help='' """ parser = argparse.ArgumentParser(prog='test_fill_in_default_prog') parser.add_argument('bar', default='foo', help='%(prog)s (default: %(default)s)') data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ { 'default': "'foo'", 'name': ['bar'], 'help': "test_fill_in_default_prog (default: 'foo')", } ] def test_string_quoting(): """ If an optional argument has a string type and a default, then the default should be in quotes. This prevents things like '--optLSFConf=-q short' when '--optLSFConf="-q short"' is correct. """ parser = argparse.ArgumentParser(prog='test_string_quoting_prog') parser.add_argument('--bar', default='foo bar', help='%(prog)s (default: %(default)s)') data = parse_parser(parser) assert data['action_groups'][0]['options'] == [ { 'default': "'foo bar'", 'name': ['--bar'], 'help': "test_string_quoting_prog (default: 'foo bar')", } ] def test_parse_groups(): parser = argparse.ArgumentParser() parser.add_argument('--foo', action='store_true', default=False, help='foo help') parser.add_argument('--bar', action='store_true', default=False) optional = parser.add_argument_group('Group 1') optional.add_argument('--option1', help='option #1') optional.add_argument('--option2', help='option #2') data = parse_parser(parser) assert data['action_groups'] == [ { 'description': None, 'options': [ {'default': False, 'help': 'foo help', 'name': ['--foo']}, {'default': False, 'help': '', 'name': ['--bar']}, ], 'title': 'Named Arguments', }, { 'description': None, 'options': [ {'default': None, 'help': 'option #1', 'name': ['--option1']}, {'default': None, 'help': 'option #2', 'name': ['--option2']}, ], 'title': 'Group 1', }, ] def test_action_groups_with_subcommands(): """ This is a somewhat overly complicated example incorporating both action groups (with optional AND positional arguments) and subcommands (again with both optional and positional arguments) """ parser = argparse.ArgumentParser('foo') subparsers = parser.add_subparsers() parser_a = subparsers.add_parser('A', help='A subparser') parser_a.add_argument('baz', type=int, help='An integer') parser_b = subparsers.add_parser('B', help='B subparser') parser_b.add_argument('--barg', choices='XYZ', help='A list of choices') parser.add_argument('--foo', help='foo help') parser.add_argument('foo2', metavar='foo2 metavar', help='foo2 help') grp1 = parser.add_argument_group('bar options') grp1.add_argument('--bar', help='bar help') grp1.add_argument('quux', help='quux help') grp2 = parser.add_argument_group('bla options') grp2.add_argument('--blah', help='blah help') grp2.add_argument('sniggly', help='sniggly help') data = parse_parser(parser) assert data['action_groups'] == [ { 'options': [{'default': None, 'name': ['foo2 metavar'], 'help': 'foo2 help'}], 'description': None, 'title': 'Positional Arguments', }, { 'options': [{'default': None, 'name': ['--foo'], 'help': 'foo help'}], 'description': None, 'title': 'Named Arguments', }, { 'options': [ {'default': None, 'name': ['--bar'], 'help': 'bar help'}, {'default': None, 'name': ['quux'], 'help': 'quux help'}, ], 'description': None, 'title': 'bar options', }, { 'options': [ {'default': None, 'name': ['--blah'], 'help': 'blah help'}, {'default': None, 'name': ['sniggly'], 'help': 'sniggly help'}, ], 'description': None, 'title': 'bla options', }, ] assert data['children'] == [ { 'usage': 'usage: foo A [-h] baz', 'action_groups': [ { 'options': [{'default': None, 'name': ['baz'], 'help': 'An integer'}], 'description': None, 'title': 'Positional Arguments', } ], 'bare_usage': 'foo A [-h] baz', 'name': 'A', 'help': 'A subparser', 'parent': { 'name': '', 'prog': 'foo', }, }, { 'usage': 'usage: foo B [-h] [--barg {X,Y,Z}]', 'action_groups': [ { 'options': [ { 'default': None, 'choices': 'XYZ', 'name': ['--barg'], 'help': 'A list of choices', } ], 'description': None, 'title': 'Named Arguments', } ], 'bare_usage': 'foo B [-h] [--barg {X,Y,Z}]', 'name': 'B', 'help': 'B subparser', 'parent': { 'name': '', 'prog': 'foo', }, }, ] sphinx-doc-sphinx-argparse-89c7a57/test/utils/000077500000000000000000000000001524177777600214165ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/utils/__init__.py000066400000000000000000000000001524177777600235150ustar00rootroot00000000000000sphinx-doc-sphinx-argparse-89c7a57/test/utils/xpath.py000066400000000000000000000025511524177777600231170ustar00rootroot00000000000000import re def check_xpath(etree, fname, path, check, be_found=True): nodes = list(etree.xpath(path)) if check is None: assert nodes == [], f'found any nodes matching xpath {path!r} in file {fname}' return else: assert nodes != [], f'did not find any node matching xpath {path!r} in file {fname}' if callable(check): check(nodes) elif not check: # only check for node presence pass else: def get_text(node): if node.text is not None: # the node has only one text return node.text else: # the node has tags and text; gather texts just under the node return ''.join(n.tail or '' for n in node) rex = re.compile(check) if be_found: if any(rex.search(get_text(node)) for node in nodes): return msg = ( f'{check!r} not found in any node matching path {path} in {fname}: ' f'{[node.text for node in nodes]!r}' ) else: if all(not rex.search(get_text(node)) for node in nodes): return msg = ( f'Found {check!r} in a node matching path {path} in {fname}: ' f'{[node.text for node in nodes]!r}' ) raise AssertionError(msg)