diff --git a/.codespellrc b/.codespellrc index 0aac70a604c..c3df6c4ef1d 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,5 +1,5 @@ [codespell] -skip = .git,*.pdf,*.svg,viz-report.html +skip = .git,*.pdf,*.svg,numpydoc.py,viz-report.html # objekt - used in the code purposefully different from object # nd - import scipy.ndimage as nd ignore-words-list = objekt,nd diff --git a/docs/sphinxext/docscrape.py b/docs/sphinxext/docscrape.py index e5c07f59ded..fb3a0b6347e 100644 --- a/docs/sphinxext/docscrape.py +++ b/docs/sphinxext/docscrape.py @@ -1,6 +1,7 @@ """Extract reference documentation from the NumPy source tree. """ + import inspect import textwrap import re @@ -11,12 +12,7 @@ import copy import sys - -# TODO: Remove try-except when support for Python 3.7 is dropped -try: - from functools import cached_property -except ImportError: # cached_property added in Python 3.8 - cached_property = property +from functools import cached_property def strip_blank_lines(l): @@ -408,7 +404,7 @@ def _parse(self): msg = "Docstring contains a Receives section but not Yields." raise ValueError(msg) - for (section, content) in sections: + for section, content in sections: if not section.startswith(".."): section = (s.capitalize() for s in section.split(" ")) section = " ".join(section) @@ -631,7 +627,6 @@ def __init__(self, obj, doc=None, config=None): class ClassDoc(NumpyDocString): - extra_public_methods = ["__call__"] def __init__(self, cls, doc=None, modulename="", func_doc=FunctionDoc, config=None): @@ -711,6 +706,7 @@ def properties(self): for name, func in inspect.getmembers(self._cls) if ( not name.startswith("_") + and not self._should_skip_member(name, self._cls) and ( func is None or isinstance(func, (property, cached_property)) @@ -720,6 +716,19 @@ def properties(self): ) ] + @staticmethod + def _should_skip_member(name, klass): + if ( + # Namedtuples should skip everything in their ._fields as the + # docstrings for each of the members is: "Alias for field number X" + issubclass(klass, tuple) + and hasattr(klass, "_asdict") + and hasattr(klass, "_fields") + and name in klass._fields + ): + return True + return False + def _is_show_member(self, name): if self.show_inherited_members: return True # show all class members @@ -728,7 +737,15 @@ def _is_show_member(self, name): return True -def get_doc_object(obj, what=None, doc=None, config=None): +def get_doc_object( + obj, + what=None, + doc=None, + config=None, + class_doc=ClassDoc, + func_doc=FunctionDoc, + obj_doc=ObjDoc, +): if what is None: if inspect.isclass(obj): what = "class" @@ -742,10 +759,10 @@ def get_doc_object(obj, what=None, doc=None, config=None): config = {} if what == "class": - return ClassDoc(obj, func_doc=FunctionDoc, doc=doc, config=config) + return class_doc(obj, func_doc=func_doc, doc=doc, config=config) elif what in ("function", "method"): - return FunctionDoc(obj, doc=doc, config=config) + return func_doc(obj, doc=doc, config=config) else: if doc is None: doc = pydoc.getdoc(obj) - return ObjDoc(obj, doc, config=config) + return obj_doc(obj, doc, config=config) diff --git a/docs/sphinxext/docscrape_sphinx.py b/docs/sphinxext/docscrape_sphinx.py index 9a62cff9ce7..771c1ea445d 100644 --- a/docs/sphinxext/docscrape_sphinx.py +++ b/docs/sphinxext/docscrape_sphinx.py @@ -11,6 +11,7 @@ from sphinx.jinja2glue import BuiltinTemplateLoader from .docscrape import NumpyDocString, FunctionDoc, ClassDoc, ObjDoc +from .docscrape import get_doc_object as get_doc_object_orig from .xref import make_xref @@ -372,9 +373,11 @@ def __str__(self, indent=0, func_role="obj"): "notes": self._str_section("Notes"), "references": self._str_references(), "examples": self._str_examples(), - "attributes": self._str_param_list("Attributes", fake_autosummary=True) - if self.attributes_as_param_list - else self._str_member_list("Attributes"), + "attributes": ( + self._str_param_list("Attributes", fake_autosummary=True) + if self.attributes_as_param_list + else self._str_member_list("Attributes") + ), "methods": self._str_member_list("Methods"), } ns = {k: "\n".join(v) for k, v in ns.items()} @@ -407,20 +410,10 @@ def __init__(self, obj, doc=None, config=None): ObjDoc.__init__(self, obj, doc=doc, config=config) -# TODO: refactor to use docscrape.get_doc_object def get_doc_object(obj, what=None, doc=None, config=None, builder=None): - if what is None: - if inspect.isclass(obj): - what = "class" - elif inspect.ismodule(obj): - what = "module" - elif isinstance(obj, Callable): - what = "function" - else: - what = "object" - if config is None: config = {} + template_dirs = [os.path.join(os.path.dirname(__file__), "templates")] if builder is not None: template_loader = BuiltinTemplateLoader() @@ -430,11 +423,12 @@ def get_doc_object(obj, what=None, doc=None, config=None, builder=None): template_env = SandboxedEnvironment(loader=template_loader) config["template"] = template_env.get_template("numpydoc_docstring.rst") - if what == "class": - return SphinxClassDoc(obj, func_doc=SphinxFunctionDoc, doc=doc, config=config) - elif what in ("function", "method"): - return SphinxFunctionDoc(obj, doc=doc, config=config) - else: - if doc is None: - doc = pydoc.getdoc(obj) - return SphinxObjDoc(obj, doc, config=config) + return get_doc_object_orig( + obj, + what=what, + doc=doc, + config=config, + class_doc=SphinxClassDoc, + func_doc=SphinxFunctionDoc, + obj_doc=SphinxObjDoc, + ) diff --git a/docs/sphinxext/numpydoc.py b/docs/sphinxext/numpydoc.py index e5bc563d444..3513f95c987 100644 --- a/docs/sphinxext/numpydoc.py +++ b/docs/sphinxext/numpydoc.py @@ -16,6 +16,7 @@ .. [1] https://github.com/numpy/numpydoc """ + from copy import deepcopy import re import pydoc @@ -24,17 +25,17 @@ import hashlib import itertools -from docutils.nodes import citation, Text, section, comment, reference +from docutils.nodes import citation, Text, section, comment, reference, inline import sphinx from sphinx.addnodes import pending_xref, desc_content from sphinx.util import logging from sphinx.errors import ExtensionError -if sphinx.__version__ < "4.2": - raise RuntimeError("Sphinx 4.2 or newer is required") +if sphinx.__version__ < "5": + raise RuntimeError("Sphinx 5 or newer is required") from .docscrape_sphinx import get_doc_object -from .validate import validate, ERROR_MSGS +from .validate import validate, ERROR_MSGS, get_validation_checks from .xref import DEFAULT_LINKS from . import __version__ @@ -149,6 +150,10 @@ def clean_backrefs(app, doc, docname): for ref in _traverse_or_findall(doc, reference, descend=True): for id_ in ref["ids"]: known_ref_ids.add(id_) + # some extensions produce backrefs to inline elements + for ref in _traverse_or_findall(doc, inline, descend=True): + for id_ in ref["ids"]: + known_ref_ids.add(id_) for citation_node in _traverse_or_findall(doc, citation, descend=True): # remove backrefs to non-existent refs citation_node["backrefs"] = [ @@ -207,7 +212,19 @@ def mangle_docstrings(app, what, name, obj, options, lines): # TODO: Currently, all validation checks are run and only those # selected via config are reported. It would be more efficient to # only run the selected checks. - errors = validate(doc)["errors"] + report = validate(doc) + errors = [ + err + for err in report["errors"] + if not ( + ( + overrides := app.config.numpydoc_validation_overrides.get( + err[0] + ) + ) + and re.search(overrides, report["docstring"]) + ) + ] if {err[0] for err in errors} & app.config.numpydoc_validation_checks: msg = ( f"[numpydoc] Validation warnings while processing " @@ -285,6 +302,7 @@ def setup(app, get_doc_object_=get_doc_object): app.add_config_value("numpydoc_xref_ignore", set(), True) app.add_config_value("numpydoc_validation_checks", set(), True) app.add_config_value("numpydoc_validation_exclude", set(), False) + app.add_config_value("numpydoc_validation_overrides", dict(), False) # Extra mangling domains app.add_domain(NumpyPythonDomain) @@ -310,17 +328,9 @@ def update_config(app, config=None): # Processing to determine whether numpydoc_validation_checks is treated # as a blocklist or allowlist - valid_error_codes = set(ERROR_MSGS.keys()) - if "all" in config.numpydoc_validation_checks: - block = deepcopy(config.numpydoc_validation_checks) - config.numpydoc_validation_checks = valid_error_codes - block - # Ensure that the validation check set contains only valid error codes - invalid_error_codes = config.numpydoc_validation_checks - valid_error_codes - if invalid_error_codes: - raise ValueError( - f"Unrecognized validation code(s) in numpydoc_validation_checks " - f"config value: {invalid_error_codes}" - ) + config.numpydoc_validation_checks = get_validation_checks( + config.numpydoc_validation_checks + ) # Generate the regexp for docstrings to ignore during validation if isinstance(config.numpydoc_validation_exclude, str): @@ -335,6 +345,11 @@ def update_config(app, config=None): ) config.numpydoc_validation_excluder = exclude_expr + for check, patterns in config.numpydoc_validation_overrides.items(): + config.numpydoc_validation_overrides[check] = re.compile( + r"|".join(exp for exp in patterns) + ) + # ------------------------------------------------------------------------------ # Docstring-mangling domains