docstring-format-checker 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docstring_format_checker/__init__.py +22 -0
- docstring_format_checker/cli.py +716 -0
- docstring_format_checker/config.py +334 -0
- docstring_format_checker/core.py +671 -0
- docstring_format_checker/utils/__init__.py +0 -0
- docstring_format_checker/utils/exceptions.py +39 -0
- docstring_format_checker-0.1.0.dist-info/METADATA +467 -0
- docstring_format_checker-0.1.0.dist-info/RECORD +10 -0
- docstring_format_checker-0.1.0.dist-info/WHEEL +4 -0
- docstring_format_checker-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
# ============================================================================ #
|
|
2
|
+
# #
|
|
3
|
+
# Title: Title #
|
|
4
|
+
# Purpose: Purpose #
|
|
5
|
+
# Notes: Notes #
|
|
6
|
+
# Author: chrimaho #
|
|
7
|
+
# Created: Created #
|
|
8
|
+
# References: References #
|
|
9
|
+
# Sources: Sources #
|
|
10
|
+
# Edited: Edited #
|
|
11
|
+
# #
|
|
12
|
+
# ============================================================================ #
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------- #
|
|
16
|
+
# #
|
|
17
|
+
# Overview ####
|
|
18
|
+
# #
|
|
19
|
+
# ---------------------------------------------------------------------------- #
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------- #
|
|
23
|
+
# Description ####
|
|
24
|
+
# ---------------------------------------------------------------------------- #
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
!!! note "Summary"
|
|
29
|
+
Core docstring checking functionality.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------- #
|
|
34
|
+
# #
|
|
35
|
+
# Setup ####
|
|
36
|
+
# #
|
|
37
|
+
# ---------------------------------------------------------------------------- #
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## --------------------------------------------------------------------------- #
|
|
41
|
+
## Imports ####
|
|
42
|
+
## --------------------------------------------------------------------------- #
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ## Python StdLib Imports ----
|
|
46
|
+
import ast
|
|
47
|
+
import fnmatch
|
|
48
|
+
import re
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import Literal, NamedTuple, Optional, Union
|
|
51
|
+
|
|
52
|
+
# ## Local First Party Imports ----
|
|
53
|
+
from docstring_format_checker.config import SectionConfig
|
|
54
|
+
from docstring_format_checker.utils.exceptions import (
|
|
55
|
+
DirectoryNotFoundError,
|
|
56
|
+
DocstringError,
|
|
57
|
+
InvalidFileError,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## --------------------------------------------------------------------------- #
|
|
62
|
+
## Exports ####
|
|
63
|
+
## --------------------------------------------------------------------------- #
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__: list[str] = [
|
|
67
|
+
"DocstringChecker",
|
|
68
|
+
"FunctionAndClassDetails",
|
|
69
|
+
"SectionConfig",
|
|
70
|
+
"DocstringError",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------- #
|
|
75
|
+
# #
|
|
76
|
+
# Main Section ####
|
|
77
|
+
# #
|
|
78
|
+
# ---------------------------------------------------------------------------- #
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FunctionAndClassDetails(NamedTuple):
|
|
82
|
+
"""
|
|
83
|
+
Details about a function or class found in the AST.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
item_type: Literal["function", "class", "method"]
|
|
87
|
+
name: str
|
|
88
|
+
node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef]
|
|
89
|
+
lineno: int
|
|
90
|
+
parent_class: Optional[str] = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DocstringChecker:
|
|
94
|
+
"""
|
|
95
|
+
Main class for checking docstring format and completeness.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, sections_config: list[SectionConfig]) -> None:
|
|
99
|
+
"""
|
|
100
|
+
!!! note "Summary"
|
|
101
|
+
Initialize the docstring checker.
|
|
102
|
+
|
|
103
|
+
Params:
|
|
104
|
+
sections_config (list[SectionConfig]):
|
|
105
|
+
List of section configurations to check against.
|
|
106
|
+
"""
|
|
107
|
+
self.sections_config: list[SectionConfig] = sections_config
|
|
108
|
+
self.required_sections: list[SectionConfig] = [s for s in sections_config if s.required]
|
|
109
|
+
self.optional_sections: list[SectionConfig] = [s for s in sections_config if not s.required]
|
|
110
|
+
|
|
111
|
+
def check_file(self, file_path: Union[str, Path]) -> list[DocstringError]:
|
|
112
|
+
"""
|
|
113
|
+
!!! note "Summary"
|
|
114
|
+
Check docstrings in a Python file.
|
|
115
|
+
|
|
116
|
+
Params:
|
|
117
|
+
file_path (Union[str, Path]):
|
|
118
|
+
Path to the Python file to check.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
(list[DocstringError]):
|
|
122
|
+
List of DocstringError objects for any validation failures.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
(FileNotFoundError):
|
|
126
|
+
If the file doesn't exist.
|
|
127
|
+
(InvalidFileError):
|
|
128
|
+
If the file is not a Python file.
|
|
129
|
+
(UnicodeError):
|
|
130
|
+
If the file can't be decoded.
|
|
131
|
+
(SyntaxError):
|
|
132
|
+
If the file contains invalid Python syntax.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
file_path = Path(file_path)
|
|
136
|
+
if not file_path.exists():
|
|
137
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
138
|
+
|
|
139
|
+
if file_path.suffix != ".py":
|
|
140
|
+
raise InvalidFileError(f"File must be a Python file (.py): {file_path}")
|
|
141
|
+
|
|
142
|
+
# Read and parse the file
|
|
143
|
+
try:
|
|
144
|
+
with open(file_path, encoding="utf-8") as f:
|
|
145
|
+
content: str = f.read()
|
|
146
|
+
except UnicodeDecodeError as e:
|
|
147
|
+
raise UnicodeError(f"Cannot decode file {file_path}: {e}") from e
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
tree: ast.Module = ast.parse(content)
|
|
151
|
+
except SyntaxError as e:
|
|
152
|
+
raise SyntaxError(f"Invalid Python syntax in {file_path}: {e}") from e
|
|
153
|
+
|
|
154
|
+
# Extract all functions and classes
|
|
155
|
+
items: list[FunctionAndClassDetails] = self._extract_items(tree)
|
|
156
|
+
|
|
157
|
+
# Check each item
|
|
158
|
+
errors: list[DocstringError] = []
|
|
159
|
+
for item in items:
|
|
160
|
+
try:
|
|
161
|
+
self._check_single_docstring(item, str(file_path))
|
|
162
|
+
except DocstringError as e:
|
|
163
|
+
errors.append(e)
|
|
164
|
+
|
|
165
|
+
return errors
|
|
166
|
+
|
|
167
|
+
def check_directory(
|
|
168
|
+
self,
|
|
169
|
+
directory_path: Union[str, Path],
|
|
170
|
+
recursive: bool = True,
|
|
171
|
+
exclude_patterns: Optional[list[str]] = None,
|
|
172
|
+
) -> dict[str, list[DocstringError]]:
|
|
173
|
+
"""
|
|
174
|
+
!!! note "Summary"
|
|
175
|
+
Check docstrings in all Python files in a directory.
|
|
176
|
+
|
|
177
|
+
Params:
|
|
178
|
+
directory_path (Union[str, Path]):
|
|
179
|
+
Path to the directory to check.
|
|
180
|
+
recursive (bool):
|
|
181
|
+
Whether to check subdirectories recursively.
|
|
182
|
+
exclude_patterns (Optional[list[str]]):
|
|
183
|
+
List of glob patterns to exclude.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
(FileNotFoundError):
|
|
187
|
+
If the directory doesn't exist.
|
|
188
|
+
(DirectoryNotFoundError):
|
|
189
|
+
If the path is not a directory.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
(dict[str, list[DocstringError]]):
|
|
193
|
+
Dictionary mapping file paths to lists of DocstringError objects.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
directory_path = Path(directory_path)
|
|
197
|
+
if not directory_path.exists():
|
|
198
|
+
raise FileNotFoundError(f"Directory not found: {directory_path}")
|
|
199
|
+
|
|
200
|
+
if not directory_path.is_dir():
|
|
201
|
+
raise DirectoryNotFoundError(f"Path is not a directory: {directory_path}")
|
|
202
|
+
|
|
203
|
+
# Find all Python files
|
|
204
|
+
if recursive:
|
|
205
|
+
pattern = "**/*.py"
|
|
206
|
+
else:
|
|
207
|
+
pattern = "*.py"
|
|
208
|
+
|
|
209
|
+
python_files: list[Path] = list(directory_path.glob(pattern))
|
|
210
|
+
|
|
211
|
+
# Filter out excluded patterns
|
|
212
|
+
if exclude_patterns:
|
|
213
|
+
filtered_files: list[Path] = []
|
|
214
|
+
for file_path in python_files:
|
|
215
|
+
relative_path: Path = file_path.relative_to(directory_path)
|
|
216
|
+
should_exclude = False
|
|
217
|
+
for pattern in exclude_patterns:
|
|
218
|
+
if fnmatch.fnmatch(str(relative_path), pattern):
|
|
219
|
+
should_exclude = True
|
|
220
|
+
break
|
|
221
|
+
if not should_exclude:
|
|
222
|
+
filtered_files.append(file_path)
|
|
223
|
+
python_files = filtered_files
|
|
224
|
+
|
|
225
|
+
# Check each file
|
|
226
|
+
results: dict[str, list[DocstringError]] = {}
|
|
227
|
+
for file_path in python_files:
|
|
228
|
+
try:
|
|
229
|
+
errors: list[DocstringError] = self.check_file(file_path)
|
|
230
|
+
if errors: # Only include files with errors
|
|
231
|
+
results[str(file_path)] = errors
|
|
232
|
+
except (FileNotFoundError, ValueError, SyntaxError) as e:
|
|
233
|
+
# Create a special error for file-level issues
|
|
234
|
+
error = DocstringError(
|
|
235
|
+
message=str(e),
|
|
236
|
+
file_path=str(file_path),
|
|
237
|
+
line_number=0,
|
|
238
|
+
item_name="",
|
|
239
|
+
item_type="file",
|
|
240
|
+
)
|
|
241
|
+
results[str(file_path)] = [error]
|
|
242
|
+
|
|
243
|
+
return results
|
|
244
|
+
|
|
245
|
+
def _extract_items(self, tree: ast.AST) -> list[FunctionAndClassDetails]:
|
|
246
|
+
"""
|
|
247
|
+
!!! note "Summary"
|
|
248
|
+
Extract all functions and classes from the AST.
|
|
249
|
+
|
|
250
|
+
Params:
|
|
251
|
+
tree (ast.AST):
|
|
252
|
+
The Abstract Syntax Tree (AST) to extract items from.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
(list[FunctionAndClassDetails]):
|
|
256
|
+
A list of extracted function and class details.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
items: list[FunctionAndClassDetails] = []
|
|
260
|
+
|
|
261
|
+
class ItemVisitor(ast.NodeVisitor):
|
|
262
|
+
|
|
263
|
+
def __init__(self) -> None:
|
|
264
|
+
self.class_stack: list[str] = []
|
|
265
|
+
|
|
266
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
267
|
+
if not node.name.startswith("_"): # Skip private classes
|
|
268
|
+
items.append(
|
|
269
|
+
FunctionAndClassDetails(
|
|
270
|
+
item_type="class",
|
|
271
|
+
name=node.name,
|
|
272
|
+
node=node,
|
|
273
|
+
lineno=node.lineno,
|
|
274
|
+
parent_class=None,
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Visit methods in this class
|
|
279
|
+
self.class_stack.append(node.name)
|
|
280
|
+
self.generic_visit(node)
|
|
281
|
+
self.class_stack.pop()
|
|
282
|
+
|
|
283
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
284
|
+
self._visit_function(node)
|
|
285
|
+
|
|
286
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
287
|
+
self._visit_function(node)
|
|
288
|
+
|
|
289
|
+
def _visit_function(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> None:
|
|
290
|
+
"""Visit function definition node (sync or async)."""
|
|
291
|
+
if not node.name.startswith("_"): # Skip private functions
|
|
292
|
+
item_type: Literal["function", "method"] = "method" if self.class_stack else "function"
|
|
293
|
+
parent_class: Optional[str] = self.class_stack[-1] if self.class_stack else None
|
|
294
|
+
|
|
295
|
+
items.append(
|
|
296
|
+
FunctionAndClassDetails(
|
|
297
|
+
item_type=item_type,
|
|
298
|
+
name=node.name,
|
|
299
|
+
node=node,
|
|
300
|
+
lineno=node.lineno,
|
|
301
|
+
parent_class=parent_class,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
self.generic_visit(node)
|
|
306
|
+
|
|
307
|
+
visitor = ItemVisitor()
|
|
308
|
+
visitor.visit(tree)
|
|
309
|
+
|
|
310
|
+
return items
|
|
311
|
+
|
|
312
|
+
def _check_single_docstring(self, item: FunctionAndClassDetails, file_path: str) -> None:
|
|
313
|
+
"""
|
|
314
|
+
!!! note "Summary"
|
|
315
|
+
Check a single function or class docstring.
|
|
316
|
+
|
|
317
|
+
Params:
|
|
318
|
+
item (FunctionAndClassDetails):
|
|
319
|
+
The function or class to check.
|
|
320
|
+
file_path (str):
|
|
321
|
+
The path to the file containing the item.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
(None):
|
|
325
|
+
Nothing is returned.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
docstring: Optional[str] = ast.get_docstring(item.node)
|
|
329
|
+
|
|
330
|
+
# Check if any required sections apply to this item type
|
|
331
|
+
requires_docstring = False
|
|
332
|
+
applicable_sections: list[SectionConfig] = []
|
|
333
|
+
|
|
334
|
+
for section in self.sections_config:
|
|
335
|
+
if section.required:
|
|
336
|
+
# Check if this section applies to this item type
|
|
337
|
+
if section.type == "free_text":
|
|
338
|
+
# Free text sections apply only to functions and methods, not classes
|
|
339
|
+
if isinstance(item.node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
340
|
+
requires_docstring = True
|
|
341
|
+
applicable_sections.append(section)
|
|
342
|
+
elif section.type == "list_name_and_type":
|
|
343
|
+
if section.name.lower() == "params" and isinstance(
|
|
344
|
+
item.node, (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
345
|
+
):
|
|
346
|
+
# Params only apply to functions/methods
|
|
347
|
+
requires_docstring = True
|
|
348
|
+
applicable_sections.append(section)
|
|
349
|
+
elif section.name.lower() in ["returns", "return"] and isinstance(
|
|
350
|
+
item.node, (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
351
|
+
):
|
|
352
|
+
# Returns only apply to functions/methods
|
|
353
|
+
requires_docstring = True
|
|
354
|
+
applicable_sections.append(section)
|
|
355
|
+
elif section.type in ["list_type", "list_name"]:
|
|
356
|
+
# These sections apply to functions/methods that might have them
|
|
357
|
+
if isinstance(item.node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
358
|
+
requires_docstring = True
|
|
359
|
+
applicable_sections.append(section)
|
|
360
|
+
|
|
361
|
+
if not docstring:
|
|
362
|
+
if requires_docstring:
|
|
363
|
+
message: str = f"Missing docstring for {item.item_type}"
|
|
364
|
+
raise DocstringError(
|
|
365
|
+
message=message,
|
|
366
|
+
file_path=file_path,
|
|
367
|
+
line_number=item.lineno,
|
|
368
|
+
item_name=item.name,
|
|
369
|
+
item_type=item.item_type,
|
|
370
|
+
)
|
|
371
|
+
return # No docstring required
|
|
372
|
+
|
|
373
|
+
# Validate docstring sections if docstring exists
|
|
374
|
+
self._validate_docstring_sections(docstring, item, file_path)
|
|
375
|
+
|
|
376
|
+
def _validate_docstring_sections(
|
|
377
|
+
self,
|
|
378
|
+
docstring: str,
|
|
379
|
+
item: FunctionAndClassDetails,
|
|
380
|
+
file_path: str,
|
|
381
|
+
) -> None:
|
|
382
|
+
"""
|
|
383
|
+
!!! note "Summary"
|
|
384
|
+
Validate the sections within a docstring.
|
|
385
|
+
|
|
386
|
+
Params:
|
|
387
|
+
docstring (str):
|
|
388
|
+
The docstring to validate.
|
|
389
|
+
item (FunctionAndClassDetails):
|
|
390
|
+
The function or class to check.
|
|
391
|
+
file_path (str):
|
|
392
|
+
The path to the file containing the item.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
(None):
|
|
396
|
+
Nothing is returned.
|
|
397
|
+
"""
|
|
398
|
+
errors: list[str] = []
|
|
399
|
+
|
|
400
|
+
# Check each required section
|
|
401
|
+
for section in self.required_sections:
|
|
402
|
+
if section.type == "free_text":
|
|
403
|
+
if not self._check_free_text_section(docstring, section):
|
|
404
|
+
errors.append(f"Missing required section: {section.name}")
|
|
405
|
+
|
|
406
|
+
elif section.type == "list_name_and_type":
|
|
407
|
+
if section.name.lower() == "params" and isinstance(item.node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
408
|
+
if not self._check_params_section(docstring, item.node):
|
|
409
|
+
errors.append("Missing or invalid Params section")
|
|
410
|
+
elif section.name.lower() in ["returns", "return"]:
|
|
411
|
+
if not self._check_returns_section(docstring):
|
|
412
|
+
errors.append("Missing or invalid Returns section")
|
|
413
|
+
|
|
414
|
+
elif section.type == "list_type":
|
|
415
|
+
if section.name.lower() in ["raises", "raise"]:
|
|
416
|
+
if not self._check_raises_section(docstring):
|
|
417
|
+
errors.append("Missing or invalid Raises section")
|
|
418
|
+
elif section.name.lower() in ["yields", "yield"]:
|
|
419
|
+
if not self._check_yields_section(docstring):
|
|
420
|
+
errors.append("Missing or invalid Yields section")
|
|
421
|
+
|
|
422
|
+
elif section.type == "list_name":
|
|
423
|
+
# Simple name sections - check if they exist
|
|
424
|
+
if not self._check_simple_section(docstring, section.name):
|
|
425
|
+
errors.append(f"Missing required section: {section.name}")
|
|
426
|
+
|
|
427
|
+
# Check section order
|
|
428
|
+
order_errors: list[str] = self._check_section_order(docstring)
|
|
429
|
+
errors.extend(order_errors)
|
|
430
|
+
|
|
431
|
+
# Check for mutual exclusivity (returns vs yields)
|
|
432
|
+
if self._has_both_returns_and_yields(docstring):
|
|
433
|
+
errors.append("Docstring cannot have both Returns and Yields sections")
|
|
434
|
+
|
|
435
|
+
if errors:
|
|
436
|
+
combined_message: str = "; ".join(errors)
|
|
437
|
+
raise DocstringError(
|
|
438
|
+
message=combined_message,
|
|
439
|
+
file_path=file_path,
|
|
440
|
+
line_number=item.lineno,
|
|
441
|
+
item_name=item.name,
|
|
442
|
+
item_type=item.item_type,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def _check_free_text_section(self, docstring: str, section: SectionConfig) -> bool:
|
|
446
|
+
"""
|
|
447
|
+
!!! note "Summary"
|
|
448
|
+
Check if a free text section exists in the docstring.
|
|
449
|
+
|
|
450
|
+
Params:
|
|
451
|
+
docstring (str):
|
|
452
|
+
The docstring to check.
|
|
453
|
+
section (SectionConfig):
|
|
454
|
+
The section configuration to validate.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
(bool):
|
|
458
|
+
`True` if the section exists, `False` otherwise.
|
|
459
|
+
"""
|
|
460
|
+
if section.admonition and section.prefix:
|
|
461
|
+
# Format like: !!! note "Summary"
|
|
462
|
+
pattern = rf'{re.escape(section.prefix)}\s+{re.escape(section.admonition)}\s+".*{re.escape(section.name)}"'
|
|
463
|
+
return bool(re.search(pattern, docstring, re.IGNORECASE))
|
|
464
|
+
elif section.name.lower() in ["summary"]:
|
|
465
|
+
# For summary, accept either formal format or simple docstring
|
|
466
|
+
formal_pattern = r'!!! note "Summary"'
|
|
467
|
+
if re.search(formal_pattern, docstring, re.IGNORECASE):
|
|
468
|
+
return True
|
|
469
|
+
# Accept any non-empty docstring as summary
|
|
470
|
+
return len(docstring.strip()) > 0
|
|
471
|
+
elif section.name.lower() in ["examples", "example"]:
|
|
472
|
+
# Look for examples section
|
|
473
|
+
return bool(re.search(r'\?\?\?\+ example "Examples"', docstring, re.IGNORECASE))
|
|
474
|
+
|
|
475
|
+
return True # Default to true for unknown free text sections
|
|
476
|
+
|
|
477
|
+
def _check_params_section(self, docstring: str, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> bool:
|
|
478
|
+
"""
|
|
479
|
+
!!! note "Summary"
|
|
480
|
+
Check if the Params section exists and documents all parameters.
|
|
481
|
+
|
|
482
|
+
Params:
|
|
483
|
+
docstring (str):
|
|
484
|
+
The docstring to check.
|
|
485
|
+
node (Union[ast.FunctionDef, ast.AsyncFunctionDef]):
|
|
486
|
+
The function node to check.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
(bool):
|
|
490
|
+
`True` if the section exists and is valid, `False` otherwise.
|
|
491
|
+
"""
|
|
492
|
+
# Get function parameters (excluding 'self' for methods)
|
|
493
|
+
params: list[str] = [arg.arg for arg in node.args.args if arg.arg != "self"]
|
|
494
|
+
|
|
495
|
+
if not params:
|
|
496
|
+
return True # No parameters to document
|
|
497
|
+
|
|
498
|
+
# Check if Params section exists
|
|
499
|
+
if not re.search(r"Params:", docstring):
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
# Check each parameter is documented
|
|
503
|
+
for param in params:
|
|
504
|
+
param_pattern: str = rf"{re.escape(param)}\s*\([^)]+\):"
|
|
505
|
+
if not re.search(param_pattern, docstring):
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
return True
|
|
509
|
+
|
|
510
|
+
def _check_returns_section(self, docstring: str) -> bool:
|
|
511
|
+
"""
|
|
512
|
+
!!! note "Summary"
|
|
513
|
+
Check if the Returns section exists.
|
|
514
|
+
|
|
515
|
+
Params:
|
|
516
|
+
docstring (str):
|
|
517
|
+
The docstring to check.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
(bool):
|
|
521
|
+
`True` if the section exists, `False` otherwise.
|
|
522
|
+
"""
|
|
523
|
+
return bool(re.search(r"Returns:", docstring))
|
|
524
|
+
|
|
525
|
+
def _check_raises_section(self, docstring: str) -> bool:
|
|
526
|
+
"""
|
|
527
|
+
!!! note "Summary"
|
|
528
|
+
Check if the Raises section exists.
|
|
529
|
+
|
|
530
|
+
Params:
|
|
531
|
+
docstring (str):
|
|
532
|
+
The docstring to check.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
(bool):
|
|
536
|
+
`True` if the section exists, `False` otherwise.
|
|
537
|
+
"""
|
|
538
|
+
return bool(re.search(r"Raises:", docstring))
|
|
539
|
+
|
|
540
|
+
def _has_both_returns_and_yields(self, docstring: str) -> bool:
|
|
541
|
+
"""
|
|
542
|
+
!!! note "Summary"
|
|
543
|
+
Check if docstring has both Returns and Yields sections.
|
|
544
|
+
|
|
545
|
+
Params:
|
|
546
|
+
docstring (str):
|
|
547
|
+
The docstring to check.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
(bool):
|
|
551
|
+
`True` if the section exists, `False` otherwise.
|
|
552
|
+
"""
|
|
553
|
+
has_returns = bool(re.search(r"Returns:", docstring))
|
|
554
|
+
has_yields = bool(re.search(r"Yields:", docstring))
|
|
555
|
+
return has_returns and has_yields
|
|
556
|
+
|
|
557
|
+
def _check_section_order(self, docstring: str) -> list[str]:
|
|
558
|
+
"""
|
|
559
|
+
!!! note "Summary"
|
|
560
|
+
Check that sections appear in the correct order.
|
|
561
|
+
|
|
562
|
+
Params:
|
|
563
|
+
docstring (str):
|
|
564
|
+
The docstring to check.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
(list[str]):
|
|
568
|
+
A list of error messages, if any.
|
|
569
|
+
"""
|
|
570
|
+
# Build expected order from configuration
|
|
571
|
+
section_patterns: list[tuple[str, str]] = []
|
|
572
|
+
for section in sorted(self.sections_config, key=lambda x: x.order):
|
|
573
|
+
if section.type == "free_text" and section.admonition and section.prefix:
|
|
574
|
+
pattern: str = (
|
|
575
|
+
rf'{re.escape(section.prefix)}\s+{re.escape(section.admonition)}\s+".*{re.escape(section.name)}"'
|
|
576
|
+
)
|
|
577
|
+
section_patterns.append((pattern, section.name))
|
|
578
|
+
elif section.name.lower() == "params":
|
|
579
|
+
section_patterns.append((r"Params:", "Params"))
|
|
580
|
+
elif section.name.lower() in ["returns", "return"]:
|
|
581
|
+
section_patterns.append((r"Returns:", "Returns"))
|
|
582
|
+
elif section.name.lower() in ["yields", "yield"]:
|
|
583
|
+
section_patterns.append((r"Yields:", "Yields"))
|
|
584
|
+
elif section.name.lower() in ["raises", "raise"]:
|
|
585
|
+
section_patterns.append((r"Raises:", "Raises"))
|
|
586
|
+
|
|
587
|
+
# Add some default patterns for common sections
|
|
588
|
+
default_patterns: list[tuple[str, str]] = [
|
|
589
|
+
(r'!!! note "Summary"', "Summary"),
|
|
590
|
+
(r'!!! details "Details"', "Details"),
|
|
591
|
+
(r'\?\?\?\+ example "Examples"', "Examples"),
|
|
592
|
+
(r'\?\?\?\+ success "Credit"', "Credit"),
|
|
593
|
+
(r'\?\?\?\+ calculation "Equation"', "Equation"),
|
|
594
|
+
(r'\?\?\?\+ info "Notes"', "Notes"),
|
|
595
|
+
(r'\?\?\? question "References"', "References"),
|
|
596
|
+
(r'\?\?\? tip "See Also"', "See Also"),
|
|
597
|
+
]
|
|
598
|
+
|
|
599
|
+
all_patterns: list[tuple[str, str]] = section_patterns + default_patterns
|
|
600
|
+
|
|
601
|
+
found_sections: list[tuple[int, str]] = []
|
|
602
|
+
for pattern, section_name in all_patterns:
|
|
603
|
+
match: Optional[re.Match[str]] = re.search(pattern, docstring, re.IGNORECASE)
|
|
604
|
+
if match:
|
|
605
|
+
found_sections.append((match.start(), section_name))
|
|
606
|
+
|
|
607
|
+
# Sort by position in docstring
|
|
608
|
+
found_sections.sort(key=lambda x: x[0])
|
|
609
|
+
|
|
610
|
+
# Build expected order
|
|
611
|
+
expected_order: list[str] = [s.name.title() for s in sorted(self.sections_config, key=lambda x: x.order)]
|
|
612
|
+
expected_order.extend(
|
|
613
|
+
[
|
|
614
|
+
"Summary",
|
|
615
|
+
"Details",
|
|
616
|
+
"Examples",
|
|
617
|
+
"Credit",
|
|
618
|
+
"Equation",
|
|
619
|
+
"Notes",
|
|
620
|
+
"References",
|
|
621
|
+
"See Also",
|
|
622
|
+
]
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Check order matches expected order
|
|
626
|
+
errors: list[str] = []
|
|
627
|
+
last_expected_index = -1
|
|
628
|
+
for _, section_name in found_sections:
|
|
629
|
+
try:
|
|
630
|
+
current_index: int = expected_order.index(section_name)
|
|
631
|
+
if current_index < last_expected_index:
|
|
632
|
+
errors.append(f"Section '{section_name}' appears out of order")
|
|
633
|
+
last_expected_index: int = current_index
|
|
634
|
+
except ValueError:
|
|
635
|
+
# Section not in expected order list - might be OK
|
|
636
|
+
pass
|
|
637
|
+
|
|
638
|
+
return errors
|
|
639
|
+
|
|
640
|
+
def _check_yields_section(self, docstring: str) -> bool:
|
|
641
|
+
"""
|
|
642
|
+
!!! note "Summary"
|
|
643
|
+
Check if the Yields section exists.
|
|
644
|
+
|
|
645
|
+
Params:
|
|
646
|
+
docstring (str):
|
|
647
|
+
The docstring to check.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
(bool):
|
|
651
|
+
`True` if the section exists, `False` otherwise.
|
|
652
|
+
"""
|
|
653
|
+
return bool(re.search(r"Yields:", docstring))
|
|
654
|
+
|
|
655
|
+
def _check_simple_section(self, docstring: str, section_name: str) -> bool:
|
|
656
|
+
"""
|
|
657
|
+
!!! note "Summary"
|
|
658
|
+
Check if a simple named section exists.
|
|
659
|
+
|
|
660
|
+
Params:
|
|
661
|
+
docstring (str):
|
|
662
|
+
The docstring to check.
|
|
663
|
+
section_name (str):
|
|
664
|
+
The name of the section to check for.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
(bool):
|
|
668
|
+
`True` if the section exists, `False` otherwise.
|
|
669
|
+
"""
|
|
670
|
+
pattern = rf"{re.escape(section_name)}:"
|
|
671
|
+
return bool(re.search(pattern, docstring, re.IGNORECASE))
|
|
File without changes
|