From 3ab23a5875e98e7a0225bea5f0993d384e27ba47 Mon Sep 17 00:00:00 2001 From: brockdyer03 Date: Wed, 27 May 2026 13:40:38 -0400 Subject: [PATCH] Add rough implementation for custom section names --- numpydoc/docscrape.py | 15 ++++++++++ numpydoc/docscrape_sphinx.py | 53 ++++++++++++++++++++++++++++++++---- numpydoc/numpydoc.py | 4 +++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index b014967a..c7024072 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -11,6 +11,9 @@ from functools import cached_property from warnings import warn +from sphinx.util import logging + +logger = logging.getLogger(__name__) def strip_blank_lines(l): "Remove leading and trailing blank lines from a list of lines" @@ -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(): + 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) diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py index ee1187d4..6a9e281a 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -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" @@ -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") 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): @@ -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): + 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) @@ -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, diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py index 8a0cefe7..1bc7387f 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -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 @@ -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)