Skip to content
Draft
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
15 changes: 15 additions & 0 deletions numpydoc/docscrape.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
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 +143,18 @@ 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
53 changes: 48 additions & 5 deletions numpydoc/docscrape_sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
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 +33,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 +37 to +38

@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")
config["template"] = template_env.get_template(template_file.name)

# string conversion routines
def _str_header(self, name):
Expand Down Expand Up @@ -377,6 +389,31 @@ 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

match fmt:
case "param_list":
ns.update({section.lower(): self._str_param_list(section)})
case "member_list":
ns.update({section.lower(): self._str_member_list(section)})
case "returns":
ns.update({section.lower(): self._str_returns(section)})
case "warnings":
ns.update({section.lower(): self._str_warnings(section)})
case "see_also":
ns.update({section.lower(): self._str_see_also(section)})
case "notes":
ns.update({section.lower(): self._str_section(section)})
case "references":
ns.update({section.lower(): self._str_references(section)})
case "examples":
ns.update({section.lower(): self._str_examples(section)})
case _:
logger.warning("[numpydoc] Unknown section format: %r", fmt)

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

rendered = self.template.render(**ns)
Expand Down Expand Up @@ -411,14 +448,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
Loading