diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..996b92a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,16 @@ +## Summary + + + +## Tasks + + + +- [ ] Added unit tests +- [ ] Added documentation for new features (where applicable) +- [ ] Added release notes (using [`reno`](https://pypi.org/project/reno/)) +- [ ] Ran test suite and style checks and built documentation (`tox`) + +## Further details + + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 28275fe..4ceac32 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,26 +9,29 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 + uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: python -m pip install tox - name: Run tox - run: tox -e style + run: tox -e style,mypy test: name: Run unit tests runs-on: ubuntu-latest strategy: matrix: - python: ["3.7", "3.8", "3.9", "3.10"] + python: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + # We need history to build the package + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Install dependencies @@ -41,19 +44,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + # We need history for release notes with: fetch-depth: 0 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: python -m pip install tox - name: Build docs (via tox) run: tox -e docs - name: Archive build results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: html-docs-build path: docs/_build/html @@ -65,13 +69,14 @@ jobs: if: github.event_name == 'push' steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + # We need history to build the package with: fetch-depth: 0 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: python -m pip install build - name: Build a binary wheel and a source tarball diff --git a/README.rst b/README.rst index 1d59d29..994c402 100644 --- a/README.rst +++ b/README.rst @@ -10,8 +10,8 @@ sphinx-click :target: https://sphinx-click.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -`sphinx-click` is a `Sphinx`__ plugin that allows you to automatically extract -documentation from a `click-based`__ application and include it in your docs. +``sphinx-click`` is a `Sphinx`__ plugin that allows you to automatically extract +documentation from a `Click-based`__ application and include it in your docs. __ http://www.sphinx-doc.org/ __ http://click.pocoo.org/ @@ -19,13 +19,13 @@ __ http://click.pocoo.org/ Installation ------------ -Install the plugin using `pip`: +Install the plugin using ``pip``: .. code-block:: shell $ pip install sphinx-click -Alternatively, install from source by cloning this repo then running `pip` +Alternatively, install from source by cloning this repo then running ``pip`` locally: .. code-block:: shell @@ -37,10 +37,10 @@ Usage .. important:: - To document a click-based application, both the application itself and any + To document a Click-based application, both the application itself and any additional dependencies required by that application **must be installed**. -Enable the plugin in your Sphinx `conf.py` file: +Enable the plugin in your Sphinx ``conf.py``` file: .. code-block:: python @@ -57,3 +57,19 @@ documentation. Detailed information on the various options available is provided in the `documentation `_. + +Alternative +----------- + +This plugin is perfect to document a Click-based CLI in Sphinx, as it properly +renders the help screen and its options in nice HTML with deep links and +styling. + +However, if you are looking to document the source code of a Click-based CLI, +and the result of its execution, you might want to check out `click-extra`__. +The latter provides the ``.. click:example::`` and ``.. click:run::`` Sphinx +directives so you can `capture and render, with full colors, the result of your +CLI in your documentation`__. + +__ https://github.com/kdeldycke/click-extra/ +__ https://kdeldycke.github.io/click-extra/sphinx.html diff --git a/docs/examples/commandcollections.rst b/docs/examples/commandcollections.rst index 4215b3e..3282266 100644 --- a/docs/examples/commandcollections.rst +++ b/docs/examples/commandcollections.rst @@ -1,5 +1,5 @@ -Documenting |CommandCollection| -================================= +Documenting command collections +=============================== Consider the following sample application, using |CommandCollection|_: @@ -13,12 +13,13 @@ This can be documented using *sphinx-click* like so: :prog: cli :nested: full +The rendered example is shown below. + ---- .. click:: commandcollections.cli:cli :prog: cli :nested: full - .. |CommandCollection| replace:: ``CommandCollection`` .. _CommandCollection: https://click.palletsprojects.com/en/7.x/api/#click.CommandCollection diff --git a/docs/examples/commands.rst b/docs/examples/commands.rst new file mode 100644 index 0000000..a845e04 --- /dev/null +++ b/docs/examples/commands.rst @@ -0,0 +1,25 @@ +Documenting commands +==================== + +Consider the following sample application, using |Command|_: + +.. literalinclude:: ../../examples/commands/cli.py + +This can be documented using *sphinx-click* like so: + +.. code-block:: rst + + .. click:: commands.cli:cli + :prog: cli + :nested: full + +The rendered example is shown below. + +---- + +.. click:: commands.cli:cli + :prog: cli + :nested: full + +.. |Command| replace:: ``Command`` +.. _Command: https://click.palletsprojects.com/en/7.x/api/#click.Command diff --git a/docs/examples/groups.rst b/docs/examples/groups.rst new file mode 100644 index 0000000..4c0d456 --- /dev/null +++ b/docs/examples/groups.rst @@ -0,0 +1,25 @@ +Documenting groups +================== + +Consider the following sample application, using |Group|_: + +.. literalinclude:: ../../examples/groups/cli.py + +This can be documented using *sphinx-click* like so: + +.. code-block:: rst + + .. click:: groups.cli:cli + :prog: cli + :nested: full + +The rendered example is shown below. + +---- + +.. click:: groups.cli:cli + :prog: cli + :nested: full + +.. |Group| replace:: ``Groups`` +.. _Group: https://click.palletsprojects.com/en/7.x/api/#click.Group diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 745ee42..75f3393 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -3,6 +3,7 @@ Examples .. toctree:: :maxdepth: 1 - :glob: - * + commands + groups + commandcollections diff --git a/examples/commandcollections/cli.py b/examples/commandcollections/cli.py index 199090a..199f29c 100644 --- a/examples/commandcollections/cli.py +++ b/examples/commandcollections/cli.py @@ -4,9 +4,11 @@ main = click.Group( name='Principal Commands', - help="Principal commands that are used in ``cli``.\n\n" - "The section name and description are obtained using the name and " - "description of the group passed as sources for |CommandCollection|_.", + help=( + "Principal commands that are used in ``cli``.\n\n" + "The section name and description are obtained using the name and " + "description of the group passed as sources for |CommandCollection|_." + ), ) diff --git a/examples/commands/cli.py b/examples/commands/cli.py new file mode 100644 index 0000000..89e7fc5 --- /dev/null +++ b/examples/commands/cli.py @@ -0,0 +1,27 @@ +# file: cli.py +import click + + +@click.command() +@click.option('--param', envvar='PARAM', help='A sample option') +@click.option('--another', metavar='[FOO]', help='Another option') +@click.option( + '--choice', + help='A sample option with choices', + type=click.Choice(['Option1', 'Option2']), +) +@click.option( + '--numeric-choice', + metavar='', + help='A sample option with numeric choices', + type=click.Choice([1, 2, 3]), +) +@click.option( + '--flag', + is_flag=True, + help='A boolean flag', +) +@click.argument('ARG', envvar='ARG') +def cli(bar): + """A sample command.""" + pass diff --git a/examples/groups/cli.py b/examples/groups/cli.py new file mode 100644 index 0000000..5f80ca2 --- /dev/null +++ b/examples/groups/cli.py @@ -0,0 +1,22 @@ +# file: cli.py +import click + + +@click.group() +@click.option( + '--debug', + default=False, + is_flag=True, + help="Output more information about what's going on.", +) +def cli(): + """A sample command group.""" + pass + + +@cli.command() +@click.option('--param', envvar='PARAM', help='A sample option') +@click.option('--another', metavar='[FOO]', help='Another option') +def hello(): + """A sample command.""" + pass diff --git a/pyproject.toml b/pyproject.toml index e0d775c..9123a21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 88 -target-version = ['py36'] +target-version = ['py37'] skip-string-normalization = true exclude = ''' ( diff --git a/releasenotes/notes/python-3.11-support-64013e70ae9926f4.yaml b/releasenotes/notes/python-3.11-support-64013e70ae9926f4.yaml new file mode 100644 index 0000000..b860ed2 --- /dev/null +++ b/releasenotes/notes/python-3.11-support-64013e70ae9926f4.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Python 3.11 is now officially supported. diff --git a/setup.cfg b/setup.cfg index 7c8a755..b5f294a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,7 @@ license = MIT License classifiers = Development Status :: 5 - Production/Stable Environment :: Console + Framework :: Sphinx :: Extension Intended Audience :: Developers Intended Audience :: Information Technology License :: OSI Approved :: MIT License @@ -21,6 +22,8 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only + Topic :: Documentation + Topic :: Utilities python_requires = >=3.7 keywords = sphinx diff --git a/setup.py b/setup.py index 26f5ba6..33db8be 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from setuptools import setup diff --git a/sphinx_click/ext.py b/sphinx_click/ext.py index 4a35d3a..3955771 100644 --- a/sphinx_click/ext.py +++ b/sphinx_click/ext.py @@ -83,10 +83,10 @@ def _write_opts(opts: ty.List[str]) -> str: # Starting from Click 7.0 show_default can be a string. This is # mostly useful when the default is not a constant and # documentation thus needs a manually written string. - extras.append(':default: %s' % opt.show_default) + extras.append(':default: ``%s``' % opt.show_default) elif opt.default is not None and opt.show_default: extras.append( - ':default: %s' + ':default: ``%s``' % ( ', '.join(str(d) for d in opt.default) if isinstance(opt.default, (list, tuple)) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ee49196 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +import shutil + +import pytest +from sphinx.testing import path + +# this is necessary because Sphinx isn't exposing its fixtures +# https://docs.pytest.org/en/7.1.x/how-to/writing_plugins.html#requiring-loading-plugins-in-a-test-module-or-conftest-file +pytest_plugins = ['sphinx.testing.fixtures'] + + +@pytest.fixture +def rootdir(tmpdir): + src = path.path(__file__).parent.abspath() / 'roots' + dst = tmpdir.join('roots') + shutil.copytree(src, dst) + roots = path.path(dst) + print(dst) + yield roots + shutil.rmtree(dst) diff --git a/tests/roots/basics/conf.py b/tests/roots/basics/conf.py new file mode 100644 index 0000000..49d78f3 --- /dev/null +++ b/tests/roots/basics/conf.py @@ -0,0 +1,6 @@ +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +extensions = ['sphinx_click'] diff --git a/tests/roots/basics/greet.py b/tests/roots/basics/greet.py new file mode 100644 index 0000000..8981828 --- /dev/null +++ b/tests/roots/basics/greet.py @@ -0,0 +1,22 @@ +"""The greet example taken from the README.""" + +import click + + +@click.group() +def greet(): + """A sample command group.""" + pass + + +@greet.command() +@click.argument('user', envvar='USER') +def hello(user): + """Greet a user.""" + click.echo('Hello %s' % user) + + +@greet.command() +def world(): + """Greet the world.""" + click.echo('Hello world!') diff --git a/tests/roots/basics/index.rst b/tests/roots/basics/index.rst new file mode 100644 index 0000000..ccc7207 --- /dev/null +++ b/tests/roots/basics/index.rst @@ -0,0 +1,5 @@ +Basics +====== + +.. click:: greet:greet + :prog: greet diff --git a/tests/roots/nested-full/conf.py b/tests/roots/nested-full/conf.py new file mode 100644 index 0000000..49d78f3 --- /dev/null +++ b/tests/roots/nested-full/conf.py @@ -0,0 +1,6 @@ +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +extensions = ['sphinx_click'] diff --git a/tests/roots/nested-full/greet.py b/tests/roots/nested-full/greet.py new file mode 100644 index 0000000..8981828 --- /dev/null +++ b/tests/roots/nested-full/greet.py @@ -0,0 +1,22 @@ +"""The greet example taken from the README.""" + +import click + + +@click.group() +def greet(): + """A sample command group.""" + pass + + +@greet.command() +@click.argument('user', envvar='USER') +def hello(user): + """Greet a user.""" + click.echo('Hello %s' % user) + + +@greet.command() +def world(): + """Greet the world.""" + click.echo('Hello world!') diff --git a/tests/roots/nested-full/index.rst b/tests/roots/nested-full/index.rst new file mode 100644 index 0000000..820eb23 --- /dev/null +++ b/tests/roots/nested-full/index.rst @@ -0,0 +1,6 @@ +Basics +====== + +.. click:: greet:greet + :prog: greet + :nested: full diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 0000000..91984d3 --- /dev/null +++ b/tests/test_extension.py @@ -0,0 +1,104 @@ +import pickle + +from docutils import nodes +from sphinx import addnodes as sphinx_nodes + + +def test_basics(make_app, rootdir): + srcdir = rootdir / 'basics' + app = make_app('xml', srcdir=srcdir) + app.build() + + # TODO: rather than using the pickled doctree, we should decode the XML + content = pickle.loads((app.doctreedir / 'index.doctree').read_bytes()) + + # doc has format like so: + # + # document: + # section: + # title: + # section: + # title: + # paragraph: + # literal_block: + # rubric: + # index: + # desc: + # desc_signature: + # desc_signature: + # index: + # desc: + # desc_signature: + # desc_signature: + + section = content[0][1] + assert isinstance(section, nodes.section) + + assert isinstance(section[0], nodes.title) + assert section[0].astext() == 'greet' + assert isinstance(section[1], nodes.paragraph) + assert section[1].astext() == 'A sample command group.' + assert isinstance(section[2], nodes.literal_block) + + assert isinstance(section[3], nodes.rubric) + assert section[3].astext() == 'Commands' + assert isinstance(section[4], sphinx_nodes.index) + assert isinstance(section[5], sphinx_nodes.desc) + assert isinstance(section[6], sphinx_nodes.index) + assert isinstance(section[7], sphinx_nodes.desc) + + +def test_nested_full(make_app, rootdir): + srcdir = rootdir / 'nested-full' + app = make_app('xml', srcdir=srcdir) + app.build() + + # TODO: rather than using the pickled doctree, we should decode the XML + content = pickle.loads((app.doctreedir / 'index.doctree').read_bytes()) + + # doc has format like so: + # + # document: + # section: + # title: + # section: + # title: + # paragraph: + # literal_block: + # section: + # title + # paragraph + # literal_block + # ... + # section: + # title + # paragraph + # literal_block + + section = content[0][1] + assert isinstance(section, nodes.section) + + assert isinstance(section[0], nodes.title) + assert section[0].astext() == 'greet' + assert isinstance(section[1], nodes.paragraph) + assert section[1].astext() == 'A sample command group.' + assert isinstance(section[2], nodes.literal_block) + + subsection_a = section[3] + assert isinstance(subsection_a, nodes.section) + + assert isinstance(subsection_a[0], nodes.title) + assert subsection_a[0].astext() == 'hello' + assert isinstance(subsection_a[1], nodes.paragraph) + assert subsection_a[1].astext() == 'Greet a user.' + assert isinstance(subsection_a[2], nodes.literal_block) + # we don't need to verify the rest of this: that's done elsewhere + + subsection_b = section[4] + assert isinstance(subsection_b, nodes.section) + + assert isinstance(subsection_b[0], nodes.title) + assert subsection_b[0].astext() == 'world' + assert isinstance(subsection_b[1], nodes.paragraph) + assert subsection_b[1].astext() == 'Greet the world.' + assert isinstance(subsection_b[2], nodes.literal_block) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 099027f..5d9fbe2 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -213,19 +213,19 @@ def foobar(bar): .. option:: --num-param - :default: 42 + :default: ``42`` .. option:: --param - :default: Something computed at runtime + :default: ``Something computed at runtime`` .. option:: --group - :default: ('foo', 'bar') + :default: ``('foo', 'bar')`` .. option:: --only-show-default - :default: Some default computed at runtime! + :default: ``Some default computed at runtime!`` """ ).lstrip(), '\n'.join(output), @@ -348,7 +348,7 @@ def foobar(): .. option:: --param - :default: Something computed at runtime + :default: ``Something computed at runtime`` A sample epilog. """ diff --git a/tox.ini b/tox.ini index 820a53c..4e743e2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,21 @@ [tox] minversion = 2.0 -envlist = py{37,38,39}-click{7,8,8-async},py{310}-click{8,8-async},style,docs +envlist = py{37,38,39}-click{7,8,8-async},py{310,311}-click{8,8-async},style,docs [testenv] +setenv = + PYTHONDEVMODE = 1 + PYTHONWARNINGS = all + PYTEST_ADDOPTS = {env:PYTEST_ADDOPTS:} --color yes deps = + pytest + pytest-cov coverage click7: click>=7.0,<8.0 click8: click>=8.0,<9.0 click8-async: asyncclick>=8.0,<9.0 commands = - coverage run --source={toxinidir}/sphinx_click -m unittest {posargs} - coverage report + python -m pytest --cov {toxinidir}/sphinx_click {posargs} pip_pre = pre: true