From ec739084b1479ba1537a38a31af59572183ed7d3 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 24 Jun 2023 12:18:07 -0400 Subject: [PATCH] feat: update docs based on repo-review Signed-off-by: Henry Schreiner --- docs/pages/guides/docs.md | 306 ++++++++++++++++++ docs/pages/guides/index.md | 4 +- .../writing-docs.md => tutorials/docs.md} | 40 +-- docs/pages/tutorials/packaging.md | 2 +- .../.readthedocs.yml | 5 - {{cookiecutter.project_name}}/README.md | 2 + {{cookiecutter.project_name}}/docs/conf.py | 62 ++-- {{cookiecutter.project_name}}/docs/index.md | 17 + {{cookiecutter.project_name}}/docs/index.rst | 24 -- {{cookiecutter.project_name}}/noxfile.py | 34 +- {{cookiecutter.project_name}}/pyproject.toml | 5 +- ...ptools','pybind11'] %}setup.cfg{% endif %} | 1 + 12 files changed, 397 insertions(+), 105 deletions(-) create mode 100644 docs/pages/guides/docs.md rename docs/pages/{guides/writing-docs.md => tutorials/docs.md} (82%) create mode 100644 {{cookiecutter.project_name}}/docs/index.md delete mode 100644 {{cookiecutter.project_name}}/docs/index.rst diff --git a/docs/pages/guides/docs.md b/docs/pages/guides/docs.md new file mode 100644 index 00000000..06b280d2 --- /dev/null +++ b/docs/pages/guides/docs.md @@ -0,0 +1,306 @@ +--- +layout: page +title: Writing documentation +permalink: /guides/docs/ +nav_order: 4 +parent: Topical Guides +--- + +{% include toc.html %} + +# Documentation + +Documentation used to require learning RestructureText (sometimes referred to as +ReST / RST), but today we have great choices for documentation in markdown, the +same format used by GitHub, Wikipedia, and others. There are two two major +documentation toolchains, sphinx and mkdocs. This guide covers Sphinx, and uses +the modern MyST plugin to get markdown support. + +## What to include + +Ideally, software documentation should include: + +- Introductory **tutorials**, to help new users (or potential users) understand + what the software can do and take their first steps. +- Task-oriented **guides**, examples that address specific uses. +- **Reference**, specifying the detailed inputs and outputs of every public + object in the codebase. +- **Explanations** to convey deeper understanding of why and how the software + operates the way it does. + +This overall framework has a name, [Diátaxis][], and you can read more about it +if you are interested. + +## Hand-written docs + +Create `docs/` directory within your project (i.e. next to `src/`). There is a +sphinx-quickstart tool, but unnecessary files (make/bat, we recommend a +cross-platform noxfile instead), and uses rst instead of markdown. Instead, this +is our recommended starting point for `conf.py`: + +### conf.py + +```python +from __future__ import annotations + +import importlib.metadata + +project = "" +copyright = "2023, Your Name" +author = "Your Name" +version = release = importlib.metadata.version("") + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", + "sphinx_copybutton", +] + +source_suffix = [".md", ".rst"] +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + "**.ipynb_checkpoints", + ".env", + ".venv", +] + +html_theme = "furo" + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} + +myst_enable_extensions = [ + "colon_fence", +] + +nitpick_ignore = [ + ("py:class", "_io.StringIO"), + ("py:class", "_io.BytesIO"), +] + +always_document_param_types = True +``` + +We start by setting some configuration values, but most notably we are getting +the package version from the installed version of your package. We are listing +several good extensions: + +- [`myst_parser`][myst] is the markdown parsing engine for sphinx. +- `sphinx.ext.autodoc` will help us build API docs via Restructured Text and + dynamic analysis. Also see the package [sphinx-autodoc2][], which supports + markdown and uses static analysis; it might not be as battle tested at this + time, though. +- `sphinx.ext.intersphinx` will cross-link to other documentation. +- `sphinx.ext.mathjax` allows you to include mathematical formulas. +- [`sphinx.ext.napoleon`][] adds support for several common documentation styles + like numpydoc. +- `sphinx_autodoc_typehints` handles type hints +- `sphinx_copybutton` adds a handle little copy button to code snipits. + +We are including both possible file extensions. We are also avoiding some common +file patterns, just in case. + +For theme, you have several good options. The clean, light-weight `furo` theme +is shown above; a related theme from the same author is being used for the +CPython documentation in CPython 3.13. Many scientific packages choose the +`sphinx-py-data` theme, which is also a good choice (no dark mode, though). + +We are enabling a useful MyST extension: `colon_fence` allows you to use three +colons for directives, which might be highlighted better if the directive +contains text than three backticks. See more built-in extensions in +[MyST's docs](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html). + +One key feature of Sphinx is interphinx, which allows documentation to +cross-reference each other. You can list other projects you are using, but a +good minimum is to at least link to the CPython docs. You need to provide the +path to the `objects.inv` file, usually at the main documentation URL. + +We are going to be enabling nitpick mode, and when we do, there's a chance some +classes will complain if they don't link with intersphinx. A couple of common +examples are listed here (StringIO/BytesIO don't point at the right thing) - +feel free to add/remove as needed. + +Finally, when we have static types, we'll want them always listed in the +docstrings, even if the parameter isn't documented yet. Feel free to check +[sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) +for more options. + +## index.md + +Your `index.md` file can start out like this: + +````md +# Project name + +```{toctree} +:maxdepth: 2 +:hidden: + +``` + +```{include} ../README.md +:start-after: +``` + +## Indices and tables + +- {ref}`genindex` +- {ref}`modindex` +- {ref}`search` +```` + +You can put your project name in as the title. The `toctree` directive houses +your table of contents; you'll list each new page you add inside that directive. + +If you want to inject a readme, you can use the `include` directive shown above. +You don't want to add the README's title (and probably your badges) to your +docs, so you can add a expression to your README (`` above) +to mark where you want the docs portion to start. + +You can add the standard indices and tables at the end. + +### pyproject.toml additions + +Setting a `docs` extra looks like this: + +```toml +[project.optional-dependencies] +docs = [ + "furo", + "myst_parser >=0.13", + "sphinx >=4.0", + "sphinx-copybutton", + "sphinx-autodoc-typehints", +] +``` + +While there are other ways to specify docs, and you don't have to make the docs +requirements an extra, this is a good idea as it forces docs building to always +install the project, rather than being tempted to install only sphinx and +plugins and try to build against an uninstalled version of your project. + +### .readthedocs.yaml + +In order to use to build, host, and preview your +documentation, you must have a `.reathedocs.yml` file like this: + +```yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + extra_requirements: + - docs +``` + +This sets the readthedocs config version (2 is required). + +The `build` table is the modern way to specify a runner. You need an `os` (a +modern ubuntu should be fine), a `tools` table (we'll use python, several +languages are supported here). + +Adding a `sphinx` table tells readthedocs to enable Sphinx integration. MkDocs +is supported too. + +Finally, we have a `python` table with an `install` key to describe how to +install our project. This will enable our "docs" extra. + +### noxfile.py additions + +Add a session to your `noxfile.py` to generate docs: + +```python +@nox.session(reuse_venv=True) +def docs(session: nox.Session) -> None: + """ + Build the docs. Pass "--serve" to serve. + """ + + parser = argparse.ArgumentParser() + parser.add_argument("--serve", action="store_true", help="Serve after building") + args, posargs = parser.parse_known_args(session.posargs) + + session.install(".[docs]") + session.chdir("docs") + + session.run( + "sphinx-build", + "-b", + "html", + ".", + f"_build/html", + *posargs, + ) + + if args.serve: + session.log("Launching docs at http://localhost:8000/ - use Ctrl-C to quit") + session.run("python", "-m", "http.server", "8000", "-d", "_build/html") +``` + +## API docs + +To build API docs, you need to add the following nox job: + +### noxfile.py additions + +```python +@nox.session +def build_api_docs(session: nox.Session) -> None: + """ + Build (regenerate) API docs. + """ + session.install("sphinx") + session.chdir("docs") + session.run( + "sphinx-apidoc", + "-o", + "api/", + "--module-first", + "--no-toc", + "--force", + "../src/", + ) +``` + +And you'll need this added to your `docs/index.md`: + +````md +```{toctree} +:maxdepth: 2 +:hidden: +:caption: API + +api/ +``` +```` + +Note that your docstrings are still parsed as Restructured Text. + + +[diátaxis]: https://diataxis.fr/ +[sphinx]: https://www.sphinx-doc.org/ +[myst]: https://myst-parser.readthedocs.io/ +[organizing content]: https://myst-parser.readthedocs.io/en/latest/syntax/organising_content.html +[sphinx-autodoc2]: https://sphinx-autodoc2.readthedocs.io/ +[`sphinx.ext.napoleon`]: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html + diff --git a/docs/pages/guides/index.md b/docs/pages/guides/index.md index 73693535..99c58196 100644 --- a/docs/pages/guides/index.md +++ b/docs/pages/guides/index.md @@ -22,7 +22,8 @@ a consistent developer and user experience when working with distribution. A section on CI follows, with a [general setup guide][gha_basic], and then two choices for using CI to distribute your package, on for [pure Python][gha_pure], and one for [compiled extensions][gha_wheels]. You can read about setting up -good tests on the [pytest page][pytest]. +good tests on the [pytest page][pytest]. There's also a page on setting up +[docs][], as well. Once you have completed the guidelines, there is a [copier][]/[cookiecutter][] project, [scientific-python/cookie][], that implements these guidelines and lets @@ -36,6 +37,7 @@ You can also evaluate your repository against the guidelines by using [tutorials]: {% link pages/tutorials/index.md %} [style]: {% link pages/guides/style.md %} [mypy]: {% link pages/guides/mypy.md %} +[docs]: {% link pages/guides/docs.md %} [simple packaging]: {% link pages/guides/packaging_simple.md %} [classic packaging]: {% link pages/guides/packaging_classic.md %} [gha_basic]: {% link pages/guides/gha_basic.md %} diff --git a/docs/pages/guides/writing-docs.md b/docs/pages/tutorials/docs.md similarity index 82% rename from docs/pages/guides/writing-docs.md rename to docs/pages/tutorials/docs.md index e3b116b5..ef1cd1d3 100644 --- a/docs/pages/guides/writing-docs.md +++ b/docs/pages/tutorials/docs.md @@ -1,30 +1,15 @@ --- layout: page title: Writing documentation -permalink: /guides/writing-docs/ -nav_order: 4 -parent: Topical Guides +permalink: /tutorials/docs/ +nav_order: 5 +parent: Tutorials --- {% include toc.html %} # Writing Documentation -## What to include - -Ideally, software documentation should include: - -- Introductory **tutorials**, to help new users (or potential users) understand - what the software can do and take their first steps -- Task-oriented **guides**, examples that address specific uses -- **Reference**, specifying the detailed inputs and outputs of every public - object in the codebase -- **Explanations** to convey deeper understanding of why and how the software - operates the way it does - -This overall framework has a name, [Diátaxis][], and you can read more about it -if you are interested. - ## Build the documentation Scientific Python software documentation can be written in the Markdown syntax, @@ -70,6 +55,7 @@ In this directory, we will create a minimal Sphinx configuration file at project = "example" extensions = ["myst_parser"] +source_suffix = [".rst", ".md"] ``` And, at `docs/index.md`, we will create a minimal front page for our @@ -168,29 +154,22 @@ or drifting out of sync over time. MyST recommends using [sphinx-autodoc2][]. However, we currently recommend using the built-in sphinx `autodoc` and `autosummary` extensions because they interoperates well with docstrings written to the numpydoc standard. To invoke -them, we need to employ yet another syntax (reST). F:ortunately, you can simply +them, we need to employ yet another syntax (reST). Fortunately, you can simply copy/paste these examples. -Install `numpydoc`. - -```bash -pip install numpydoc -``` - In `docs/conf.py`, add to the list of extensions. ```py extensions = [ # whatever you already have in here... - - "numpydoc", "sphinx.ext.autodoc", "sphinx.ext.autosummary", + "sphinx.ext.napoleon", ] ``` -(Note that `sphinx.ext.autodoc` and `sphinx.ext.autosummary` come installed with -`sphinx`, so there is nothing more to install.) +(Note that these extensions come with `sphinx`, so there is nothing more to +install.) You can document a single object (e.g. function), shown inline on the page @@ -214,6 +193,9 @@ Or you can generate a table that links out to documentation for each object. ``` ```` +See the [guide]({% link pages/guides/docs.md %}) for more information on how to +integrate this into a package, and setup for nox. + [diátaxis]: https://diataxis.fr/ [sphinx]: https://www.sphinx-doc.org/ diff --git a/docs/pages/tutorials/packaging.md b/docs/pages/tutorials/packaging.md index 4db44f24..b274c0ae 100644 --- a/docs/pages/tutorials/packaging.md +++ b/docs/pages/tutorials/packaging.md @@ -1,6 +1,6 @@ --- layout: page -title: Package +title: Packaging permalink: /tutorials/packaging/ nav_order: 3 parent: Tutorials diff --git a/{{cookiecutter.project_name}}/.readthedocs.yml b/{{cookiecutter.project_name}}/.readthedocs.yml index 5a136f01..7e496574 100644 --- a/{{cookiecutter.project_name}}/.readthedocs.yml +++ b/{{cookiecutter.project_name}}/.readthedocs.yml @@ -1,17 +1,12 @@ -# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -# Required version: 2 -# Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" - -# Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py diff --git a/{{cookiecutter.project_name}}/README.md b/{{cookiecutter.project_name}}/README.md index 93a080b2..08301f7c 100644 --- a/{{cookiecutter.project_name}}/README.md +++ b/{{cookiecutter.project_name}}/README.md @@ -13,6 +13,8 @@ [![Scikit-HEP][sk-badge]](https://scikit-hep.org/) {%- endif %} + + [actions-badge]: {{cookiecutter.url}}/workflows/CI/badge.svg [actions-link]: {{cookiecutter.url}}/actions diff --git a/{{cookiecutter.project_name}}/docs/conf.py b/{{cookiecutter.project_name}}/docs/conf.py index 5bcf0cbe..3f65adf2 100644 --- a/{{cookiecutter.project_name}}/docs/conf.py +++ b/{{cookiecutter.project_name}}/docs/conf.py @@ -1,61 +1,45 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - from __future__ import annotations -# Warning: do not change the path here. To use autodoc, you need to install the -# package first. - -# -- Project information ----------------------------------------------------- +import importlib.metadata project = "{{ cookiecutter.project_name }}" copyright = "{{ cookiecutter.__year }}, {{ cookiecutter.full_name }}" author = "{{ cookiecutter.full_name }}" +version = release = importlib.metadata.version("{{ cookiecutter.__project_slug }}") - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = [ "myst_parser", "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", "sphinx.ext.mathjax", "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", "sphinx_copybutton", ] -# Add any paths that contain templates here, relative to this directory. -templates_path = [] - -# Include both markdown and rst files source_suffix = [".rst", ".md"] +exclude_patterns = [ + "_build", + "**.ipynb_checkpoints", + "Thumbs.db", + ".DS_Store", + ".env", + ".venv", +] -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "**.ipynb_checkpoints", "Thumbs.db", ".DS_Store", ".env"] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = "furo" -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path: list[str] = [] - - -# -- Extension configuration ------------------------------------------------- myst_enable_extensions = [ "colon_fence", - "deflist", ] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} + +nitpick_ignore = [ + ("py:class", "_io.StringIO"), + ("py:class", "_io.BytesIO"), +] + +always_document_param_types = True diff --git a/{{cookiecutter.project_name}}/docs/index.md b/{{cookiecutter.project_name}}/docs/index.md new file mode 100644 index 00000000..db7007b7 --- /dev/null +++ b/{{cookiecutter.project_name}}/docs/index.md @@ -0,0 +1,17 @@ +# {{ cookiecutter.project_name }} + +```{toctree} +:maxdepth: 2 +:hidden: + +``` + +```{include} ../README.md +:start-after: +``` + +## Indices and tables + +- {ref}`genindex` +- {ref}`modindex` +- {ref}`search` diff --git a/{{cookiecutter.project_name}}/docs/index.rst b/{{cookiecutter.project_name}}/docs/index.rst deleted file mode 100644 index d9634ecd..00000000 --- a/{{cookiecutter.project_name}}/docs/index.rst +++ /dev/null @@ -1,24 +0,0 @@ - -Welcome to the documentation! -============================= - - -Introduction ------------- - -This should be updated! - -.. toctree:: - :maxdepth: 2 - :titlesonly: - :caption: Contents - :glob: - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/{{cookiecutter.project_name}}/noxfile.py b/{{cookiecutter.project_name}}/noxfile.py index 56bd199d..280ec408 100644 --- a/{{cookiecutter.project_name}}/noxfile.py +++ b/{{cookiecutter.project_name}}/noxfile.py @@ -53,14 +53,38 @@ def docs(session: nox.Session) -> None: parser = argparse.ArgumentParser() parser.add_argument("--serve", action="store_true", help="Serve after building") - args = parser.parse_args(session.posargs) + parser.add_argument( + "-b", dest="builder", default="html", help="Build target (default: html)" + ) + args, posargs = parser.parse_known_args(session.posargs) + + if args.builder != "html" and args.serve: + session.error("Must not specify non-HTML builder with --serve") session.install(".[docs]") session.chdir("docs") - session.run("sphinx-build", "-M", "html", ".", "_build") + + if args.builder == "linkcheck": + session.run( + "sphinx-build", "-b", "linkcheck", ".", "_build/linkcheck", *posargs + ) + return + + session.run( + "sphinx-build", + "-n", # nitpicky mode + "-T", # full tracebacks + "-W", # Warnings as errors + "--keep-going", # See all errors + "-b", + args.builder, + ".", + f"_build/{args.builder}", + *posargs, + ) if args.serve: - print("Launching docs at http://localhost:8000/ - use Ctrl-C to quit") + session.log("Launching docs at http://localhost:8000/ - use Ctrl-C to quit") session.run("python", "-m", "http.server", "8000", "-d", "_build/html") @@ -76,10 +100,10 @@ def build_api_docs(session: nox.Session) -> None: "sphinx-apidoc", "-o", "api/", + "--module-first", "--no-toc", "--force", - "--module-first", - "../src/{{ cookiecutter.project_name.replace('-', '_') }}", + "../src/{{ cookiecutter.__project_slug }}", ) diff --git a/{{cookiecutter.project_name}}/pyproject.toml b/{{cookiecutter.project_name}}/pyproject.toml index f0b1f664..400357c1 100644 --- a/{{cookiecutter.project_name}}/pyproject.toml +++ b/{{cookiecutter.project_name}}/pyproject.toml @@ -99,6 +99,7 @@ pytest = { version = ">=6", optional = true } pytest-cov = { version = ">=3", optional = true } sphinx = { version = ">=4.0", optional = true } sphinx_copybutton = { version = ">=0.3.0", optional = true } +sphinx-autodoc-typehints = { version = "*", optional = true } [tool.poetry.dev-dependencies] pytest = ">= 6" @@ -111,6 +112,7 @@ docs = [ "furo", "myst_parser", "sphinx", + "sphinx_autodoc_typehints", "sphinx_copybutton", ] @@ -176,8 +178,9 @@ dev = [ docs = [ "sphinx>=4.0", "myst_parser>=0.13", - "sphinx-book-theme>=0.1.0", + "sphinx_book_theme>=0.1.0", "sphinx_copybutton", + "sphinx_autodoc_typehints", "furo", ] diff --git a/{{cookiecutter.project_name}}/{% if cookiecutter.backend in ['setuptools','pybind11'] %}setup.cfg{% endif %} b/{{cookiecutter.project_name}}/{% if cookiecutter.backend in ['setuptools','pybind11'] %}setup.cfg{% endif %} index fa5b2575..ca6bdccd 100644 --- a/{{cookiecutter.project_name}}/{% if cookiecutter.backend in ['setuptools','pybind11'] %}setup.cfg{% endif %} +++ b/{{cookiecutter.project_name}}/{% if cookiecutter.backend in ['setuptools','pybind11'] %}setup.cfg{% endif %} @@ -62,6 +62,7 @@ docs = furo>=22 myst-parser>=0.13 sphinx>=4.0 + sphinx-autodoc-typehints sphinx-copybutton test = pytest>=6