Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ Features added
Patch by Jean-François B.
* #13508: Initial support for :pep:`695` type aliases.
Patch by Martin Matouš, Jeremy Maitin-Shepard, and Adam Turner.
* apidoc: Allow configuring template directories via
:confval:`apidoc_template_dir` and the per-module ``template_dir`` option.
Patch by Chidera Okara.

Bugs fixed
----------
Expand Down
12 changes: 12 additions & 0 deletions doc/usage/extensions/apidoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ The apidoc extension uses the following configuration values:
:code-py:`'automodule_options'`
See :confval:`apidoc_automodule_options`.

:code-py:`'template_dir'`
A directory containing templates used when generating documentation files.
Paths are interpreted relative to the configuration directory when not
absolute. See :confval:`apidoc_template_dir`.

.. confval:: apidoc_exclude_patterns
:type: :code-py:`Sequence[str]`
:default: :code-py:`()`
Expand Down Expand Up @@ -170,3 +175,10 @@ The apidoc extension uses the following configuration values:
:default: :code-py:`{'members', 'show-inheritance', 'undoc-members'}`

Options to pass to generated :rst:dir:`automodule` directives.

.. confval:: apidoc_template_dir
:type: :code-py:`str`
:default: :code-py:`None`

A directory containing templates that override the built-in apidoc templates.
Paths are interpreted relative to the configuration directory.
5 changes: 5 additions & 0 deletions sphinx/ext/apidoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

from __future__ import annotations

from pathlib import Path
from types import NoneType
from typing import TYPE_CHECKING

import sphinx
Expand Down Expand Up @@ -54,6 +56,9 @@ def setup(app: Sphinx) -> ExtensionMetadata:
'env',
types=frozenset({frozenset, list, set, tuple}),
)
app.add_config_value(
'apidoc_template_dir', None, 'env', types=frozenset({NoneType, str, Path})
)
app.add_config_value('apidoc_modules', (), 'env', types=frozenset({list, tuple}))

# Entry point to run apidoc
Expand Down
3 changes: 2 additions & 1 deletion sphinx/ext/apidoc/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,5 @@ def _full_quickstart(opts: ApidocOptions, /, *, modules: list[str]) -> None:
d['extensions'].extend(ext.split(','))

if not opts.dry_run:
qs.generate(d, silent=True, overwrite=opts.force, templatedir=opts.template_dir)
templatedir = str(opts.template_dir) if opts.template_dir is not None else None
qs.generate(d, silent=True, overwrite=opts.force, templatedir=templatedir)
36 changes: 35 additions & 1 deletion sphinx/ext/apidoc/_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
'exclude_patterns',
'automodule_options',
'max_depth',
'template_dir',
})


Expand Down Expand Up @@ -169,7 +170,24 @@ def _parse_module_options(
)
]

# TODO template_dir
template_dir = _normalize_template_dir(defaults.template_dir, confdir=confdir)
if 'template_dir' in options:
if options['template_dir'] is None:
template_dir = None
elif isinstance(options['template_dir'], str):
template_dir = _normalize_template_dir(
options['template_dir'], confdir=confdir
)
else:
LOGGER.warning(
__("apidoc_modules item %i '%s' must be a string"),
i,
'template_dir',
type='apidoc',
)
template_dir = _normalize_template_dir(
defaults.template_dir, confdir=confdir
)

max_depth = defaults.max_depth
if 'max_depth' in options:
Expand Down Expand Up @@ -227,6 +245,7 @@ def _parse_module_options(
implicit_namespaces=bool_options['implicit_namespaces'],
automodule_options=automodule_options,
header=module_path.name,
template_dir=template_dir,
)


Expand Down Expand Up @@ -261,3 +280,18 @@ def _check_collection_of_strings(
)
return default
return options[key]


def _normalize_template_dir(
value: str | Path | None,
*,
confdir: Path,
) -> Path | None:
"""Return a template directory path resolved relative to *confdir*."""
if value is None:
return None

path = Path(value)
if not path.is_absolute():
path = confdir / path
return path
4 changes: 3 additions & 1 deletion sphinx/ext/apidoc/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class ApidocOptions:
version: str | None = None
release: str | None = None
extensions: Sequence[str] | None = None
template_dir: str | None = None
template_dir: str | Path | None = None


@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
Expand All @@ -82,6 +82,7 @@ class ApidocDefaults:
no_headings: bool
module_first: bool
implicit_namespaces: bool
template_dir: str | Path | None

@classmethod
def from_config(cls, config: Config, /) -> Self:
Expand All @@ -96,4 +97,5 @@ def from_config(cls, config: Config, /) -> Self:
no_headings=config.apidoc_no_headings,
module_first=config.apidoc_module_first,
implicit_namespaces=config.apidoc_implicit_namespaces,
template_dir=config.apidoc_template_dir,
)
2 changes: 1 addition & 1 deletion sphinx/transforms/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ def apply(self, **kwargs: Any) -> None:
# There is no point in having noqa on literal blocks because
# they cannot contain references. Recognizing it would just
# completely prevent escaping the noqa. Outside of literal
# blocks, one can always write \#noqa.
# blocks, one can always write ``\#`` followed by ``noqa``.
if not isinstance(node, LITERAL_TYPE_NODES):
msgstr, _ = parse_noqa(msgstr)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.. custom-template-marker

{{ qualname }} documented with the custom template.

.. automodule:: {{ qualname }}
:members:

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.. default-template-marker

{{ qualname }} documented with the default template.

.. automodule:: {{ qualname }}
:members:

15 changes: 15 additions & 0 deletions tests/roots/test-ext-apidoc-template-dir/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
extensions = ['sphinx.ext.apidoc']

apidoc_separate_modules = True
apidoc_template_dir = '_templates/default'
apidoc_modules = [
{
'path': 'src/pkg_default',
'destination': 'generated/default',
},
{
'path': 'src/pkg_custom',
'destination': 'generated/custom',
'template_dir': '_templates/custom',
},
]
6 changes: 6 additions & 0 deletions tests/roots/test-ext-apidoc-template-dir/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Test apidoc template directory
==============================

This project exercises the per-module ``template_dir`` support added to the
``sphinx.ext.apidoc`` extension.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Custom template package."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Example module that should use the custom template."""


def ping() -> str:
return 'custom template pong'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Default template package."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Example module that should use the default template."""


def ping() -> str:
return 'default template pong'
57 changes: 55 additions & 2 deletions tests/test_extensions/test_ext_apidoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
from __future__ import annotations

from collections import namedtuple
from pathlib import Path
from shutil import copytree
from types import SimpleNamespace
from typing import TYPE_CHECKING

import pytest

import sphinx.ext.apidoc._generate
from sphinx.ext.apidoc._cli import main as apidoc_main
from sphinx.ext.apidoc._extension import run_apidoc

if TYPE_CHECKING:
from pathlib import Path

from sphinx.testing.util import SphinxTestApp

_apidoc = namedtuple('_apidoc', 'coderoot,outdir') # NoQA: PYI024
Expand Down Expand Up @@ -96,6 +98,57 @@ def test_custom_templates(make_app, apidoc):
assert 'The Jinja module template was found!' in txt


def test_extension_template_dir_option(rootdir, tmp_path):
srcdir = tmp_path / 'project'
copytree(rootdir / 'test-ext-apidoc-template-dir', srcdir)
config = SimpleNamespace(
apidoc_exclude_patterns=[],
apidoc_automodule_options=frozenset({
'members',
'undoc-members',
'show-inheritance',
}),
apidoc_max_depth=4,
apidoc_follow_links=False,
apidoc_separate_modules=True,
apidoc_include_private=False,
apidoc_no_headings=False,
apidoc_module_first=False,
apidoc_implicit_namespaces=False,
apidoc_template_dir='_templates/default',
apidoc_modules=[
{
'path': 'src/pkg_default',
'destination': 'generated/default',
},
{
'path': 'src/pkg_custom',
'destination': 'generated/custom',
'template_dir': '_templates/custom',
},
],
)
app = SimpleNamespace(config=config, srcdir=srcdir, confdir=srcdir)
run_apidoc(app)

generated_default = (
Path(srcdir) / 'generated' / 'default' / 'pkg_default.module_default.rst'
)
generated_custom = (
Path(srcdir) / 'generated' / 'custom' / 'pkg_custom.module_custom.rst'
)

assert generated_default.is_file()
assert generated_custom.is_file()

default_text = generated_default.read_text(encoding='utf-8')
custom_text = generated_custom.read_text(encoding='utf-8')

assert '.. default-template-marker' in default_text
assert '.. custom-template-marker' in custom_text
assert '.. custom-template-marker' not in default_text


@pytest.mark.apidoc(
coderoot='test-ext-apidoc-pep420/a',
options=['--implicit-namespaces'],
Expand Down
Loading