docstring-format-checker 0.2.0__tar.gz → 0.4.0__tar.gz
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-0.2.0 → docstring_format_checker-0.4.0}/PKG-INFO +1 -1
- {docstring_format_checker-0.2.0 → docstring_format_checker-0.4.0}/pyproject.toml +1 -1
- {docstring_format_checker-0.2.0 → docstring_format_checker-0.4.0}/src/docstring_format_checker/__init__.py +1 -1
- {docstring_format_checker-0.2.0 → docstring_format_checker-0.4.0}/src/docstring_format_checker/cli.py +33 -3
- {docstring_format_checker-0.2.0 → docstring_format_checker-0.4.0}/src/docstring_format_checker/config.py +34 -2
- {docstring_format_checker-0.2.0 → docstring_format_checker-0.4.0}/src/docstring_format_checker/core.py +299 -5
- {docstring_format_checker-0.2.0 → docstring_format_checker-0.4.0}/README.md +0 -0
- {docstring_format_checker-0.2.0 → docstring_format_checker-0.4.0}/src/docstring_format_checker/utils/__init__.py +0 -0
- {docstring_format_checker-0.2.0 → docstring_format_checker-0.4.0}/src/docstring_format_checker/utils/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docstring-format-checker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A CLI tool to check and validate Python docstring formatting and completeness
|
|
5
5
|
Author: Chris Mahoney
|
|
6
6
|
Author-email: Chris Mahoney <docstring-format-checker@data-science-extensions.com>
|
|
@@ -4,7 +4,7 @@ Docstring Format Checker.
|
|
|
4
4
|
A CLI tool to check and validate Python docstring formatting and completeness.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "v0.
|
|
7
|
+
__version__ = "v0.4.0"
|
|
8
8
|
__author__ = "Chris Mahoney"
|
|
9
9
|
__email__ = "docstring-format-checker@data-science-extensions.com"
|
|
10
10
|
|
|
@@ -302,6 +302,29 @@ def _show_check_examples_callback(ctx: Context, param: CallbackParam, value: boo
|
|
|
302
302
|
raise Exit()
|
|
303
303
|
|
|
304
304
|
|
|
305
|
+
def _format_error_messages(error_message: str) -> str:
|
|
306
|
+
"""
|
|
307
|
+
!!! note "Summary"
|
|
308
|
+
Format error messages for better readability in CLI output.
|
|
309
|
+
|
|
310
|
+
Params:
|
|
311
|
+
error_message (str):
|
|
312
|
+
The raw error message that may contain semicolon-separated errors
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
(str):
|
|
316
|
+
Formatted error message with each error prefixed with "- " and separated by ";\n"
|
|
317
|
+
"""
|
|
318
|
+
if "; " in error_message:
|
|
319
|
+
# Split by semicolon and rejoin with proper formatting
|
|
320
|
+
errors: list[str] = error_message.split("; ")
|
|
321
|
+
formatted_errors: list[str] = [f"- {error.strip()}" for error in errors if error.strip()]
|
|
322
|
+
return ";\n".join(formatted_errors) + "."
|
|
323
|
+
else:
|
|
324
|
+
# Single error message
|
|
325
|
+
return f"- {error_message.strip()}."
|
|
326
|
+
|
|
327
|
+
|
|
305
328
|
def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verbose: bool) -> int:
|
|
306
329
|
"""
|
|
307
330
|
!!! note "Summary"
|
|
@@ -340,12 +363,16 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
|
|
|
340
363
|
for file_path, errors in results.items():
|
|
341
364
|
for i, error in enumerate(errors):
|
|
342
365
|
file_display: str = file_path if i == 0 else ""
|
|
366
|
+
|
|
367
|
+
# Format error message with improved formatting
|
|
368
|
+
formatted_error_message: str = _format_error_messages(error.message)
|
|
369
|
+
|
|
343
370
|
table.add_row(
|
|
344
371
|
file_display,
|
|
345
372
|
str(error.line_number) if error.line_number > 0 else "",
|
|
346
373
|
error.item_name,
|
|
347
374
|
error.item_type,
|
|
348
|
-
|
|
375
|
+
formatted_error_message,
|
|
349
376
|
)
|
|
350
377
|
console.print(table)
|
|
351
378
|
|
|
@@ -354,12 +381,15 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
|
|
|
354
381
|
for file_path, errors in results.items():
|
|
355
382
|
console.print(f"{NEW_LINE}[cyan]{file_path}[/cyan]")
|
|
356
383
|
for error in errors:
|
|
384
|
+
# Format error message with improved formatting
|
|
385
|
+
formatted_error_message: str = _format_error_messages(error.message)
|
|
386
|
+
|
|
357
387
|
if error.line_number > 0:
|
|
358
388
|
console.print(
|
|
359
|
-
f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {
|
|
389
|
+
f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {formatted_error_message}"
|
|
360
390
|
)
|
|
361
391
|
else:
|
|
362
|
-
console.print(f" [red]Error[/red]: {
|
|
392
|
+
console.print(f" [red]Error[/red]: {formatted_error_message}")
|
|
363
393
|
|
|
364
394
|
# Summary
|
|
365
395
|
console.print(f"{NEW_LINE}[red]Found {total_errors} error(s) in {total_files} file(s)[/red]")
|
|
@@ -111,16 +111,43 @@ class SectionConfig:
|
|
|
111
111
|
order: int
|
|
112
112
|
name: str
|
|
113
113
|
type: Literal["free_text", "list_name", "list_type", "list_name_and_type"]
|
|
114
|
-
admonition: str =
|
|
114
|
+
admonition: Union[bool, str] = False
|
|
115
115
|
prefix: str = "" # Support any prefix string
|
|
116
116
|
required: bool = False
|
|
117
117
|
message: str = "" # Optional message for validation errors
|
|
118
118
|
|
|
119
119
|
def __post_init__(self) -> None:
|
|
120
120
|
"""Validate configuration after initialization."""
|
|
121
|
+
self._validate_types()
|
|
122
|
+
self._validate_admonition_prefix_combination()
|
|
123
|
+
|
|
124
|
+
def _validate_types(self) -> None:
|
|
125
|
+
"""Validate the 'type' field."""
|
|
121
126
|
if self.type not in VALID_TYPES:
|
|
122
127
|
raise InvalidTypeValuesError(f"Invalid section type: {self.type}. Valid types: {VALID_TYPES}")
|
|
123
128
|
|
|
129
|
+
def _validate_admonition_prefix_combination(self) -> None:
|
|
130
|
+
"""Validate admonition and prefix combination rules."""
|
|
131
|
+
|
|
132
|
+
if isinstance(self.admonition, bool):
|
|
133
|
+
# Rule: admonition cannot be True (only False or string)
|
|
134
|
+
if self.admonition is True:
|
|
135
|
+
raise ValueError(f"Section '{self.name}': admonition cannot be True, must be False or a string")
|
|
136
|
+
|
|
137
|
+
# Rule: if admonition is False, prefix cannot be provided
|
|
138
|
+
if self.admonition is False and self.prefix:
|
|
139
|
+
raise ValueError(f"Section '{self.name}': when admonition=False, prefix cannot be provided")
|
|
140
|
+
|
|
141
|
+
elif isinstance(self.admonition, str):
|
|
142
|
+
# Rule: if admonition is a string, prefix must be provided
|
|
143
|
+
if not self.prefix:
|
|
144
|
+
raise ValueError(f"Section '{self.name}': when admonition is a string, prefix must be provided")
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Section '{self.name}': admonition must be a boolean or string, got {type(self.admonition)}"
|
|
149
|
+
)
|
|
150
|
+
|
|
124
151
|
|
|
125
152
|
## --------------------------------------------------------------------------- #
|
|
126
153
|
## Validations ####
|
|
@@ -271,11 +298,16 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> list[SectionC
|
|
|
271
298
|
sections_data = tool_config["sections"]
|
|
272
299
|
for section_data in sections_data:
|
|
273
300
|
try:
|
|
301
|
+
# Get admonition value with proper default handling
|
|
302
|
+
admonition_value: Union[str, bool] = section_data.get("admonition")
|
|
303
|
+
if admonition_value is None:
|
|
304
|
+
admonition_value = False # Use SectionConfig default
|
|
305
|
+
|
|
274
306
|
section = SectionConfig(
|
|
275
307
|
order=section_data.get("order", 0),
|
|
276
308
|
name=section_data.get("name", ""),
|
|
277
309
|
type=section_data.get("type", ""),
|
|
278
|
-
admonition=
|
|
310
|
+
admonition=admonition_value,
|
|
279
311
|
prefix=section_data.get("prefix", ""),
|
|
280
312
|
required=section_data.get("required", False),
|
|
281
313
|
)
|
|
@@ -47,7 +47,7 @@ import ast
|
|
|
47
47
|
import fnmatch
|
|
48
48
|
import re
|
|
49
49
|
from pathlib import Path
|
|
50
|
-
from typing import Literal, NamedTuple, Optional, Union
|
|
50
|
+
from typing import Iterator, Literal, NamedTuple, Optional, Union
|
|
51
51
|
|
|
52
52
|
# ## Local First Party Imports ----
|
|
53
53
|
from docstring_format_checker.config import SectionConfig
|
|
@@ -255,6 +255,7 @@ class DocstringChecker:
|
|
|
255
255
|
(bool):
|
|
256
256
|
True if the function has @overload decorator, False otherwise.
|
|
257
257
|
"""
|
|
258
|
+
|
|
258
259
|
for decorator in node.decorator_list:
|
|
259
260
|
# Handle direct name reference: @overload
|
|
260
261
|
if isinstance(decorator, ast.Name) and decorator.id == "overload":
|
|
@@ -422,6 +423,7 @@ class DocstringChecker:
|
|
|
422
423
|
(None):
|
|
423
424
|
Nothing is returned.
|
|
424
425
|
"""
|
|
426
|
+
|
|
425
427
|
errors: list[str] = []
|
|
426
428
|
|
|
427
429
|
# Check each required section
|
|
@@ -459,6 +461,26 @@ class DocstringChecker:
|
|
|
459
461
|
if self._has_both_returns_and_yields(docstring):
|
|
460
462
|
errors.append("Docstring cannot have both Returns and Yields sections")
|
|
461
463
|
|
|
464
|
+
# Check for undefined sections in docstring
|
|
465
|
+
undefined_errors: list[str] = self._check_undefined_sections(docstring)
|
|
466
|
+
errors.extend(undefined_errors)
|
|
467
|
+
|
|
468
|
+
# Check admonition values match configuration
|
|
469
|
+
admonition_errors: list[str] = self._check_admonition_values(docstring)
|
|
470
|
+
errors.extend(admonition_errors)
|
|
471
|
+
|
|
472
|
+
# Check colon usage for admonition vs non-admonition sections
|
|
473
|
+
colon_errors: list[str] = self._check_colon_usage(docstring)
|
|
474
|
+
errors.extend(colon_errors)
|
|
475
|
+
|
|
476
|
+
# Check title case for non-admonition sections
|
|
477
|
+
title_case_errors: list[str] = self._check_title_case_sections(docstring)
|
|
478
|
+
errors.extend(title_case_errors)
|
|
479
|
+
|
|
480
|
+
# Check parentheses for list type sections
|
|
481
|
+
parentheses_errors: list[str] = self._check_parentheses_validation(docstring)
|
|
482
|
+
errors.extend(parentheses_errors)
|
|
483
|
+
|
|
462
484
|
if errors:
|
|
463
485
|
combined_message: str = "; ".join(errors)
|
|
464
486
|
raise DocstringError(
|
|
@@ -484,9 +506,12 @@ class DocstringChecker:
|
|
|
484
506
|
(bool):
|
|
485
507
|
`True` if the section exists, `False` otherwise.
|
|
486
508
|
"""
|
|
487
|
-
|
|
509
|
+
|
|
510
|
+
if isinstance(section.admonition, str) and section.admonition and section.prefix:
|
|
488
511
|
# Format like: !!! note "Summary"
|
|
489
|
-
|
|
512
|
+
# Make the section name part case-insensitive too
|
|
513
|
+
escaped_name = re.escape(section.name)
|
|
514
|
+
pattern = rf'{re.escape(section.prefix)}\s+{re.escape(section.admonition)}\s+"[^"]*{escaped_name}[^"]*"'
|
|
490
515
|
return bool(re.search(pattern, docstring, re.IGNORECASE))
|
|
491
516
|
elif section.name.lower() in ["summary"]:
|
|
492
517
|
# For summary, accept either formal format or simple docstring
|
|
@@ -516,6 +541,7 @@ class DocstringChecker:
|
|
|
516
541
|
(bool):
|
|
517
542
|
`True` if the section exists and is valid, `False` otherwise.
|
|
518
543
|
"""
|
|
544
|
+
|
|
519
545
|
# Get function parameters (excluding 'self' for methods)
|
|
520
546
|
params: list[str] = [arg.arg for arg in node.args.args if arg.arg != "self"]
|
|
521
547
|
|
|
@@ -547,6 +573,7 @@ class DocstringChecker:
|
|
|
547
573
|
(bool):
|
|
548
574
|
`True` if the section exists, `False` otherwise.
|
|
549
575
|
"""
|
|
576
|
+
|
|
550
577
|
return bool(re.search(r"Returns:", docstring))
|
|
551
578
|
|
|
552
579
|
def _check_raises_section(self, docstring: str) -> bool:
|
|
@@ -562,6 +589,7 @@ class DocstringChecker:
|
|
|
562
589
|
(bool):
|
|
563
590
|
`True` if the section exists, `False` otherwise.
|
|
564
591
|
"""
|
|
592
|
+
|
|
565
593
|
return bool(re.search(r"Raises:", docstring))
|
|
566
594
|
|
|
567
595
|
def _has_both_returns_and_yields(self, docstring: str) -> bool:
|
|
@@ -577,6 +605,7 @@ class DocstringChecker:
|
|
|
577
605
|
(bool):
|
|
578
606
|
`True` if the section exists, `False` otherwise.
|
|
579
607
|
"""
|
|
608
|
+
|
|
580
609
|
has_returns = bool(re.search(r"Returns:", docstring))
|
|
581
610
|
has_yields = bool(re.search(r"Yields:", docstring))
|
|
582
611
|
return has_returns and has_yields
|
|
@@ -594,10 +623,16 @@ class DocstringChecker:
|
|
|
594
623
|
(list[str]):
|
|
595
624
|
A list of error messages, if any.
|
|
596
625
|
"""
|
|
626
|
+
|
|
597
627
|
# Build expected order from configuration
|
|
598
628
|
section_patterns: list[tuple[str, str]] = []
|
|
599
629
|
for section in sorted(self.sections_config, key=lambda x: x.order):
|
|
600
|
-
if
|
|
630
|
+
if (
|
|
631
|
+
section.type == "free_text"
|
|
632
|
+
and isinstance(section.admonition, str)
|
|
633
|
+
and section.admonition
|
|
634
|
+
and section.prefix
|
|
635
|
+
):
|
|
601
636
|
pattern: str = (
|
|
602
637
|
rf'{re.escape(section.prefix)}\s+{re.escape(section.admonition)}\s+".*{re.escape(section.name)}"'
|
|
603
638
|
)
|
|
@@ -677,6 +712,7 @@ class DocstringChecker:
|
|
|
677
712
|
(bool):
|
|
678
713
|
`True` if the section exists, `False` otherwise.
|
|
679
714
|
"""
|
|
715
|
+
|
|
680
716
|
return bool(re.search(r"Yields:", docstring))
|
|
681
717
|
|
|
682
718
|
def _check_simple_section(self, docstring: str, section_name: str) -> bool:
|
|
@@ -694,5 +730,263 @@ class DocstringChecker:
|
|
|
694
730
|
(bool):
|
|
695
731
|
`True` if the section exists, `False` otherwise.
|
|
696
732
|
"""
|
|
697
|
-
|
|
733
|
+
|
|
734
|
+
pattern: str = rf"{re.escape(section_name)}:"
|
|
698
735
|
return bool(re.search(pattern, docstring, re.IGNORECASE))
|
|
736
|
+
|
|
737
|
+
def _check_undefined_sections(self, docstring: str) -> list[str]:
|
|
738
|
+
"""
|
|
739
|
+
!!! note "Summary"
|
|
740
|
+
Check for sections in docstring that are not defined in configuration.
|
|
741
|
+
|
|
742
|
+
Params:
|
|
743
|
+
docstring (str):
|
|
744
|
+
The docstring to check.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
(list[str]):
|
|
748
|
+
A list of error messages for undefined sections.
|
|
749
|
+
"""
|
|
750
|
+
|
|
751
|
+
errors: list[str] = []
|
|
752
|
+
|
|
753
|
+
# Get all configured section names (case-insensitive)
|
|
754
|
+
configured_sections: set[str] = {section.name.lower() for section in self.sections_config}
|
|
755
|
+
|
|
756
|
+
# Common patterns for different section types
|
|
757
|
+
section_patterns: list[tuple[str, str]] = [
|
|
758
|
+
# Standard sections with colons (but not inside quotes)
|
|
759
|
+
(r"^(\w+):\s*", "colon"),
|
|
760
|
+
# Admonition sections with various prefixes
|
|
761
|
+
(r"(?:\?\?\?[+]?|!!!)\s+\w+\s+\"([^\"]+)\"", "admonition"),
|
|
762
|
+
]
|
|
763
|
+
|
|
764
|
+
found_sections: set[str] = set()
|
|
765
|
+
|
|
766
|
+
for pattern, pattern_type in section_patterns:
|
|
767
|
+
matches: Iterator[re.Match[str]] = re.finditer(pattern, docstring, re.IGNORECASE | re.MULTILINE)
|
|
768
|
+
for match in matches:
|
|
769
|
+
section_name: str = match.group(1).lower().strip()
|
|
770
|
+
|
|
771
|
+
# Remove colon if present (for colon pattern matches)
|
|
772
|
+
section_name = section_name.rstrip(":")
|
|
773
|
+
|
|
774
|
+
# Skip empty matches or common docstring content
|
|
775
|
+
if not section_name or section_name in ["", "py", "python", "sh", "shell"]:
|
|
776
|
+
continue
|
|
777
|
+
|
|
778
|
+
# Skip code blocks and inline code
|
|
779
|
+
if any(char in section_name for char in ["`", ".", "/", "\\"]):
|
|
780
|
+
continue
|
|
781
|
+
|
|
782
|
+
found_sections.add(section_name)
|
|
783
|
+
|
|
784
|
+
# Check which found sections are not configured
|
|
785
|
+
for section_name in found_sections:
|
|
786
|
+
if section_name not in configured_sections:
|
|
787
|
+
errors.append(f"Section '{section_name}' found in docstring but not defined in configuration")
|
|
788
|
+
|
|
789
|
+
return errors
|
|
790
|
+
|
|
791
|
+
def _check_admonition_values(self, docstring: str) -> list[str]:
|
|
792
|
+
"""
|
|
793
|
+
!!! note "Summary"
|
|
794
|
+
Check that admonition values in docstring match configuration.
|
|
795
|
+
|
|
796
|
+
Params:
|
|
797
|
+
docstring (str):
|
|
798
|
+
The docstring to check.
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
(list[str]):
|
|
802
|
+
A list of error messages for mismatched admonitions.
|
|
803
|
+
"""
|
|
804
|
+
|
|
805
|
+
errors: list[str] = []
|
|
806
|
+
|
|
807
|
+
# Create mapping of section names to expected admonitions
|
|
808
|
+
section_admonitions: dict[str, str] = {}
|
|
809
|
+
for section in self.sections_config:
|
|
810
|
+
if section.type == "free_text" and isinstance(section.admonition, str) and section.admonition:
|
|
811
|
+
section_admonitions[section.name.lower()] = section.admonition.lower()
|
|
812
|
+
|
|
813
|
+
# Pattern to find all admonition sections
|
|
814
|
+
admonition_pattern = r"(?:\?\?\?[+]?|!!!)\s+(\w+)\s+\"([^\"]+)\""
|
|
815
|
+
matches: Iterator[re.Match[str]] = re.finditer(admonition_pattern, docstring, re.IGNORECASE)
|
|
816
|
+
|
|
817
|
+
for match in matches:
|
|
818
|
+
actual_admonition: str = match.group(1).lower()
|
|
819
|
+
section_title: str = match.group(2).lower()
|
|
820
|
+
|
|
821
|
+
# Check if this section is configured with a specific admonition
|
|
822
|
+
if section_title in section_admonitions:
|
|
823
|
+
expected_admonition: str = section_admonitions[section_title]
|
|
824
|
+
if actual_admonition != expected_admonition:
|
|
825
|
+
errors.append(
|
|
826
|
+
f"Section '{section_title}' has incorrect admonition '{actual_admonition}', "
|
|
827
|
+
f"expected '{expected_admonition}'"
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Check if section shouldn't have admonition but does
|
|
831
|
+
section_config: Optional[SectionConfig] = next(
|
|
832
|
+
(s for s in self.sections_config if s.name.lower() == section_title), None
|
|
833
|
+
)
|
|
834
|
+
if section_config and section_config.admonition is False:
|
|
835
|
+
errors.append(f"Section '{section_title}' is configured as non-admonition but found as admonition")
|
|
836
|
+
|
|
837
|
+
return errors
|
|
838
|
+
|
|
839
|
+
def _check_colon_usage(self, docstring: str) -> list[str]:
|
|
840
|
+
"""
|
|
841
|
+
Check that colons are used correctly for admonition vs non-admonition sections.
|
|
842
|
+
"""
|
|
843
|
+
|
|
844
|
+
errors: list[str] = []
|
|
845
|
+
|
|
846
|
+
# Check admonition sections (should not end with colon)
|
|
847
|
+
admonition_pattern = r"(?:\?\?\?[+]?|!!!)\s+\w+\s+\"([^\"]+)\""
|
|
848
|
+
matches: Iterator[re.Match[str]] = re.finditer(admonition_pattern, docstring, re.IGNORECASE)
|
|
849
|
+
|
|
850
|
+
for match in matches:
|
|
851
|
+
section_title: str = match.group(1)
|
|
852
|
+
has_colon: bool = section_title.endswith(":")
|
|
853
|
+
section_title_clean: str = section_title.rstrip(":").lower()
|
|
854
|
+
|
|
855
|
+
# Find config for this section
|
|
856
|
+
section_config: Optional[SectionConfig] = next(
|
|
857
|
+
(s for s in self.sections_config if s.name.lower() == section_title_clean), None
|
|
858
|
+
)
|
|
859
|
+
if section_config and isinstance(section_config.admonition, str) and section_config.admonition:
|
|
860
|
+
if has_colon:
|
|
861
|
+
errors.append(
|
|
862
|
+
f"Section '{section_title_clean}' is an admonition, therefore it should not end with ':', "
|
|
863
|
+
f"see: '{match.group(0)}'"
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
# Check non-admonition sections (should end with colon)
|
|
867
|
+
non_admonition_pattern = r"^(\w+)(:?)$"
|
|
868
|
+
for line in docstring.split("\n"):
|
|
869
|
+
line: str = line.strip()
|
|
870
|
+
match: Optional[re.Match[str]] = re.match(non_admonition_pattern, line)
|
|
871
|
+
if match:
|
|
872
|
+
section_name: str = match.group(1).lower()
|
|
873
|
+
has_colon: bool = match.group(2) == ":"
|
|
874
|
+
|
|
875
|
+
# Find config for this section
|
|
876
|
+
section_config = next((s for s in self.sections_config if s.name.lower() == section_name), None)
|
|
877
|
+
if section_config and section_config.admonition is False:
|
|
878
|
+
if not has_colon:
|
|
879
|
+
errors.append(
|
|
880
|
+
f"Section '{section_name}' is non-admonition, therefore it must end with ':', "
|
|
881
|
+
f"see: '{line}'"
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
return errors
|
|
885
|
+
|
|
886
|
+
def _check_title_case_sections(self, docstring: str) -> list[str]:
|
|
887
|
+
"""
|
|
888
|
+
Check that non-admonition sections are single word, title case, and match config name.
|
|
889
|
+
"""
|
|
890
|
+
|
|
891
|
+
errors: list[str] = []
|
|
892
|
+
|
|
893
|
+
# Pattern to find section headers (single word followed by optional colon)
|
|
894
|
+
section_pattern = r"^(\w+):?$"
|
|
895
|
+
|
|
896
|
+
for line in docstring.split("\n"):
|
|
897
|
+
line: str = line.strip()
|
|
898
|
+
match: Optional[re.Match[str]] = re.match(section_pattern, line)
|
|
899
|
+
if match:
|
|
900
|
+
section_word: str = match.group(1)
|
|
901
|
+
section_name_lower: str = section_word.lower()
|
|
902
|
+
|
|
903
|
+
# Check if this is a configured non-admonition section
|
|
904
|
+
section_config: Optional[SectionConfig] = next(
|
|
905
|
+
(s for s in self.sections_config if s.name.lower() == section_name_lower), None
|
|
906
|
+
)
|
|
907
|
+
if section_config and section_config.admonition is False:
|
|
908
|
+
# Check if it's title case
|
|
909
|
+
expected_title_case: str = section_config.name.title()
|
|
910
|
+
if section_word != expected_title_case:
|
|
911
|
+
errors.append(
|
|
912
|
+
f"Section '{section_name_lower}' must be in title case as '{expected_title_case}', "
|
|
913
|
+
f"found: '{section_word}'"
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
return errors
|
|
917
|
+
|
|
918
|
+
def _check_parentheses_validation(self, docstring: str) -> list[str]:
|
|
919
|
+
"""
|
|
920
|
+
Check that list_type and list_name_and_type sections have proper parentheses.
|
|
921
|
+
"""
|
|
922
|
+
|
|
923
|
+
errors: list[str] = []
|
|
924
|
+
|
|
925
|
+
# Get sections that require parentheses
|
|
926
|
+
parentheses_sections: list[SectionConfig] = [
|
|
927
|
+
s for s in self.sections_config if s.type in ["list_type", "list_name_and_type"]
|
|
928
|
+
]
|
|
929
|
+
|
|
930
|
+
if not parentheses_sections:
|
|
931
|
+
return errors
|
|
932
|
+
|
|
933
|
+
# Check each line in the docstring
|
|
934
|
+
lines: list[str] = docstring.split("\n")
|
|
935
|
+
current_section = None
|
|
936
|
+
|
|
937
|
+
for i, line in enumerate(lines):
|
|
938
|
+
stripped_line: str = line.strip()
|
|
939
|
+
|
|
940
|
+
# Detect section headers
|
|
941
|
+
# Admonition sections
|
|
942
|
+
admonition_match: Optional[re.Match[str]] = re.match(
|
|
943
|
+
r"(?:\?\?\?[+]?|!!!)\s+\w+\s+\"([^\"]+)\"", stripped_line, re.IGNORECASE
|
|
944
|
+
)
|
|
945
|
+
if admonition_match:
|
|
946
|
+
section_name: str = admonition_match.group(1).lower()
|
|
947
|
+
current_section: Optional[SectionConfig] = next(
|
|
948
|
+
(s for s in parentheses_sections if s.name.lower() == section_name), None
|
|
949
|
+
)
|
|
950
|
+
continue
|
|
951
|
+
|
|
952
|
+
# Non-admonition sections - only match actual section headers, not indented content
|
|
953
|
+
# Section headers should be at the start of the line (no leading whitespace)
|
|
954
|
+
if not line.startswith((" ", "\t")): # Not indented
|
|
955
|
+
simple_section_match: Optional[re.Match[str]] = re.match(r"^(\w+):?$", stripped_line)
|
|
956
|
+
if simple_section_match:
|
|
957
|
+
section_name: str = simple_section_match.group(1).lower()
|
|
958
|
+
# Only consider it a section if it matches our known sections
|
|
959
|
+
potential_section: Optional[SectionConfig] = next(
|
|
960
|
+
(s for s in self.sections_config if s.name.lower() == section_name), None
|
|
961
|
+
)
|
|
962
|
+
if potential_section:
|
|
963
|
+
# This is a real section header
|
|
964
|
+
current_section = next(
|
|
965
|
+
(s for s in parentheses_sections if s.name.lower() == section_name), None
|
|
966
|
+
)
|
|
967
|
+
continue
|
|
968
|
+
# If it doesn't match a known section, fall through to content processing
|
|
969
|
+
|
|
970
|
+
# Check content lines if we're in a parentheses-required section
|
|
971
|
+
if current_section and stripped_line and not stripped_line.startswith(("!", "?", "#")):
|
|
972
|
+
# Look for parameter/type definitions
|
|
973
|
+
if ":" in stripped_line:
|
|
974
|
+
# For list_name_and_type sections, check format like "name (type):" or "(type):"
|
|
975
|
+
if current_section.type == "list_name_and_type":
|
|
976
|
+
# Pattern: name (type): or (type):
|
|
977
|
+
if not re.search(r"\([^)]+\):", stripped_line):
|
|
978
|
+
errors.append(
|
|
979
|
+
f"Section '{current_section.name}' (type: '{current_section.type}') requires "
|
|
980
|
+
f"parenthesized types, see: '{stripped_line}'"
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
# For list_type sections, check format like "(Type):"
|
|
984
|
+
elif current_section.type == "list_type":
|
|
985
|
+
# Pattern: (Type):
|
|
986
|
+
if not re.search(r"^\s*\([^)]+\):", stripped_line):
|
|
987
|
+
errors.append(
|
|
988
|
+
f"Section '{current_section.name}' (type: '{current_section.type}') requires "
|
|
989
|
+
f"parenthesized types, see: '{stripped_line}'"
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
return errors
|
|
File without changes
|
|
File without changes
|
|
File without changes
|