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
37 changes: 37 additions & 0 deletions doc/format.rst
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,42 @@ Note that license and author info, while often included in source files, do not
belong in docstrings.


Adding custom sections
----------------------

A recent addition to numpydoc is the ability to add custom sections for your
docstrings. This can be done in four steps:

1. Copy the existing numpydoc template file into a new file.

- This file should probably go in your documentation's ``_static`` directory.

2. Add your custom section to the file

.. code-block:: rest

...
{{references}}
{{examples}}
{{custom_section}}

3. Pass the path to that file through the ``numpydoc_template_file`` variable in
your ``conf.py`` file.

.. code-block:: python

numpydoc_template_file = Path(__file__).parent / "_static/numpydoc_template.rst"

4. Choose the format for your custom section with the ``numpydoc_extra_sections``
variable in your ``conf.py`` file.

.. code-block:: python

numpydoc_extra_sections = {
"Custom Section": "member_list",
}


Other points to keep in mind
----------------------------
* Equations : as discussed in the :ref:`Notes <notes>` section above, LaTeX
Expand Down Expand Up @@ -791,3 +827,4 @@ This document itself was written in ReStructuredText.
.. _vim-flake8: https://github.com/nvie/vim-flake8
.. _SciPy: https://www.scipy.org
.. _numpy-discussion list: https://mail.python.org/mailman3/lists/numpy-discussion.python.org/
.. _numpydoc template file: https://github.com/numpy/numpydoc/blob/main/numpydoc/templates/numpydoc_docstring.rst?plain=1
31 changes: 31 additions & 0 deletions doc/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,34 @@ numpydoc_validation_overrides : dict
``numpydoc_validation_checks`` is not an empty set. Use
:ref:`inline ignore comments <inline_ignore_comments>` to turn off
specific checks for parts of your code.
numpydoc_template_file : path_like
A path to a template file with all of the sections you wish to include
in your docstrings. This includes default numpydoc sections and any
custom sections you wish to add.
See the `default numpydoc_docstring.rst file <https://github.com/numpy/numpydoc/blob/main/numpydoc/templates/numpydoc_docstring.rst?plain=1>`_
for an example of the expected formatting. Note that the section names
should be all lowercase in the template file.
numpydoc_extra_sections : dict
A dictionary mapping the name of any custom sections you've added to
your custom template file to an existing one of numpydoc's section
formats.

The available formats are:

- ``"param_list"``
- ``"member_list"``
- ``"returns"``
- ``"warnings"``
- ``"see_also"``
- ``"notes"``
- ``"references"``
- ``"examples"``

For example, if you wanted to add a custom section called ``Members``,
you would use

.. code-block:: python

numpydoc_extra_sections = {
"Members": "member_list",
}
25 changes: 25 additions & 0 deletions numpydoc/docscrape.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
from functools import cached_property
from warnings import warn

from sphinx.util import logging

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're trying to move away from having a Sphinx dependency, so probably don't want this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be fine removing it if there's a replacement that has already been agreed on.


logger = logging.getLogger(__name__)


def strip_blank_lines(l):
"Remove leading and trailing blank lines from a list of lines"
Expand Down Expand Up @@ -140,6 +144,27 @@ def __init__(self, docstring, config=None):
orig_docstring = docstring
docstring = textwrap.dedent(docstring).split("\n")

if config is not None:
extra_sections = config.get("extra_sections", dict())
for section, fmt in extra_sections.items():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably handle this with set intersections.

if fmt in (
"param_list",
"member_list",
"returns",
"warnings",
"see_also",
"notes",
):
default = []
elif fmt in ("references", "examples"):
default = ""
else:
logger.warning(
"Unrecognized section format %r for section %s", fmt, section
)

NumpyDocString.sections.update({section: default})

self._doc = Reader(docstring)
self._parsed_data = copy.deepcopy(self.sections)

Expand Down
61 changes: 55 additions & 6 deletions numpydoc/docscrape_sphinx.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import inspect
import os
import pydoc
import re
import textwrap
from pathlib import Path

from jinja2 import FileSystemLoader
from jinja2.sandbox import SandboxedEnvironment
from sphinx.jinja2glue import BuiltinTemplateLoader
from sphinx.util import logging

from .docscrape import ClassDoc, FunctionDoc, NumpyDocString, ObjDoc
from .docscrape import get_doc_object as get_doc_object_orig
from .xref import make_xref

logger = logging.getLogger(__name__)

IMPORT_MATPLOTLIB_RE = r"\b(import +matplotlib|from +matplotlib +import)\b"


Expand All @@ -29,12 +32,20 @@ def load_config(self, config):
self.xref_param_type = config.get("xref_param_type", False)
self.xref_aliases = config.get("xref_aliases", dict())
self.xref_ignore = config.get("xref_ignore", set())
self.template = config.get("template", None)
self.extra_sections = config.get("extra_sections", dict())
self.template = config.get("template")
self.template_file = config.get("template_file")
Comment on lines +36 to +37

@stefanv stefanv May 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes related to the PR (looks like they are)? If so, document along with other config options.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I originally had put the code for the extra templating information there, but realized that it is additionally set in get_doc_object() at the end of that module. I am unsure if it needs to be in both places, but I think it is probably good to have it in both since the one you commented on sets the attributes of SphinxDocString, which already has attributes for the other config options.

if self.template is None:
template_dirs = [os.path.join(os.path.dirname(__file__), "templates")]
if self.template_file is not None:
template_file = Path(template_file).resolve(strict=True)
template_dirs = [template_file.parent]
else:
template_dirs = [Path(__file__).parent / "templates"]
template_file = template_dirs[0] / "numpydoc_docstring.rst"

template_loader = FileSystemLoader(template_dirs)
template_env = SandboxedEnvironment(loader=template_loader)
self.template = template_env.get_template("numpydoc_docstring.rst")
self.template = template_env.get_template(template_file.name)

# string conversion routines
def _str_header(self, name):
Expand Down Expand Up @@ -377,6 +388,38 @@ def __str__(self, indent=0, func_role="obj"):
"references": self._str_references(),
"examples": self._str_examples(),
}

for section, fmt in self.extra_sections.items():
if not self.get(section):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change doesn't look quite right; ignores the dictionary above?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dictionary above it is the standard numpydoc sections, which I suppose can be hard-coded for now. After those are set, I iterate over the extra sections that the user provides, and use them to update the above dictionary.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, but do we need to re-handle existing sections in there, or can those be removed and handled via the standard mechanism?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing sections aren't touched there. The reason it looks like it is because I am re-using the names of the _str_<section_format>() functions to select the format, which are also the identifiers in numpydoc_docstring.rst.

continue

# The sections in `numpydoc_template.rst` are snake case
# even if they aren't in the docstring itself.
section_key = section.lower().replace(" ", "_")
match fmt:
case "param_list":
ns.update({section_key: self._str_param_list(section)})
case "member_list":
ns.update({section_key: self._str_member_list(section)})
case "returns":
ns.update({section_key: self._str_returns(section)})
case "warnings":
ns.update({section_key: self._str_warnings(section)})
case "see_also":
ns.update({section_key: self._str_see_also(section)})
case "notes":
ns.update({section_key: self._str_section(section)})
case "references":
ns.update({section_key: self._str_references(section)})
case "examples":
ns.update({section_key: self._str_examples(section)})
case _:
logger.warning(
"[numpydoc] Unknown section format %r for section %s",
fmt,
section,
)

ns = {k: "\n".join(v) for k, v in ns.items()}

rendered = self.template.render(**ns)
Expand Down Expand Up @@ -411,14 +454,20 @@ def get_doc_object(obj, what=None, doc=None, config=None, builder=None):
if config is None:
config = {}

template_dirs = [os.path.join(os.path.dirname(__file__), "templates")]
if (template_file := config.get("template_file")) is not None:
template_file = Path(template_file).resolve(strict=True)
template_dirs = [template_file.parent]
else:
template_dirs = [Path(__file__).parent / "templates"]
template_file = template_dirs[0] / "numpydoc_docstring.rst"

if builder is not None:
template_loader = BuiltinTemplateLoader()
template_loader.init(builder, dirs=template_dirs)
else:
template_loader = FileSystemLoader(template_dirs)
template_env = SandboxedEnvironment(loader=template_loader)
config["template"] = template_env.get_template("numpydoc_docstring.rst")
config["template"] = template_env.get_template(template_file.name)

return get_doc_object_orig(
obj,
Expand Down
4 changes: 4 additions & 0 deletions numpydoc/numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ def mangle_docstrings(app: SphinxApp, what, name, obj, options, lines):
"xref_param_type": app.config.numpydoc_xref_param_type,
"xref_aliases": app.config.numpydoc_xref_aliases_complete,
"xref_ignore": app.config.numpydoc_xref_ignore,
"template_file": app.config.numpydoc_template_file,
"extra_sections": app.config.numpydoc_extra_sections,
}
# TODO: Find a cleaner way to take care of this change away from dict
# https://github.com/sphinx-doc/sphinx/issues/13942
Expand Down Expand Up @@ -348,6 +350,8 @@ def setup(app: SphinxApp, get_doc_object_=get_doc_object):
app.add_config_value("numpydoc_validation_exclude", set(), False)
app.add_config_value("numpydoc_validation_exclude_files", set(), False)
app.add_config_value("numpydoc_validation_overrides", dict(), False)
app.add_config_value("numpydoc_template_file", None, True)
app.add_config_value("numpydoc_extra_sections", dict(), True)

# Extra mangling domains
app.add_domain(NumpyPythonDomain)
Expand Down
2 changes: 2 additions & 0 deletions numpydoc/tests/test_numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class MockConfig:
numpydoc_validation_exclude = set()
numpydoc_validation_exclude_files = set()
numpydoc_validation_overrides = dict()
numpydoc_template_file = None
numpydoc_extra_sections = dict()


class MockBuilder:
Expand Down