Skip to content

Commit 2e09ea6

Browse files
committed
feat(numpydoc.py): Added config option numpydoc_validation_exclude_files for Sphinx plugin
Uses very similar regex processing to `numpydoc_validation_exclude` but instead applies to a module check before any numpydoc validation is performed.
1 parent 6fadb95 commit 2e09ea6

File tree

3 files changed

+95
-7
lines changed

3 files changed

+95
-7
lines changed

numpydoc/numpydoc.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from docutils.nodes import Text, citation, comment, inline, reference, section
2929
from sphinx.addnodes import desc_content, pending_xref
30+
from sphinx.application import Sphinx as SphinxApp
3031
from sphinx.util import logging
3132

3233
from . import __version__
@@ -52,7 +53,7 @@ def _traverse_or_findall(node, condition, **kwargs):
5253
)
5354

5455

55-
def rename_references(app, what, name, obj, options, lines):
56+
def rename_references(app: SphinxApp, what, name, obj, options, lines):
5657
# decorate reference numbers so that there are no duplicates
5758
# these are later undecorated in the doctree, in relabel_references
5859
references = set()
@@ -114,7 +115,7 @@ def is_docstring_section(node):
114115
return False
115116

116117

117-
def relabel_references(app, doc):
118+
def relabel_references(app: SphinxApp, doc):
118119
# Change 'hash-ref' to 'ref' in label text
119120
for citation_node in _traverse_or_findall(doc, citation):
120121
if not _is_cite_in_numpydoc_docstring(citation_node):
@@ -141,7 +142,7 @@ def matching_pending_xref(node):
141142
ref.replace(ref_text, new_text.copy())
142143

143144

144-
def clean_backrefs(app, doc, docname):
145+
def clean_backrefs(app: SphinxApp, doc, docname):
145146
# only::latex directive has resulted in citation backrefs without reference
146147
known_ref_ids = set()
147148
for ref in _traverse_or_findall(doc, reference, descend=True):
@@ -161,7 +162,7 @@ def clean_backrefs(app, doc, docname):
161162
DEDUPLICATION_TAG = " !! processed by numpydoc !!"
162163

163164

164-
def mangle_docstrings(app, what, name, obj, options, lines):
165+
def mangle_docstrings(app: SphinxApp, what, name, obj, options, lines):
165166
if DEDUPLICATION_TAG in lines:
166167
return
167168
show_inherited_class_members = app.config.numpydoc_show_inherited_class_members
@@ -190,6 +191,19 @@ def mangle_docstrings(app, what, name, obj, options, lines):
190191
title_re = re.compile(pattern, re.IGNORECASE | re.DOTALL)
191192
lines[:] = title_re.sub("", u_NL.join(lines)).split(u_NL)
192193
else:
194+
# Test the obj to find the module path, and skip the check if it's path is matched by
195+
# numpydoc_validation_exclude_files
196+
if app.config.numpydoc_validation_exclude_files:
197+
excluder = app.config.numpydoc_validation_files_excluder
198+
module = getattr(obj, "__module__", None)
199+
if module:
200+
# Perform the exclusion check solely on the module if there's no __path__.
201+
path = getattr(obj, "__path__", module)
202+
exclude_from_validation = excluder.search(path) if excluder else False
203+
if exclude_from_validation:
204+
# Skip validation for this object.
205+
return
206+
193207
try:
194208
doc = get_doc_object(
195209
obj, what, u_NL.join(lines), config=cfg, builder=app.builder
@@ -239,7 +253,7 @@ def mangle_docstrings(app, what, name, obj, options, lines):
239253
lines += ["..", DEDUPLICATION_TAG]
240254

241255

242-
def mangle_signature(app, what, name, obj, options, sig, retann):
256+
def mangle_signature(app: SphinxApp, what, name, obj, options, sig, retann):
243257
# Do not try to inspect classes that don't define `__init__`
244258
if inspect.isclass(obj) and (
245259
not hasattr(obj, "__init__")
@@ -273,7 +287,7 @@ def _clean_text_signature(sig):
273287
return start_sig + sig + ")"
274288

275289

276-
def setup(app, get_doc_object_=get_doc_object):
290+
def setup(app: SphinxApp, get_doc_object_=get_doc_object):
277291
if not hasattr(app, "add_config_value"):
278292
return None # probably called by nose, better bail out
279293

@@ -299,6 +313,7 @@ def setup(app, get_doc_object_=get_doc_object):
299313
app.add_config_value("numpydoc_xref_ignore", set(), True, types=[set, str])
300314
app.add_config_value("numpydoc_validation_checks", set(), True)
301315
app.add_config_value("numpydoc_validation_exclude", set(), False)
316+
app.add_config_value("numpydoc_validation_exclude_files", set(), False)
302317
app.add_config_value("numpydoc_validation_overrides", dict(), False)
303318

304319
# Extra mangling domains
@@ -309,7 +324,7 @@ def setup(app, get_doc_object_=get_doc_object):
309324
return metadata
310325

311326

312-
def update_config(app, config=None):
327+
def update_config(app: SphinxApp, config=None):
313328
"""Update the configuration with default values."""
314329
if config is None: # needed for testing and old Sphinx
315330
config = app.config
@@ -342,6 +357,21 @@ def update_config(app, config=None):
342357
)
343358
config.numpydoc_validation_excluder = exclude_expr
344359

360+
# Generate the regexp for files to ignore during validation
361+
if isinstance(config.numpydoc_validation_exclude_files, str):
362+
raise ValueError(
363+
f"numpydoc_validation_exclude_files must be a container of strings, "
364+
f"e.g. [{config.numpydoc_validation_exclude_files!r}]."
365+
)
366+
367+
config.numpydoc_validation_files_excluder = None
368+
if config.numpydoc_validation_exclude_files:
369+
exclude_files_expr = re.compile(
370+
r"|".join(exp for exp in config.numpydoc_validation_exclude_files)
371+
)
372+
config.numpydoc_validation_files_excluder = exclude_files_expr
373+
374+
# Generate the regexp for validation overrides
345375
for check, patterns in config.numpydoc_validation_overrides.items():
346376
config.numpydoc_validation_overrides[check] = re.compile(
347377
r"|".join(exp for exp in patterns)

numpydoc/tests/test_docscrape.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,6 +1593,7 @@ def __init__(self, a, b):
15931593
# numpydoc.update_config fails if this config option not present
15941594
self.numpydoc_validation_checks = set()
15951595
self.numpydoc_validation_exclude = set()
1596+
self.numpydoc_validation_exclude_files = set()
15961597
self.numpydoc_validation_overrides = dict()
15971598

15981599
xref_aliases_complete = deepcopy(DEFAULT_LINKS)

numpydoc/tests/test_numpydoc.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class MockConfig:
3131
numpydoc_attributes_as_param_list = True
3232
numpydoc_validation_checks = set()
3333
numpydoc_validation_exclude = set()
34+
numpydoc_validation_exclude_files = set()
3435
numpydoc_validation_overrides = dict()
3536

3637

@@ -287,6 +288,62 @@ def test_clean_backrefs():
287288
assert "id1" in citation["backrefs"]
288289

289290

291+
@pytest.mark.parametrize(
292+
"exclude_files, has_warnings",
293+
[
294+
(
295+
[
296+
r"^doesnt_match_any_file$",
297+
],
298+
True,
299+
),
300+
(
301+
[
302+
r"^test_numpydoc$",
303+
],
304+
False,
305+
),
306+
],
307+
)
308+
def test_mangle_skip_exclude_files(exclude_files, has_warnings):
309+
"""
310+
Check that the regex expressions in numpydoc_validation_files_exclude
311+
are correctly used to skip checks on files that match the patterns.
312+
"""
313+
314+
def process_something_noop_function():
315+
"""Process something."""
316+
317+
app = MockApp()
318+
app.config.numpydoc_validation_checks = {"all"}
319+
320+
# Class attributes for config persist - need to reset them to unprocessed states.
321+
app.config.numpydoc_validation_exclude = set() # Reset to default...
322+
app.config.numpydoc_validation_overrides = dict() # Reset to default...
323+
324+
app.config.numpydoc_validation_exclude_files = exclude_files
325+
update_config(app)
326+
327+
# Setup for catching warnings
328+
status, warning = StringIO(), StringIO()
329+
logging.setup(app, status, warning)
330+
331+
# Simulate a file that matches the exclude pattern
332+
docname = "test_numpydoc"
333+
mangle_docstrings(
334+
app,
335+
"function",
336+
process_something_noop_function.__name__,
337+
process_something_noop_function,
338+
None,
339+
process_something_noop_function.__doc__.split("\n"),
340+
)
341+
342+
# Are warnings generated?
343+
print(warning.getvalue())
344+
assert bool(warning.getvalue()) is has_warnings
345+
346+
290347
if __name__ == "__main__":
291348
import pytest
292349

0 commit comments

Comments
 (0)