Skip to content

Commit d0638ec

Browse files
committed
Port inline comments for ignoring checks to sphinx side.
1 parent 506f7f5 commit d0638ec

File tree

6 files changed

+153
-107
lines changed

6 files changed

+153
-107
lines changed

numpydoc/hooks/validate_docstrings.py

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import os
77
import re
88
import sys
9-
import tokenize
109

1110
try:
1211
import tomllib
@@ -20,11 +19,6 @@
2019

2120
from .. import docscrape, validate
2221
from .utils import find_project_root
23-
from ..utils import get_validation_checks
24-
25-
26-
# inline comments that can suppress individual checks per line
27-
IGNORE_COMMENT_PATTERN = re.compile("(?:.* numpydoc ignore[=|:] ?)(.+)")
2822

2923

3024
class AstValidator(validate.Validator):
@@ -171,7 +165,7 @@ def _ignore_issue(self, node: ast.AST, check: str) -> bool:
171165
Return
172166
------
173167
bool
174-
Whether the issue should be exluded from the report.
168+
Whether the issue should be excluded from the report.
175169
"""
176170
if check not in self.config["checks"]:
177171
return True
@@ -328,7 +322,7 @@ def extract_check_overrides(options, config_items):
328322
except configparser.NoSectionError:
329323
pass
330324

331-
options["checks"] = get_validation_checks(options["checks"])
325+
options["checks"] = validate.get_validation_checks(options["checks"])
332326
options["exclude"] = compile_regex(options["exclude"])
333327
return options
334328

@@ -352,23 +346,10 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]":
352346
with open(filepath) as file:
353347
module_node = ast.parse(file.read(), filepath)
354348

355-
with open(filepath) as file:
356-
numpydoc_ignore_comments = {}
357-
last_declaration = 1
358-
declarations = ["def", "class"]
359-
for token in tokenize.generate_tokens(file.readline):
360-
if token.type == tokenize.NAME and token.string in declarations:
361-
last_declaration = token.start[0]
362-
if token.type == tokenize.COMMENT:
363-
match = re.match(IGNORE_COMMENT_PATTERN, token.string)
364-
if match:
365-
rules = match.group(1).split(",")
366-
numpydoc_ignore_comments[last_declaration] = rules
367-
368349
docstring_visitor = DocstringVisitor(
369350
filepath=str(filepath),
370351
config=config,
371-
numpydoc_ignore_comments=numpydoc_ignore_comments,
352+
numpydoc_ignore_comments=validate.extract_ignore_validation_comments(filepath),
372353
)
373354
docstring_visitor.visit(module_node)
374355

numpydoc/numpydoc.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@
3434
raise RuntimeError("Sphinx 5 or newer is required")
3535

3636
from .docscrape_sphinx import get_doc_object
37-
from .utils import get_validation_checks
38-
from .validate import validate, ERROR_MSGS
37+
from .validate import validate, ERROR_MSGS, get_validation_checks
3938
from .xref import DEFAULT_LINKS
4039
from . import __version__
4140

numpydoc/tests/test_utils.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

numpydoc/tests/test_validate.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,75 @@
11
import pytest
22
import warnings
3-
import numpydoc.validate
3+
4+
from numpydoc import validate
45
import numpydoc.tests
56

67

7-
validate_one = numpydoc.validate.validate
8+
validate_one = validate.validate
9+
10+
ALL_CHECKS = set(validate.ERROR_MSGS.keys())
11+
12+
13+
@pytest.mark.parametrize(
14+
["checks", "expected"],
15+
[
16+
[{"all"}, ALL_CHECKS],
17+
[set(), set()],
18+
[{"EX01"}, {"EX01"}],
19+
[{"EX01", "SA01"}, {"EX01", "SA01"}],
20+
[{"all", "EX01", "SA01"}, ALL_CHECKS - {"EX01", "SA01"}],
21+
[{"all", "PR01"}, ALL_CHECKS - {"PR01"}],
22+
],
23+
)
24+
def test_utils_get_validation_checks(checks, expected):
25+
"""Ensure check selection is working."""
26+
assert validate.get_validation_checks(checks) == expected
27+
28+
29+
@pytest.mark.parametrize(
30+
"checks",
31+
[
32+
{"every"},
33+
{None},
34+
{"SM10"},
35+
{"EX01", "SM10"},
36+
],
37+
)
38+
def test_get_validation_checks_validity(checks):
39+
"""Ensure that invalid checks are flagged."""
40+
with pytest.raises(ValueError, match="Unrecognized validation code"):
41+
_ = validate.get_validation_checks(checks)
42+
43+
44+
@pytest.mark.parametrize(
45+
["file_contents", "expected"],
46+
[
47+
["class MyClass:\n pass", {}],
48+
["class MyClass: # numpydoc ignore=EX01\n pass", {1: ["EX01"]}],
49+
[
50+
"class MyClass: # numpydoc ignore= EX01,SA01\n pass",
51+
{1: ["EX01", "SA01"]},
52+
],
53+
[
54+
"class MyClass:\n def my_method(): # numpydoc ignore:EX01\n pass",
55+
{2: ["EX01"]},
56+
],
57+
[
58+
"class MyClass:\n def my_method(): # numpydoc ignore: EX01,PR01\n pass",
59+
{2: ["EX01", "PR01"]},
60+
],
61+
[
62+
"class MyClass: # numpydoc ignore=GL08\n def my_method(): # numpydoc ignore:EX01,PR01\n pass",
63+
{1: ["GL08"], 2: ["EX01", "PR01"]},
64+
],
65+
],
66+
)
67+
def test_extract_ignore_validation_comments(tmp_path, file_contents, expected):
68+
"""Test that extraction of validation ignore comments is working."""
69+
filepath = tmp_path / "ignore_comments.py"
70+
with open(filepath, "w") as file:
71+
file.write(file_contents)
72+
assert validate.extract_ignore_validation_comments(filepath) == expected
873

974

1075
class GoodDocStrings:

numpydoc/utils.py

Lines changed: 0 additions & 41 deletions
This file was deleted.

numpydoc/validate.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55
Call ``validate(object_name_to_validate)`` to get a dictionary
66
with all the detected errors.
77
"""
8+
9+
from copy import deepcopy
10+
from typing import Set
811
import ast
912
import collections
1013
import importlib
1114
import inspect
15+
import os
1216
import pydoc
1317
import re
1418
import textwrap
19+
import tokenize
20+
1521
from .docscrape import get_doc_object
1622

1723

@@ -101,6 +107,72 @@
101107
# Ignore these when evaluating end-of-line-"." checks
102108
IGNORE_STARTS = (" ", "* ", "- ")
103109

110+
# inline comments that can suppress individual checks per line
111+
IGNORE_COMMENT_PATTERN = re.compile("(?:.* numpydoc ignore[=|:] ?)(.+)")
112+
113+
114+
def extract_ignore_validation_comments(filepath: os.PathLike) -> dict[int, list[str]]:
115+
"""
116+
Extract inline comments indicating certain validation checks should be ignored.
117+
118+
Parameters
119+
----------
120+
filepath : os.PathLike
121+
Path to the file being inspected.
122+
123+
Returns
124+
-------
125+
dict[int, list[str]]
126+
Mapping of line number to a list of checks to ignore.
127+
"""
128+
with open(filepath) as file:
129+
numpydoc_ignore_comments = {}
130+
last_declaration = 1
131+
declarations = ["def", "class"]
132+
for token in tokenize.generate_tokens(file.readline):
133+
if token.type == tokenize.NAME and token.string in declarations:
134+
last_declaration = token.start[0]
135+
if token.type == tokenize.COMMENT:
136+
match = re.match(IGNORE_COMMENT_PATTERN, token.string)
137+
if match:
138+
rules = match.group(1).split(",")
139+
numpydoc_ignore_comments[last_declaration] = rules
140+
return numpydoc_ignore_comments
141+
142+
143+
def get_validation_checks(validation_checks: Set[str]) -> Set[str]:
144+
"""
145+
Get the set of validation checks to report on.
146+
147+
Parameters
148+
----------
149+
validation_checks : set[str]
150+
A set of validation checks to report on. If the set is ``{"all"}``,
151+
all checks will be reported. If the set contains just specific checks,
152+
only those will be reported on. If the set contains both ``"all"`` and
153+
specific checks, all checks except those included in the set will be
154+
reported on.
155+
156+
Returns
157+
-------
158+
set[str]
159+
The set of validation checks to report on.
160+
"""
161+
valid_error_codes = set(ERROR_MSGS.keys())
162+
if "all" in validation_checks:
163+
block = deepcopy(validation_checks)
164+
validation_checks = valid_error_codes - block
165+
166+
# Ensure that the validation check set contains only valid error codes
167+
invalid_error_codes = validation_checks - valid_error_codes
168+
if invalid_error_codes:
169+
raise ValueError(
170+
f"Unrecognized validation code(s) in numpydoc_validation_checks "
171+
f"config value: {invalid_error_codes}"
172+
)
173+
174+
return validation_checks
175+
104176

105177
def error(code, **kwargs):
106178
"""
@@ -506,9 +578,15 @@ def validate(obj_name, validator_cls=None, **validator_kwargs):
506578
else:
507579
doc = validator_cls(obj_name=obj_name, **validator_kwargs)
508580

581+
# module docstring will be lineno 0, which we change to 1 for readability of the output
582+
ignore_validation_comments = extract_ignore_validation_comments(
583+
doc.source_file_name
584+
).get(doc.source_file_def_line or 1, [])
585+
509586
errs = []
510587
if not doc.raw_doc:
511-
errs.append(error("GL08"))
588+
if "GL08" not in ignore_validation_comments:
589+
errs.append(error("GL08"))
512590
return {
513591
"type": doc.type,
514592
"docstring": doc.clean_doc,
@@ -630,6 +708,9 @@ def validate(obj_name, validator_cls=None, **validator_kwargs):
630708

631709
if not doc.examples:
632710
errs.append(error("EX01"))
711+
712+
errs = [err for err in errs if err[0] not in ignore_validation_comments]
713+
633714
return {
634715
"type": doc.type,
635716
"docstring": doc.clean_doc,

0 commit comments

Comments
 (0)