docstringify 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docstringify-0.5.0/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
- docstringify-0.5.0/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- docstringify-0.5.0/.github/workflows/greetings.yml +16 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/PKG-INFO +10 -10
- {docstringify-0.3.0 → docstringify-0.5.0}/README.md +9 -9
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/__init__.py +1 -1
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/cli.py +3 -4
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/converters/base.py +23 -4
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/converters/google.py +14 -4
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/converters/numpydoc.py +14 -4
- docstringify-0.5.0/src/docstringify/nodes/__init__.py +1 -0
- docstringify-0.5.0/src/docstringify/nodes/base.py +56 -0
- docstringify-0.5.0/src/docstringify/nodes/function.py +168 -0
- docstringify-0.5.0/src/docstringify/traversal/__init__.py +9 -0
- docstringify-0.5.0/src/docstringify/traversal/transformer.py +61 -0
- docstringify-0.5.0/src/docstringify/traversal/visitor.py +99 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/PKG-INFO +10 -10
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/SOURCES.txt +10 -4
- docstringify-0.3.0/src/docstringify/docstring_generator.py +0 -157
- docstringify-0.3.0/src/docstringify/transformer.py +0 -53
- docstringify-0.3.0/src/docstringify/visitor.py +0 -79
- {docstringify-0.3.0 → docstringify-0.5.0}/.github/dependabot.yml +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/.github/release.yml +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/.github/workflows/ci.yml +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/.github/workflows/pypi-publish.yml +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/.gitignore +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/.pre-commit-config.yaml +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/.pre-commit-hooks.yaml +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/LICENSE +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/pyproject.toml +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/setup.cfg +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/__main__.py +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/components.py +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/converters/__init__.py +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/exceptions.py +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/dependency_links.txt +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/entry_points.txt +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/requires.txt +0 -0
- {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Something isn't working
|
|
4
|
+
title: "[BUG] "
|
|
5
|
+
labels: bug
|
|
6
|
+
assignees: ''
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
**Required attestation**
|
|
11
|
+
- [ ] I have checked that this issue has not already been reported.
|
|
12
|
+
- [ ] I have confirmed this bug exists on the latest version of docstringify.
|
|
13
|
+
- [ ] I have confirmed this bug exists on the `main` branch of docstringify.
|
|
14
|
+
|
|
15
|
+
**Describe the bug**
|
|
16
|
+
A clear and concise description of what the bug is.
|
|
17
|
+
|
|
18
|
+
**To Reproduce**
|
|
19
|
+
Steps to reproduce the behavior.
|
|
20
|
+
|
|
21
|
+
**Expected behavior**
|
|
22
|
+
A clear and concise description of what you expected to happen.
|
|
23
|
+
|
|
24
|
+
**Screenshots**
|
|
25
|
+
If applicable, add screenshots to help explain your problem.
|
|
26
|
+
|
|
27
|
+
**Environment**
|
|
28
|
+
- OS: [e.g. iOS]
|
|
29
|
+
- Python Version [e.g. 3.12]
|
|
30
|
+
|
|
31
|
+
**Additional context**
|
|
32
|
+
Add any other context about the problem here.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Suggest an idea for this project
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
assignees: ''
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
**Is your feature request related to a problem? Please describe.**
|
|
11
|
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
12
|
+
|
|
13
|
+
**Describe the solution you'd like**
|
|
14
|
+
A clear and concise description of what you want to happen.
|
|
15
|
+
|
|
16
|
+
**Describe alternatives you've considered**
|
|
17
|
+
A clear and concise description of any alternative solutions or features you've considered.
|
|
18
|
+
|
|
19
|
+
**Additional context**
|
|
20
|
+
Add any other context or screenshots about the feature request here.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Greetings
|
|
2
|
+
|
|
3
|
+
on: [pull_request_target, issues]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
greeting:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
permissions:
|
|
9
|
+
issues: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/first-interaction@34f15e814fe48ac9312ccf29db4e74fa767cbab7 # v1.3.0
|
|
13
|
+
with:
|
|
14
|
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
15
|
+
issue-message: "It looks like this is your first issue here – welcome! Please familiarize yourself with the [contributing guidelines](https://github.com/stefmolin/docstringify/blob/main/CONTRIBUTING.md), if you haven't already."
|
|
16
|
+
pr-message: "Congratulations on making your first pull request to `docstringify`! Please familiarize yourself with the [contributing guidelines](https://github.com/stefmolin/docstringify/blob/main/CONTRIBUTING.md), if you haven't already."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docstringify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Flag missing docstrings and, optionally, generate them from signatures and type annotations.
|
|
5
5
|
Author-email: Stefanie Molin <docstringify@stefaniemolin.com>
|
|
6
6
|
License: MIT License
|
|
@@ -60,7 +60,7 @@ Add the following to your `.pre-commit-config.yaml` file to block commits with m
|
|
|
60
60
|
|
|
61
61
|
```yaml
|
|
62
62
|
- repo: https://github.com/stefmolin/docstringify
|
|
63
|
-
rev: 0.
|
|
63
|
+
rev: 0.5.0
|
|
64
64
|
hooks:
|
|
65
65
|
- id: docstringify
|
|
66
66
|
```
|
|
@@ -69,7 +69,7 @@ By default, all docstrings are required. If you want to be more lenient, you can
|
|
|
69
69
|
|
|
70
70
|
```yaml
|
|
71
71
|
- repo: https://github.com/stefmolin/docstringify
|
|
72
|
-
rev: 0.
|
|
72
|
+
rev: 0.5.0
|
|
73
73
|
hooks:
|
|
74
74
|
- id: docstringify
|
|
75
75
|
args: [--threshold=0.75]
|
|
@@ -79,7 +79,7 @@ If you would like to see suggested docstring templates (inferred from type annot
|
|
|
79
79
|
|
|
80
80
|
```yaml
|
|
81
81
|
- repo: https://github.com/stefmolin/docstringify
|
|
82
|
-
rev: 0.
|
|
82
|
+
rev: 0.5.0
|
|
83
83
|
hooks:
|
|
84
84
|
- id: docstringify
|
|
85
85
|
args: [--suggest-changes=numpydoc]
|
|
@@ -89,7 +89,7 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
|
|
|
89
89
|
|
|
90
90
|
```yaml
|
|
91
91
|
- repo: https://github.com/stefmolin/docstringify
|
|
92
|
-
rev: 0.
|
|
92
|
+
rev: 0.5.0
|
|
93
93
|
hooks:
|
|
94
94
|
- id: docstringify
|
|
95
95
|
args: [--make-changes=google]
|
|
@@ -126,7 +126,7 @@ $ python -m pip install docstringify
|
|
|
126
126
|
Then, use the `DocstringVisitor()` class on individual files to see spots where docstrings are missing:
|
|
127
127
|
|
|
128
128
|
```pycon
|
|
129
|
-
>>> from docstringify.
|
|
129
|
+
>>> from docstringify.traversal import DocstringVisitor
|
|
130
130
|
>>> visitor = DocstringVisitor('test.py')
|
|
131
131
|
>>> visitor.process_file()
|
|
132
132
|
test is missing a docstring
|
|
@@ -137,9 +137,9 @@ If you would like to see suggested docstring templates (inferred from type annot
|
|
|
137
137
|
|
|
138
138
|
```pycon
|
|
139
139
|
>>> from docstringify.converters import NumpydocDocstringConverter
|
|
140
|
-
>>> from docstringify.
|
|
140
|
+
>>> from docstringify.traversal import DocstringVisitor
|
|
141
141
|
>>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
|
|
142
|
-
>>>
|
|
142
|
+
>>> visitor.process_file()
|
|
143
143
|
test is missing a docstring
|
|
144
144
|
Hint:
|
|
145
145
|
"""__description__"""
|
|
@@ -161,9 +161,9 @@ To make changes to your files, you will need to use the `DocstringTransformer` i
|
|
|
161
161
|
|
|
162
162
|
```pycon
|
|
163
163
|
>>> from docstringify.converters import GoogleDocstringConverter
|
|
164
|
-
>>> from docstringify.
|
|
164
|
+
>>> from docstringify.traversal import DocstringTransformer
|
|
165
165
|
>>> transformer = DocstringTransformer('test.py', converter=GoogleDocstringConverter)
|
|
166
|
-
>>>
|
|
166
|
+
>>> transformer.process_file()
|
|
167
167
|
test is missing a docstring
|
|
168
168
|
test.say_hello is missing a docstring
|
|
169
169
|
Docstring templates written to /.../test_docstringify.py
|
|
@@ -9,7 +9,7 @@ Add the following to your `.pre-commit-config.yaml` file to block commits with m
|
|
|
9
9
|
|
|
10
10
|
```yaml
|
|
11
11
|
- repo: https://github.com/stefmolin/docstringify
|
|
12
|
-
rev: 0.
|
|
12
|
+
rev: 0.5.0
|
|
13
13
|
hooks:
|
|
14
14
|
- id: docstringify
|
|
15
15
|
```
|
|
@@ -18,7 +18,7 @@ By default, all docstrings are required. If you want to be more lenient, you can
|
|
|
18
18
|
|
|
19
19
|
```yaml
|
|
20
20
|
- repo: https://github.com/stefmolin/docstringify
|
|
21
|
-
rev: 0.
|
|
21
|
+
rev: 0.5.0
|
|
22
22
|
hooks:
|
|
23
23
|
- id: docstringify
|
|
24
24
|
args: [--threshold=0.75]
|
|
@@ -28,7 +28,7 @@ If you would like to see suggested docstring templates (inferred from type annot
|
|
|
28
28
|
|
|
29
29
|
```yaml
|
|
30
30
|
- repo: https://github.com/stefmolin/docstringify
|
|
31
|
-
rev: 0.
|
|
31
|
+
rev: 0.5.0
|
|
32
32
|
hooks:
|
|
33
33
|
- id: docstringify
|
|
34
34
|
args: [--suggest-changes=numpydoc]
|
|
@@ -38,7 +38,7 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
|
|
|
38
38
|
|
|
39
39
|
```yaml
|
|
40
40
|
- repo: https://github.com/stefmolin/docstringify
|
|
41
|
-
rev: 0.
|
|
41
|
+
rev: 0.5.0
|
|
42
42
|
hooks:
|
|
43
43
|
- id: docstringify
|
|
44
44
|
args: [--make-changes=google]
|
|
@@ -75,7 +75,7 @@ $ python -m pip install docstringify
|
|
|
75
75
|
Then, use the `DocstringVisitor()` class on individual files to see spots where docstrings are missing:
|
|
76
76
|
|
|
77
77
|
```pycon
|
|
78
|
-
>>> from docstringify.
|
|
78
|
+
>>> from docstringify.traversal import DocstringVisitor
|
|
79
79
|
>>> visitor = DocstringVisitor('test.py')
|
|
80
80
|
>>> visitor.process_file()
|
|
81
81
|
test is missing a docstring
|
|
@@ -86,9 +86,9 @@ If you would like to see suggested docstring templates (inferred from type annot
|
|
|
86
86
|
|
|
87
87
|
```pycon
|
|
88
88
|
>>> from docstringify.converters import NumpydocDocstringConverter
|
|
89
|
-
>>> from docstringify.
|
|
89
|
+
>>> from docstringify.traversal import DocstringVisitor
|
|
90
90
|
>>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
|
|
91
|
-
>>>
|
|
91
|
+
>>> visitor.process_file()
|
|
92
92
|
test is missing a docstring
|
|
93
93
|
Hint:
|
|
94
94
|
"""__description__"""
|
|
@@ -110,9 +110,9 @@ To make changes to your files, you will need to use the `DocstringTransformer` i
|
|
|
110
110
|
|
|
111
111
|
```pycon
|
|
112
112
|
>>> from docstringify.converters import GoogleDocstringConverter
|
|
113
|
-
>>> from docstringify.
|
|
113
|
+
>>> from docstringify.traversal import DocstringTransformer
|
|
114
114
|
>>> transformer = DocstringTransformer('test.py', converter=GoogleDocstringConverter)
|
|
115
|
-
>>>
|
|
115
|
+
>>> transformer.process_file()
|
|
116
116
|
test is missing a docstring
|
|
117
117
|
test.say_hello is missing a docstring
|
|
118
118
|
Docstring templates written to /.../test_docstringify.py
|
|
@@ -7,8 +7,7 @@ from typing import Sequence
|
|
|
7
7
|
|
|
8
8
|
from . import __version__
|
|
9
9
|
from .converters import GoogleDocstringConverter, NumpydocDocstringConverter
|
|
10
|
-
from .
|
|
11
|
-
from .visitor import DocstringVisitor
|
|
10
|
+
from .traversal import DocstringTransformer, DocstringVisitor
|
|
12
11
|
|
|
13
12
|
PROG = __package__
|
|
14
13
|
|
|
@@ -77,13 +76,13 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
77
76
|
for file in args.filenames:
|
|
78
77
|
processor = get_docstring_processor(file)
|
|
79
78
|
processor.process_file()
|
|
80
|
-
missing_docstrings += processor.missing_docstrings
|
|
79
|
+
missing_docstrings += len(processor.missing_docstrings)
|
|
81
80
|
docstrings_processed += processor.docstrings_inspected
|
|
82
81
|
|
|
83
82
|
if docstrings_processed and (
|
|
84
83
|
missing_percentage := (missing_docstrings / docstrings_processed)
|
|
85
84
|
) > (1 - args.threshold):
|
|
86
|
-
print(f'
|
|
85
|
+
print(f'Missing {missing_percentage:.0%} of docstrings', file=sys.stderr)
|
|
87
86
|
print(
|
|
88
87
|
f'Your settings require {args.threshold:.0%} of docstrings to be present',
|
|
89
88
|
file=sys.stderr,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import ast
|
|
5
6
|
import textwrap
|
|
6
7
|
from abc import ABC, abstractmethod
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
@@ -9,7 +10,8 @@ from typing import TYPE_CHECKING
|
|
|
9
10
|
from ..exceptions import InvalidDocstringError
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
|
-
from ..components import
|
|
13
|
+
from ..components import Parameter
|
|
14
|
+
from ..nodes.base import DocstringNode
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class DocstringConverter(ABC):
|
|
@@ -24,15 +26,15 @@ class DocstringConverter(ABC):
|
|
|
24
26
|
self._quote = quote
|
|
25
27
|
|
|
26
28
|
@abstractmethod
|
|
27
|
-
def to_class_docstring(self,
|
|
29
|
+
def to_class_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
|
|
28
30
|
pass
|
|
29
31
|
|
|
30
32
|
@abstractmethod
|
|
31
|
-
def to_function_docstring(self,
|
|
33
|
+
def to_function_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
|
|
32
34
|
pass
|
|
33
35
|
|
|
34
36
|
@abstractmethod
|
|
35
|
-
def to_module_docstring(self,
|
|
37
|
+
def to_module_docstring(self, docstring_node: DocstringNode) -> str:
|
|
36
38
|
pass
|
|
37
39
|
|
|
38
40
|
@abstractmethod
|
|
@@ -71,3 +73,20 @@ class DocstringConverter(ABC):
|
|
|
71
73
|
raise InvalidDocstringError(type(docstring).__name__)
|
|
72
74
|
|
|
73
75
|
return textwrap.indent(sep.join(docstring), prefix)
|
|
76
|
+
|
|
77
|
+
def suggest_docstring(
|
|
78
|
+
self,
|
|
79
|
+
docstring_node: DocstringNode,
|
|
80
|
+
indent: int = 0,
|
|
81
|
+
) -> str | None:
|
|
82
|
+
if isinstance(docstring_node.ast_node, ast.AsyncFunctionDef | ast.FunctionDef):
|
|
83
|
+
return (
|
|
84
|
+
self.to_function_docstring(docstring_node, indent=indent)
|
|
85
|
+
if docstring_node.docstring_required
|
|
86
|
+
else None
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if isinstance(docstring_node.ast_node, ast.Module):
|
|
90
|
+
return self.to_module_docstring(docstring_node)
|
|
91
|
+
|
|
92
|
+
return self.to_class_docstring(docstring_node, indent=indent)
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..components import DESCRIPTION_PLACEHOLDER, NO_DEFAULT, Parameter
|
|
6
8
|
from .base import DocstringConverter
|
|
7
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..nodes.base import DocstringNode
|
|
12
|
+
from ..nodes.function import FunctionDocstringNode
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
class GoogleDocstringConverter(DocstringConverter):
|
|
10
16
|
def __init__(self, quote: bool) -> None:
|
|
@@ -14,7 +20,11 @@ class GoogleDocstringConverter(DocstringConverter):
|
|
|
14
20
|
quote=quote,
|
|
15
21
|
)
|
|
16
22
|
|
|
17
|
-
def to_function_docstring(
|
|
23
|
+
def to_function_docstring(
|
|
24
|
+
self, docstring_node: FunctionDocstringNode, indent: int
|
|
25
|
+
) -> str:
|
|
26
|
+
function = docstring_node.to_function()
|
|
27
|
+
|
|
18
28
|
docstring = [DESCRIPTION_PLACEHOLDER]
|
|
19
29
|
|
|
20
30
|
if parameters_section := self.parameters_section(function.parameters):
|
|
@@ -37,8 +47,8 @@ class GoogleDocstringConverter(DocstringConverter):
|
|
|
37
47
|
return f'{return_type}: {DESCRIPTION_PLACEHOLDER}'
|
|
38
48
|
return ''
|
|
39
49
|
|
|
40
|
-
def to_module_docstring(self,
|
|
50
|
+
def to_module_docstring(self, docstring_node: DocstringNode) -> str:
|
|
41
51
|
return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
|
|
42
52
|
|
|
43
|
-
def to_class_docstring(self,
|
|
53
|
+
def to_class_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
|
|
44
54
|
return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=indent)
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..components import DESCRIPTION_PLACEHOLDER, NO_DEFAULT, Parameter
|
|
6
8
|
from .base import DocstringConverter
|
|
7
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..nodes.base import DocstringNode
|
|
12
|
+
from ..nodes.function import FunctionDocstringNode
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
class NumpydocDocstringConverter(DocstringConverter):
|
|
10
16
|
def __init__(self, quote: bool) -> None:
|
|
@@ -14,7 +20,11 @@ class NumpydocDocstringConverter(DocstringConverter):
|
|
|
14
20
|
quote=quote,
|
|
15
21
|
)
|
|
16
22
|
|
|
17
|
-
def to_function_docstring(
|
|
23
|
+
def to_function_docstring(
|
|
24
|
+
self, docstring_node: FunctionDocstringNode, indent: int
|
|
25
|
+
) -> str:
|
|
26
|
+
function = docstring_node.to_function()
|
|
27
|
+
|
|
18
28
|
docstring = [DESCRIPTION_PLACEHOLDER]
|
|
19
29
|
|
|
20
30
|
if parameters_section := self.parameters_section(function.parameters):
|
|
@@ -38,8 +48,8 @@ class NumpydocDocstringConverter(DocstringConverter):
|
|
|
38
48
|
return f'{return_type}\n {DESCRIPTION_PLACEHOLDER}'
|
|
39
49
|
return ''
|
|
40
50
|
|
|
41
|
-
def to_module_docstring(self,
|
|
51
|
+
def to_module_docstring(self, docstring_node: DocstringNode) -> str:
|
|
42
52
|
return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
|
|
43
53
|
|
|
44
|
-
def to_class_docstring(self,
|
|
54
|
+
def to_class_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
|
|
45
55
|
return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=indent)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Docstringify nodes."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import Callable, overload
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DocstringNode:
|
|
9
|
+
@overload
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
node: ast.Module,
|
|
13
|
+
module_name: str,
|
|
14
|
+
source_code: str,
|
|
15
|
+
parent: None = None,
|
|
16
|
+
) -> None: ...
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
node: ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef,
|
|
22
|
+
module_name: str,
|
|
23
|
+
source_code: str,
|
|
24
|
+
parent: DocstringNode,
|
|
25
|
+
) -> None: ...
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
node: ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef,
|
|
30
|
+
module_name: str,
|
|
31
|
+
source_code: str,
|
|
32
|
+
parent: DocstringNode | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.module_name: str = module_name
|
|
35
|
+
self.parent: DocstringNode | None = parent
|
|
36
|
+
self.docstring_required: bool = True
|
|
37
|
+
|
|
38
|
+
self.ast_node: (
|
|
39
|
+
ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
|
|
40
|
+
) = node
|
|
41
|
+
self.name: str = getattr(node, 'name', self.module_name)
|
|
42
|
+
|
|
43
|
+
docstring = ast.get_docstring(node)
|
|
44
|
+
self.docstring = docstring if docstring is None else docstring.strip()
|
|
45
|
+
|
|
46
|
+
self.get_source_segment: Callable[[ast.AST], str] = partial(
|
|
47
|
+
ast.get_source_segment, source_code
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def fully_qualified_name(self) -> str:
|
|
52
|
+
return (
|
|
53
|
+
f'{self.parent.fully_qualified_name}.{self.name}'
|
|
54
|
+
if self.parent
|
|
55
|
+
else self.name
|
|
56
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import itertools
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from ..components import (
|
|
8
|
+
NO_DEFAULT,
|
|
9
|
+
PARAMETER_TYPE_PLACEHOLDER,
|
|
10
|
+
RETURN_TYPE_PLACEHOLDER,
|
|
11
|
+
Function,
|
|
12
|
+
Parameter,
|
|
13
|
+
)
|
|
14
|
+
from .base import DocstringNode
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FunctionDocstringNode(DocstringNode):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
21
|
+
module_name: str,
|
|
22
|
+
source_code: str,
|
|
23
|
+
parent: DocstringNode,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__(node, module_name, source_code, parent)
|
|
26
|
+
self.decorators: list[str] = [
|
|
27
|
+
self.get_source_segment(decorator) for decorator in node.decorator_list
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
self.is_method: bool = self.parent and isinstance(
|
|
31
|
+
self.parent.ast_node, ast.ClassDef
|
|
32
|
+
)
|
|
33
|
+
self.is_abstract_method: bool = self.is_method and (
|
|
34
|
+
'abstractmethod' in self.decorators
|
|
35
|
+
or 'abc.abstractmethod' in self.decorators
|
|
36
|
+
)
|
|
37
|
+
self.is_class_method: bool = self.is_method and 'classmethod' in self.decorators
|
|
38
|
+
self.is_static_method: bool = (
|
|
39
|
+
self.is_method and 'staticmethod' in self.decorators
|
|
40
|
+
)
|
|
41
|
+
self.is_instance_method: bool = (
|
|
42
|
+
self.is_method and not self.is_class_method and not self.is_static_method
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# don't require docstring for the __init__ method if the class has a docstring
|
|
46
|
+
self.docstring_required: bool = not (
|
|
47
|
+
self.is_method and self.name == '__init__' and self.parent.docstring
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.arguments: ast.arguments | None = getattr(node, 'args', None)
|
|
51
|
+
self.return_annotation: str | None = self._extract_return_annotation()
|
|
52
|
+
self.return_statements: list[ast.Return] = []
|
|
53
|
+
|
|
54
|
+
def _extract_default_values(
|
|
55
|
+
self, default: ast.Constant | None | Literal[NO_DEFAULT], is_keyword_only: bool
|
|
56
|
+
) -> str | Literal[NO_DEFAULT]:
|
|
57
|
+
if (not is_keyword_only and default is not NO_DEFAULT) or (
|
|
58
|
+
is_keyword_only and default
|
|
59
|
+
):
|
|
60
|
+
try:
|
|
61
|
+
default_value = default.value
|
|
62
|
+
except AttributeError:
|
|
63
|
+
default_value = f'`{default.id}`'
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
f'"{default_value}"'
|
|
67
|
+
if isinstance(default_value, str) and not default_value.startswith('`')
|
|
68
|
+
else default_value
|
|
69
|
+
)
|
|
70
|
+
return NO_DEFAULT
|
|
71
|
+
|
|
72
|
+
def _extract_star_args(self) -> list[Parameter | None, Parameter | None]:
|
|
73
|
+
return [
|
|
74
|
+
Parameter(
|
|
75
|
+
name=f'*{arg.arg}' if arg_type == 'vararg' else f'**{arg.arg}',
|
|
76
|
+
type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
|
|
77
|
+
category=None,
|
|
78
|
+
default=NO_DEFAULT,
|
|
79
|
+
)
|
|
80
|
+
if arg
|
|
81
|
+
else None
|
|
82
|
+
for arg_type in ['vararg', 'kwarg']
|
|
83
|
+
for arg in [getattr(self.arguments, arg_type)]
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
def _extract_positional_args(self) -> list[Parameter, ...]:
|
|
87
|
+
if (default_count := len(positional_defaults := self.arguments.defaults)) < (
|
|
88
|
+
positional_arguments_count := len(self.arguments.posonlyargs)
|
|
89
|
+
+ len(self.arguments.args)
|
|
90
|
+
):
|
|
91
|
+
positional_defaults = [NO_DEFAULT] * (
|
|
92
|
+
positional_arguments_count - default_count
|
|
93
|
+
) + positional_defaults
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
Parameter(
|
|
97
|
+
name=arg.arg,
|
|
98
|
+
type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
|
|
99
|
+
category=category,
|
|
100
|
+
default=self._extract_default_values(default, False),
|
|
101
|
+
)
|
|
102
|
+
for (arg, category), default in zip(
|
|
103
|
+
itertools.chain(
|
|
104
|
+
zip(
|
|
105
|
+
self.arguments.posonlyargs, itertools.repeat('positional-only')
|
|
106
|
+
),
|
|
107
|
+
zip(self.arguments.args, itertools.repeat(None)),
|
|
108
|
+
),
|
|
109
|
+
positional_defaults,
|
|
110
|
+
)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
def _extract_keyword_args(self) -> list[Parameter, ...]:
|
|
114
|
+
return [
|
|
115
|
+
Parameter(
|
|
116
|
+
name=arg.arg,
|
|
117
|
+
type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
|
|
118
|
+
category='keyword-only',
|
|
119
|
+
default=self._extract_default_values(default, True),
|
|
120
|
+
)
|
|
121
|
+
for arg, default in zip(
|
|
122
|
+
self.arguments.kwonlyargs, self.arguments.kw_defaults
|
|
123
|
+
)
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
def extract_arguments(self) -> tuple[Parameter, ...]:
|
|
127
|
+
params = self._extract_positional_args()
|
|
128
|
+
|
|
129
|
+
varargs, kwargs = self._extract_star_args()
|
|
130
|
+
|
|
131
|
+
if varargs:
|
|
132
|
+
params.append(varargs)
|
|
133
|
+
|
|
134
|
+
params.extend(self._extract_keyword_args())
|
|
135
|
+
|
|
136
|
+
if kwargs:
|
|
137
|
+
params.append(kwargs)
|
|
138
|
+
|
|
139
|
+
params = tuple(params)
|
|
140
|
+
if params and (
|
|
141
|
+
(self.is_class_method and params[0].name == 'cls')
|
|
142
|
+
or (self.is_instance_method and params[0].name == 'self')
|
|
143
|
+
):
|
|
144
|
+
return params[1:]
|
|
145
|
+
return params
|
|
146
|
+
|
|
147
|
+
def _extract_return_annotation(self) -> str | None:
|
|
148
|
+
if return_annotation_node := self.ast_node.returns:
|
|
149
|
+
if isinstance(return_annotation_node, ast.Constant):
|
|
150
|
+
return return_annotation_node.value
|
|
151
|
+
if isinstance(return_annotation_node, ast.Name):
|
|
152
|
+
return return_annotation_node.id
|
|
153
|
+
return self.get_source_segment(return_annotation_node)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def extract_returns(self) -> str | None:
|
|
157
|
+
if self.return_annotation:
|
|
158
|
+
return self.return_annotation
|
|
159
|
+
if any(
|
|
160
|
+
not isinstance(return_value := return_node.value, ast.Constant)
|
|
161
|
+
or return_value.value
|
|
162
|
+
for return_node in self.return_statements
|
|
163
|
+
):
|
|
164
|
+
return RETURN_TYPE_PLACEHOLDER
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def to_function(self) -> Function:
|
|
168
|
+
return Function(self.extract_arguments(), self.extract_returns())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .visitor import DocstringVisitor
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..converters import DocstringConverter
|
|
10
|
+
from ..nodes.base import DocstringNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DocstringTransformer(ast.NodeTransformer, DocstringVisitor):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
filename: str,
|
|
17
|
+
converter: type[DocstringConverter],
|
|
18
|
+
overwrite: bool = False,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(filename, converter)
|
|
21
|
+
self.overwrite = overwrite
|
|
22
|
+
|
|
23
|
+
def save(self) -> None:
|
|
24
|
+
if self.missing_docstrings:
|
|
25
|
+
output = (
|
|
26
|
+
self.source_file
|
|
27
|
+
if self.overwrite
|
|
28
|
+
else self.source_file.parent
|
|
29
|
+
/ (
|
|
30
|
+
self.source_file.stem
|
|
31
|
+
+ '_docstringify'
|
|
32
|
+
+ ''.join(self.source_file.suffixes)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
edited_code = ast.unparse(self.tree)
|
|
36
|
+
output.write_text(edited_code)
|
|
37
|
+
print(f'Docstring templates written to {output}')
|
|
38
|
+
|
|
39
|
+
def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
|
|
40
|
+
suggested_docstring = self.docstring_converter.suggest_docstring(
|
|
41
|
+
docstring_node,
|
|
42
|
+
indent=0
|
|
43
|
+
if isinstance(docstring_node.ast_node, ast.Module)
|
|
44
|
+
else docstring_node.ast_node.col_offset + 4,
|
|
45
|
+
)
|
|
46
|
+
docstring_ast_node = ast.Expr(ast.Constant(suggested_docstring))
|
|
47
|
+
|
|
48
|
+
if docstring_node.docstring is not None:
|
|
49
|
+
# If the docstring is empty, we replace it with the suggested docstring
|
|
50
|
+
docstring_node.ast_node.body[0] = docstring_ast_node
|
|
51
|
+
else:
|
|
52
|
+
# If the docstring is missing, we insert the suggested docstring
|
|
53
|
+
docstring_node.ast_node.body.insert(0, docstring_ast_node)
|
|
54
|
+
|
|
55
|
+
docstring_node.ast_node = ast.fix_missing_locations(docstring_node.ast_node)
|
|
56
|
+
|
|
57
|
+
return docstring_node
|
|
58
|
+
|
|
59
|
+
def process_file(self) -> None:
|
|
60
|
+
super().process_file()
|
|
61
|
+
self.save()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..nodes.base import DocstringNode
|
|
9
|
+
from ..nodes.function import FunctionDocstringNode
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..converters import DocstringConverter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DocstringVisitor(ast.NodeVisitor):
|
|
16
|
+
def __init__(
|
|
17
|
+
self, filename: str, converter: type[DocstringConverter] | None = None
|
|
18
|
+
) -> None:
|
|
19
|
+
self.source_file: Path = Path(filename).expanduser().resolve()
|
|
20
|
+
self.source_code: str = self.source_file.read_text()
|
|
21
|
+
self.tree: ast.Module = ast.parse(self.source_code)
|
|
22
|
+
|
|
23
|
+
self.docstrings_inspected: int = 0
|
|
24
|
+
self.missing_docstrings: list[DocstringNode] = []
|
|
25
|
+
|
|
26
|
+
self.module_name: str = self.source_file.stem
|
|
27
|
+
self.stack: list[DocstringNode] = []
|
|
28
|
+
|
|
29
|
+
self.docstring_converter: DocstringConverter | None = (
|
|
30
|
+
converter(quote=not issubclass(self.__class__, ast.NodeTransformer))
|
|
31
|
+
if converter
|
|
32
|
+
else None
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def report_missing_docstrings(self) -> None:
|
|
36
|
+
if not self.missing_docstrings:
|
|
37
|
+
print(f'No missing docstrings found in {self.source_file}.')
|
|
38
|
+
else:
|
|
39
|
+
for docstring_node in self.missing_docstrings:
|
|
40
|
+
print(
|
|
41
|
+
f'{docstring_node.fully_qualified_name} is missing a docstring',
|
|
42
|
+
file=sys.stderr,
|
|
43
|
+
)
|
|
44
|
+
self.handle_missing_docstring(docstring_node)
|
|
45
|
+
|
|
46
|
+
def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
|
|
47
|
+
if self.docstring_converter:
|
|
48
|
+
print('Hint:')
|
|
49
|
+
print(self.docstring_converter.suggest_docstring(docstring_node))
|
|
50
|
+
print()
|
|
51
|
+
|
|
52
|
+
def process_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
|
|
53
|
+
if docstring_node.docstring_required and not docstring_node.docstring:
|
|
54
|
+
self.missing_docstrings.append(docstring_node)
|
|
55
|
+
|
|
56
|
+
self.docstrings_inspected += 1
|
|
57
|
+
return docstring_node
|
|
58
|
+
|
|
59
|
+
def visit_docstring(
|
|
60
|
+
self,
|
|
61
|
+
node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module,
|
|
62
|
+
docstring_class: type[DocstringNode],
|
|
63
|
+
) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
|
|
64
|
+
docstring_node = docstring_class(
|
|
65
|
+
node,
|
|
66
|
+
self.module_name,
|
|
67
|
+
self.source_code,
|
|
68
|
+
parent=self.stack[-1] if self.stack else None,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.stack.append(docstring_node)
|
|
72
|
+
|
|
73
|
+
docstring_node = self.process_docstring(docstring_node)
|
|
74
|
+
self.generic_visit(docstring_node.ast_node)
|
|
75
|
+
self.stack.pop()
|
|
76
|
+
return docstring_node.ast_node
|
|
77
|
+
|
|
78
|
+
def visit_Module(self, node: ast.Module) -> ast.Module: # noqa: N802
|
|
79
|
+
return self.visit_docstring(node, DocstringNode)
|
|
80
|
+
|
|
81
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: # noqa: N802
|
|
82
|
+
return self.visit_docstring(node, DocstringNode)
|
|
83
|
+
|
|
84
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: # noqa: N802
|
|
85
|
+
return self.visit_docstring(node, FunctionDocstringNode)
|
|
86
|
+
|
|
87
|
+
def visit_AsyncFunctionDef( # noqa: N802
|
|
88
|
+
self, node: ast.AsyncFunctionDef
|
|
89
|
+
) -> ast.AsyncFunctionDef:
|
|
90
|
+
return self.visit_docstring(node, FunctionDocstringNode)
|
|
91
|
+
|
|
92
|
+
def visit_Return(self, node: ast.Return) -> ast.Return: # noqa: N802
|
|
93
|
+
if isinstance(self.stack[-1], FunctionDocstringNode):
|
|
94
|
+
self.stack[-1].return_statements.append(node)
|
|
95
|
+
return node
|
|
96
|
+
|
|
97
|
+
def process_file(self) -> None:
|
|
98
|
+
self.visit(self.tree)
|
|
99
|
+
self.report_missing_docstrings()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docstringify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Flag missing docstrings and, optionally, generate them from signatures and type annotations.
|
|
5
5
|
Author-email: Stefanie Molin <docstringify@stefaniemolin.com>
|
|
6
6
|
License: MIT License
|
|
@@ -60,7 +60,7 @@ Add the following to your `.pre-commit-config.yaml` file to block commits with m
|
|
|
60
60
|
|
|
61
61
|
```yaml
|
|
62
62
|
- repo: https://github.com/stefmolin/docstringify
|
|
63
|
-
rev: 0.
|
|
63
|
+
rev: 0.5.0
|
|
64
64
|
hooks:
|
|
65
65
|
- id: docstringify
|
|
66
66
|
```
|
|
@@ -69,7 +69,7 @@ By default, all docstrings are required. If you want to be more lenient, you can
|
|
|
69
69
|
|
|
70
70
|
```yaml
|
|
71
71
|
- repo: https://github.com/stefmolin/docstringify
|
|
72
|
-
rev: 0.
|
|
72
|
+
rev: 0.5.0
|
|
73
73
|
hooks:
|
|
74
74
|
- id: docstringify
|
|
75
75
|
args: [--threshold=0.75]
|
|
@@ -79,7 +79,7 @@ If you would like to see suggested docstring templates (inferred from type annot
|
|
|
79
79
|
|
|
80
80
|
```yaml
|
|
81
81
|
- repo: https://github.com/stefmolin/docstringify
|
|
82
|
-
rev: 0.
|
|
82
|
+
rev: 0.5.0
|
|
83
83
|
hooks:
|
|
84
84
|
- id: docstringify
|
|
85
85
|
args: [--suggest-changes=numpydoc]
|
|
@@ -89,7 +89,7 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
|
|
|
89
89
|
|
|
90
90
|
```yaml
|
|
91
91
|
- repo: https://github.com/stefmolin/docstringify
|
|
92
|
-
rev: 0.
|
|
92
|
+
rev: 0.5.0
|
|
93
93
|
hooks:
|
|
94
94
|
- id: docstringify
|
|
95
95
|
args: [--make-changes=google]
|
|
@@ -126,7 +126,7 @@ $ python -m pip install docstringify
|
|
|
126
126
|
Then, use the `DocstringVisitor()` class on individual files to see spots where docstrings are missing:
|
|
127
127
|
|
|
128
128
|
```pycon
|
|
129
|
-
>>> from docstringify.
|
|
129
|
+
>>> from docstringify.traversal import DocstringVisitor
|
|
130
130
|
>>> visitor = DocstringVisitor('test.py')
|
|
131
131
|
>>> visitor.process_file()
|
|
132
132
|
test is missing a docstring
|
|
@@ -137,9 +137,9 @@ If you would like to see suggested docstring templates (inferred from type annot
|
|
|
137
137
|
|
|
138
138
|
```pycon
|
|
139
139
|
>>> from docstringify.converters import NumpydocDocstringConverter
|
|
140
|
-
>>> from docstringify.
|
|
140
|
+
>>> from docstringify.traversal import DocstringVisitor
|
|
141
141
|
>>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
|
|
142
|
-
>>>
|
|
142
|
+
>>> visitor.process_file()
|
|
143
143
|
test is missing a docstring
|
|
144
144
|
Hint:
|
|
145
145
|
"""__description__"""
|
|
@@ -161,9 +161,9 @@ To make changes to your files, you will need to use the `DocstringTransformer` i
|
|
|
161
161
|
|
|
162
162
|
```pycon
|
|
163
163
|
>>> from docstringify.converters import GoogleDocstringConverter
|
|
164
|
-
>>> from docstringify.
|
|
164
|
+
>>> from docstringify.traversal import DocstringTransformer
|
|
165
165
|
>>> transformer = DocstringTransformer('test.py', converter=GoogleDocstringConverter)
|
|
166
|
-
>>>
|
|
166
|
+
>>> transformer.process_file()
|
|
167
167
|
test is missing a docstring
|
|
168
168
|
test.say_hello is missing a docstring
|
|
169
169
|
Docstring templates written to /.../test_docstringify.py
|
|
@@ -6,16 +6,16 @@ README.md
|
|
|
6
6
|
pyproject.toml
|
|
7
7
|
.github/dependabot.yml
|
|
8
8
|
.github/release.yml
|
|
9
|
+
.github/ISSUE_TEMPLATE/bug_report.md
|
|
10
|
+
.github/ISSUE_TEMPLATE/feature_request.md
|
|
9
11
|
.github/workflows/ci.yml
|
|
12
|
+
.github/workflows/greetings.yml
|
|
10
13
|
.github/workflows/pypi-publish.yml
|
|
11
14
|
src/docstringify/__init__.py
|
|
12
15
|
src/docstringify/__main__.py
|
|
13
16
|
src/docstringify/cli.py
|
|
14
17
|
src/docstringify/components.py
|
|
15
|
-
src/docstringify/docstring_generator.py
|
|
16
18
|
src/docstringify/exceptions.py
|
|
17
|
-
src/docstringify/transformer.py
|
|
18
|
-
src/docstringify/visitor.py
|
|
19
19
|
src/docstringify.egg-info/PKG-INFO
|
|
20
20
|
src/docstringify.egg-info/SOURCES.txt
|
|
21
21
|
src/docstringify.egg-info/dependency_links.txt
|
|
@@ -25,4 +25,10 @@ src/docstringify.egg-info/top_level.txt
|
|
|
25
25
|
src/docstringify/converters/__init__.py
|
|
26
26
|
src/docstringify/converters/base.py
|
|
27
27
|
src/docstringify/converters/google.py
|
|
28
|
-
src/docstringify/converters/numpydoc.py
|
|
28
|
+
src/docstringify/converters/numpydoc.py
|
|
29
|
+
src/docstringify/nodes/__init__.py
|
|
30
|
+
src/docstringify/nodes/base.py
|
|
31
|
+
src/docstringify/nodes/function.py
|
|
32
|
+
src/docstringify/traversal/__init__.py
|
|
33
|
+
src/docstringify/traversal/transformer.py
|
|
34
|
+
src/docstringify/traversal/visitor.py
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
from typing import TYPE_CHECKING, Literal
|
|
5
|
-
|
|
6
|
-
from .components import (
|
|
7
|
-
NO_DEFAULT,
|
|
8
|
-
PARAMETER_TYPE_PLACEHOLDER,
|
|
9
|
-
RETURN_TYPE_PLACEHOLDER,
|
|
10
|
-
Function,
|
|
11
|
-
Parameter,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from .converters import DocstringConverter
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class DocstringGenerator:
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
converter: type[DocstringConverter],
|
|
22
|
-
module_name: str,
|
|
23
|
-
source_code: str,
|
|
24
|
-
quote: bool,
|
|
25
|
-
) -> None:
|
|
26
|
-
self.converter = converter(quote=quote)
|
|
27
|
-
self.module_name = module_name
|
|
28
|
-
self.source_code = source_code
|
|
29
|
-
|
|
30
|
-
def _extract_default_values(
|
|
31
|
-
self, default: ast.Constant | None | Literal[NO_DEFAULT], is_keyword_only: bool
|
|
32
|
-
) -> str | Literal[NO_DEFAULT]:
|
|
33
|
-
if (not is_keyword_only and default is not NO_DEFAULT) or (
|
|
34
|
-
is_keyword_only and default
|
|
35
|
-
):
|
|
36
|
-
try:
|
|
37
|
-
default_value = default.value
|
|
38
|
-
except AttributeError:
|
|
39
|
-
default_value = f'`{default.id}`'
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
f'"{default_value}"'
|
|
43
|
-
if isinstance(default_value, str) and not default_value.startswith('`')
|
|
44
|
-
else default_value
|
|
45
|
-
)
|
|
46
|
-
return NO_DEFAULT
|
|
47
|
-
|
|
48
|
-
def extract_arguments(
|
|
49
|
-
self, node: ast.AsyncFunctionDef | ast.FunctionDef
|
|
50
|
-
) -> tuple[Parameter, ...]:
|
|
51
|
-
modifiers = {
|
|
52
|
-
'posonlyargs': 'positional-only',
|
|
53
|
-
'kwonlyargs': 'keyword-only',
|
|
54
|
-
}
|
|
55
|
-
params = []
|
|
56
|
-
|
|
57
|
-
positional_arguments_count = len(node.args.posonlyargs) + len(node.args.args)
|
|
58
|
-
if (
|
|
59
|
-
default_count := len(positional_defaults := node.args.defaults)
|
|
60
|
-
) < positional_arguments_count:
|
|
61
|
-
positional_defaults = [NO_DEFAULT] * (
|
|
62
|
-
positional_arguments_count - default_count
|
|
63
|
-
) + positional_defaults
|
|
64
|
-
|
|
65
|
-
keyword_defaults = node.args.kw_defaults
|
|
66
|
-
|
|
67
|
-
processed_positional_args = 0
|
|
68
|
-
for arg_type, args in ast.iter_fields(node.args):
|
|
69
|
-
if arg_type.endswith('defaults'):
|
|
70
|
-
continue
|
|
71
|
-
modifier = modifiers.get(arg_type)
|
|
72
|
-
if arg_type in ['vararg', 'kwarg']:
|
|
73
|
-
if args:
|
|
74
|
-
params.append(
|
|
75
|
-
Parameter(
|
|
76
|
-
name=f'*{args.arg}'
|
|
77
|
-
if arg_type == 'vararg'
|
|
78
|
-
else f'**{args.arg}',
|
|
79
|
-
type_=getattr(
|
|
80
|
-
args.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER
|
|
81
|
-
),
|
|
82
|
-
category=modifier,
|
|
83
|
-
default=NO_DEFAULT,
|
|
84
|
-
)
|
|
85
|
-
)
|
|
86
|
-
else:
|
|
87
|
-
is_keyword_only = arg_type.startswith('kw')
|
|
88
|
-
params.extend(
|
|
89
|
-
[
|
|
90
|
-
Parameter(
|
|
91
|
-
name=arg.arg,
|
|
92
|
-
type_=getattr(
|
|
93
|
-
arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER
|
|
94
|
-
),
|
|
95
|
-
category=modifier,
|
|
96
|
-
default=self._extract_default_values(
|
|
97
|
-
default, is_keyword_only
|
|
98
|
-
),
|
|
99
|
-
)
|
|
100
|
-
for arg, default in zip(
|
|
101
|
-
args,
|
|
102
|
-
keyword_defaults
|
|
103
|
-
if is_keyword_only
|
|
104
|
-
else positional_defaults[processed_positional_args:],
|
|
105
|
-
)
|
|
106
|
-
]
|
|
107
|
-
)
|
|
108
|
-
if not is_keyword_only:
|
|
109
|
-
processed_positional_args += len(args)
|
|
110
|
-
|
|
111
|
-
params = tuple(params)
|
|
112
|
-
if (
|
|
113
|
-
params
|
|
114
|
-
and isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
115
|
-
and params[0].name.startswith(('self', 'cls'))
|
|
116
|
-
):
|
|
117
|
-
return params[1:]
|
|
118
|
-
return params
|
|
119
|
-
|
|
120
|
-
def extract_returns(
|
|
121
|
-
self, node: ast.AsyncFunctionDef | ast.FunctionDef
|
|
122
|
-
) -> str | None:
|
|
123
|
-
if return_node := node.returns:
|
|
124
|
-
if isinstance(return_node, ast.Constant):
|
|
125
|
-
return return_node.value
|
|
126
|
-
if isinstance(return_node, ast.Name):
|
|
127
|
-
return return_node.id
|
|
128
|
-
return ast.get_source_segment(self.source_code, return_node)
|
|
129
|
-
if (
|
|
130
|
-
return_nodes := [
|
|
131
|
-
body_node
|
|
132
|
-
for body_node in node.body
|
|
133
|
-
if isinstance(body_node, ast.Return)
|
|
134
|
-
]
|
|
135
|
-
) and any(
|
|
136
|
-
not isinstance(return_value := body_return_node.value, ast.Constant)
|
|
137
|
-
or return_value.value
|
|
138
|
-
for body_return_node in return_nodes
|
|
139
|
-
):
|
|
140
|
-
return RETURN_TYPE_PLACEHOLDER
|
|
141
|
-
return return_node
|
|
142
|
-
|
|
143
|
-
def suggest_docstring(
|
|
144
|
-
self,
|
|
145
|
-
node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module,
|
|
146
|
-
indent: int = 0,
|
|
147
|
-
) -> str:
|
|
148
|
-
if isinstance(node, ast.AsyncFunctionDef | ast.FunctionDef):
|
|
149
|
-
return self.converter.to_function_docstring(
|
|
150
|
-
Function(self.extract_arguments(node), self.extract_returns(node)),
|
|
151
|
-
indent=indent,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
if isinstance(node, ast.Module):
|
|
155
|
-
return self.converter.to_module_docstring(self.module_name)
|
|
156
|
-
|
|
157
|
-
return self.converter.to_class_docstring(node.name, indent=indent)
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
from .visitor import DocstringVisitor
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from .converters import DocstringConverter
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DocstringTransformer(ast.NodeTransformer, DocstringVisitor):
|
|
13
|
-
def __init__(
|
|
14
|
-
self, filename: str, converter: DocstringConverter, overwrite: bool = False
|
|
15
|
-
) -> None:
|
|
16
|
-
super().__init__(filename, converter)
|
|
17
|
-
self.overwrite = overwrite
|
|
18
|
-
|
|
19
|
-
def save(self, new_ast: ast.Module) -> None:
|
|
20
|
-
if self.missing_docstrings:
|
|
21
|
-
output = (
|
|
22
|
-
self.source_file
|
|
23
|
-
if self.overwrite
|
|
24
|
-
else self.source_file.parent
|
|
25
|
-
/ (
|
|
26
|
-
self.source_file.stem
|
|
27
|
-
+ '_docstringify'
|
|
28
|
-
+ ''.join(self.source_file.suffixes)
|
|
29
|
-
)
|
|
30
|
-
)
|
|
31
|
-
edited_code = ast.unparse(new_ast)
|
|
32
|
-
output.write_text(edited_code)
|
|
33
|
-
print(f'Docstring templates written to {output}')
|
|
34
|
-
else:
|
|
35
|
-
print(
|
|
36
|
-
f'No missing docstrings found in {self.source_file}; no changes made.'
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
def handle_missing_docstring(
|
|
40
|
-
self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
|
|
41
|
-
) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
|
|
42
|
-
suggested_docstring = self.docstring_generator.suggest_docstring(
|
|
43
|
-
node, indent=0 if isinstance(node, ast.Module) else node.col_offset + 4
|
|
44
|
-
)
|
|
45
|
-
docstring = ast.Expr(ast.Constant(suggested_docstring))
|
|
46
|
-
node.body.insert(0, docstring)
|
|
47
|
-
return ast.fix_missing_locations(node)
|
|
48
|
-
|
|
49
|
-
def process_file(self) -> ast.Module:
|
|
50
|
-
root = ast.parse(self.source_code)
|
|
51
|
-
new_ast = self.visit(root)
|
|
52
|
-
self.save(new_ast)
|
|
53
|
-
return new_ast
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
|
|
8
|
-
from .docstring_generator import DocstringGenerator
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from .converters import DocstringConverter
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class DocstringVisitor(ast.NodeVisitor):
|
|
15
|
-
def __init__(
|
|
16
|
-
self, filename: str, converter: DocstringConverter | None = None
|
|
17
|
-
) -> None:
|
|
18
|
-
self.source_file: Path = Path(filename).expanduser().resolve()
|
|
19
|
-
self.source_code: str = self.source_file.read_text()
|
|
20
|
-
|
|
21
|
-
self.docstrings_inspected: int = 0
|
|
22
|
-
self.missing_docstrings: int = 0
|
|
23
|
-
|
|
24
|
-
self.module_name: str = self.source_file.stem
|
|
25
|
-
self.stack: list[str] = []
|
|
26
|
-
|
|
27
|
-
self.provide_hints: bool = converter is not None
|
|
28
|
-
if self.provide_hints:
|
|
29
|
-
self.docstring_generator = DocstringGenerator(
|
|
30
|
-
converter,
|
|
31
|
-
self.module_name,
|
|
32
|
-
self.source_code,
|
|
33
|
-
quote=not issubclass(self.__class__, ast.NodeTransformer),
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
def report_missing_docstring(self) -> None:
|
|
37
|
-
self.missing_docstrings += 1
|
|
38
|
-
print(f'{".".join(self.stack)} is missing a docstring', file=sys.stderr)
|
|
39
|
-
|
|
40
|
-
def handle_missing_docstring(
|
|
41
|
-
self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
|
|
42
|
-
) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
|
|
43
|
-
if self.provide_hints:
|
|
44
|
-
print('Hint:')
|
|
45
|
-
print(self.docstring_generator.suggest_docstring(node))
|
|
46
|
-
print()
|
|
47
|
-
return node
|
|
48
|
-
|
|
49
|
-
def process_docstring(
|
|
50
|
-
self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
|
|
51
|
-
) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
|
|
52
|
-
if not ast.get_docstring(node):
|
|
53
|
-
self.report_missing_docstring()
|
|
54
|
-
node = self.handle_missing_docstring(node)
|
|
55
|
-
|
|
56
|
-
self.docstrings_inspected += 1
|
|
57
|
-
return node
|
|
58
|
-
|
|
59
|
-
def visit(self, node: ast.AST) -> ast.AST:
|
|
60
|
-
if isinstance(
|
|
61
|
-
node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
|
|
62
|
-
):
|
|
63
|
-
self.stack.append(
|
|
64
|
-
self.module_name if isinstance(node, ast.Module) else node.name
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
node = self.process_docstring(node)
|
|
68
|
-
|
|
69
|
-
self.generic_visit(node)
|
|
70
|
-
_ = self.stack.pop()
|
|
71
|
-
return node
|
|
72
|
-
|
|
73
|
-
def process_file(self) -> ast.Module:
|
|
74
|
-
root = self.visit(ast.parse(self.source_code))
|
|
75
|
-
|
|
76
|
-
if not self.missing_docstrings:
|
|
77
|
-
print(f'No missing docstrings found in {self.source_file}.')
|
|
78
|
-
|
|
79
|
-
return root
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|