Skip to content

ENH: Enable validation during sphinx-build process #302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Feb 8, 2021
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