docstring-format-checker 0.1.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstring-format-checker
3
- Version: 0.1.0
3
+ Version: 0.3.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>
@@ -1,217 +1,217 @@
1
- [project]
2
- name = "docstring-format-checker"
3
- version = "0.1.0"
4
- description = "A CLI tool to check and validate Python docstring formatting and completeness"
5
- readme = "README.md"
6
- license = "MIT"
7
- authors = [
8
- { name="Chris Mahoney", email="docstring-format-checker@data-science-extensions.com" },
9
- ]
10
- maintainers = [
11
- { name="Chris Mahoney", email="docstring-format-checker@data-science-extensions.com" },
12
- ]
13
- classifiers = [
14
- "Development Status :: 4 - Beta",
15
- "License :: OSI Approved :: MIT License",
16
- "Topic :: Software Development :: Quality Assurance",
17
- "Topic :: Software Development :: Libraries :: Python Modules",
18
- "Topic :: Software Development :: Testing :: Unit",
19
- "Topic :: Utilities",
20
- "Programming Language :: Python :: 3",
21
- "Programming Language :: Python :: 3.9",
22
- "Programming Language :: Python :: 3.10",
23
- "Programming Language :: Python :: 3.11",
24
- "Programming Language :: Python :: 3.12",
25
- "Programming Language :: Python :: 3.13",
26
- "Intended Audience :: Developers",
27
- "Environment :: Console",
28
- ]
29
- requires-python = ">=3.9"
30
- dependencies = [
31
- "typer>=0.9.0",
32
- "tomli>=2.0.0;python_version<'3.11'",
33
- "rich>=13.0.0",
34
- "toolbox-python==1.*",
35
- ]
36
-
37
- [project.urls]
38
- Homepage = "https://github.com/data-science-extensions/docstring-format-checker"
39
- Documentation = "https://github.com/data-science-extensions/docstring-format-checker/blob/main/README.md"
40
- Repository = "https://github.com/data-science-extensions/docstring-format-checker"
41
- Changelog = "https://github.com/data-science-extensions/docstring-format-checker/releases"
42
- Issues = "https://github.com/data-science-extensions/docstring-format-checker/issues"
43
-
44
- [project.scripts]
45
- docstring-format-checker = "docstring_format_checker.cli:entry_point"
46
- dfc = "docstring_format_checker.cli:entry_point"
47
-
48
- [dependency-groups]
49
- dev = [
50
- "black==25.*",
51
- "blacken-docs==1.*",
52
- "codespell==2.*",
53
- "ipykernel==6.*",
54
- "isort==6.*",
55
- "pre-commit==4.*",
56
- "pycln==2.*",
57
- "pylint==3.*",
58
- "pyupgrade==3.*",
59
- "uv==0.*",
60
- "pip==25.*",
61
- ]
62
- docs = [
63
- "black==25.*",
64
- "docstring-inheritance==2.*",
65
- "livereload==2.*",
66
- "mike==2.*",
67
- "mkdocs==1.*",
68
- "mkdocs-autorefs==1.*",
69
- "mkdocs-coverage==1.*",
70
- "mkdocs-material==9.*",
71
- "mkdocstrings==0.*",
72
- "mkdocstrings-python==1.*",
73
- "pygithub==2.*",
74
- ]
75
- test = [
76
- "mypy==1.*,!=1.17.*",
77
- "parameterized==0.*",
78
- "pytest==8.*",
79
- "pytest-clarity==1.*",
80
- "pytest-cov==6.*",
81
- "pytest-icdiff==0.*",
82
- "pytest-sugar==1.*",
83
- "pytest-xdist==3.*",
84
- "requests==2.*",
85
- ]
86
-
87
- [tool.black]
88
- line-length = 120
89
- color = true
90
- exclude = '''
91
- /(
92
- \.git
93
- | \.hg
94
- | \.mypy_cache
95
- | \.tox
96
- | \.venv
97
- | _build
98
- | buck-out
99
- | build
100
- | dist
101
- | env
102
- | venv
103
- )/
104
- '''
105
-
106
- [tool.pytest.ini_options]
107
- filterwarnings = [
108
- # "ignore::typeguard.InstrumentationWarning",
109
- "ignore::DeprecationWarning",
110
- ]
111
- addopts = [
112
- "--verbose",
113
- "--verbose",
114
- "--cov=src/docstring_format_checker",
115
- "--cov-report=term",
116
- "--cov-report=html:cov-report/html",
117
- "--cov-report=xml:cov-report/xml/cov-report.xml",
118
- ]
119
- testpaths = [
120
- "src/tests",
121
- ]
122
- python_files = ["test_*.py"]
123
- python_classes = ["Test*"]
124
- python_functions = ["test_*"]
125
-
126
- [tool.mypy]
127
- ignore_missing_imports = true
128
- pretty = true
129
- disable_error_code = [
130
- "valid-type",
131
- "attr-defined",
132
- "no-redef",
133
- ]
134
-
135
- [tool.isort]
136
- import_heading_future = "## Future Python Library Imports ----"
137
- import_heading_stdlib = "## Python StdLib Imports ----"
138
- import_heading_thirdparty = "## Python Third Party Imports ----"
139
- import_heading_firstparty = "## Local First Party Imports ----"
140
- import_heading_localfolder = "## Local Module Imports ----"
141
- profile = "black"
142
- split_on_trailing_comma = true
143
- combine_as_imports = true
144
- lines_after_imports = 2
145
-
146
- [tool.codespell]
147
- ignore-words-list = "demog"
148
-
149
- [tool.bump_version.replacements]
150
- files = [
151
- { file = "src/docstring_format_checker/__init__.py", pattern = "__version__ = \"{VERSION}\"" },
152
- { file = "src/tests/test_version.py", pattern = "__version__ = \"{VERSION}\"" },
153
- { file = "pyproject.toml", pattern = "version = \"{VERSION}\"" },
154
- ]
155
-
156
- [tool.dfc]
157
- # Default configuration for docstring format checker
158
-
159
- [[tool.dfc.sections]]
160
- order = 1
161
- name = "summary"
162
- type = "free_text"
163
- admonition = "note"
164
- prefix = "!!!"
165
- required = true
166
-
167
- [[tool.dfc.sections]]
168
- order = 2
169
- name = "details"
170
- type = "free_text"
171
- admonition = "info"
172
- prefix = "???+"
173
- required = false
174
-
175
- [[tool.dfc.sections]]
176
- order = 3
177
- name = "params"
178
- type = "list_name_and_type"
179
- required = true
180
-
181
- [[tool.dfc.sections]]
182
- order = 4
183
- name = "returns"
184
- type = "list_name_and_type"
185
- required = false
186
-
187
- [[tool.dfc.sections]]
188
- order = 5
189
- name = "yields"
190
- type = "list_type"
191
- required = false
192
-
193
- [[tool.dfc.sections]]
194
- order = 6
195
- name = "raises"
196
- type = "list_type"
197
- required = false
198
-
199
- [[tool.dfc.sections]]
200
- order = 7
201
- name = "examples"
202
- type = "free_text"
203
- admonition = "example"
204
- prefix = "???+"
205
- required = false
206
-
207
- [[tool.dfc.sections]]
208
- order = 8
209
- name = "notes"
210
- type = "free_text"
211
- admonition = "note"
212
- prefix = "???"
213
- required = false
214
-
215
- [build-system]
216
- requires = ["uv_build>=0.7.19,<0.8.0"]
217
- build-backend = "uv_build"
1
+ [project]
2
+ name = "docstring-format-checker"
3
+ version = "v0.3.0"
4
+ description = "A CLI tool to check and validate Python docstring formatting and completeness"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name="Chris Mahoney", email="docstring-format-checker@data-science-extensions.com" },
9
+ ]
10
+ maintainers = [
11
+ { name="Chris Mahoney", email="docstring-format-checker@data-science-extensions.com" },
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Topic :: Software Development :: Quality Assurance",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ "Topic :: Software Development :: Testing :: Unit",
19
+ "Topic :: Utilities",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Intended Audience :: Developers",
27
+ "Environment :: Console",
28
+ ]
29
+ requires-python = ">=3.9"
30
+ dependencies = [
31
+ "typer>=0.9.0",
32
+ "tomli>=2.0.0;python_version<'3.11'",
33
+ "rich>=13.0.0",
34
+ "toolbox-python==1.*",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/data-science-extensions/docstring-format-checker"
39
+ Documentation = "https://github.com/data-science-extensions/docstring-format-checker/blob/main/README.md"
40
+ Repository = "https://github.com/data-science-extensions/docstring-format-checker"
41
+ Changelog = "https://github.com/data-science-extensions/docstring-format-checker/releases"
42
+ Issues = "https://github.com/data-science-extensions/docstring-format-checker/issues"
43
+
44
+ [project.scripts]
45
+ docstring-format-checker = "docstring_format_checker.cli:entry_point"
46
+ dfc = "docstring_format_checker.cli:entry_point"
47
+
48
+ [dependency-groups]
49
+ dev = [
50
+ "black==25.*",
51
+ "blacken-docs==1.*",
52
+ "codespell==2.*",
53
+ "ipykernel==6.*",
54
+ "isort==6.*",
55
+ "pre-commit==4.*",
56
+ "pycln==2.*",
57
+ "pylint==3.*",
58
+ "pyupgrade==3.*",
59
+ "uv==0.*",
60
+ "pip==25.*",
61
+ ]
62
+ docs = [
63
+ "black==25.*",
64
+ "docstring-inheritance==2.*",
65
+ "livereload==2.*",
66
+ "mike==2.*",
67
+ "mkdocs==1.*",
68
+ "mkdocs-autorefs==1.*",
69
+ "mkdocs-coverage==1.*",
70
+ "mkdocs-material==9.*",
71
+ "mkdocstrings==0.*",
72
+ "mkdocstrings-python==1.*",
73
+ "pygithub==2.*",
74
+ ]
75
+ test = [
76
+ "mypy==1.*,!=1.17.*",
77
+ "parameterized==0.*",
78
+ "pytest==8.*",
79
+ "pytest-clarity==1.*",
80
+ "pytest-cov==6.*",
81
+ "pytest-icdiff==0.*",
82
+ "pytest-sugar==1.*",
83
+ "pytest-xdist==3.*",
84
+ "requests==2.*",
85
+ ]
86
+
87
+ [tool.black]
88
+ line-length = 120
89
+ color = true
90
+ exclude = '''
91
+ /(
92
+ \.git
93
+ | \.hg
94
+ | \.mypy_cache
95
+ | \.tox
96
+ | \.venv
97
+ | _build
98
+ | buck-out
99
+ | build
100
+ | dist
101
+ | env
102
+ | venv
103
+ )/
104
+ '''
105
+
106
+ [tool.pytest.ini_options]
107
+ filterwarnings = [
108
+ # "ignore::typeguard.InstrumentationWarning",
109
+ "ignore::DeprecationWarning",
110
+ ]
111
+ addopts = [
112
+ "--verbose",
113
+ "--verbose",
114
+ "--cov=src/docstring_format_checker",
115
+ "--cov-report=term",
116
+ "--cov-report=html:cov-report/html",
117
+ "--cov-report=xml:cov-report/xml/cov-report.xml",
118
+ ]
119
+ testpaths = [
120
+ "src/tests",
121
+ ]
122
+ python_files = ["test_*.py"]
123
+ python_classes = ["Test*"]
124
+ python_functions = ["test_*"]
125
+
126
+ [tool.mypy]
127
+ ignore_missing_imports = true
128
+ pretty = true
129
+ disable_error_code = [
130
+ "valid-type",
131
+ "attr-defined",
132
+ "no-redef",
133
+ ]
134
+
135
+ [tool.isort]
136
+ import_heading_future = "## Future Python Library Imports ----"
137
+ import_heading_stdlib = "## Python StdLib Imports ----"
138
+ import_heading_thirdparty = "## Python Third Party Imports ----"
139
+ import_heading_firstparty = "## Local First Party Imports ----"
140
+ import_heading_localfolder = "## Local Module Imports ----"
141
+ profile = "black"
142
+ split_on_trailing_comma = true
143
+ combine_as_imports = true
144
+ lines_after_imports = 2
145
+
146
+ [tool.codespell]
147
+ ignore-words-list = "demog"
148
+
149
+ [tool.bump_version.replacements]
150
+ files = [
151
+ { file = "src/docstring_format_checker/__init__.py", pattern = "__version__ = \"{VERSION}\"" },
152
+ { file = "src/tests/test_version.py", pattern = "__version__ = \"{VERSION}\"" },
153
+ { file = "pyproject.toml", pattern = "version = \"{VERSION}\"" },
154
+ ]
155
+
156
+ [tool.dfc]
157
+ # Default configuration for docstring format checker
158
+
159
+ [[tool.dfc.sections]]
160
+ order = 1
161
+ name = "summary"
162
+ type = "free_text"
163
+ admonition = "note"
164
+ prefix = "!!!"
165
+ required = true
166
+
167
+ [[tool.dfc.sections]]
168
+ order = 2
169
+ name = "details"
170
+ type = "free_text"
171
+ admonition = "info"
172
+ prefix = "???+"
173
+ required = false
174
+
175
+ [[tool.dfc.sections]]
176
+ order = 3
177
+ name = "params"
178
+ type = "list_name_and_type"
179
+ required = true
180
+
181
+ [[tool.dfc.sections]]
182
+ order = 4
183
+ name = "returns"
184
+ type = "list_name_and_type"
185
+ required = false
186
+
187
+ [[tool.dfc.sections]]
188
+ order = 5
189
+ name = "yields"
190
+ type = "list_type"
191
+ required = false
192
+
193
+ [[tool.dfc.sections]]
194
+ order = 6
195
+ name = "raises"
196
+ type = "list_type"
197
+ required = false
198
+
199
+ [[tool.dfc.sections]]
200
+ order = 7
201
+ name = "examples"
202
+ type = "free_text"
203
+ admonition = "example"
204
+ prefix = "???+"
205
+ required = false
206
+
207
+ [[tool.dfc.sections]]
208
+ order = 8
209
+ name = "notes"
210
+ type = "free_text"
211
+ admonition = "note"
212
+ prefix = "???"
213
+ required = false
214
+
215
+ [build-system]
216
+ requires = ["uv_build>=0.7.19,<0.8.0"]
217
+ build-backend = "uv_build"
@@ -1,22 +1,22 @@
1
- """
2
- Docstring Format Checker.
3
-
4
- A CLI tool to check and validate Python docstring formatting and completeness.
5
- """
6
-
7
- __version__ = "0.1.0"
8
- __author__ = "Chris Mahoney"
9
- __email__ = "docstring-format-checker@data-science-extensions.com"
10
-
11
-
12
- # ## Local First Party Imports ----
13
- from docstring_format_checker.config import DEFAULT_CONFIG, load_config
14
- from docstring_format_checker.core import DocstringChecker, SectionConfig
15
-
16
-
17
- __all__: list[str] = [
18
- "DocstringChecker",
19
- "SectionConfig",
20
- "load_config",
21
- "DEFAULT_CONFIG",
22
- ]
1
+ """
2
+ Docstring Format Checker.
3
+
4
+ A CLI tool to check and validate Python docstring formatting and completeness.
5
+ """
6
+
7
+ __version__ = "v0.3.0"
8
+ __author__ = "Chris Mahoney"
9
+ __email__ = "docstring-format-checker@data-science-extensions.com"
10
+
11
+
12
+ # ## Local First Party Imports ----
13
+ from docstring_format_checker.config import DEFAULT_CONFIG, load_config
14
+ from docstring_format_checker.core import DocstringChecker, SectionConfig
15
+
16
+
17
+ __all__: list[str] = [
18
+ "DocstringChecker",
19
+ "SectionConfig",
20
+ "load_config",
21
+ "DEFAULT_CONFIG",
22
+ ]
@@ -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=section_data.get("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
@@ -242,6 +242,29 @@ class DocstringChecker:
242
242
 
243
243
  return results
244
244
 
245
+ def _is_overload_function(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> bool:
246
+ """
247
+ !!! note "Summary"
248
+ Check if a function definition is decorated with @overload.
249
+
250
+ Params:
251
+ node (Union[ast.FunctionDef, ast.AsyncFunctionDef]):
252
+ The function node to check for @overload decorator.
253
+
254
+ Returns:
255
+ (bool):
256
+ True if the function has @overload decorator, False otherwise.
257
+ """
258
+
259
+ for decorator in node.decorator_list:
260
+ # Handle direct name reference: @overload
261
+ if isinstance(decorator, ast.Name) and decorator.id == "overload":
262
+ return True
263
+ # Handle attribute reference: @typing.overload
264
+ elif isinstance(decorator, ast.Attribute) and decorator.attr == "overload":
265
+ return True
266
+ return False
267
+
245
268
  def _extract_items(self, tree: ast.AST) -> list[FunctionAndClassDetails]:
246
269
  """
247
270
  !!! note "Summary"
@@ -260,8 +283,9 @@ class DocstringChecker:
260
283
 
261
284
  class ItemVisitor(ast.NodeVisitor):
262
285
 
263
- def __init__(self) -> None:
286
+ def __init__(self, checker: DocstringChecker) -> None:
264
287
  self.class_stack: list[str] = []
288
+ self.checker: DocstringChecker = checker
265
289
 
266
290
  def visit_ClassDef(self, node: ast.ClassDef) -> None:
267
291
  if not node.name.startswith("_"): # Skip private classes
@@ -288,23 +312,27 @@ class DocstringChecker:
288
312
 
289
313
  def _visit_function(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> None:
290
314
  """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
315
 
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,
316
+ if not node.name.startswith("_"): # Skip private functions
317
+ # Skip @overload functions - they don't need docstrings
318
+
319
+ if not self.checker._is_overload_function(node):
320
+ item_type: Literal["function", "method"] = "method" if self.class_stack else "function"
321
+ parent_class: Optional[str] = self.class_stack[-1] if self.class_stack else None
322
+
323
+ items.append(
324
+ FunctionAndClassDetails(
325
+ item_type=item_type,
326
+ name=node.name,
327
+ node=node,
328
+ lineno=node.lineno,
329
+ parent_class=parent_class,
330
+ )
302
331
  )
303
- )
304
332
 
305
333
  self.generic_visit(node)
306
334
 
307
- visitor = ItemVisitor()
335
+ visitor = ItemVisitor(self)
308
336
  visitor.visit(tree)
309
337
 
310
338
  return items
@@ -395,6 +423,7 @@ class DocstringChecker:
395
423
  (None):
396
424
  Nothing is returned.
397
425
  """
426
+
398
427
  errors: list[str] = []
399
428
 
400
429
  # Check each required section
@@ -432,6 +461,26 @@ class DocstringChecker:
432
461
  if self._has_both_returns_and_yields(docstring):
433
462
  errors.append("Docstring cannot have both Returns and Yields sections")
434
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
+
435
484
  if errors:
436
485
  combined_message: str = "; ".join(errors)
437
486
  raise DocstringError(
@@ -457,9 +506,12 @@ class DocstringChecker:
457
506
  (bool):
458
507
  `True` if the section exists, `False` otherwise.
459
508
  """
460
- if section.admonition and section.prefix:
509
+
510
+ if isinstance(section.admonition, str) and section.admonition and section.prefix:
461
511
  # Format like: !!! note "Summary"
462
- pattern = rf'{re.escape(section.prefix)}\s+{re.escape(section.admonition)}\s+".*{re.escape(section.name)}"'
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}[^"]*"'
463
515
  return bool(re.search(pattern, docstring, re.IGNORECASE))
464
516
  elif section.name.lower() in ["summary"]:
465
517
  # For summary, accept either formal format or simple docstring
@@ -489,6 +541,7 @@ class DocstringChecker:
489
541
  (bool):
490
542
  `True` if the section exists and is valid, `False` otherwise.
491
543
  """
544
+
492
545
  # Get function parameters (excluding 'self' for methods)
493
546
  params: list[str] = [arg.arg for arg in node.args.args if arg.arg != "self"]
494
547
 
@@ -520,6 +573,7 @@ class DocstringChecker:
520
573
  (bool):
521
574
  `True` if the section exists, `False` otherwise.
522
575
  """
576
+
523
577
  return bool(re.search(r"Returns:", docstring))
524
578
 
525
579
  def _check_raises_section(self, docstring: str) -> bool:
@@ -535,6 +589,7 @@ class DocstringChecker:
535
589
  (bool):
536
590
  `True` if the section exists, `False` otherwise.
537
591
  """
592
+
538
593
  return bool(re.search(r"Raises:", docstring))
539
594
 
540
595
  def _has_both_returns_and_yields(self, docstring: str) -> bool:
@@ -550,6 +605,7 @@ class DocstringChecker:
550
605
  (bool):
551
606
  `True` if the section exists, `False` otherwise.
552
607
  """
608
+
553
609
  has_returns = bool(re.search(r"Returns:", docstring))
554
610
  has_yields = bool(re.search(r"Yields:", docstring))
555
611
  return has_returns and has_yields
@@ -567,10 +623,16 @@ class DocstringChecker:
567
623
  (list[str]):
568
624
  A list of error messages, if any.
569
625
  """
626
+
570
627
  # Build expected order from configuration
571
628
  section_patterns: list[tuple[str, str]] = []
572
629
  for section in sorted(self.sections_config, key=lambda x: x.order):
573
- if section.type == "free_text" and section.admonition and section.prefix:
630
+ if (
631
+ section.type == "free_text"
632
+ and isinstance(section.admonition, str)
633
+ and section.admonition
634
+ and section.prefix
635
+ ):
574
636
  pattern: str = (
575
637
  rf'{re.escape(section.prefix)}\s+{re.escape(section.admonition)}\s+".*{re.escape(section.name)}"'
576
638
  )
@@ -650,6 +712,7 @@ class DocstringChecker:
650
712
  (bool):
651
713
  `True` if the section exists, `False` otherwise.
652
714
  """
715
+
653
716
  return bool(re.search(r"Yields:", docstring))
654
717
 
655
718
  def _check_simple_section(self, docstring: str, section_name: str) -> bool:
@@ -667,5 +730,263 @@ class DocstringChecker:
667
730
  (bool):
668
731
  `True` if the section exists, `False` otherwise.
669
732
  """
670
- pattern = rf"{re.escape(section_name)}:"
733
+
734
+ pattern: str = rf"{re.escape(section_name)}:"
671
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, missing in: '{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, missing in: '{stripped_line}'"
990
+ )
991
+
992
+ return errors