📖 sphinx#

Sphinx documentation generation utilities.

This subpackage provides a comprehensive set of utilities for automatically generating and managing Sphinx documentation for Python projects. It handles the creation of configuration files, index pages, version management, and HTML generation.

The work is split by concern: forges knows where source files live on the web, theming decides how code is coloured, conf_file writes the generated conf.py, index_page builds the landing page, module_pages the API reference, and builder runs the whole thing.

Example of usage:

import stouputils as stp
from stouputils.applications import automatic_docs

if __name__ == "__main__":
    automatic_docs.sphinx_docs(
        root_path=stp.get_root_path(__file__, go_up=1),
        project="stouputils",
        author="Stoupy",
        copyright="2025, Stoupy",
        html_logo="https://avatars.githubusercontent.com/u/35665974",
        html_favicon="https://avatars.githubusercontent.com/u/35665974",
        html_theme="breeze",    # Available themes: breeze, furo, pydata_sphinx_theme, sphinx_rtd_theme, or other you installed
        github_user="Stoupy51",
        github_repo="stouputils",
        version="1.2.0",
        skip_undocumented=True,
    )

builder#

Orchestration of a documentation build: lay out the folders, write the generated files, then run Sphinx.

sphinx_docs is the only entry point most projects ever call. Every step it performs is a parameter, so a project needing a different landing page or a different build command replaces that one callable instead of forking the whole routine.

generate_documentation(
source_dir: str,
modules_dir: str,
project_dir: str,
build_dir: str,
) → None[source]#

Generate documentation using Sphinx.

Parameters:
  • source_dir – Source directory

  • modules_dir – Modules directory

  • project_dir – Project directory

  • build_dir – Build directory

write_version_switcher(
path: str,
site_url: str,
versions: list[str],
) → None[source]#

Write the switcher.json breeze fetches to fill its version dropdown.

Parameters:
  • site_url – Absolute URL of the folder holding one subfolder per version, ending with a slash

  • versions – Versions without their “v” prefix, “latest” included

sphinx_docs(root_path: str, project: str, project_dir: str = '', author: str = 'Author', copyright: str = '2025, Author', html_logo: str = '', html_favicon: str = '', html_theme: str = 'breeze', github_user: str = '', github_repo: str = '', repo_url: str = '', repo_provider: str = 'github', repo_branch: str = 'main', edit_link_path: str = '', pygments_light_style: str = 'vscode-light-plus', pygments_dark_style: str = 'vscode-dark-plus', default_mode: str = 'dark', autodoc_mock_imports: list[str] | None = None, version: str | None = None, skip_undocumented: bool = True, recent_minor_versions: int = 2, external_links: list[str] | None = None, get_versions_function: ~collections.abc.Callable[[str, str, int], list[str]] = <functools._lru_cache_wrapper object>, generate_index_function: ~collections.abc.Callable[[...], None] = <function generate_index_md>, generate_docs_function: ~collections.abc.Callable[[...], None] = <function generate_documentation>, generate_redirect_function: ~collections.abc.Callable[[str], None] = <function generate_redirect_html>, get_conf_content_function: ~collections.abc.Callable[[...], str] = <function get_sphinx_conf_content>) → None[source]#

Update the Sphinx documentation.

Parameters:
  • root_path – Root path of the project

  • project – Name of the project

  • project_dir – Path to the project directory (to be used with generate_docs_function)

  • author – Author of the project

  • copyright – Copyright information

  • html_logo – URL to the logo

  • html_favicon – URL to the favicon

  • html_theme – Theme to use for the documentation. Defaults to “breeze”

  • github_user – GitHub username

  • github_repo – GitHub repository name

  • repo_url – Repository URL used for source links, defaulting to the GitHub one built from the two above

  • repo_provider – Which key of FORGES describes the repository URL. Defaults to “github”

  • repo_branch – Branch the source links point at. Defaults to “main”

  • edit_link_path – Where the Sphinx sources are tracked, enabling the “edit this page” link, ex: “docs/source”

  • pygments_light_style – Pygments style used in light mode

  • pygments_dark_style – Pygments style used in dark mode

  • default_mode – Colour mode a first-time visitor gets: “auto”, “light” or “dark”

  • autodoc_mock_imports – Packages autodoc stubs out instead of importing, defaulting to none

  • version – Version to build documentation for (e.g. “1.0.0”, defaults to “latest”)

  • skip_undocumented – Whether to skip undocumented members. Defaults to True

  • recent_minor_versions – Number of recent minor versions to show all patches for. Defaults to 2

  • external_links – URLs shown as icons in the header before the repository one, ex: a Discord invite or a PyPI page

  • get_versions_function – Function to get versions from GitHub

  • generate_index_function – Function to generate index.md

  • generate_docs_function – Function to generate documentation

  • generate_redirect_function – Function to create redirect file

  • get_conf_content_function – Function to get Sphinx conf.py content

conf_file#

Generation of the docs/source/conf.py file Sphinx reads.

The file is produced as text rather than imported from a template, because a fair part of it is decided by the caller’s arguments: which forge hosts the sources, which theme renders them, and which pygments styles colour them.

python_literal(value: dict[str, Any]) → str[source]#

Render a mapping as a Python literal fit for the generated conf.py.

Parameters:

value – Mapping to render

Returns:

The literal, with JSON booleans translated back to Python ones

>>> python_literal({"a": True, "b": False})
'{\n\t"a": True,\n\t"b": False\n}\n'
get_sphinx_conf_content(
project: str,
project_dir: str,
author: str,
current_version: str,
copyright: str,
html_logo: str,
html_favicon: str,
html_theme: str = 'breeze',
github_user: str = '',
github_repo: str = '',
version_switcher_url: str = '',
skip_undocumented: bool = True,
repo_url: str = '',
repo_provider: str = 'github',
repo_branch: str = 'main',
source_prefix: str = '',
edit_link_path: str = '',
pygments_light_style: str = 'vscode-light-plus',
pygments_dark_style: str = 'vscode-dark-plus',
default_mode: str = 'dark',
autodoc_mock_imports: list[str] | None = None,
external_links: list[str] | None = None,
) → str[source]#

Get the content of the Sphinx configuration file.

Parameters:
  • project – Name of the project

  • project_dir – Path to the project directory

  • author – Author of the project

  • current_version – Current version

  • copyright – Copyright information

  • html_logo – URL to the logo

  • html_favicon – URL to the favicon

  • html_theme – Theme rendering the documentation. Defaults to “breeze”

  • github_user – GitHub username

  • github_repo – GitHub repository name

  • version_switcher_url – Absolute URL of the switcher.json listing every published version, empty for no switcher

  • skip_undocumented – Whether to skip undocumented members. Defaults to True

  • repo_url – Repository URL used for source links, ex: “https://gitlab.example.com/group/project”

  • repo_provider – Which key of FORGES describes the repository URL. Defaults to “github”

  • repo_branch – Branch the source links point at. Defaults to “main”

  • source_prefix – Path from the repository root to the importable package’s parent, ex: “src/”

  • edit_link_path – Where the Sphinx sources are tracked, enabling the “edit this page” link Leave it empty when those sources are generated, since editing them would be pointless.

  • pygments_light_style – Pygments style used in light mode

  • pygments_dark_style – Pygments style used in dark mode

  • default_mode – Colour mode a first-time visitor gets: “auto”, “light” or “dark”

  • autodoc_mock_imports – Packages autodoc replaces with a stub instead of importing Only name packages the documented code never calls at import time, since a mock answers every attribute with another mock, which turns an ordinary decorator or metaclass into a failed import.

  • external_links – URLs shown as icons in the header before the repository one, ex: a Discord invite or a PyPI page

Returns:

Content of the Sphinx configuration file

forges#

Code forge URL conventions, used to link a documented object back to its source.

Every forge agrees that a URL needs a repository, a branch and a path, and no two of them agree on the order. This module holds that knowledge in one table so the rest of the generator never has to care.

class ForgeUrls(edit: str, blob: str)[source]#

Bases: object

URL patterns of a code forge, built from a repository URL.

edit: str[source]#

Pattern for editing a file, with {repo}, {branch} and {path} placeholders.

blob: str[source]#

Pattern for viewing a file, with the same placeholders.

FORGES: dict[str, ForgeUrls] = {'bitbucket': ForgeUrls(edit='{repo}/src/{branch}/{path}?mode=edit', blob='{repo}/src/{branch}/{path}'), 'codeberg': ForgeUrls(edit='{repo}/_edit/{branch}/{path}', blob='{repo}/src/branch/{branch}/{path}'), 'github': ForgeUrls(edit='{repo}/edit/{branch}/{path}', blob='{repo}/blob/{branch}/{path}'), 'gitlab': ForgeUrls(edit='{repo}/-/edit/{branch}/{path}', blob='{repo}/-/blob/{branch}/{path}')}[source]#

Path conventions of each supported forge, since no two of them agree on where to put the branch.

get_source_url(
repo_url: str,
repo_provider: str,
repo_branch: str,
) → str[source]#

Build the linkcode template pointing at a module’s source file.

Parameters:
Returns:

URL with a remaining {filename} placeholder, empty when no repository is known

>>> get_source_url("https://github.com/Stoupy51/stouputils", "github", "main")
'https://github.com/Stoupy51/stouputils/blob/main/{filename}.py'
>>> get_source_url("", "github", "main")
''
get_edit_url(
repo_url: str,
repo_provider: str,
repo_branch: str,
edit_link_path: str,
) → str[source]#

Build the “edit this page” template the theme fills with the page path.

Parameters:
  • repo_url – Repository URL, ex: “https://github.com/Stoupy51/stouputils”

  • repo_provider – Which key of FORGES describes the repository URL

  • repo_branch – Branch the edit links point at

  • edit_link_path – Where the Sphinx sources are tracked, ex: “docs/source”

Returns:

URL ending in the theme’s %s placeholder, empty when either argument is missing

>>> get_edit_url("https://gitlab.com/g/p", "gitlab", "main", "docs/source")
'https://gitlab.com/g/p/-/edit/main/docs/source/%s'
>>> get_edit_url("https://gitlab.com/g/p", "gitlab", "main", "")
''

index_page#

Generation of the documentation landing page.

The default page is the README, with a hidden toctree making the API reference a tab of the header. Projects wanting something else pass their own callable as generate_index_function.

generate_index_md(
readme_path: str,
index_path: str,
project: str,
) → None[source]#

Generate index.md (MyST) from README.md content, kept as Markdown.

Parameters:
  • readme_path – Path to the README.md file

  • index_path – Path where index.md should be created

  • project – Name of the project, lowercased into the name of its root package

module_pages#

Generation of the API reference, one page per package.

A page opens with the package docstring, lists its subpackages, then gives each module of the package a section. When a subpackage’s __init__.py imports names from its modules, those names are its API and the only ones documented, which leaves out the modules it imports nothing from. The root package’s modules are imported by their own path, so they are all kept. An emoji opening the docstring of a package or module also opens its title, and breeze shows it in the navigation.

ROOT_TITLE: str = 'API reference'[source]#

Title of the root package’s page, which the header shows as a tab.

write_module_pages(
project_dir: str,
modules_dir: str,
) → None[source]#

Write one reStructuredText page per package of project_dir into modules_dir, named after its dotted path.

A folder holding Python files without an __init__.py counts as a namespace package. Packages and modules whose name starts with an underscore are left out.

is_public(relative_path: Path) → bool[source]#

Whether no part of a path under the project starts with an underscore or a dot.

>>> is_public(Path("typing/runtime.py")), is_public(Path("typing/__init__.py")), is_public(Path("__pycache__"))
(True, False, False)
titled(source: Path) → str[source]#

Name of a module or package, after the emoji opening its docstring if any.

Parameters:

source – The module’s file, or the __init__.py of a package, which may not exist for a namespace package

exported_names(init: Path) → dict[str, list[str]][source]#

Names an __init__.py re-exports from each module beside it, keyed by module name, empty when the file does not exist.

A re-export is an import x as x or a name listed in __all__, the convention type checkers follow. A plain import is the package using a helper of its own, which stays out of its API.

dunder_all(tree: Module) → set[str][source]#

Names a module lists in __all__.

>>> sorted(dunder_all(ast.parse("__all__ = ['b', 'a']")))
['a', 'b']
heading(title: str, underline: str) → list[str][source]#

Lines of a reStructuredText heading, underlined twice its length since an emoji takes two columns.

package_page(
package: Path,
dotted: str,
title: str,
subpackages: list[str],
exported: dict[str, list[str]],
) → str[source]#

reStructuredText of a package’s page.

Parameters:
  • dotted – Import path of the package, ex: “stouputils.typing”

  • subpackages – Import paths of the packages directly below it, which get pages of their own

  • exported – Names the package imports from each of its modules, documenting every module in full when none matches

drop_overloads() → None[source]#

Make autodoc show the signature of a function’s implementation instead of one line per @overload above it.

Autodoc reads the overloads from the module analyzer it caches, and only skips them along with every annotation of the site.

exact_builtin_references(
app: Any,
doctree: Any,
) → None[source]#

Handler for doctree-read keeping the builtin names of annotations, like type, from linking to project objects.

Sphinx looks up an annotation by suffix whenever its node carries refspecific, which it always does, even set to False. A builtin then resolves to every attribute of the project sharing its name, and links to the first one.

theming#

Theme, syntax highlighting and stylesheet concerns of the generated documentation.

Pygments’ Python lexer is coarse: it only tags an identifier as Name.Function or Name.Class when a literal def or class introduces it, and emits a bare Name for every call, attribute, argument and variable. A palette designed for a finer grammar therefore paints most of the code in whichever colour it gave Name. That is why the styles below are picked so that Name keeps the foreground colour, leaving hues for the tokens the lexer genuinely recognises.

DEFAULT_LIGHT_STYLE: str = 'vscode-light-plus'[source]#

Pygments style for light mode, transcribed from VS Code so a snippet matches the reader’s editor.

DEFAULT_DARK_STYLE: str = 'vscode-dark-plus'[source]#

Pygments style for dark mode.

The theme’s own default, github-dark-high-contrast, paints Name in #DBB7FF. Since a Python page emits about a hundred bare Name tokens for each def, that turns whole snippets violet.

CUSTOM_CSS: str = '\n/* Custom CSS for Sphinx documentation */\n/* Reduce heading sizes */\nh1 { font-size: 2.0em !important; }\nh2 { font-size: 1.6em !important; }\nh3 { font-size: 1.4em !important; }\nh4 { font-size: 1.2em !important; }\nh5 { font-size: 1.0em !important; }\nh6 { font-size: 0.9em !important; }\n\n/* Gradient animation keyframes */\n@keyframes shine-slide {\n\t0% { background-position: -200% center; }\n\t100% { background-position: 200% center; }\n}\n\n/* Adjustments to abmonition */\n.admonition {\n\ttext-decoration: none;\n\tpadding: 1rem;\n\tdisplay: block;\n}\n\n/* On hover animation for various elements */\na, h1, h2, h3, h4, h5, h6, .admonition {\n\ttransition: transform 0.3s;\n}\n\na:hover, h1:hover, h2:hover, h3:hover, h4:hover, h5:hover, h6:hover, .admonition:hover {\n\ttransform: scale(1.05);\n}\na:hover, a:hover span {\n\tbackground: linear-gradient(\n\t\t110deg,\n\t\tcurrentColor 0%,\n\t\tcurrentColor 40%,\n\t\twhite 50%,\n\t\tcurrentColor 60%,\n\t\tcurrentColor 100%\n\t);\n\tbackground-size: 200% 100%;\n\tbackground-clip: text;\n\t-webkit-background-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tanimation: shine-slide 3.5s linear infinite;\n}\n\n/* Code is indented with tabs, which browsers render eight columns wide by default */\npre, code, .highlight pre { tab-size: 4; }\n\n/* The page title reserves room under the page actions, so it must not cover them while its hover transform runs */\n.bz-page-actions { z-index: 2; }\n\n/* A doctest prompt is a marker, not code, so it should never end up in what a reader copies by hand */\n.highlight .gp { user-select: none; }\n'[source]#

Stylesheet written to _static/custom.css and loaded on top of the theme.

SIDEBAR_NAV: str = '{%- set content = toctree(level=1, maxdepth=-1)\n\tor toctree(level=0, maxdepth=-1) | replace("<details>", "<details open>", 1) -%}\n{%- if content -%}\n<nav class="bz-sidebar-nav" aria-label="{{ _(\'Section Navigation\') }}">\n  {%- if theme_emojis_sidebar_nav | tobool -%}{{ wrap_emoji(content) }}{%- else -%}{{ replace_emoji(content) }}{%- endif -%}\n</nav>\n{%- endif -%}\n'[source]#

Breeze’s sidebar-nav.html, falling back to the whole tree where breeze lists only the current header tab.

The landing page belongs to no tab, so breeze alone leaves its sidebar empty. Breeze only unfolds the branch holding the current page, so the fallback unfolds the first one itself. Both list every depth, where breeze stops at the :maxdepth: of the subpackage lists the pages show.

check_dependencies(html_theme: str) → None[source]#

Check that the requested theme, and every base requirement, is installed.

Parameters:

html_theme – HTML theme used by the documentation, ex: “breeze”, “pydata_sphinx_theme”, “furo”

Raises:

ImportError – If the theme or any base requirement is missing

get_theme_options(
html_theme: str,
default_mode: str,
external_links: list[str],
) → dict[str, str | bool | list[str]][source]#

Build the html_theme_options mapping, holding only keys the chosen theme understands.

default_mode reaches breeze through html_theme_options, and pydata through html_context. Passing it to a theme that knows neither only earns an “unsupported theme option” warning, so it is filtered here.

Parameters:
  • html_theme – HTML theme used by the documentation

  • default_mode – Colour mode a first-time visitor gets, one of “auto”, “light” or “dark”

  • external_links – URLs breeze shows as icons in the header and footer, named after their domain

Returns:

Options to write into the generated conf.py

>>> get_theme_options("breeze", "dark", ["https://github.com/Stoupy51/stouputils"])["external_links"]
['https://github.com/Stoupy51/stouputils']
>>> get_theme_options("furo", "dark", ["https://github.com/Stoupy51/stouputils"])
{'navigation_with_keys': True}
write_custom_css(static_dir: str) → None[source]#

Write CUSTOM_CSS into the static folder the generated conf.py points at.

Parameters:

static_dir – The docs/source/_static folder

write_sidebar_nav(templates_dir: str) → None[source]#

Write SIDEBAR_NAV into the templates folder, where it takes precedence over breeze’s own.

Parameters:

templates_dir – The docs/source/_templates folder