Skip to content

Commit ce6666a

Browse files
rossbarlarsoner
andauthored
ENH: Enable validation during sphinx-build process (#302)
* WIP: Move get_doc_obj to docscrape * WIP: mv _obj property to NumpyDocString * Proof-of-concept: Docstring attrs covered by refactor. Running the test suite on this patch demonstrates that refactoring the boundary between NumpyDocString and SphinxDocString provides the necessary info to (potentially) do away with the validate.Docstring class. * NOTE TO SELF: get_doc_object in docscrape_sphinx * Docstring -> Validator. * Activate validation during sphinx-build. Add a conf option to turn on/off. TODO: test * Replace logger.warn with warning. logger.warn is apparently deprecated * DOC: Add numpydoc_validate to conf docs. * Add mechanism for validation check selection. Adds a config option with a set to allow users to select which validation checks are used. Default is an empty set, which means none of the validation checks raise warnings during the build process. Add documentation for new option and activate in the doc build. * TST: modify how MockApp sets builder app. * TST: Add test of validation warnings. * Specify some sensible validation defaults. * Add docstring name to validation warnings. * Add all keyword to validation_check configuration. More flexibility in configuring which validation checks to run during sphinx build. If 'all' is present, treat the rest of the set as a blocklist, else an allowlist. * Fix failing test. * Make validation error mapping easier to read. * Add check for invalid error codes in configuration. plus test. * Add feature to exclude patterns from docstring validation. Modify updated config name to avoid sphinx warning. Add documentation for exclusion config value. * Be explicit about regex syntax for exclude config val Co-authored-by: Eric Larson <[email protected]> * Rm redundant numpydoc_validate config param. Co-authored-by: Eric Larson <[email protected]>
1 parent da816b5 commit ce6666a

11 files changed

+299
-53
lines changed

doc/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@
8484
version = re.sub(r'(\.dev\d+).*?$', r'\1', version)
8585
numpydoc_xref_param_type = True
8686
numpydoc_xref_ignore = {'optional', 'type_without_description', 'BadException'}
87+
# Run docstring validation as part of build process
88+
numpydoc_validation_checks = {"all", "GL01", "SA04", "RT03"}
8789

8890
# The language for content autogenerated by Sphinx. Refer to documentation
8991
# for a list of supported languages.

doc/install.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,42 @@ numpydoc_xref_ignore : set or ``"all"``
9696
desired cross reference mappings in ``numpydoc_xref_aliases`` and setting
9797
``numpydoc_xref_ignore="all"`` is more convenient than explicitly listing
9898
terms to ignore in a set.
99+
numpydoc_validation_checks : set
100+
The set of validation checks to report during the sphinx build process.
101+
The default is an empty set, so docstring validation is not run by
102+
default.
103+
If ``"all"`` is in the set, then the results of all of the
104+
:ref:`built-in validation checks <validation_checks>` are reported.
105+
If the set includes ``"all"`` and additional error codes, then all
106+
validation checks *except* the listed error codes will be run.
107+
If the set contains *only* individual error codes, then only those checks
108+
will be run.
109+
For example::
110+
111+
# Report warnings for all validation checks
112+
numpydoc_validation_checks = {"all"}
113+
114+
# Report warnings for all checks *except* for GL01, GL02, and GL05
115+
numpydoc_validation_checks = {"all", "GL01", "GL02", "GL05"}
116+
117+
# Only report warnings for the SA01 and EX01 checks
118+
numpydoc_validation_checks = {"SA01", "EX01"}
119+
numpydoc_validation_exclude : set
120+
A container of strings using :py:mod:`re` syntax specifying patterns to
121+
ignore for docstring validation.
122+
For example, to skip docstring validation for all objects in
123+
``mypkg.mymodule``::
124+
125+
numpydoc_validation_exclude = {"mypkg.mymodule."}
126+
127+
If you wanted to also skip getter methods of ``MyClass``::
128+
129+
numpydoc_validation_exclude = {r"mypkg\.mymodule\.", r"MyClass\.get$"}
130+
131+
The default is an empty set meaning no objects are excluded from docstring
132+
validation.
133+
Only has an effect when docstring validation is activated, i.e.
134+
``numpydoc_validation_checks`` is not an empty set.
99135
numpydoc_edit_link : bool
100136
.. deprecated:: 0.7.0
101137

doc/validation.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,21 @@ For an exhaustive validation of the formatting of the docstring, use the
1515
``--validate`` parameter. This will report the errors detected, such as
1616
incorrect capitalization, wrong order of the sections, and many other
1717
issues.
18+
19+
.. _validation_checks:
20+
21+
Built-in Validation Checks
22+
--------------------------
23+
24+
The ``numpydoc.validation`` module provides a mapping with all of the checks
25+
that are run as part of the validation procedure.
26+
The mapping is of the form: ``error_code : <explanation>`` where ``error_code``
27+
provides a shorthand for the check being run, and ``<explanation>`` provides
28+
a more detailed message. For example::
29+
30+
"EX01" : "No examples section found"
31+
32+
The full mapping of validation checks is given below.
33+
34+
.. literalinclude:: ../numpydoc/validate.py
35+
:lines: 36-90

numpydoc/__main__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
import ast
77

88
from .docscrape_sphinx import get_doc_object
9-
from .validate import validate, Docstring
9+
from .validate import validate, Validator
1010

1111

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

numpydoc/docscrape.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,16 @@ def _parse(self):
411411
else:
412412
self[section] = content
413413

414+
@property
415+
def _obj(self):
416+
if hasattr(self, '_cls'):
417+
return self._cls
418+
elif hasattr(self, '_f'):
419+
return self._f
420+
return None
421+
414422
def _error_location(self, msg, error=True):
415-
if hasattr(self, '_obj') and self._obj is not None:
423+
if self._obj is not None:
416424
# we know where the docs came from:
417425
try:
418426
filename = inspect.getsourcefile(self._obj)
@@ -581,6 +589,12 @@ def __str__(self):
581589
return out
582590

583591

592+
class ObjDoc(NumpyDocString):
593+
def __init__(self, obj, doc=None, config={}):
594+
self._f = obj
595+
NumpyDocString.__init__(self, doc, config=config)
596+
597+
584598
class ClassDoc(NumpyDocString):
585599

586600
extra_public_methods = ['__call__']
@@ -663,3 +677,24 @@ def _is_show_member(self, name):
663677
if name not in self._cls.__dict__:
664678
return False # class member is inherited, we do not show it
665679
return True
680+
681+
682+
def get_doc_object(obj, what=None, doc=None, config={}):
683+
if what is None:
684+
if inspect.isclass(obj):
685+
what = 'class'
686+
elif inspect.ismodule(obj):
687+
what = 'module'
688+
elif isinstance(obj, Callable):
689+
what = 'function'
690+
else:
691+
what = 'object'
692+
693+
if what == 'class':
694+
return ClassDoc(obj, func_doc=FunctionDoc, doc=doc, config=config)
695+
elif what in ('function', 'method'):
696+
return FunctionDoc(obj, doc=doc, config=config)
697+
else:
698+
if doc is None:
699+
doc = pydoc.getdoc(obj)
700+
return ObjDoc(obj, doc, config=config)

numpydoc/docscrape_sphinx.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import sphinx
1111
from sphinx.jinja2glue import BuiltinTemplateLoader
1212

13-
from .docscrape import NumpyDocString, FunctionDoc, ClassDoc
13+
from .docscrape import NumpyDocString, FunctionDoc, ClassDoc, ObjDoc
1414
from .xref import make_xref
1515

1616

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

230230
return out
231231

232-
@property
233-
def _obj(self):
234-
if hasattr(self, '_cls'):
235-
return self._cls
236-
elif hasattr(self, '_f'):
237-
return self._f
238-
return None
239-
240232
def _str_member_list(self, name):
241233
"""
242234
Generate a member listing, autosummary:: table where possible,
@@ -411,13 +403,13 @@ def __init__(self, obj, doc=None, func_doc=None, config={}):
411403
ClassDoc.__init__(self, obj, doc=doc, func_doc=None, config=config)
412404

413405

414-
class SphinxObjDoc(SphinxDocString):
406+
class SphinxObjDoc(SphinxDocString, ObjDoc):
415407
def __init__(self, obj, doc=None, config={}):
416-
self._f = obj
417408
self.load_config(config)
418-
SphinxDocString.__init__(self, doc, config=config)
409+
ObjDoc.__init__(self, obj, doc=doc, config=config)
419410

420411

412+
# TODO: refactor to use docscrape.get_doc_object
421413
def get_doc_object(obj, what=None, doc=None, config={}, builder=None):
422414
if what is None:
423415
if inspect.isclass(obj):

numpydoc/numpydoc.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
raise RuntimeError("Sphinx 1.6.5 or newer is required")
3535

3636
from .docscrape_sphinx import get_doc_object
37+
from .validate import validate, ERROR_MSGS
3738
from .xref import DEFAULT_LINKS
3839
from . import __version__
3940

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

177+
if app.config.numpydoc_validation_checks:
178+
# If the user has supplied patterns to ignore via the
179+
# numpydoc_validation_exclude config option, skip validation for
180+
# any objs whose name matches any of the patterns
181+
excluder = app.config.numpydoc_validation_excluder
182+
exclude_from_validation = excluder.search(name) if excluder else False
183+
if not exclude_from_validation:
184+
# TODO: Currently, all validation checks are run and only those
185+
# selected via config are reported. It would be more efficient to
186+
# only run the selected checks.
187+
errors = validate(doc)["errors"]
188+
if {err[0] for err in errors} & app.config.numpydoc_validation_checks:
189+
msg = (
190+
f"[numpydoc] Validation warnings while processing "
191+
f"docstring for {name!r}:\n"
192+
)
193+
for err in errors:
194+
if err[0] in app.config.numpydoc_validation_checks:
195+
msg += f" {err[0]}: {err[1]}\n"
196+
logger.warning(msg)
197+
198+
176199
if (app.config.numpydoc_edit_link and hasattr(obj, '__name__') and
177200
obj.__name__):
178201
if hasattr(obj, '__module__'):
@@ -254,6 +277,8 @@ def setup(app, get_doc_object_=get_doc_object):
254277
app.add_config_value('numpydoc_xref_param_type', False, True)
255278
app.add_config_value('numpydoc_xref_aliases', dict(), True)
256279
app.add_config_value('numpydoc_xref_ignore', set(), True)
280+
app.add_config_value('numpydoc_validation_checks', set(), True)
281+
app.add_config_value('numpydoc_validation_exclude', set(), False)
257282

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

306+
# Processing to determine whether numpydoc_validation_checks is treated
307+
# as a blocklist or allowlist
308+
valid_error_codes = set(ERROR_MSGS.keys())
309+
if "all" in config.numpydoc_validation_checks:
310+
block = deepcopy(config.numpydoc_validation_checks)
311+
config.numpydoc_validation_checks = valid_error_codes - block
312+
# Ensure that the validation check set contains only valid error codes
313+
invalid_error_codes = config.numpydoc_validation_checks - valid_error_codes
314+
if invalid_error_codes:
315+
raise ValueError(
316+
f"Unrecognized validation code(s) in numpydoc_validation_checks "
317+
f"config value: {invalid_error_codes}"
318+
)
319+
320+
# Generate the regexp for docstrings to ignore during validation
321+
if isinstance(config.numpydoc_validation_exclude, str):
322+
raise ValueError(
323+
f"numpydoc_validation_exclude must be a container of strings, "
324+
f"e.g. [{config.numpydoc_validation_exclude!r}]."
325+
)
326+
config.numpydoc_validation_excluder = None
327+
if config.numpydoc_validation_exclude:
328+
exclude_expr = re.compile(
329+
r"|".join(exp for exp in config.numpydoc_validation_exclude)
330+
)
331+
config.numpydoc_validation_excluder = exclude_expr
332+
281333

282334
# ------------------------------------------------------------------------------
283335
# Docstring-mangling domains

numpydoc/tests/test_docscrape.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,9 @@ class Config():
14971497
def __init__(self, a, b):
14981498
self.numpydoc_xref_aliases = a
14991499
self.numpydoc_xref_aliases_complete = b
1500+
# numpydoc.update_config fails if this config option not present
1501+
self.numpydoc_validation_checks = set()
1502+
self.numpydoc_validation_exclude = set()
15001503

15011504
xref_aliases_complete = deepcopy(DEFAULT_LINKS)
15021505
for key in xref_aliases:

0 commit comments

Comments
 (0)