google-docstring-parser 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ """Google Docstring Parser package.
2
+
3
+ A lightweight, efficient parser for Google-style Python docstrings that converts them into structured dictionaries.
4
+ """
@@ -0,0 +1,133 @@
1
+ """Parser for Google-style docstrings.
2
+
3
+ This module provides functions to parse Google-style docstrings into structured dictionaries.
4
+
5
+ # CUSTOM LICENSE NOTICE FOR GOOGLE DOCSTRING PARSER
6
+ #
7
+ # Copyright (c) 2025 Vladimir Iglovikov
8
+ #
9
+ # ⚠️ IMPORTANT LICENSE NOTICE ⚠️
10
+ # This package requires a PAID LICENSE for all users EXCEPT the Albumentations Team.
11
+ #
12
+ # - Free for Albumentations Team projects (https://github.com/albumentations-team)
13
+ # - Paid license required for all other users
14
+ #
15
+ # Contact iglovikov@gmail.com to obtain a license before using this software.
16
+ # See the LICENSE file for complete details.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from typing import Any
23
+
24
+ from docstring_parser import parse
25
+
26
+
27
+ def _extract_sections(docstring: str) -> dict[str, str]:
28
+ """Extract sections from a docstring.
29
+
30
+ Args:
31
+ docstring (str): The docstring to extract sections from
32
+
33
+ Returns:
34
+ A dictionary mapping section names to their content
35
+ """
36
+ sections: dict[str, str] = {}
37
+ current_section = "Description"
38
+ lines = docstring.split("\n")
39
+
40
+ section_content: list[str] = []
41
+ indent_level: int | None = None
42
+
43
+ for line in lines:
44
+ if not (stripped := line.strip()) and not section_content:
45
+ continue
46
+
47
+ # Check if this is a section header
48
+ if section_match := re.match(r"^([A-Za-z][A-Za-z0-9 ]+):$", stripped):
49
+ # Save previous section content
50
+ if section_content:
51
+ sections[current_section] = "\n".join(section_content).strip()
52
+ section_content = []
53
+
54
+ # Set new current section
55
+ current_section = section_match[1]
56
+ indent_level = None
57
+ else:
58
+ # If this is the first content line after a section header, determine indent level
59
+ if indent_level is None and stripped:
60
+ indent_level = len(line) - len(line.lstrip())
61
+
62
+ # Add line to current section content, removing one level of indentation
63
+ if stripped or section_content: # Only add empty lines if we already have content
64
+ if indent_level is not None and line.startswith(" " * indent_level):
65
+ # Remove one level of indentation
66
+ processed_line = line[indent_level:]
67
+ section_content.append(processed_line)
68
+ else:
69
+ section_content.append(line)
70
+
71
+ # Add the last section
72
+ if section_content:
73
+ sections[current_section] = "\n".join(section_content).strip()
74
+
75
+ return sections
76
+
77
+
78
+ def parse_google_docstring(docstring: str) -> dict[str, Any]:
79
+ """Parse a Google-style docstring into a structured dictionary.
80
+
81
+ Args:
82
+ docstring (str): The docstring to parse
83
+
84
+ Returns:
85
+ A dictionary with parsed docstring sections
86
+ """
87
+ if not docstring:
88
+ return {}
89
+
90
+ # Clean up the docstring
91
+ docstring = docstring.strip()
92
+
93
+ # Initialize result dictionary with only description
94
+ result: dict[str, Any] = {"Description": ""}
95
+
96
+ # Extract sections and parse docstring
97
+ sections = _extract_sections(docstring)
98
+ parsed = parse(docstring)
99
+
100
+ # Process description
101
+ if parsed.description:
102
+ result["Description"] = parsed.description.rstrip()
103
+
104
+ # Process args (only if present)
105
+ if "Args" in sections and (
106
+ args := [
107
+ {
108
+ "name": arg.arg_name.rstrip() if arg.arg_name is not None else None,
109
+ "type": arg.type_name.rstrip() if arg.type_name is not None else None,
110
+ "description": arg.description.rstrip() if arg.description is not None else None,
111
+ }
112
+ for arg in parsed.params
113
+ ]
114
+ ):
115
+ result["Args"] = args
116
+
117
+ # Process returns
118
+ if (
119
+ "Returns" in sections
120
+ and (returns_lines := sections["Returns"].split("\n"))
121
+ and (return_match := re.match(r"^(?:(\w+):\s*)?(.*)$", returns_lines[0].strip()))
122
+ and (return_desc := return_match[2])
123
+ ):
124
+ result["Returns"] = [{"type": return_match[1], "description": return_desc.rstrip()}]
125
+ else:
126
+ result["Returns"] = []
127
+
128
+ # Add other sections directly using dict union
129
+ return result | {
130
+ section: content.rstrip()
131
+ for section, content in sections.items()
132
+ if section not in ["Description", "Args", "Returns"]
133
+ }
@@ -0,0 +1,58 @@
1
+ # CUSTOM LICENSE AGREEMENT FOR GOOGLE DOCSTRING PARSER
2
+
3
+ Copyright (c) 2025 Vladimir Iglovikov
4
+
5
+ ## 1. DEFINITIONS
6
+
7
+ "Software" refers to the Google Docstring Parser library, including all source code, documentation, and associated files.
8
+
9
+ "Licensor" refers to Vladimir Iglovikov, the copyright holder of the Software.
10
+
11
+ "Albumentations Team" refers to all projects hosted under the GitHub organization at https://github.com/albumentations-team.
12
+
13
+ "User" refers to any individual or entity that uses, copies, modifies, or distributes the Software.
14
+
15
+ ## 2. GRANT OF LICENSE
16
+
17
+ ### 2.1 Albumentations Team Projects Use
18
+ The Licensor hereby grants, free of charge, to all projects within the Albumentations Team GitHub organization and their official contributors the right to use, copy, modify, merge, publish, and distribute the Software for the purpose of developing and maintaining any project within the Albumentations Team organization.
19
+
20
+ ### 2.2 All Other Use
21
+ All other Users, including but not limited to individuals, commercial entities, non-profit organizations, and open source projects other than Albumentations, must obtain a paid license from the Licensor before using, copying, modifying, or distributing the Software for any purpose.
22
+
23
+ ## 3. RESTRICTIONS
24
+
25
+ ### 3.1 General Restrictions
26
+ Without a paid license, no User (except as specified in Section 2.1) may:
27
+ - Use the Software for any purpose
28
+ - Copy, modify, or distribute the Software
29
+ - Create derivative works based on the Software
30
+ - Sublicense the Software
31
+
32
+ ### 3.2 Albumentations Team Restrictions
33
+ The Albumentations Team projects may not:
34
+ - Grant rights to the Software to any third party beyond what is necessary for the development and maintenance of the Albumentations Team projects
35
+ - Use the Software for purposes unrelated to the Albumentations Team projects
36
+
37
+ ## 4. OBTAINING A PAID LICENSE
38
+
39
+ To obtain a paid license for the Software, please contact:
40
+
41
+ Vladimir Iglovikov
42
+ iglovikov@gmail.com
43
+
44
+ ## 5. WARRANTY DISCLAIMER
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
47
+
48
+ ## 6. TERMINATION
49
+
50
+ This license automatically terminates if you violate any of its terms. Upon termination, you must cease all use of the Software and destroy all copies of the Software in your possession.
51
+
52
+ ## 7. MISCELLANEOUS
53
+
54
+ This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
55
+
56
+ ## 8. GOVERNING LAW
57
+
58
+ This License shall be governed by the laws of the United States of America.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.2
2
+ Name: google-docstring-parser
3
+ Version: 0.0.1
4
+ Summary: A lightweight, efficient parser for Google-style Python docstrings that converts them into structured dictionaries.
5
+ Author: Vladimir Iglovikov
6
+ Maintainer: Vladimir Iglovikov
7
+ License: Custom License - See LICENSE file for details
8
+ Keywords: docstring,documentation,google-style,parser,python,static-analysis,type-hints
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: Other/Proprietary License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Software Development :: Documentation
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: docstring-parser>=0.16
30
+ Requires-Dist: typing-extensions>=4.9; python_version < "3.10"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pre-commit>=3.5; extra == "dev"
33
+ Requires-Dist: pytest>=8.3.3; extra == "dev"
34
+
35
+ # Google Docstring Parser
36
+
37
+ > [!IMPORTANT]
38
+ > This package requires a PAID LICENSE for all users EXCEPT the Albumentations Team.
39
+ > Contact iglovikov@gmail.com to obtain a license before using this software.
40
+
41
+ A Python package for parsing Google-style docstrings into structured dictionaries.
42
+
43
+ ## License Information
44
+
45
+ This package is available under a custom license:
46
+ - **Free for Albumentations Team projects** (https://github.com/albumentations-team)
47
+ - **Paid license required for all other users** (individuals, companies, and other open-source projects)
48
+
49
+ See the [LICENSE](LICENSE) file for complete details.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install google-docstring-parser
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ```python
60
+ from google_docstring_parser import parse_google_docstring
61
+
62
+ docstring = '''Apply elastic deformation to images, masks, bounding boxes, and keypoints.
63
+
64
+ This transformation introduces random elastic distortions to the input data. It's particularly
65
+ useful for data augmentation in training deep learning models, especially for tasks like
66
+ image segmentation or object detection where you want to maintain the relative positions of
67
+ features while introducing realistic deformations.
68
+
69
+ Args:
70
+ alpha (float): Scaling factor for the random displacement fields. Higher values result in
71
+ more pronounced distortions. Default: 1.0
72
+ sigma (float): Standard deviation of the Gaussian filter used to smooth the displacement
73
+ fields. Higher values result in smoother, more global distortions. Default: 50.0
74
+
75
+ Example:
76
+ >>> import albumentations as A
77
+ >>> transform = A.ElasticTransform(alpha=1, sigma=50, p=0.5)
78
+ '''
79
+
80
+ parsed = parse_google_docstring(docstring)
81
+ print(parsed)
82
+ ```
83
+
84
+ Output:
85
+ ```python
86
+ {
87
+ 'Description': 'Apply elastic deformation to images, masks, bounding boxes, and keypoints.\n\nThis transformation introduces random elastic distortions to the input data. It\'s particularly\nuseful for data augmentation in training deep learning models, especially for tasks like\nimage segmentation or object detection where you want to maintain the relative positions of\nfeatures while introducing realistic deformations.',
88
+ 'Args': [
89
+ {
90
+ 'name': 'alpha',
91
+ 'type': 'float',
92
+ 'description': 'Scaling factor for the random displacement fields. Higher values result in\nmore pronounced distortions. Default: 1.0'
93
+ },
94
+ {
95
+ 'name': 'sigma',
96
+ 'type': 'float',
97
+ 'description': 'Standard deviation of the Gaussian filter used to smooth the displacement\nfields. Higher values result in smoother, more global distortions. Default: 50.0'
98
+ }
99
+ ],
100
+ 'Example': '>>> import albumentations as A\n>>> transform = A.ElasticTransform(alpha=1, sigma=50, p=0.5)'
101
+ }
102
+ ```
103
+
104
+ ## Features
105
+
106
+ - Parses Google-style docstrings into structured dictionaries
107
+ - Extracts parameter names, types, and descriptions
108
+ - Preserves other sections like Examples, Notes, etc.
109
+ - Handles multi-line descriptions and indentation properly
110
+
111
+
112
+ ## Pre-commit Hook
113
+
114
+ This package includes a pre-commit hook that checks if Google-style docstrings in your codebase can be parsed correctly.
115
+
116
+ ### Usage in Other Projects
117
+
118
+ To use this hook in another project, add the following to your `.pre-commit-config.yaml`:
119
+
120
+ ```yaml
121
+ - repo: https://github.com/ternaus/google-docstring-parser
122
+ rev: v0.0.1 # Use the latest version
123
+ hooks:
124
+ - id: check-google-docstrings
125
+ additional_dependencies: ["tomli>=2.0.0"] # Required for pyproject.toml configuration
126
+ ```
127
+
128
+ ### Configuration
129
+
130
+ The hook is configured via pyproject.toml, following modern Python tooling conventions like those used by mypy, ruff, and other tools.
131
+
132
+ Add a `[tool.docstring_checker]` section to your pyproject.toml:
133
+
134
+ ```toml
135
+ [tool.docstring_checker]
136
+ paths = ["src", "tests"] # Directories or files to scan
137
+ require_param_types = true # Require parameter types in docstrings
138
+ exclude_files = ["conftest.py", "__init__.py"] # Files to exclude from checks
139
+ verbose = false # Enable verbose output
140
+ ```
141
+
142
+ This approach has several advantages:
143
+ - Keeps all your project configuration in one place
144
+ - Follows modern Python tooling conventions
145
+ - Makes it easier to maintain and update configuration
146
+ - Provides better IDE support and documentation
147
+
148
+ For more details, see the [tools README](tools/README.md).
@@ -0,0 +1,9 @@
1
+ google_docstring_parser/__init__.py,sha256=SGFCYmhurVwW9Xg1c5aJihIvjvhZ0-yBO6UNJqpY91A,157
2
+ google_docstring_parser/google_docstring_parser.py,sha256=gTtuCa_l8yhCdvf-NIOA0nRxpjLtxgvbrvSLpumgNNw,4379
3
+ tools/__init__.py,sha256=kcECZzlVqWRVzmpblGFiuaEs-0iFCriLWzjJRNlpjMw,53
4
+ tools/check_docstrings.py,sha256=4lnEy1wbzh0VA14cnKsqG0T0eemDobfk_o0RQJlIBD0,13139
5
+ google_docstring_parser-0.0.1.dist-info/LICENSE,sha256=K28e2_I4KXVBq-N8hSFwN4QP4SA4GnUypO0Dy1k4VdE,3003
6
+ google_docstring_parser-0.0.1.dist-info/METADATA,sha256=CFOV68kgnS0jQv8dcMyoBek4Izr017WvQTu-zD4HSlk,5955
7
+ google_docstring_parser-0.0.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
8
+ google_docstring_parser-0.0.1.dist-info/top_level.txt,sha256=_0utD2jjoqeoDpyqjmzAM1MBO1TdgbwvB0HTmbpyI8c,30
9
+ google_docstring_parser-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (76.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ google_docstring_parser
2
+ tools
tools/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Tools for the google-docstring-parser package."""
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env python
2
+ """Check that docstrings in specified folders can be parsed.
3
+
4
+ This script scans Python files in specified directories and checks if their
5
+ docstrings can be parsed with the google_docstring_parser.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import ast
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any, NamedTuple
16
+
17
+ import tomli
18
+
19
+ from google_docstring_parser.google_docstring_parser import parse_google_docstring
20
+
21
+ # Default configuration
22
+ DEFAULT_CONFIG = {
23
+ "paths": ["."],
24
+ "require_param_types": False,
25
+ "exclude_files": [],
26
+ "verbose": False,
27
+ }
28
+
29
+
30
+ class DocstringContext(NamedTuple):
31
+ """Context for docstring processing.
32
+
33
+ Args:
34
+ file_path (Path): Path to the file
35
+ line_no (int): Line number
36
+ name (str): Name of the function or class
37
+ verbose (bool): Whether to print verbose output
38
+ require_param_types (bool): Whether parameter types are required
39
+ """
40
+
41
+ file_path: Path
42
+ line_no: int
43
+ name: str
44
+ verbose: bool
45
+ require_param_types: bool = False
46
+
47
+
48
+ def load_pyproject_config() -> dict[str, Any]:
49
+ """Load configuration from pyproject.toml if it exists.
50
+
51
+ Returns:
52
+ Dictionary with configuration values
53
+ """
54
+ config = DEFAULT_CONFIG.copy()
55
+
56
+ # Look for pyproject.toml in the current directory
57
+ pyproject_path = Path("pyproject.toml")
58
+ if not pyproject_path.is_file():
59
+ return config
60
+
61
+ try:
62
+ with pyproject_path.open("rb") as f:
63
+ pyproject_data = tomli.load(f)
64
+
65
+ # Check if our tool is configured
66
+ tool_config = pyproject_data.get("tool", {}).get("docstring_checker", {})
67
+ if not tool_config:
68
+ return config
69
+
70
+ # Update config with values from pyproject.toml
71
+ if "paths" in tool_config:
72
+ config["paths"] = tool_config["paths"]
73
+ if "require_param_types" in tool_config:
74
+ config["require_param_types"] = bool(tool_config["require_param_types"])
75
+ if "exclude_files" in tool_config:
76
+ config["exclude_files"] = tool_config["exclude_files"]
77
+ if "verbose" in tool_config:
78
+ config["verbose"] = bool(tool_config["verbose"])
79
+
80
+ except Exception as e:
81
+ print(f"Warning: Failed to load configuration from pyproject.toml: {e}")
82
+
83
+ return config
84
+
85
+
86
+ def get_docstrings(file_path: Path) -> list[tuple[str, int, str | None, ast.AST | None]]:
87
+ """Extract docstrings from a Python file.
88
+
89
+ Args:
90
+ file_path (Path): Path to the Python file
91
+
92
+ Returns:
93
+ List of tuples containing (function/class name, line number, docstring, node)
94
+ """
95
+ with file_path.open(encoding="utf-8") as f:
96
+ content = f.read()
97
+
98
+ try:
99
+ tree = ast.parse(content)
100
+ except SyntaxError as e:
101
+ print(f"Syntax error in {file_path}: {e}")
102
+ return []
103
+
104
+ docstrings = []
105
+
106
+ # Get module docstring
107
+ if len(tree.body) > 0 and isinstance(tree.body[0], ast.Expr) and isinstance(tree.body[0].value, ast.Constant):
108
+ docstrings.append(("module", 1, tree.body[0].value.value, None))
109
+
110
+ # Get function and class docstrings
111
+ for node in ast.walk(tree):
112
+ if isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)):
113
+ docstring = ast.get_docstring(node)
114
+ if docstring:
115
+ docstrings.append((node.name, node.lineno, docstring, node))
116
+
117
+ return docstrings
118
+
119
+
120
+ def check_param_types(docstring_dict: dict[str, Any], require_types: bool) -> list[str]:
121
+ """Check if all parameters have types if required.
122
+
123
+ Args:
124
+ docstring_dict (dict[str, Any]): Parsed docstring dictionary
125
+ require_types (bool): Whether parameter types are required
126
+
127
+ Returns:
128
+ List of error messages for parameters missing types or having invalid types
129
+ """
130
+ if not require_types or "Args" not in docstring_dict:
131
+ return []
132
+
133
+ errors = []
134
+ for arg in docstring_dict["Args"]:
135
+ if arg["type"] is None:
136
+ errors.append(f"Parameter '{arg['name']}' is missing a type in docstring")
137
+ elif "invalid type" in arg["type"].lower():
138
+ errors.append(f"Parameter '{arg['name']}' has an invalid type in docstring: '{arg['type']}'")
139
+
140
+ return errors
141
+
142
+
143
+ def validate_docstring(docstring: str) -> list[str]:
144
+ """Perform additional validation on a docstring.
145
+
146
+ Args:
147
+ docstring (str): The docstring to validate
148
+
149
+ Returns:
150
+ List of validation error messages
151
+ """
152
+ errors = []
153
+
154
+ # Check for unclosed parentheses in parameter types
155
+ lines = docstring.split("\n")
156
+ for line in lines:
157
+ stripped_line = line.strip()
158
+ if not stripped_line:
159
+ continue
160
+
161
+ # Check for parameter definitions with unclosed parentheses
162
+ # Improved regex to better detect unclosed parentheses and brackets
163
+ if param_match := re.match(
164
+ r"^\s*(\w+)\s+\(([^)]*$|.*\[[^\]]*$)",
165
+ stripped_line,
166
+ ):
167
+ errors.append(f"Unclosed parenthesis in parameter type: '{stripped_line}'")
168
+
169
+ # Check for invalid type declarations
170
+ param_match = re.match(r"^\s*(\w+)\s+\((invalid type)\)", stripped_line)
171
+ if param_match:
172
+ errors.append(f"Invalid type declaration: '{stripped_line}'")
173
+
174
+ return errors
175
+
176
+
177
+ def _format_error(context: DocstringContext, error: str) -> str:
178
+ """Format an error message consistently.
179
+
180
+ Args:
181
+ context (DocstringContext): Docstring context
182
+ error (str): Error message
183
+
184
+ Returns:
185
+ str: Formatted error message
186
+ """
187
+ msg = f"{context.file_path}:{context.line_no}: {error} in '{context.name}'"
188
+ if context.verbose:
189
+ print(msg)
190
+ return msg
191
+
192
+
193
+ def _process_docstring(context: DocstringContext, docstring: str) -> list[str]:
194
+ """Process a single docstring.
195
+
196
+ Args:
197
+ context (DocstringContext): Docstring context
198
+ docstring (str): The docstring to process
199
+
200
+ Returns:
201
+ list[str]: List of error messages
202
+ """
203
+ errors = []
204
+ if not docstring:
205
+ return errors
206
+
207
+ # Validate docstring inline
208
+ try:
209
+ val_errors = validate_docstring(docstring)
210
+ if val_errors:
211
+ errors.extend(_format_error(context, err) for err in val_errors)
212
+ except Exception as e:
213
+ errors.append(_format_error(context, f"Error validating docstring: {e}"))
214
+ return errors
215
+
216
+ # Parse docstring inline
217
+ try:
218
+ parsed = parse_google_docstring(docstring)
219
+ except Exception as e:
220
+ errors.append(_format_error(context, f"Error parsing docstring: {e}"))
221
+ return errors
222
+
223
+ # Check parameter types (if required) inline
224
+ if context.require_param_types:
225
+ try:
226
+ type_errors = check_param_types(parsed, context.require_param_types)
227
+ errors.extend(_format_error(context, err) for err in type_errors)
228
+ except Exception as e:
229
+ errors.append(_format_error(context, f"Error checking parameter types: {e}"))
230
+
231
+ return errors
232
+
233
+
234
+ def check_file(
235
+ file_path: Path,
236
+ require_param_types: bool = False,
237
+ verbose: bool = False,
238
+ ) -> list[str]:
239
+ """Check docstrings in a file.
240
+
241
+ Args:
242
+ file_path (Path): Path to the Python file
243
+ require_param_types (bool): Whether parameter types are required
244
+ verbose (bool): Whether to print verbose output
245
+
246
+ Returns:
247
+ List of error messages
248
+ """
249
+ if verbose:
250
+ print(f"Checking {file_path}")
251
+
252
+ errors = []
253
+
254
+ try:
255
+ docstrings = get_docstrings(file_path)
256
+ except Exception as e:
257
+ error_msg = f"{file_path}: Error getting docstrings: {e!s}"
258
+ errors.append(error_msg)
259
+ if verbose:
260
+ print(error_msg)
261
+ return errors
262
+
263
+ for name, line_no, docstring, _ in docstrings:
264
+ context = DocstringContext(
265
+ file_path=file_path,
266
+ line_no=line_no,
267
+ name=name,
268
+ verbose=verbose,
269
+ require_param_types=require_param_types,
270
+ )
271
+ errors.extend(_process_docstring(context, docstring))
272
+
273
+ return errors
274
+
275
+
276
+ def scan_directory(
277
+ directory: Path,
278
+ exclude_files: list[str] | None = None,
279
+ require_param_types: bool = False,
280
+ verbose: bool = False,
281
+ ) -> list[str]:
282
+ """Scan a directory for Python files and check their docstrings.
283
+
284
+ Args:
285
+ directory (Path): Directory to scan
286
+ exclude_files (list[str] | None): List of filenames to exclude
287
+ require_param_types (bool): Whether parameter types are required
288
+ verbose (bool): Whether to print verbose output
289
+
290
+ Returns:
291
+ List of error messages
292
+ """
293
+ if exclude_files is None:
294
+ exclude_files = []
295
+
296
+ errors = []
297
+ for py_file in directory.glob("**/*.py"):
298
+ # Check if the file should be excluded
299
+ should_exclude = False
300
+ for exclude_pattern in exclude_files:
301
+ # Check if the filename matches exactly
302
+ if py_file.name == exclude_pattern:
303
+ should_exclude = True
304
+ break
305
+ # Check if the path ends with the pattern (for subdirectories)
306
+ if str(py_file).endswith(f"/{exclude_pattern}"):
307
+ should_exclude = True
308
+ break
309
+
310
+ if not should_exclude:
311
+ errors.extend(check_file(py_file, require_param_types, verbose))
312
+ return errors
313
+
314
+
315
+ def _parse_args() -> argparse.Namespace:
316
+ """Parse command line arguments.
317
+
318
+ Returns:
319
+ Parsed arguments
320
+ """
321
+ parser = argparse.ArgumentParser(
322
+ description="Check that docstrings in specified folders can be parsed.",
323
+ )
324
+ parser.add_argument(
325
+ "paths",
326
+ nargs="*",
327
+ help="Directories or files to scan for Python docstrings",
328
+ )
329
+ parser.add_argument(
330
+ "--require-param-types",
331
+ action="store_true",
332
+ help="Require parameter types in docstrings",
333
+ )
334
+ parser.add_argument(
335
+ "--exclude-files",
336
+ help="Comma-separated list of filenames to exclude",
337
+ default="",
338
+ )
339
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
340
+ return parser.parse_args()
341
+
342
+
343
+ def _get_config_values(args: argparse.Namespace, config: dict[str, Any]) -> tuple[list[str], bool, bool, list[str]]:
344
+ """Get configuration values from command line args and config.
345
+
346
+ Args:
347
+ args (argparse.Namespace): Command line arguments
348
+ config (dict[str, Any]): Configuration from pyproject.toml
349
+
350
+ Returns:
351
+ Tuple of (paths, require_param_types, verbose, exclude_files)
352
+ """
353
+ # Get paths
354
+ paths = args.paths or config["paths"]
355
+
356
+ # Get require_param_types
357
+ require_param_types = args.require_param_types or config["require_param_types"]
358
+
359
+ # Get verbose
360
+ verbose = args.verbose or config["verbose"]
361
+
362
+ # Get exclude_files
363
+ exclude_files = []
364
+ if args.exclude_files:
365
+ exclude_files = [f.strip() for f in args.exclude_files.split(",") if f.strip()]
366
+
367
+ # If no exclude_files specified on command line, use the ones from config
368
+ if not exclude_files:
369
+ exclude_files = config["exclude_files"]
370
+
371
+ return paths, require_param_types, verbose, exclude_files
372
+
373
+
374
+ def _process_paths(
375
+ paths: list[str],
376
+ exclude_files: list[str],
377
+ require_param_types: bool,
378
+ verbose: bool,
379
+ ) -> list[str]:
380
+ """Process paths and collect errors.
381
+
382
+ Args:
383
+ paths (list[str]): List of paths to process
384
+ exclude_files (list[str]): List of files to exclude
385
+ require_param_types (bool): Whether to require parameter types
386
+ verbose (bool): Whether to print verbose output
387
+
388
+ Returns:
389
+ List of error messages
390
+ """
391
+ all_errors = []
392
+ for path_str in paths:
393
+ path = Path(path_str)
394
+ if path.is_dir():
395
+ errors = scan_directory(path, exclude_files, require_param_types, verbose)
396
+ all_errors.extend(errors)
397
+ elif path.is_file() and path.suffix == ".py":
398
+ errors = check_file(path, require_param_types, verbose)
399
+ all_errors.extend(errors)
400
+ else:
401
+ print(f"Error: {path} is not a directory or Python file")
402
+ return all_errors
403
+
404
+
405
+ def main():
406
+ """Run the docstring checker."""
407
+ # Load configuration from pyproject.toml
408
+ config = load_pyproject_config()
409
+
410
+ # Parse command line arguments
411
+ args = _parse_args()
412
+
413
+ # Get configuration values
414
+ paths, require_param_types, verbose, exclude_files = _get_config_values(args, config)
415
+
416
+ # Print configuration if verbose
417
+ if verbose:
418
+ print("Configuration:")
419
+ print(f" Paths: {paths}")
420
+ print(f" Require parameter types: {require_param_types}")
421
+ print(f" Exclude files: {exclude_files}")
422
+
423
+ if all_errors := _process_paths(
424
+ paths,
425
+ exclude_files,
426
+ require_param_types,
427
+ verbose,
428
+ ):
429
+ for error in all_errors:
430
+ print(error)
431
+ print(f"\nFound {len(all_errors)} error{'s' if len(all_errors) != 1 else ''}")
432
+ sys.exit(1)
433
+ elif verbose:
434
+ print("All docstrings parsed successfully!")
435
+
436
+ sys.exit(0)
437
+
438
+
439
+ if __name__ == "__main__":
440
+ main()