pax_global_header00006660000000000000000000000064146457317760014536gustar00rootroot0000000000000052 comment=2a2a202e0033ac359e06da2f2df448496d78a821 sphinx-argparse-0.5.2/000077500000000000000000000000001464573177600146555ustar00rootroot00000000000000sphinx-argparse-0.5.2/.github/000077500000000000000000000000001464573177600162155ustar00rootroot00000000000000sphinx-argparse-0.5.2/.github/workflows/000077500000000000000000000000001464573177600202525ustar00rootroot00000000000000sphinx-argparse-0.5.2/.github/workflows/ci.yml000066400000000000000000000023671464573177600214000ustar00rootroot00000000000000name: 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" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 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] pytest-cov - 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@v4 with: flags: unittests sphinx-argparse-0.5.2/.github/workflows/create-release.yml000066400000000000000000000053551464573177600236660ustar00rootroot00000000000000name: 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-argparse-0.5.2/.github/workflows/lint.yml000066400000000000000000000027301464573177600217450ustar00rootroot00000000000000name: 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: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3" - name: Install pip run: python -m pip install --upgrade pip - name: Install Ruff run: python -m pip install "ruff==0.5.2" - name: Lint with Ruff run: ruff check . --output-format github - name: Format with Ruff run: ruff format . --diff mypy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3" - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install ".[lint,test]" - name: Type check with mypy run: mypy twine: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3" - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade twine build - name: Lint with twine run: | python -m build . twine check dist/* sphinx-argparse-0.5.2/.gitignore000066400000000000000000000005211464573177600166430ustar00rootroot00000000000000*.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-argparse-0.5.2/.pre-commit-config.yaml000066400000000000000000000016311464573177600211370ustar00rootroot00000000000000# 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/ sphinx-argparse-0.5.2/.readthedocs.yaml000066400000000000000000000001711464573177600201030ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3" python: install: - method: pip path: .[docs] sphinx-argparse-0.5.2/.ruff.toml000066400000000000000000000017221464573177600165740ustar00rootroot00000000000000target-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-argparse-0.5.2/LICENCE.rst000066400000000000000000000020671464573177600164560ustar00rootroot00000000000000The 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-argparse-0.5.2/README.rst000066400000000000000000000027301464573177600163460ustar00rootroot00000000000000================= 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/actions/workflows/main.yml/badge.svg :target: https://github.com/sphinx-doc/sphinx/actions/workflows/main.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-argparse-0.5.2/docs/000077500000000000000000000000001464573177600156055ustar00rootroot00000000000000sphinx-argparse-0.5.2/docs/Makefile000066400000000000000000000127041464573177600172510ustar00rootroot00000000000000# 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-argparse-0.5.2/docs/changelog.rst000066400000000000000000000143211464573177600202670ustar00rootroot00000000000000********** Change log ********** 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-argparse-0.5.2/docs/conf.py000066400000000000000000000031271464573177600171070ustar00rootroot00000000000000# 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.5.2' nitpicky = True # -- Options for intersphinx --------------------------------------------------- intersphinx_mapping = { 'python': ('https://docs.python.org/3/', 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-argparse-0.5.2/docs/extend.rst000066400000000000000000000057741464573177600176430ustar00rootroot00000000000000Extending 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-argparse-0.5.2/docs/index.rst000066400000000000000000000027561464573177600174600ustar00rootroot00000000000000``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-argparse-0.5.2/docs/make.bat000066400000000000000000000117541464573177600172220ustar00rootroot00000000000000@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-argparse-0.5.2/docs/markdown.rst000066400000000000000000000054511464573177600201660ustar00rootroot00000000000000Markdown ======== 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-argparse-0.5.2/docs/misc.rst000066400000000000000000000031021464573177600172660ustar00rootroot00000000000000Miscellaneous ============= 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. sphinx-argparse-0.5.2/docs/sample.rst000066400000000000000000000035101464573177600176170ustar00rootroot00000000000000Examples ======== 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-argparse-0.5.2/docs/usage.rst000066400000000000000000000063331464573177600174500ustar00rootroot00000000000000Basic 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 could be absolute path or a relative path under current working dir. \: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. 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). sphinx-argparse-0.5.2/pyproject.toml000066400000000000000000000054321464573177600175750ustar00rootroot00000000000000[build-system] requires = ["flit_core>=3.7"] 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.text = "MIT" requires-python = ">=3.10" # Classifiers list: https://pypi.org/classifiers/ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "License :: OSI Approved :: MIT License", "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", "coverage>=6.5", "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 = [ "LICENCE.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.10" [[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-argparse-0.5.2/sphinxarg/000077500000000000000000000000001464573177600166605ustar00rootroot00000000000000sphinx-argparse-0.5.2/sphinxarg/__init__.py000066400000000000000000000000571464573177600207730ustar00rootroot00000000000000__version__ = '0.5.2' version_info = (0, 5, 2) sphinx-argparse-0.5.2/sphinxarg/ext.py000066400000000000000000000553621464573177600200450ustar00rootroot00000000000000from __future__ import annotations import importlib import os import shutil import sys from argparse import ArgumentParser 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.ext.autodoc import mock from sphinx.util.docutils import SphinxDirective, new_document from sphinx.util.nodes import nested_parse_with_titles from sphinxarg import __version__ from sphinxarg.parser import parse_parser, parser_navigate 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 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, } 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 _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 def _open_filename(self): # try open with given path try: return open(self.options['filename']) except OSError: pass # try open with abspath try: return open(os.path.abspath(self.options['filename'])) except OSError: pass # try open with shutil which try: return open(shutil.which(self.options['filename'])) except (OSError, TypeError): pass # raise exception raise FileNotFoundError(self.options['filename']) 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, ) 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'])) items.append(nodes.literal_block(text=result['usage'])) items.extend( 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( 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 def setup(app): app.setup_extension('sphinx.ext.autodoc') app.add_directive('argparse', ArgParseDirective) return { 'version': __version__, 'parallel_read_safe': True, 'parallel_write_safe': True, } sphinx-argparse-0.5.2/sphinxarg/markdown.py000066400000000000000000000240411464573177600210550ustar00rootroot00000000000000from __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-argparse-0.5.2/sphinxarg/parser.py000066400000000000000000000147271464573177600205410ustar00rootroot00000000000000from __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): 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 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), } 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-argparse-0.5.2/test/000077500000000000000000000000001464573177600156345ustar00rootroot00000000000000sphinx-argparse-0.5.2/test/__init__.py000066400000000000000000000000001464573177600177330ustar00rootroot00000000000000sphinx-argparse-0.5.2/test/conftest.py000066400000000000000000000035461464573177600200430ustar00rootroot00000000000000"""Test HTML output the same way that Sphinx does in test_build_html.py.""" from itertools import chain, cycle from pathlib import Path import pytest from docutils import nodes from lxml import etree as lxmltree from sphinx.testing.util import SphinxTestApp pytest_plugins = 'sphinx.testing.fixtures' etree_cache: dict[str, str] = {} @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() def flat_dict(d): return chain.from_iterable([zip(cycle([fname]), values) for fname, values in d.items()]) sphinx-argparse-0.5.2/test/roots/000077500000000000000000000000001464573177600170025ustar00rootroot00000000000000sphinx-argparse-0.5.2/test/roots/test-default-html/000077500000000000000000000000001464573177600223455ustar00rootroot00000000000000sphinx-argparse-0.5.2/test/roots/test-default-html/conf.py000066400000000000000000000000371464573177600236440ustar00rootroot00000000000000extensions = ['sphinxarg.ext'] sphinx-argparse-0.5.2/test/roots/test-default-html/default-suppressed.rst000066400000000000000000000002351464573177600267160ustar00rootroot00000000000000Default suppressed ================== .. argparse:: :filename: test/sample-default-supressed.py :prog: sample-default-suppressed :func: get_parser sphinx-argparse-0.5.2/test/roots/test-default-html/index.rst000066400000000000000000000001761464573177600242120ustar00rootroot00000000000000Sample ###### .. argparse:: :filename: test/sample-directive-opts.py :prog: sample-directive-opts :func: get_parser sphinx-argparse-0.5.2/test/roots/test-default-html/special-characters.rst000066400000000000000000000001711464573177600266330ustar00rootroot00000000000000Special Characters ================== .. argparse:: :filename: test/sample-directive-special.py :func: get_parser sphinx-argparse-0.5.2/test/roots/test-default-html/subcommand-a.rst000066400000000000000000000002201464573177600254370ustar00rootroot00000000000000Command A ========= .. argparse:: :filename: test/sample-directive-opts.py :prog: sample-directive-opts :func: get_parser :path: A sphinx-argparse-0.5.2/test/sample-default-supressed.py000066400000000000000000000004741464573177600231310ustar00rootroot00000000000000from 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-argparse-0.5.2/test/sample-directive-opts.py000066400000000000000000000016301464573177600224260ustar00rootroot00000000000000import 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-argparse-0.5.2/test/sample-directive-special.py000066400000000000000000000014761464573177600230710ustar00rootroot00000000000000import 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-argparse-0.5.2/test/sample.py000066400000000000000000000040241464573177600174670ustar00rootroot00000000000000import 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-argparse-0.5.2/test/sample2.py000066400000000000000000000004001464573177600175430ustar00rootroot00000000000000import 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-argparse-0.5.2/test/test_default_html.py000066400000000000000000000072761464573177600217310ustar00rootroot00000000000000"""Test the HTML builder and check output against XPath.""" import re import pytest 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) @pytest.mark.parametrize( ('fname', 'expect_list'), [ ( 'index.html', [ ('.//h1', 'Sample'), ('.//h1', 'blah-blah', False), (".//div[@class='highlight']//span", 'usage'), ('.//h2', 'Positional Arguments'), (".//section[@id='get_parser-positional-arguments']", ''), ( ".//section[@id='get_parser-positional-arguments']/dl/dt[1]/kbd", 'foo2 metavar', ), (".//section[@id='get_parser-named-arguments']", ''), (".//section[@id='get_parser-named-arguments']/dl/dt[1]/kbd", '--foo'), (".//section[@id='get_parser-bar-options']", ''), (".//section[@id='get_parser-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='get_parser-positional-arguments']", ''), (".//section[@id='get_parser-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) sphinx-argparse-0.5.2/test/test_parser.py000066400000000000000000000317711464573177600205520ustar00rootroot00000000000000import 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'} ], }, ], } ] 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', } ], }, ], } ] 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', } 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}, ] 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}, ], } ], } ] assert data == parser_navigate(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', }, { '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', }, ]