google-docstring-parser 0.0.9__tar.gz → 0.0.11__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.
Files changed (16) hide show
  1. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/PKG-INFO +8 -1
  2. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/README.md +7 -0
  3. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser/google_docstring_parser.py +12 -12
  4. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser/type_validation.py +11 -11
  5. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser.egg-info/PKG-INFO +8 -1
  6. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/pyproject.toml +3 -1
  7. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/tools/check_docstrings.py +381 -46
  8. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/LICENSE +0 -0
  9. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser/__init__.py +0 -0
  10. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser/py.typed +0 -0
  11. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser.egg-info/SOURCES.txt +0 -0
  12. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser.egg-info/dependency_links.txt +0 -0
  13. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser.egg-info/requires.txt +0 -0
  14. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/google_docstring_parser.egg-info/top_level.txt +0 -0
  15. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/setup.cfg +0 -0
  16. {google_docstring_parser-0.0.9 → google_docstring_parser-0.0.11}/tools/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-docstring-parser
3
- Version: 0.0.9
3
+ Version: 0.0.11
4
4
  Summary: A lightweight, efficient parser for Google-style Python docstrings that converts them into structured dictionaries.
5
5
  Author: Vladimir Iglovikov
6
6
  Maintainer: Vladimir Iglovikov
@@ -153,6 +153,10 @@ References:
153
153
 
154
154
  Each reference is parsed into a dictionary with `description` and `source` keys. URLs in the source are properly handled, ensuring colons in URLs are not confused with the separator colon.
155
155
 
156
+ ## Short Description (Meta Description)
157
+
158
+ The checker extracts the **short description** as the first paragraph (up to the first blank line). Multi-line first paragraphs are joined with spaces. This is useful for meta descriptions on documentation sites. SEO best practice: 120-160 characters. Use `min_short_description_length` and `max_short_description_length` to enforce bounds (0 to disable).
159
+
156
160
  ## Pre-commit Hook
157
161
 
158
162
  This package includes a pre-commit hook that checks if Google-style docstrings in your codebase can be parsed correctly.
@@ -180,6 +184,9 @@ Add a `[tool.docstring_checker]` section to your pyproject.toml:
180
184
  paths = ["src", "tests"] # Directories or files to scan
181
185
  require_param_types = true # Require parameter types in docstrings
182
186
  check_references = true # Check references for proper format
187
+ check_type_consistency = true # Compare docstring types with annotations
183
188
  exclude_files = ["conftest.py", "__init__.py"] # Files to exclude from checks
189
+ min_short_description_length = 50 # Minimum short description length; 0 to disable
190
+ max_short_description_length = 160 # Maximum short description length; 0 to disable (SEO: 120-160)
184
191
  verbose = false # Enable verbose output
185
192
  ```
@@ -119,6 +119,10 @@ References:
119
119
 
120
120
  Each reference is parsed into a dictionary with `description` and `source` keys. URLs in the source are properly handled, ensuring colons in URLs are not confused with the separator colon.
121
121
 
122
+ ## Short Description (Meta Description)
123
+
124
+ The checker extracts the **short description** as the first paragraph (up to the first blank line). Multi-line first paragraphs are joined with spaces. This is useful for meta descriptions on documentation sites. SEO best practice: 120-160 characters. Use `min_short_description_length` and `max_short_description_length` to enforce bounds (0 to disable).
125
+
122
126
  ## Pre-commit Hook
123
127
 
124
128
  This package includes a pre-commit hook that checks if Google-style docstrings in your codebase can be parsed correctly.
@@ -146,6 +150,9 @@ Add a `[tool.docstring_checker]` section to your pyproject.toml:
146
150
  paths = ["src", "tests"] # Directories or files to scan
147
151
  require_param_types = true # Require parameter types in docstrings
148
152
  check_references = true # Check references for proper format
153
+ check_type_consistency = true # Compare docstring types with annotations
149
154
  exclude_files = ["conftest.py", "__init__.py"] # Files to exclude from checks
155
+ min_short_description_length = 50 # Minimum short description length; 0 to disable
156
+ max_short_description_length = 160 # Maximum short description length; 0 to disable (SEO: 120-160)
150
157
  verbose = false # Enable verbose output
151
158
  ```
@@ -40,7 +40,7 @@ __all__ = [
40
40
 
41
41
 
42
42
  class ReferenceFormatError(ValueError):
43
- """Error raised when a reference format is invalid.
43
+ """Error raised when a reference format is invalid or malformed.
44
44
 
45
45
  Args:
46
46
  code (str): Error code identifying the specific format issue
@@ -88,7 +88,7 @@ class EmptyDescriptionError(ReferenceFormatError):
88
88
 
89
89
 
90
90
  def _extract_sections(docstring: str) -> dict[str, str]:
91
- """Extract sections from a docstring.
91
+ """Extract named sections from a Google-style docstring.
92
92
 
93
93
  Args:
94
94
  docstring (str): The docstring to extract sections from
@@ -170,7 +170,7 @@ def _find_separator_colon(content: str) -> int:
170
170
 
171
171
 
172
172
  def _parse_reference_line(line: str, *, is_single: bool = False) -> dict[str, str]:
173
- """Parse a single reference line.
173
+ """Parse a single reference line into description and source.
174
174
 
175
175
  Args:
176
176
  line (str): The line to parse
@@ -252,7 +252,7 @@ def _identify_main_reference_lines(lines: list[str]) -> list[str]:
252
252
 
253
253
 
254
254
  def _process_single_reference(main_line: str, all_lines: list[str]) -> dict[str, str]:
255
- """Process a single reference entry.
255
+ """Process a single reference entry from the References section.
256
256
 
257
257
  Args:
258
258
  main_line (str): The main reference line
@@ -285,7 +285,7 @@ def _process_single_reference(main_line: str, all_lines: list[str]) -> dict[str,
285
285
 
286
286
 
287
287
  def _process_multiple_references(lines: list[str]) -> list[dict[str, str]]:
288
- """Process multiple reference entries.
288
+ """Process multiple reference entries from the References section.
289
289
 
290
290
  Args:
291
291
  lines (list[str]): Lines containing multiple references
@@ -338,7 +338,7 @@ def _process_multiple_references(lines: list[str]) -> list[dict[str, str]]:
338
338
 
339
339
 
340
340
  def _parse_references(reference_content: str) -> list[dict[str, str]]:
341
- """Parse references section content.
341
+ """Parse references section content into structured reference entries.
342
342
 
343
343
  Args:
344
344
  reference_content (str): Content of the references section
@@ -373,7 +373,7 @@ def _parse_references(reference_content: str) -> list[dict[str, str]]:
373
373
 
374
374
 
375
375
  def _validate_type_with_error_handling(type_str: str, result: dict[str, Any], collect_errors: bool) -> None:
376
- """Validate a type annotation and handle any errors.
376
+ """Validate a type annotation and handle any validation errors.
377
377
 
378
378
  This function validates type annotations and handles errors differently based on the collect_errors flag:
379
379
  - When collect_errors is True: Errors are added to result["errors"] list instead of being raised
@@ -408,7 +408,7 @@ def _process_args_with_validation(
408
408
  validate_types: bool,
409
409
  collect_errors: bool,
410
410
  ) -> None:
411
- """Process the Args section with type validation.
411
+ """Process the Args section with type validation and error collection.
412
412
 
413
413
  Args:
414
414
  sections (dict[str, str]): The sections dictionary
@@ -439,7 +439,7 @@ def _process_args_with_validation(
439
439
 
440
440
 
441
441
  def _parse_returns_section(sections: dict[str, str], *, validate_types: bool) -> dict[str, str] | str:
442
- """Process the Returns section of a docstring.
442
+ """Process the Returns section of a docstring into type and description.
443
443
 
444
444
  Args:
445
445
  sections (dict[str, str]): The sections dictionary
@@ -482,7 +482,7 @@ def _process_returns_with_validation(
482
482
  validate_types: bool,
483
483
  collect_errors: bool,
484
484
  ) -> None:
485
- """Process the Returns section with type validation.
485
+ """Process the Returns section with type validation and error handling.
486
486
 
487
487
  Args:
488
488
  sections (dict[str, str]): The sections dictionary
@@ -506,7 +506,7 @@ def _process_returns_with_validation(
506
506
 
507
507
 
508
508
  def _process_references_section(sections: dict[str, str], result: dict[str, Any]) -> None:
509
- """Process the References section.
509
+ """Process the References section into structured reference entries.
510
510
 
511
511
  Args:
512
512
  sections (dict[str, str]): The sections dictionary
@@ -527,7 +527,7 @@ def parse_google_docstring(
527
527
  validate_types: bool = True,
528
528
  collect_errors: bool = True,
529
529
  ) -> dict[str, Any]:
530
- """Parse a Google-style docstring.
530
+ """Parse a Google-style docstring into a structured dictionary.
531
531
 
532
532
  Args:
533
533
  docstring (str): The docstring to parse
@@ -89,7 +89,7 @@ NESTING_KEYWORD = "with"
89
89
 
90
90
 
91
91
  class InvalidTypeAnnotationError(ValueError):
92
- """Error raised when a type annotation is invalid.
92
+ """Error raised when a type annotation is invalid or malformed.
93
93
 
94
94
  Args:
95
95
  message (str): The error message.
@@ -100,7 +100,7 @@ class InvalidTypeAnnotationError(ValueError):
100
100
  INVALID_NESTED_TYPE = "Invalid nested type: {}"
101
101
 
102
102
  def __init__(self, message: str) -> None:
103
- """Initialize the error with a message.
103
+ """Initialize the error instance with a descriptive message.
104
104
 
105
105
  Args:
106
106
  message (str): The error message.
@@ -125,7 +125,7 @@ class BracketValidationError(ValueError):
125
125
  WRONG_BRACKET_TYPE = "Collection '{}' must use square brackets for type arguments, not '{}'"
126
126
 
127
127
  def __init__(self, error_type: str) -> None:
128
- """Initialize with a specific error type.
128
+ """Initialize the error with a specific bracket validation type.
129
129
 
130
130
  Args:
131
131
  error_type (str): One of the predefined error types.
@@ -137,7 +137,7 @@ class BracketValidationError(ValueError):
137
137
 
138
138
 
139
139
  def is_collection_type(type_name: str) -> bool:
140
- """Check if a type name is a known collection type.
140
+ """Check if a type name is a known collection type (list, dict, etc).
141
141
 
142
142
  Args:
143
143
  type_name (str): The type name to check.
@@ -261,7 +261,7 @@ def _is_within_string_literal(text: str, position: int) -> bool:
261
261
 
262
262
 
263
263
  def _looks_like_type_annotation(text: str) -> bool:
264
- """Check if text looks like a type annotation.
264
+ """Check if text looks like a type annotation using heuristics.
265
265
 
266
266
  Args:
267
267
  text (str): The text to check
@@ -277,7 +277,7 @@ def _looks_like_type_annotation(text: str) -> bool:
277
277
 
278
278
 
279
279
  def _process_string_literals(text: str) -> tuple[str, list[str]]:
280
- """Process string literals in text.
280
+ """Process string literals in text by replacing them with placeholders.
281
281
 
282
282
  Args:
283
283
  text (str): The text to process
@@ -386,7 +386,7 @@ def _check_for_opening_bracket(
386
386
  bracket_stack: list[str],
387
387
  collection_stack: list[tuple[str, str]],
388
388
  ) -> None:
389
- """Check for opening bracket in type declaration.
389
+ """Check for opening bracket in type declaration and update stacks.
390
390
 
391
391
  Args:
392
392
  tokens (list[str]): List of tokens
@@ -409,7 +409,7 @@ def _check_for_opening_bracket(
409
409
 
410
410
 
411
411
  def _check_for_closing_bracket(token: str, bracket_stack: list[str], collection_stack: list[tuple[str, str]]) -> None:
412
- """Check for closing bracket in type declaration.
412
+ """Check for closing bracket in type declaration and validate pairing.
413
413
 
414
414
  Args:
415
415
  token (str): Current token
@@ -440,7 +440,7 @@ def _check_for_closing_bracket(token: str, bracket_stack: list[str], collection_
440
440
 
441
441
 
442
442
  def _check_for_bare_collection(tokens: list[str], i: int, token: str) -> None:
443
- """Check for bare collection type usage.
443
+ """Check for bare collection type usage without type arguments.
444
444
 
445
445
  Args:
446
446
  tokens (list[str]): List of tokens
@@ -487,7 +487,7 @@ def _is_bare_collection_in_nested_type(token: str, tokens: list[str], i: int, br
487
487
 
488
488
 
489
489
  def _check_tokens_for_collection_type_usage(tokens: list[str]) -> None:
490
- """Check tokens for proper collection type usage.
490
+ """Check tokens for proper collection type usage and brackets.
491
491
 
492
492
  Args:
493
493
  tokens (list[str]): List of tokens to check
@@ -539,7 +539,7 @@ def _check_tokens_for_collection_type_usage(tokens: list[str]) -> None:
539
539
 
540
540
 
541
541
  def _validate_type_declaration(declaration: str) -> None:
542
- """Validate a type declaration.
542
+ """Validate a type declaration for syntax and collection usage.
543
543
 
544
544
  Args:
545
545
  declaration (str): The type declaration to validate
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-docstring-parser
3
- Version: 0.0.9
3
+ Version: 0.0.11
4
4
  Summary: A lightweight, efficient parser for Google-style Python docstrings that converts them into structured dictionaries.
5
5
  Author: Vladimir Iglovikov
6
6
  Maintainer: Vladimir Iglovikov
@@ -153,6 +153,10 @@ References:
153
153
 
154
154
  Each reference is parsed into a dictionary with `description` and `source` keys. URLs in the source are properly handled, ensuring colons in URLs are not confused with the separator colon.
155
155
 
156
+ ## Short Description (Meta Description)
157
+
158
+ The checker extracts the **short description** as the first paragraph (up to the first blank line). Multi-line first paragraphs are joined with spaces. This is useful for meta descriptions on documentation sites. SEO best practice: 120-160 characters. Use `min_short_description_length` and `max_short_description_length` to enforce bounds (0 to disable).
159
+
156
160
  ## Pre-commit Hook
157
161
 
158
162
  This package includes a pre-commit hook that checks if Google-style docstrings in your codebase can be parsed correctly.
@@ -180,6 +184,9 @@ Add a `[tool.docstring_checker]` section to your pyproject.toml:
180
184
  paths = ["src", "tests"] # Directories or files to scan
181
185
  require_param_types = true # Require parameter types in docstrings
182
186
  check_references = true # Check references for proper format
187
+ check_type_consistency = true # Compare docstring types with annotations
183
188
  exclude_files = ["conftest.py", "__init__.py"] # Files to exclude from checks
189
+ min_short_description_length = 50 # Minimum short description length; 0 to disable
190
+ max_short_description_length = 160 # Maximum short description length; 0 to disable (SEO: 120-160)
184
191
  verbose = false # Enable verbose output
185
192
  ```
@@ -5,7 +5,7 @@ requires = [ "setuptools>=45", "wheel" ]
5
5
 
6
6
  [project]
7
7
  name = "google-docstring-parser"
8
- version = "0.0.9"
8
+ version = "0.0.11"
9
9
 
10
10
  description = "A lightweight, efficient parser for Google-style Python docstrings that converts them into structured dictionaries."
11
11
  readme = "README.md"
@@ -119,6 +119,7 @@ lint.per-file-ignores = { "__init__.py" = [
119
119
  "BLE001",
120
120
  "FBT002",
121
121
  "ANN201",
122
+ "PLR0913",
122
123
  ] }
123
124
 
124
125
  lint.fixable = [ "ALL" ]
@@ -146,5 +147,6 @@ paths = [ "google_docstring_parser", "tools" ]
146
147
  require_param_types = true
147
148
  check_references = true
148
149
  check_type_consistency = true
150
+ min_short_description_length = 50
149
151
  exclude_files = [ "test_malformed_docstrings.py" ]
150
152
  verbose = false
@@ -20,18 +20,24 @@ from google_docstring_parser.google_docstring_parser import (
20
20
  parse_google_docstring,
21
21
  )
22
22
 
23
+ # Preview length for short description error messages
24
+ SHORT_DESC_PREVIEW_LENGTH = 50
25
+
23
26
  # Default configuration
24
27
  DEFAULT_CONFIG = {
25
28
  "paths": [], # Empty by default, so no directories are scanned unless explicitly specified
26
29
  "require_param_types": False,
27
30
  "check_references": True,
31
+ "check_type_consistency": False,
32
+ "min_short_description_length": 0,
33
+ "max_short_description_length": 0, # 160 recommended for SEO meta descriptions
28
34
  "exclude_files": [],
29
35
  "verbose": False,
30
36
  }
31
37
 
32
38
 
33
39
  class DocstringContext(NamedTuple):
34
- """Context for docstring processing.
40
+ """Context for docstring processing, validation, and error reporting.
35
41
 
36
42
  Args:
37
43
  file_path (Path): Path to the file
@@ -40,6 +46,10 @@ class DocstringContext(NamedTuple):
40
46
  verbose (bool): Whether to print verbose output
41
47
  require_param_types (bool): Whether parameter types are required
42
48
  check_references (bool): Whether to check references for errors
49
+ check_type_consistency (bool): Whether to compare docstring types with annotations
50
+ min_short_description_length (int): Minimum length for short description
51
+ max_short_description_length (int): Maximum length for short description (0 to disable)
52
+ node (ast.AST | None): AST node for the function or class
43
53
 
44
54
  Returns:
45
55
  DocstringContext: A named tuple containing docstring processing context
@@ -51,6 +61,35 @@ class DocstringContext(NamedTuple):
51
61
  verbose: bool
52
62
  require_param_types: bool = False
53
63
  check_references: bool = True
64
+ check_type_consistency: bool = False
65
+ min_short_description_length: int = 0
66
+ max_short_description_length: int = 0
67
+ node: ast.AST | None = None
68
+
69
+
70
+ _CONFIG_KEYS: dict[str, tuple[str, type]] = {
71
+ "paths": ("paths", list),
72
+ "require_param_types": ("require_param_types", bool),
73
+ "check_references": ("check_references", bool),
74
+ "check_type_consistency": ("check_type_consistency", bool),
75
+ "min_short_description_length": ("min_short_description_length", int),
76
+ "max_short_description_length": ("max_short_description_length", int),
77
+ "exclude_files": ("exclude_files", list),
78
+ "verbose": ("verbose", bool),
79
+ }
80
+
81
+
82
+ def _config_keys_match() -> bool:
83
+ """Return True if DEFAULT_CONFIG and _CONFIG_KEYS have the same keys."""
84
+ return set(DEFAULT_CONFIG.keys()) == set(_CONFIG_KEYS.keys())
85
+
86
+
87
+ def _apply_tool_config(config: dict[str, Any], tool_config: dict[str, Any]) -> None:
88
+ """Apply tool_config values to config. Modifies config in place."""
89
+ for key, (config_key, converter) in _CONFIG_KEYS.items():
90
+ if key in tool_config:
91
+ raw = tool_config[key]
92
+ config[config_key] = raw if converter is list else converter(raw)
54
93
 
55
94
 
56
95
  def load_pyproject_config() -> dict[str, Any]:
@@ -60,8 +99,6 @@ def load_pyproject_config() -> dict[str, Any]:
60
99
  dict[str, Any]: Dictionary with configuration values
61
100
  """
62
101
  config = DEFAULT_CONFIG.copy()
63
-
64
- # Look for pyproject.toml in the current directory
65
102
  pyproject_path = Path("pyproject.toml")
66
103
  if not pyproject_path.is_file():
67
104
  return config
@@ -69,24 +106,10 @@ def load_pyproject_config() -> dict[str, Any]:
69
106
  try:
70
107
  with pyproject_path.open("rb") as f:
71
108
  pyproject_data = tomli.load(f)
72
-
73
- # Check if our tool is configured
74
109
  tool_config = pyproject_data.get("tool", {}).get("docstring_checker", {})
75
110
  if not tool_config:
76
111
  return config
77
-
78
- # Update config with values from pyproject.toml
79
- if "paths" in tool_config:
80
- config["paths"] = tool_config["paths"]
81
- if "require_param_types" in tool_config:
82
- config["require_param_types"] = bool(tool_config["require_param_types"])
83
- if "check_references" in tool_config:
84
- config["check_references"] = bool(tool_config["check_references"])
85
- if "exclude_files" in tool_config:
86
- config["exclude_files"] = tool_config["exclude_files"]
87
- if "verbose" in tool_config:
88
- config["verbose"] = bool(tool_config["verbose"])
89
-
112
+ _apply_tool_config(config, tool_config)
90
113
  except Exception as e:
91
114
  print(f"Warning: Failed to load configuration from pyproject.toml: {e}")
92
115
 
@@ -94,7 +117,7 @@ def load_pyproject_config() -> dict[str, Any]:
94
117
 
95
118
 
96
119
  def get_docstrings(file_path: Path) -> list[tuple[str, int, str | None, ast.AST | None]]:
97
- """Extract docstrings from a Python file.
120
+ """Extract docstrings from a Python file using AST parsing.
98
121
 
99
122
  Args:
100
123
  file_path (Path): Path to the Python file
@@ -128,7 +151,7 @@ def get_docstrings(file_path: Path) -> list[tuple[str, int, str | None, ast.AST
128
151
 
129
152
 
130
153
  def check_param_types(docstring_dict: dict[str, Any], require_types: bool) -> list[str]:
131
- """Check if all parameters have types if required.
154
+ """Check if all parameters have types when types are required.
132
155
 
133
156
  Args:
134
157
  docstring_dict (dict[str, Any]): Parsed docstring dictionary
@@ -150,6 +173,111 @@ def check_param_types(docstring_dict: dict[str, Any], require_types: bool) -> li
150
173
  return errors
151
174
 
152
175
 
176
+ def _normalize_type(type_str: str) -> str:
177
+ """Normalize type string for comparison (quotes and whitespace only).
178
+
179
+ Python 3.10+ typing uses list, dict, tuple, X|Y - no List, Dict, Tuple, Union.
180
+ We do not normalize those; mismatches will be reported.
181
+ Internal whitespace differences (e.g., around commas, |, or brackets) are ignored.
182
+
183
+ Args:
184
+ type_str (str): Type string to normalize
185
+
186
+ Returns:
187
+ str: Normalized type string
188
+ """
189
+ normalized = type_str.strip().strip("'\"")
190
+ return re.sub(r"\s+", "", normalized)
191
+
192
+
193
+ def _annotation_to_str(annotation: ast.expr | None) -> str | None:
194
+ """Extract the type string from an AST annotation node.
195
+
196
+ Args:
197
+ annotation (ast.expr | None): AST annotation node
198
+
199
+ Returns:
200
+ str | None: Type string or None if no annotation
201
+ """
202
+ if annotation is None:
203
+ return None
204
+ if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
205
+ return annotation.value
206
+ return ast.unparse(annotation)
207
+
208
+
209
+ def _get_ast_param_types(node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, str]:
210
+ """Extract parameter names and type strings from function AST.
211
+
212
+ Args:
213
+ node (ast.FunctionDef | ast.AsyncFunctionDef): Function AST node
214
+
215
+ Returns:
216
+ dict[str, str]: Map of param name to annotation string (skips self/cls)
217
+ """
218
+ ast_params: dict[str, str] = {}
219
+ all_args: list[ast.arg] = []
220
+ all_args.extend(node.args.posonlyargs)
221
+ all_args.extend(node.args.args)
222
+ all_args.extend(node.args.kwonlyargs)
223
+ if node.args.vararg is not None:
224
+ all_args.append(node.args.vararg)
225
+ if node.args.kwarg is not None:
226
+ all_args.append(node.args.kwarg)
227
+ for arg in all_args:
228
+ if arg.arg in ("self", "cls"):
229
+ continue
230
+ if ann_str := _annotation_to_str(arg.annotation):
231
+ ast_params[arg.arg] = ann_str
232
+ return ast_params
233
+
234
+
235
+ def check_type_consistency(
236
+ parsed: dict[str, Any],
237
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
238
+ ) -> list[str]:
239
+ """Compare docstring types with function annotations.
240
+
241
+ Args:
242
+ parsed (dict[str, Any]): Parsed docstring dictionary
243
+ node (ast.FunctionDef | ast.AsyncFunctionDef): Function AST node
244
+
245
+ Returns:
246
+ list[str]: List of error messages for type mismatches
247
+ """
248
+ errors = []
249
+ ast_params = _get_ast_param_types(node)
250
+
251
+ # Compare Args
252
+ for arg in parsed.get("Args", []):
253
+ doc_type = arg.get("type")
254
+ if not doc_type:
255
+ continue
256
+ param_name = arg.get("name")
257
+ if not param_name or param_name not in ast_params:
258
+ continue
259
+ ast_type = ast_params[param_name]
260
+ if _normalize_type(doc_type) != _normalize_type(ast_type):
261
+ errors.append(
262
+ f"Parameter '{param_name}': docstring says '{doc_type}' but annotation says '{ast_type}'",
263
+ )
264
+
265
+ # Compare Returns (handle both dict and string "None" from parse_google_docstring)
266
+ returns = parsed.get("Returns")
267
+ doc_ret: str | None = None
268
+ if isinstance(returns, dict):
269
+ doc_ret = returns.get("type")
270
+ elif isinstance(returns, str):
271
+ doc_ret = returns
272
+ ast_ret = _annotation_to_str(node.returns)
273
+ if doc_ret and ast_ret and _normalize_type(doc_ret) != _normalize_type(ast_ret):
274
+ errors.append(
275
+ f"Returns: docstring says '{doc_ret}' but annotation says '{ast_ret}'",
276
+ )
277
+
278
+ return errors
279
+
280
+
153
281
  def _check_reference_fields(reference: dict[str, Any], index: int) -> list[str]:
154
282
  """Check a single reference for missing or empty fields.
155
283
 
@@ -177,7 +305,7 @@ def _check_reference_fields(reference: dict[str, Any], index: int) -> list[str]:
177
305
 
178
306
 
179
307
  def check_references(docstring_dict: dict[str, Any]) -> list[str]:
180
- """Check references section for common errors.
308
+ """Check references section for common formatting errors.
181
309
 
182
310
  Args:
183
311
  docstring_dict (dict[str, Any]): Parsed docstring dictionary
@@ -216,7 +344,7 @@ def check_references(docstring_dict: dict[str, Any]) -> list[str]:
216
344
 
217
345
 
218
346
  def validate_docstring(docstring: str) -> list[str]:
219
- """Perform additional validation on a docstring.
347
+ """Perform additional validation on docstring format and structure.
220
348
 
221
349
  Args:
222
350
  docstring (str): The docstring to validate
@@ -250,7 +378,7 @@ def validate_docstring(docstring: str) -> list[str]:
250
378
 
251
379
 
252
380
  def check_returns_section_name(docstring: str) -> list[str]:
253
- """Check for incorrect Returns section names.
381
+ """Check for incorrect Returns section names (e.g. return vs Returns).
254
382
 
255
383
  Args:
256
384
  docstring (str): The docstring to check
@@ -267,8 +395,73 @@ def check_returns_section_name(docstring: str) -> list[str]:
267
395
  return errors
268
396
 
269
397
 
398
+ def _extract_short_description(description: str) -> str:
399
+ r"""Extract short description: first paragraph (up to first blank line), normalized.
400
+
401
+ Leading and trailing whitespace (including blank lines) are stripped before
402
+ paragraph detection. Splits on one or more blank lines (\\n\\s*\\n), takes
403
+ the first part, then normalizes internal whitespace (tabs, newlines, multiple
404
+ spaces) to single spaces. Joins multi-line first paragraph for meta description use.
405
+
406
+ Args:
407
+ description (str): Full description text
408
+
409
+ Returns:
410
+ str: First paragraph as single line, or empty string
411
+ """
412
+ if not description:
413
+ return ""
414
+ parts = re.split(r"\n\s*\n", description.strip())
415
+ first_para = parts[0].strip() if parts else ""
416
+ return " ".join(first_para.split()) if first_para else ""
417
+
418
+
419
+ def check_short_description_length(
420
+ parsed: dict[str, Any],
421
+ min_length: int,
422
+ max_length: int = 0,
423
+ ) -> list[str]:
424
+ """Check that the short description meets length requirements.
425
+
426
+ Short description is the first paragraph (up to first blank line).
427
+ SEO: 120-160 chars recommended for meta descriptions.
428
+
429
+ Args:
430
+ parsed (dict[str, Any]): Parsed docstring dictionary
431
+ min_length (int): Minimum length (0 to disable)
432
+ max_length (int): Maximum length (0 to disable)
433
+
434
+ Returns:
435
+ list[str]: List of error messages for length violations
436
+ """
437
+ short_desc = _extract_short_description(parsed.get("Description") or "")
438
+ if not short_desc:
439
+ return []
440
+
441
+ errors: list[str] = []
442
+ if min_length > 0 and len(short_desc) < min_length:
443
+ preview = (
444
+ short_desc[:SHORT_DESC_PREVIEW_LENGTH] + "..."
445
+ if len(short_desc) > SHORT_DESC_PREVIEW_LENGTH
446
+ else short_desc
447
+ )
448
+ errors.append(
449
+ f"Short description too short ({len(short_desc)} chars, min {min_length}): '{preview}'",
450
+ )
451
+ if max_length > 0 and len(short_desc) > max_length:
452
+ preview = (
453
+ short_desc[:SHORT_DESC_PREVIEW_LENGTH] + "..."
454
+ if len(short_desc) > SHORT_DESC_PREVIEW_LENGTH
455
+ else short_desc
456
+ )
457
+ errors.append(
458
+ f"Short description too long ({len(short_desc)} chars, max {max_length}): '{preview}'",
459
+ )
460
+ return errors
461
+
462
+
270
463
  def check_returns_type(docstring_dict: dict[str, Any]) -> list[str]:
271
- """Check Returns type in a docstring."""
464
+ """Check that the Returns section has proper type annotation."""
272
465
  errors = []
273
466
  if returns := docstring_dict.get("Returns"):
274
467
  # Special case: Returns section just contains "None"
@@ -286,7 +479,7 @@ def check_returns_type(docstring_dict: dict[str, Any]) -> list[str]:
286
479
 
287
480
 
288
481
  def _format_error(context: DocstringContext, error: str) -> str:
289
- """Format an error message consistently.
482
+ """Format an error message consistently with file, line, and name.
290
483
 
291
484
  Args:
292
485
  context (DocstringContext): Docstring context
@@ -333,7 +526,7 @@ def safe_execute(
333
526
 
334
527
 
335
528
  def _check_returns_section(context: DocstringContext, docstring: str) -> list[str]:
336
- """Check the Returns section name.
529
+ """Check the Returns section name for correct spelling.
337
530
 
338
531
  Args:
339
532
  context (DocstringContext): Docstring context
@@ -352,7 +545,7 @@ def _check_returns_section(context: DocstringContext, docstring: str) -> list[st
352
545
 
353
546
 
354
547
  def _validate_docstring_format(context: DocstringContext, docstring: str) -> list[str]:
355
- """Validate docstring format.
548
+ """Validate docstring format for common structural issues.
356
549
 
357
550
  Args:
358
551
  context (DocstringContext): Docstring context
@@ -371,7 +564,7 @@ def _validate_docstring_format(context: DocstringContext, docstring: str) -> lis
371
564
 
372
565
 
373
566
  def _parse_and_check_returns(context: DocstringContext, docstring: str) -> tuple[list[str], dict[str, Any] | None]:
374
- """Parse docstring and check returns type.
567
+ """Parse docstring and check that the Returns section has proper type.
375
568
 
376
569
  Args:
377
570
  context (DocstringContext): Docstring context
@@ -408,7 +601,7 @@ def _parse_and_check_returns(context: DocstringContext, docstring: str) -> tuple
408
601
 
409
602
 
410
603
  def _check_additional_validations(context: DocstringContext, parsed: dict[str, Any]) -> list[str]:
411
- """Run additional validations on parsed docstring.
604
+ """Run additional validations on the parsed docstring dictionary.
412
605
 
413
606
  Args:
414
607
  context (DocstringContext): Docstring context
@@ -438,11 +631,36 @@ def _check_additional_validations(context: DocstringContext, parsed: dict[str, A
438
631
  )
439
632
  errors.extend(ref_errors)
440
633
 
634
+ if context.min_short_description_length > 0 or context.max_short_description_length > 0:
635
+ length_errors, _ = safe_execute(
636
+ context,
637
+ check_short_description_length,
638
+ parsed,
639
+ context.min_short_description_length,
640
+ context.max_short_description_length,
641
+ error_prefix="Error checking short description length",
642
+ )
643
+ errors.extend(length_errors)
644
+
645
+ if (
646
+ context.check_type_consistency
647
+ and context.node is not None
648
+ and isinstance(context.node, (ast.FunctionDef, ast.AsyncFunctionDef))
649
+ ):
650
+ consistency_errors, _ = safe_execute(
651
+ context,
652
+ check_type_consistency,
653
+ parsed,
654
+ context.node,
655
+ error_prefix="Error checking type consistency",
656
+ )
657
+ errors.extend(consistency_errors)
658
+
441
659
  return errors
442
660
 
443
661
 
444
662
  def _process_docstring(context: DocstringContext, docstring: str) -> list[str]:
445
- """Process a single docstring.
663
+ """Process a single docstring and collect all validation errors.
446
664
 
447
665
  Args:
448
666
  context (DocstringContext): Docstring context
@@ -482,14 +700,20 @@ def check_file(
482
700
  require_param_types: bool = False,
483
701
  verbose: bool = False,
484
702
  check_references: bool = True,
703
+ check_type_consistency: bool = False,
704
+ min_short_description_length: int = 0,
705
+ max_short_description_length: int = 0,
485
706
  ) -> list[str]:
486
- """Check docstrings in a file.
707
+ """Check docstrings in a Python file for parsing and validation errors.
487
708
 
488
709
  Args:
489
710
  file_path (Path): Path to the Python file
490
711
  require_param_types (bool): Whether parameter types are required
491
712
  verbose (bool): Whether to print verbose output
492
713
  check_references (bool): Whether to check references for errors
714
+ check_type_consistency (bool): Whether to compare docstring types with annotations
715
+ min_short_description_length (int): Minimum length for short description (0 to disable)
716
+ max_short_description_length (int): Maximum length for short description (0 to disable)
493
717
 
494
718
  Returns:
495
719
  list[str]: List of error messages
@@ -508,7 +732,7 @@ def check_file(
508
732
  print(error_msg)
509
733
  return errors
510
734
 
511
- for name, line_no, docstring, _ in docstrings:
735
+ for name, line_no, docstring, node in docstrings:
512
736
  context = DocstringContext(
513
737
  file_path=file_path,
514
738
  line_no=line_no,
@@ -516,6 +740,10 @@ def check_file(
516
740
  verbose=verbose,
517
741
  require_param_types=require_param_types,
518
742
  check_references=check_references,
743
+ check_type_consistency=check_type_consistency,
744
+ min_short_description_length=min_short_description_length,
745
+ max_short_description_length=max_short_description_length,
746
+ node=node,
519
747
  )
520
748
  errors.extend(_process_docstring(context, docstring))
521
749
 
@@ -528,6 +756,9 @@ def scan_directory(
528
756
  require_param_types: bool = False,
529
757
  verbose: bool = False,
530
758
  check_references: bool = True,
759
+ check_type_consistency: bool = False,
760
+ min_short_description_length: int = 0,
761
+ max_short_description_length: int = 0,
531
762
  ) -> list[str]:
532
763
  """Scan a directory for Python files and check their docstrings.
533
764
 
@@ -537,6 +768,9 @@ def scan_directory(
537
768
  require_param_types (bool): Whether parameter types are required
538
769
  verbose (bool): Whether to print verbose output
539
770
  check_references (bool): Whether to check references for errors
771
+ check_type_consistency (bool): Whether to compare docstring types with annotations
772
+ min_short_description_length (int): Minimum length for short description (0 to disable)
773
+ max_short_description_length (int): Maximum length for short description (0 to disable)
540
774
 
541
775
  Returns:
542
776
  list[str]: List of error messages
@@ -559,12 +793,22 @@ def scan_directory(
559
793
  break
560
794
 
561
795
  if not should_exclude:
562
- errors.extend(check_file(py_file, require_param_types, verbose, check_references))
796
+ errors.extend(
797
+ check_file(
798
+ py_file,
799
+ require_param_types,
800
+ verbose,
801
+ check_references,
802
+ check_type_consistency,
803
+ min_short_description_length,
804
+ max_short_description_length,
805
+ ),
806
+ )
563
807
  return errors
564
808
 
565
809
 
566
810
  def _parse_args() -> argparse.Namespace:
567
- """Parse command line arguments.
811
+ """Parse command line arguments for the docstring checker.
568
812
 
569
813
  Returns:
570
814
  argparse.Namespace: Parsed command line arguments
@@ -582,29 +826,53 @@ def _parse_args() -> argparse.Namespace:
582
826
  action="store_true",
583
827
  help="Require parameter types in docstrings",
584
828
  )
585
- parser.add_argument(
829
+ ref_group = parser.add_mutually_exclusive_group()
830
+ ref_group.add_argument(
586
831
  "--check-references",
587
832
  action="store_true",
588
833
  help="Check references for errors",
589
834
  )
590
- parser.add_argument(
835
+ ref_group.add_argument(
591
836
  "--no-check-references",
592
837
  action="store_true",
593
838
  help="Skip reference checking",
594
839
  )
840
+ type_consistency_group = parser.add_mutually_exclusive_group()
841
+ type_consistency_group.add_argument(
842
+ "--check-type-consistency",
843
+ action="store_true",
844
+ help="Compare docstring types with function annotations",
845
+ )
846
+ type_consistency_group.add_argument(
847
+ "--no-check-type-consistency",
848
+ action="store_true",
849
+ help="Skip type consistency checking",
850
+ )
595
851
  parser.add_argument(
596
852
  "--exclude-files",
597
853
  help="Comma-separated list of filenames to exclude",
598
854
  default="",
599
855
  )
600
856
  parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
857
+ parser.add_argument(
858
+ "--min-short-description-length",
859
+ type=int,
860
+ metavar="N",
861
+ help="Minimum length for short description (0 to disable)",
862
+ )
863
+ parser.add_argument(
864
+ "--max-short-description-length",
865
+ type=int,
866
+ metavar="N",
867
+ help="Maximum length for short description (0 to disable, 160 recommended for SEO)",
868
+ )
601
869
  return parser.parse_args()
602
870
 
603
871
 
604
872
  def _get_config_values(
605
873
  args: argparse.Namespace,
606
874
  config: dict[str, Any],
607
- ) -> tuple[list[str], bool, bool, bool, list[str]]:
875
+ ) -> tuple[list[str], bool, bool, bool, bool, int, int, list[str]]:
608
876
  """Get configuration values from command line arguments and config file.
609
877
 
610
878
  Args:
@@ -612,11 +880,14 @@ def _get_config_values(
612
880
  config (dict[str, Any]): Configuration dictionary
613
881
 
614
882
  Returns:
615
- tuple[list[str], bool, bool, bool, list[str]]: Tuple containing:
883
+ tuple[list[str], bool, bool, bool, bool, int, int, list[str]]: Tuple containing:
616
884
  - List of paths to check
617
885
  - Whether to require parameter types
618
- - Whether to check references
619
886
  - Whether to enable verbose output
887
+ - Whether to check references
888
+ - Whether to check type consistency
889
+ - Minimum short description length
890
+ - Maximum short description length
620
891
  - List of files to exclude
621
892
  """
622
893
  # Get paths
@@ -628,13 +899,20 @@ def _get_config_values(
628
899
  # Get verbose
629
900
  verbose = args.verbose or config["verbose"]
630
901
 
631
- # Get check_references - handle both positive and negative flags
902
+ # Get check_references - handle both positive and negative flags (mutually exclusive)
632
903
  check_references = config["check_references"]
633
904
  if args.check_references:
634
905
  check_references = True
635
906
  if args.no_check_references:
636
907
  check_references = False
637
908
 
909
+ # Get check_type_consistency - handle both positive and negative flags (mutually exclusive)
910
+ check_type_consistency = config.get("check_type_consistency", False)
911
+ if args.check_type_consistency:
912
+ check_type_consistency = True
913
+ if args.no_check_type_consistency:
914
+ check_type_consistency = False
915
+
638
916
  # Get exclude_files
639
917
  exclude_files = []
640
918
  if args.exclude_files:
@@ -644,7 +922,26 @@ def _get_config_values(
644
922
  if not exclude_files:
645
923
  exclude_files = config["exclude_files"]
646
924
 
647
- return paths, require_param_types, verbose, check_references, exclude_files
925
+ # Get min_short_description_length - CLI overrides config
926
+ min_short_description_length = config.get("min_short_description_length", 0)
927
+ if args.min_short_description_length is not None:
928
+ min_short_description_length = args.min_short_description_length
929
+
930
+ # Get max_short_description_length - CLI overrides config
931
+ max_short_description_length = config.get("max_short_description_length", 0)
932
+ if args.max_short_description_length is not None:
933
+ max_short_description_length = args.max_short_description_length
934
+
935
+ return (
936
+ paths,
937
+ require_param_types,
938
+ verbose,
939
+ check_references,
940
+ check_type_consistency,
941
+ min_short_description_length,
942
+ max_short_description_length,
943
+ exclude_files,
944
+ )
648
945
 
649
946
 
650
947
  def _process_paths(
@@ -653,8 +950,11 @@ def _process_paths(
653
950
  require_param_types: bool,
654
951
  verbose: bool,
655
952
  check_references: bool,
953
+ check_type_consistency: bool,
954
+ min_short_description_length: int,
955
+ max_short_description_length: int,
656
956
  ) -> list[str]:
657
- """Process paths and check docstrings.
957
+ """Process paths and check docstrings in each file or directory.
658
958
 
659
959
  Args:
660
960
  paths (list[str]): List of paths to check
@@ -662,6 +962,9 @@ def _process_paths(
662
962
  require_param_types (bool): Whether parameter types are required
663
963
  verbose (bool): Whether to print verbose output
664
964
  check_references (bool): Whether to check references for errors
965
+ check_type_consistency (bool): Whether to compare docstring types with annotations
966
+ min_short_description_length (int): Minimum length for short description (0 to disable)
967
+ max_short_description_length (int): Maximum length for short description (0 to disable)
665
968
 
666
969
  Returns:
667
970
  list[str]: List of error messages
@@ -670,10 +973,27 @@ def _process_paths(
670
973
  for path_str in paths:
671
974
  path = Path(path_str)
672
975
  if path.is_dir():
673
- errors = scan_directory(path, exclude_files, require_param_types, verbose, check_references)
976
+ errors = scan_directory(
977
+ path,
978
+ exclude_files,
979
+ require_param_types,
980
+ verbose,
981
+ check_references,
982
+ check_type_consistency,
983
+ min_short_description_length,
984
+ max_short_description_length,
985
+ )
674
986
  all_errors.extend(errors)
675
987
  elif path.is_file() and path.suffix == ".py":
676
- errors = check_file(path, require_param_types, verbose, check_references)
988
+ errors = check_file(
989
+ path,
990
+ require_param_types,
991
+ verbose,
992
+ check_references,
993
+ check_type_consistency,
994
+ min_short_description_length,
995
+ max_short_description_length,
996
+ )
677
997
  all_errors.extend(errors)
678
998
  else:
679
999
  print(f"Error: {path} is not a directory or Python file")
@@ -681,7 +1001,7 @@ def _process_paths(
681
1001
 
682
1002
 
683
1003
  def main() -> None:
684
- """Run the docstring checker.
1004
+ """Run the docstring checker and exit with appropriate status code.
685
1005
 
686
1006
  Returns:
687
1007
  None
@@ -693,7 +1013,16 @@ def main() -> None:
693
1013
  args = _parse_args()
694
1014
 
695
1015
  # Get configuration values
696
- paths, require_param_types, verbose, check_references, exclude_files = _get_config_values(args, config)
1016
+ (
1017
+ paths,
1018
+ require_param_types,
1019
+ verbose,
1020
+ check_references,
1021
+ check_type_consistency,
1022
+ min_short_description_length,
1023
+ max_short_description_length,
1024
+ exclude_files,
1025
+ ) = _get_config_values(args, config)
697
1026
 
698
1027
  # Print configuration if verbose
699
1028
  if verbose:
@@ -701,6 +1030,9 @@ def main() -> None:
701
1030
  print(f" Paths: {paths}")
702
1031
  print(f" Require parameter types: {require_param_types}")
703
1032
  print(f" Check references: {check_references}")
1033
+ print(f" Check type consistency: {check_type_consistency}")
1034
+ print(f" Min short description length: {min_short_description_length}")
1035
+ print(f" Max short description length: {max_short_description_length}")
704
1036
  print(f" Exclude files: {exclude_files}")
705
1037
 
706
1038
  # Check if paths is empty
@@ -717,6 +1049,9 @@ def main() -> None:
717
1049
  require_param_types,
718
1050
  verbose,
719
1051
  check_references,
1052
+ check_type_consistency,
1053
+ min_short_description_length,
1054
+ max_short_description_length,
720
1055
  ):
721
1056
  for error in all_errors:
722
1057
  print(error)