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.
- google_docstring_parser/__init__.py +4 -0
- google_docstring_parser/google_docstring_parser.py +133 -0
- google_docstring_parser-0.0.1.dist-info/LICENSE +58 -0
- google_docstring_parser-0.0.1.dist-info/METADATA +148 -0
- google_docstring_parser-0.0.1.dist-info/RECORD +9 -0
- google_docstring_parser-0.0.1.dist-info/WHEEL +5 -0
- google_docstring_parser-0.0.1.dist-info/top_level.txt +2 -0
- tools/__init__.py +1 -0
- tools/check_docstrings.py +440 -0
|
@@ -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,,
|
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()
|