From 8bae12d15013cf2d2b4119568a36ab55c6f01b1a Mon Sep 17 00:00:00 2001 From: Tiexin Guo Date: Thu, 28 Mar 2024 12:21:57 +0800 Subject: [PATCH] docs: sphinx customisation (#1165) Customisation for sphinx starter pack. --- CHANGES.md | 1 + HACKING.md | 27 ++++++++ docs/conf.py | 7 +- docs/custom_conf.py | 150 +++++++++++++++++++++++++++++++++++++++--- docs/requirements.txt | 96 ++++++++++++++++++++++----- pyproject.toml | 21 +++--- 6 files changed, 258 insertions(+), 44 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 49b17a746..a2133f09f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ * Use "integrate with" rather than "relate to" (#1145) * Updated code examples in the docstring of `ops.testing` from unittest to pytest style (#1157) +* Update Read the Docs Sphinx Furo theme to use Canonical's latest styling (#1163, #1164, #1165) # 2.11.0 - 29 Feb 2024 diff --git a/HACKING.md b/HACKING.md index 2ee4e5610..9bc7e9829 100644 --- a/HACKING.md +++ b/HACKING.md @@ -198,6 +198,33 @@ tox -e docs open docs/_build/html/index.html ``` +## How to Pull in Style Changes + +The documentation uses Canonical styling which is customised on top of the [Furo Sphinx theme](https://github.com/pradyunsg/furo). The easiest way to pull in Canonical style changes is by using the Canonical documentation starter pack, see [docs](https://canonical-starter-pack.readthedocs-hosted.com/) and [repository](https://github.com/canonical/sphinx-docs-starter-pack). + +TL;DR: + +- Clone the starter pack repository to a local directory: `git clone git@github.com:canonical/sphinx-docs-starter-pack`. +- Copy the folder `.sphinx` under the starter pack repo to the operator repo `docs/.sphinx`. + +## How to Customise Configurations + +There are two configuration files: [`docs/conf.py`](./docs/conf.py) and [`docs/custom_conf.py`](./docs/custom_conf.py), copied and customised from the starter pack repo. + +To customise, change the file [`docs/custom_conf.py`](./docs/custom_conf.py) only, and theoretically, we should not change [`docs/conf.py`](./docs/conf.py) (however, some changes are made to [`docs/conf.py`](./docs/conf.py), such as adding autodoc, PATH, fixing issues, etc.) + +## How to Pull in Dependency Changes + +The Canonical documentation starter pack uses Make to build the documentation, which will run the script [`docs/.sphinx/build_requirements.py`](./docs/.sphinx/build_requirements.py) and generate a requirement file `requirements.txt` under `docs/.sphinx/`. + +To pull in new dependency changes from the starter pack, change to the starter pack repository directory, and build with the following command. This will create a virtual environment, generate a dependency file, install the software dependencies, and build the documentation: + +```bash +make html +``` + +Then, compare the generated file `.sphinx/requirements.txt`and the `project.optional-dependencies.docs` section of [`pyproject.toml`](./pyproject.toml) and adjust the `pyproject.toml` file accordingly. + # Dependencies The Python dependencies of `ops` are kept as minimal as possible, to avoid diff --git a/docs/conf.py b/docs/conf.py index fd4e82fe2..e0cceed92 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,4 @@ +# ruff: noqa import sys import os @@ -98,12 +99,6 @@ ] exclude_patterns.extend(custom_excludes) -rst_epilog = ''' -.. include:: /reuse/links.txt -''' -if 'custom_rst_epilog' in locals(): - rst_epilog = custom_rst_epilog - source_suffix = { '.rst': 'restructuredtext', '.md': 'markdown', diff --git a/docs/custom_conf.py b/docs/custom_conf.py index 773efba7e..1a9a68c66 100644 --- a/docs/custom_conf.py +++ b/docs/custom_conf.py @@ -1,4 +1,40 @@ -import datetime +# ruff: noqa +import datetime +import pathlib +import sys + +import furo +import furo.navigation + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) + +# Furo patch to get local TOC to show in sidebar (as sphinx-rtd-theme did) +# See https://github.com/pradyunsg/furo/blob/490527b2aef00b1198770c3389a1979911ee1fcb/src/furo/__init__.py#L115-L128 + +_old_compute_navigation_tree = furo._compute_navigation_tree + + +def _compute_navigation_tree(context): + tree_html = _old_compute_navigation_tree(context) + if not tree_html and context.get("toc"): + tree_html = furo.navigation.get_navigation_tree(context["toc"]) + return tree_html + + +furo._compute_navigation_tree = _compute_navigation_tree + +# Pull in fix from https://github.com/sphinx-doc/sphinx/pull/11222/files to fix +# "invalid signature for autoattribute ('ops.pebble::ServiceDict.backoff-delay')" +import re # noqa: E402 +import sphinx.ext.autodoc # noqa: E402 +sphinx.ext.autodoc.py_ext_sig_re = re.compile( + r'''^ ([\w.]+::)? # explicit module name + ([\w.]+\.)? # module and/or class name(s) + ([^.()]+) \s* # thing name + (?: \((.*)\) # optional: arguments + (?:\s* -> \s* (.*))? # return annotation + )? $ # and nothing more + ''', re.VERBOSE) # Custom configuration for the Sphinx documentation builder. # All configuration specific to your project should be done in this file. @@ -19,8 +55,8 @@ ############################################################ # Product name -project = 'Documentation starter pack' -author = 'Canonical Group Ltd' +project = 'The ops library' +author = 'Canonical Ltd.' # The title you want to display for the documentation in the sidebar. # You might want to include a version number here. @@ -67,7 +103,7 @@ # For example: "ubuntu.com/lxd" or "microcloud.is" # If there is no product website, edit the header template to remove the # link (see the readme for instructions). - 'product_page': 'documentation.ubuntu.com', + 'product_page': 'juju.is/docs/sdk', # Add your product tag (the orange part of your logo, will be used in the # header) to ".sphinx/_static" and change the path here (start with "_static") @@ -77,21 +113,25 @@ # Change to the discourse instance you want to be able to link to # using the :discourse: metadata at the top of a file # (use an empty value if you don't want to link) - 'discourse': 'https://discourse.ubuntu.com', + 'discourse': 'https://discourse.charmhub.io/', # Change to the Mattermost channel you want to link to # (use an empty value if you don't want to link) - 'mattermost': 'https://chat.canonical.com/canonical/channels/documentation', + 'mattermost': '', + + # Change to the Matrix channel you want to link to + # (use an empty value if you don't want to link) + 'matrix': 'https://matrix.to/#/#charmhub-charmdev:ubuntu.com', # Change to the GitHub URL for your project - 'github_url': 'https://github.com/canonical/starter-pack', + 'github_url': 'https://github.com/canonical/operator', # Change to the branch for this version of the documentation 'github_version': 'main', # Change to the folder that contains the documentation # (usually "/" or "/docs/") - 'github_folder': '/', + 'github_folder': '/docs/', # Change to an empty value if your GitHub repo doesn't have issues enabled. # This will disable the feedback button and the issue link in the footer. @@ -151,7 +191,12 @@ 'canonical.related-links', 'canonical.custom-rst-roles', 'canonical.terminal-output', - 'notfound.extension' + 'notfound.extension', + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', + 'sphinx.ext.todo', + 'sphinx.ext.viewcode', ] # Add custom required Python modules that must be added to the @@ -200,3 +245,90 @@ .. role:: center :class: align-center ''' + + +# -- Options for sphinx.ext.todo --------------------------------------------- + +# If this is True, todo and todolist produce output, else they +# produce nothing. The default is False. +todo_include_todos = False + + +# -- Options for sphinx.ext.autodoc ------------------------------------------ + +# This value controls how to represents typehints. The setting takes the +# following values: +# 'signature' – Show typehints as its signature (default) +# 'description' – Show typehints as content of function or method +# 'none' – Do not show typehints +autodoc_typehints = 'signature' + +# This value selects what content will be inserted into the main body of an +# autoclass directive. The possible values are: +# 'class' - Only the class’ docstring is inserted. This is the +# default. You can still document __init__ as a separate method +# using automethod or the members option to autoclass. +# 'both' - Both the class’ and the __init__ method’s docstring are +# concatenated and inserted. +# 'init' - Only the __init__ method’s docstring is inserted. +autoclass_content = 'class' + +# This value selects if automatically documented members are sorted +# alphabetical (value 'alphabetical'), by member type (value +# 'groupwise') or by source order (value 'bysource'). The default is +# alphabetical. +autodoc_member_order = 'alphabetical' + +autodoc_default_options = { + 'members': None, # None here means "yes" + 'undoc-members': None, + 'show-inheritance': None, +} + +# -- Options for sphinx.ext.intersphinx -------------------------------------- + +# This config value contains the locations and names of other projects +# that should be linked to in this documentation. +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} + +# -- General configuration --------------------------------------------------- + +# If true, Sphinx will warn about all references where the target +# cannot be found. +nitpicky = True + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in “nitpicky mode”. Note that type should include the +# domain name if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +nitpick_ignore = [ + # Please keep this list sorted alphabetically. + ('py:class', '_ChangeDict'), + ('py:class', '_CheckInfoDict'), + ('py:class', '_FileInfoDict'), + ('py:class', '_NoticeDict'), + ('py:class', '_ProgressDict'), + ('py:class', '_Readable'), + ('py:class', '_RelationMetaDict'), + ('py:class', '_ResourceMetaDict'), + ('py:class', '_StorageMetaDict'), + ('py:class', '_TaskDict'), + ('py:class', '_TextOrBinaryIO'), + ('py:class', '_WarningDict'), + ('py:class', '_Writeable'), + ('py:class', 'ops.charm._ContainerBaseDict'), + ('py:class', 'ops.model._AddressDict'), + ('py:class', 'ops.model._ConfigOption'), + ('py:class', 'ops.model._ModelBackend'), + ('py:class', 'ops.model._ModelCache'), + ('py:class', 'ops.model._NetworkDict'), + ('py:class', 'ops.pebble._FileLikeIO'), + ('py:class', 'ops.pebble._IOSource'), + ('py:class', 'ops.pebble._ServiceInfoDict'), + ('py:class', 'ops.pebble._SystemInfoDict'), + ('py:class', 'ops.pebble._WebSocket'), + ('py:class', 'ops.storage.JujuStorage'), + ('py:class', 'ops.storage.SQLiteStorage'), + ('py:class', 'ops.testing.CharmType'), + ('py:obj', 'ops.testing.CharmType'), +] diff --git a/docs/requirements.txt b/docs/requirements.txt index a52d53a20..7004242e4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,70 +8,118 @@ alabaster==0.7.13 # via sphinx babel==2.14.0 # via sphinx -beautifulsoup4==4.12.2 +beautifulsoup4==4.12.3 # via # canonical-sphinx-extensions # furo -canonical-sphinx-extensions==0.0.18 - # via lxd-sphinx-extensions -certifi==2023.11.17 + # pyspelling +bracex==2.4 + # via wcmatch +canonical-sphinx-extensions==0.0.19 + # via ops (pyproject.toml) +certifi==2024.2.2 # via requests charset-normalizer==3.3.2 # via requests -docutils==0.18.1 +colorama==0.4.6 + # via sphinx-autobuild +docutils==0.19 # via # canonical-sphinx-extensions + # myst-parser # sphinx # sphinx-tabs -furo==2023.9.10 +furo==2024.1.29 # via ops (pyproject.toml) +html5lib==1.1 + # via pyspelling idna==3.6 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==7.0.1 - # via sphinx +importlib-metadata==7.1.0 + # via + # markdown + # sphinx jinja2==3.1.3 - # via sphinx -lxd-sphinx-extensions==0.0.16 + # via + # myst-parser + # sphinx +linkify-it-py==2.0.3 # via ops (pyproject.toml) -markupsafe==2.1.3 +livereload==2.6.3 + # via sphinx-autobuild +lxml==5.1.0 + # via pyspelling +markdown==3.6 + # via pyspelling +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # myst-parser +markupsafe==2.1.5 # via jinja2 -packaging==23.2 +mdit-py-plugins==0.4.0 + # via myst-parser +mdurl==0.1.2 + # via markdown-it-py +myst-parser==2.0.0 + # via ops (pyproject.toml) +packaging==24.0 # via sphinx pygments==2.17.2 # via # furo # sphinx # sphinx-tabs -pytz==2023.3.post1 +pyspelling==2.10 + # via ops (pyproject.toml) +pytz==2024.1 # via babel pyyaml==6.0.1 - # via ops (pyproject.toml) + # via + # myst-parser + # ops (pyproject.toml) + # pyspelling requests==2.31.0 # via # canonical-sphinx-extensions # sphinx +six==1.16.0 + # via + # html5lib + # livereload snowballstemmer==2.2.0 # via sphinx soupsieve==2.5 - # via beautifulsoup4 + # via + # beautifulsoup4 + # pyspelling sphinx==6.2.1 # via # canonical-sphinx-extensions # furo + # myst-parser # ops (pyproject.toml) + # sphinx-autobuild # sphinx-basic-ng # sphinx-copybutton # sphinx-design + # sphinx-notfound-page # sphinx-tabs + # sphinxcontrib-jquery + # sphinxext-opengraph +sphinx-autobuild==2021.3.14 + # via ops (pyproject.toml) sphinx-basic-ng==1.0.0b2 # via furo sphinx-copybutton==0.5.2 # via ops (pyproject.toml) sphinx-design==0.5.0 # via ops (pyproject.toml) -sphinx-tabs==3.4.4 +sphinx-notfound-page==1.0.0 + # via ops (pyproject.toml) +sphinx-tabs==3.4.5 # via ops (pyproject.toml) sphinxcontrib-applehelp==1.0.4 # via sphinx @@ -79,15 +127,27 @@ sphinxcontrib-devhelp==1.0.2 # via sphinx sphinxcontrib-htmlhelp==2.0.1 # via sphinx +sphinxcontrib-jquery==4.1 + # via ops (pyproject.toml) sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -urllib3==2.1.0 +sphinxext-opengraph==0.9.1 + # via ops (pyproject.toml) +tornado==6.4 + # via livereload +uc-micro-py==1.0.3 + # via linkify-it-py +urllib3==2.2.1 # via requests +wcmatch==8.5.1 + # via pyspelling +webencodings==0.5.1 + # via html5lib websocket-client==1.7.0 # via ops (pyproject.toml) -zipp==3.17.0 +zipp==3.18.1 # via importlib-metadata diff --git a/pyproject.toml b/pyproject.toml index e0083f020..6cf8c8ae8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,12 +23,19 @@ dynamic = ["version"] [project.optional-dependencies] docs = [ + "canonical-sphinx-extensions", + "furo", + "linkify-it-py", + "myst-parser", + "pyspelling", "sphinx==6.2.1", + "sphinx-autobuild", + "sphinx-copybutton", "sphinx-design", - "furo", + "sphinx-notfound-page", "sphinx-tabs", - "lxd-sphinx-extensions", - "sphinx-copybutton", + "sphinxcontrib-jquery", + "sphinxext-opengraph" ] [project.urls] @@ -159,14 +166,6 @@ ignore = [ # "Useless" expression. "B018" ] -"docs/conf.py" = [ - "CPY001", # Missing copyright notice at top of file - "D100", # Missing docstring in public module - "I001", # [*] Import block is un-sorted or un-formatted - "A001", # Variable `copyright` is shadowing a Python builtin - "RUF003", # Comment contains ambiguous {} - "RUF019", # [*] Unnecessary key check before dictionary access -] "ops/_private/timeconv.py" = [ "RUF001", # String contains ambiguous `µ` (MICRO SIGN). Did you mean `μ` (GREEK SMALL LETTER MU)? "RUF002", # Docstring contains ambiguous `µ` (MICRO SIGN). Did you mean `μ` (GREEK SMALL LETTER MU)?