Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
version = re.sub(r'(\.dev\d+).*?$', r'\1', version)
numpydoc_xref_param_type = True
numpydoc_xref_ignore = {'optional', 'type_without_description', 'BadException'}
# Run docstring validation as part of build process
numpydoc_validation_checks = {"all", "GL01", "SA04", "RT03"}

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
36 changes: 36 additions & 0 deletions doc/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,42 @@ numpydoc_xref_ignore : set or ``"all"``
desired cross reference mappings in ``numpydoc_xref_aliases`` and setting
``numpydoc_xref_ignore="all"`` is more convenient than explicitly listing
terms to ignore in a set.
numpydoc_validation_checks : set
The set of validation checks to report during the sphinx build process.
The default is an empty set, so docstring validation is not run by
default.
If ``"all"`` is in the set, then the results of all of the
:ref:`built-in validation checks <validation_checks>` are reported.
If the set includes ``"all"`` and additional error codes, then all
validation checks *except* the listed error codes will be run.
If the set contains *only* individual error codes, then only those checks
will be run.
For example::

# Report warnings for all validation checks
numpydoc_validation_checks = {"all"}

# Report warnings for all checks *except* for GL01, GL02, and GL05
numpydoc_validation_checks = {"all", "GL01", "GL02", "GL05"}

# Only report warnings for the SA01 and EX01 checks
numpydoc_validation_checks = {"SA01", "EX01"}
numpydoc_validation_exclude : set
A container of strings using :py:mod:`re` syntax specifying patterns to
ignore for docstring validation.
For example, to skip docstring validation for all objects in
``mypkg.mymodule``::

numpydoc_validation_exclude = {"mypkg.mymodule."}

If you wanted to also skip getter methods of ``MyClass``::

numpydoc_validation_exclude = {r"mypkg\.mymodule\.", r"MyClass\.get$"}

The default is an empty set meaning no objects are excluded from docstring
validation.
Only has an effect when docstring validation is activated, i.e.
``numpydoc_validation_checks`` is not an empty set.
numpydoc_edit_link : bool
.. deprecated:: 0.7.0

Expand Down
18 changes: 18 additions & 0 deletions doc/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,21 @@ For an exhaustive validation of the formatting of the docstring, use the
``--validate`` parameter. This will report the errors detected, such as
incorrect capitalization, wrong order of the sections, and many other
issues.

.. _validation_checks:

Built-in Validation Checks
--------------------------

The ``numpydoc.validation`` module provides a mapping with all of the checks
that are run as part of the validation procedure.
The mapping is of the form: ``error_code : <explanation>`` where ``error_code``
provides a shorthand for the check being run, and ``<explanation>`` provides
a more detailed message. For example::

"EX01" : "No examples section found"

The full mapping of validation checks is given below.

.. literalinclude:: ../numpydoc/validate.py
:lines: 36-90
6 changes: 3 additions & 3 deletions numpydoc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import ast

from .docscrape_sphinx import get_doc_object
from .validate import validate, Docstring
from .validate import validate, Validator


def render_object(import_path, config=None):
"""Test numpydoc docstring generation for a given object"""
# TODO: Move Docstring._load_obj to a better place than validate
print(get_doc_object(Docstring(import_path).obj,
# TODO: Move Validator._load_obj to a better place than validate
print(get_doc_object(Validator._load_obj(import_path),
config=dict(config or [])))
return 0

Expand Down
37 changes: 36 additions & 1 deletion numpydoc/docscrape.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,16 @@ def _parse(self):
else:
self[section] = content

@property
def _obj(self):
if hasattr(self, '_cls'):
return self._cls
elif hasattr(self, '_f'):
return self._f
return None

def _error_location(self, msg, error=True):
if hasattr(self, '_obj') and self._obj is not None:
if self._obj is not None:
# we know where the docs came from:
try:
filename = inspect.getsourcefile(self._obj)
Expand Down Expand Up @@ -581,6 +589,12 @@ def __str__(self):
return out


class ObjDoc(NumpyDocString):
def __init__(self, obj, doc=None, config={}):
self._f = obj
NumpyDocString.__init__(self, doc, config=config)


class ClassDoc(NumpyDocString):

extra_public_methods = ['__call__']
Expand Down Expand Up @@ -663,3 +677,24 @@ def _is_show_member(self, name):
if name not in self._cls.__dict__:
return False # class member is inherited, we do not show it
return True


def get_doc_object(obj, what=None, doc=None, config={}):
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 what == 'class':
return ClassDoc(obj, func_doc=FunctionDoc, doc=doc, config=config)
elif what in ('function', 'method'):
return FunctionDoc(obj, doc=doc, config=config)
else:
if doc is None:
doc = pydoc.getdoc(obj)
return ObjDoc(obj, doc, config=config)
16 changes: 4 additions & 12 deletions numpydoc/docscrape_sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import sphinx
from sphinx.jinja2glue import BuiltinTemplateLoader

from .docscrape import NumpyDocString, FunctionDoc, ClassDoc
from .docscrape import NumpyDocString, FunctionDoc, ClassDoc, ObjDoc
from .xref import make_xref


Expand Down Expand Up @@ -229,14 +229,6 @@ def _str_param_list(self, name, fake_autosummary=False):

return out

@property
def _obj(self):
if hasattr(self, '_cls'):
return self._cls
elif hasattr(self, '_f'):
return self._f
return None

def _str_member_list(self, name):
"""
Generate a member listing, autosummary:: table where possible,
Expand Down Expand Up @@ -411,13 +403,13 @@ def __init__(self, obj, doc=None, func_doc=None, config={}):
ClassDoc.__init__(self, obj, doc=doc, func_doc=None, config=config)


class SphinxObjDoc(SphinxDocString):
class SphinxObjDoc(SphinxDocString, ObjDoc):
def __init__(self, obj, doc=None, config={}):
self._f = obj
self.load_config(config)
SphinxDocString.__init__(self, doc, config=config)
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={}, builder=None):
if what is None:
if inspect.isclass(obj):
Expand Down
52 changes: 52 additions & 0 deletions numpydoc/numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
raise RuntimeError("Sphinx 1.6.5 or newer is required")

from .docscrape_sphinx import get_doc_object
from .validate import validate, ERROR_MSGS
from .xref import DEFAULT_LINKS
from . import __version__

Expand Down Expand Up @@ -173,6 +174,28 @@ def mangle_docstrings(app, what, name, obj, options, lines):
logger.error('[numpydoc] While processing docstring for %r', name)
raise

if app.config.numpydoc_validation_checks:
# If the user has supplied patterns to ignore via the
# numpydoc_validation_exclude config option, skip validation for
# any objs whose name matches any of the patterns
excluder = app.config.numpydoc_validation_excluder
exclude_from_validation = excluder.search(name) if excluder else False
if not exclude_from_validation:
# 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"]
if {err[0] for err in errors} & app.config.numpydoc_validation_checks:
msg = (
f"[numpydoc] Validation warnings while processing "
f"docstring for {name!r}:\n"
)
for err in errors:
if err[0] in app.config.numpydoc_validation_checks:
msg += f" {err[0]}: {err[1]}\n"
logger.warning(msg)


if (app.config.numpydoc_edit_link and hasattr(obj, '__name__') and
obj.__name__):
if hasattr(obj, '__module__'):
Expand Down Expand Up @@ -254,6 +277,8 @@ def setup(app, get_doc_object_=get_doc_object):
app.add_config_value('numpydoc_xref_param_type', False, True)
app.add_config_value('numpydoc_xref_aliases', dict(), True)
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)

# Extra mangling domains
app.add_domain(NumpyPythonDomain)
Expand All @@ -278,6 +303,33 @@ def update_config(app, config=None):
numpydoc_xref_aliases_complete[key] = value
config.numpydoc_xref_aliases_complete = numpydoc_xref_aliases_complete

# 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}"
)

# Generate the regexp for docstrings to ignore during validation
if isinstance(config.numpydoc_validation_exclude, str):
raise ValueError(
f"numpydoc_validation_exclude must be a container of strings, "
f"e.g. [{config.numpydoc_validation_exclude!r}]."
)
config.numpydoc_validation_excluder = None
if config.numpydoc_validation_exclude:
exclude_expr = re.compile(
r"|".join(exp for exp in config.numpydoc_validation_exclude)
)
config.numpydoc_validation_excluder = exclude_expr


# ------------------------------------------------------------------------------
# Docstring-mangling domains
Expand Down
3 changes: 3 additions & 0 deletions numpydoc/tests/test_docscrape.py
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,9 @@ class Config():
def __init__(self, a, b):
self.numpydoc_xref_aliases = a
self.numpydoc_xref_aliases_complete = b
# numpydoc.update_config fails if this config option not present
self.numpydoc_validation_checks = set()
self.numpydoc_validation_exclude = set()

xref_aliases_complete = deepcopy(DEFAULT_LINKS)
for key in xref_aliases:
Expand Down
Loading