docstringify 0.2.0__py3-none-any.whl → 0.3.0__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.
- docstringify/__init__.py +1 -1
- docstringify/cli.py +45 -22
- docstringify/converters/base.py +25 -3
- docstringify/converters/google.py +9 -11
- docstringify/converters/numpydoc.py +8 -10
- docstringify/docstring_generator.py +157 -0
- docstringify/exceptions.py +6 -0
- docstringify/transformer.py +53 -0
- docstringify/visitor.py +28 -145
- {docstringify-0.2.0.dist-info → docstringify-0.3.0.dist-info}/METADATA +27 -11
- docstringify-0.3.0.dist-info/RECORD +18 -0
- docstringify-0.2.0.dist-info/RECORD +0 -15
- {docstringify-0.2.0.dist-info → docstringify-0.3.0.dist-info}/WHEEL +0 -0
- {docstringify-0.2.0.dist-info → docstringify-0.3.0.dist-info}/entry_points.txt +0 -0
- {docstringify-0.2.0.dist-info → docstringify-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {docstringify-0.2.0.dist-info → docstringify-0.3.0.dist-info}/top_level.txt +0 -0
docstringify/__init__.py
CHANGED
docstringify/cli.py
CHANGED
|
@@ -2,14 +2,22 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import sys
|
|
5
|
+
from functools import partial
|
|
5
6
|
from typing import Sequence
|
|
6
7
|
|
|
7
8
|
from . import __version__
|
|
8
9
|
from .converters import GoogleDocstringConverter, NumpydocDocstringConverter
|
|
10
|
+
from .transformer import DocstringTransformer
|
|
9
11
|
from .visitor import DocstringVisitor
|
|
10
12
|
|
|
11
13
|
PROG = __package__
|
|
12
14
|
|
|
15
|
+
STYLES = {
|
|
16
|
+
'google': GoogleDocstringConverter,
|
|
17
|
+
'numpydoc': NumpydocDocstringConverter,
|
|
18
|
+
}
|
|
19
|
+
CLI_DEFAULTS = {'threshold': 1.0}
|
|
20
|
+
|
|
13
21
|
|
|
14
22
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
15
23
|
parser = argparse.ArgumentParser(prog=PROG)
|
|
@@ -22,45 +30,60 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
22
30
|
'--version', action='version', version=f'%(prog)s {__version__}'
|
|
23
31
|
)
|
|
24
32
|
|
|
25
|
-
parser.
|
|
33
|
+
run_group = parser.add_argument_group('Run options')
|
|
34
|
+
handle_missing_docstring = run_group.add_mutually_exclusive_group(required=False)
|
|
35
|
+
handle_missing_docstring.add_argument(
|
|
36
|
+
'--make-changes',
|
|
37
|
+
choices=STYLES.keys(),
|
|
38
|
+
help='Whether to insert docstring templates for items missing docstrings',
|
|
39
|
+
)
|
|
40
|
+
handle_missing_docstring.add_argument(
|
|
41
|
+
'--make-changes-inplace',
|
|
42
|
+
choices=STYLES.keys(),
|
|
43
|
+
help=(
|
|
44
|
+
'Whether to insert docstring templates for items missing docstrings, '
|
|
45
|
+
'overwriting the original file'
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
handle_missing_docstring.add_argument(
|
|
26
49
|
'--suggest-changes',
|
|
27
|
-
|
|
50
|
+
choices=STYLES.keys(),
|
|
28
51
|
help='Whether to print out docstring templates for items missing docstrings',
|
|
29
52
|
)
|
|
30
|
-
|
|
31
|
-
'--style',
|
|
32
|
-
choices=['google', 'numpydoc'],
|
|
33
|
-
default='numpydoc',
|
|
34
|
-
help='The style of docstring to use (only used when --suggest-changes is passed)',
|
|
35
|
-
)
|
|
36
|
-
parser.add_argument(
|
|
53
|
+
run_group.add_argument(
|
|
37
54
|
'--threshold',
|
|
38
55
|
type=float,
|
|
39
|
-
default=
|
|
56
|
+
default=CLI_DEFAULTS['threshold'],
|
|
40
57
|
help='The percentage of docstrings that must be present to pass',
|
|
41
58
|
)
|
|
42
59
|
args = parser.parse_args(argv)
|
|
43
60
|
|
|
44
|
-
if args.
|
|
45
|
-
converter =
|
|
46
|
-
elif args.style == 'google':
|
|
47
|
-
converter = GoogleDocstringConverter()
|
|
48
|
-
elif args.style == 'numpydoc':
|
|
49
|
-
converter = NumpydocDocstringConverter()
|
|
61
|
+
if style := args.make_changes or args.make_changes_inplace or args.suggest_changes:
|
|
62
|
+
converter = STYLES[style]
|
|
50
63
|
else:
|
|
51
|
-
|
|
64
|
+
converter = None
|
|
65
|
+
|
|
66
|
+
get_docstring_processor = (
|
|
67
|
+
partial(
|
|
68
|
+
DocstringTransformer,
|
|
69
|
+
converter=converter,
|
|
70
|
+
**{'overwrite': bool(args.make_changes_inplace)},
|
|
71
|
+
)
|
|
72
|
+
if args.make_changes or args.make_changes_inplace
|
|
73
|
+
else partial(DocstringVisitor, converter=converter)
|
|
74
|
+
)
|
|
52
75
|
|
|
53
76
|
docstrings_processed = missing_docstrings = 0
|
|
54
77
|
for file in args.filenames:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
missing_docstrings +=
|
|
58
|
-
docstrings_processed +=
|
|
78
|
+
processor = get_docstring_processor(file)
|
|
79
|
+
processor.process_file()
|
|
80
|
+
missing_docstrings += processor.missing_docstrings
|
|
81
|
+
docstrings_processed += processor.docstrings_inspected
|
|
59
82
|
|
|
60
83
|
if docstrings_processed and (
|
|
61
84
|
missing_percentage := (missing_docstrings / docstrings_processed)
|
|
62
85
|
) > (1 - args.threshold):
|
|
63
|
-
print(f'
|
|
86
|
+
print(f'\nMissing {missing_percentage:.0%} of docstrings', file=sys.stderr)
|
|
64
87
|
print(
|
|
65
88
|
f'Your settings require {args.threshold:.0%} of docstrings to be present',
|
|
66
89
|
file=sys.stderr,
|
docstringify/converters/base.py
CHANGED
|
@@ -2,26 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import textwrap
|
|
5
6
|
from abc import ABC, abstractmethod
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
9
|
+
from ..exceptions import InvalidDocstringError
|
|
10
|
+
|
|
8
11
|
if TYPE_CHECKING:
|
|
9
12
|
from ..components import Function, Parameter
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class DocstringConverter(ABC):
|
|
13
16
|
def __init__(
|
|
14
|
-
self,
|
|
17
|
+
self,
|
|
18
|
+
parameters_section_template: str,
|
|
19
|
+
returns_section_template: str,
|
|
20
|
+
quote: bool,
|
|
15
21
|
) -> None:
|
|
16
22
|
self.parameters_section_template = parameters_section_template
|
|
17
23
|
self.returns_section_template = returns_section_template
|
|
24
|
+
self._quote = quote
|
|
18
25
|
|
|
19
26
|
@abstractmethod
|
|
20
|
-
def to_class_docstring(self, class_name: str) -> str:
|
|
27
|
+
def to_class_docstring(self, class_name: str, indent: int) -> str:
|
|
21
28
|
pass
|
|
22
29
|
|
|
23
30
|
@abstractmethod
|
|
24
|
-
def to_function_docstring(self, function: Function) -> str:
|
|
31
|
+
def to_function_docstring(self, function: Function, indent: int) -> str:
|
|
25
32
|
pass
|
|
26
33
|
|
|
27
34
|
@abstractmethod
|
|
@@ -49,3 +56,18 @@ class DocstringConverter(ABC):
|
|
|
49
56
|
if return_text := self.format_return(return_type):
|
|
50
57
|
return self.returns_section_template.format(returns=return_text)
|
|
51
58
|
return ''
|
|
59
|
+
|
|
60
|
+
def quote_docstring(self, docstring: str | list[str], indent: int) -> str:
|
|
61
|
+
quote = '"""' if self._quote else ''
|
|
62
|
+
prefix = sep = ''
|
|
63
|
+
if isinstance(docstring, str):
|
|
64
|
+
docstring = [quote, docstring, quote]
|
|
65
|
+
elif isinstance(docstring, list):
|
|
66
|
+
if len(docstring) > 1:
|
|
67
|
+
prefix = ' ' * indent if indent else ''
|
|
68
|
+
sep = '\n'
|
|
69
|
+
docstring = [quote, *docstring, f'{prefix}{quote}']
|
|
70
|
+
else:
|
|
71
|
+
raise InvalidDocstringError(type(docstring).__name__)
|
|
72
|
+
|
|
73
|
+
return textwrap.indent(sep.join(docstring), prefix)
|
|
@@ -7,15 +7,15 @@ from .base import DocstringConverter
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class GoogleDocstringConverter(DocstringConverter):
|
|
10
|
-
def __init__(self) -> None:
|
|
10
|
+
def __init__(self, quote: bool) -> None:
|
|
11
11
|
super().__init__(
|
|
12
12
|
parameters_section_template='Args:\n{parameters}',
|
|
13
13
|
returns_section_template='Returns:\n {returns}',
|
|
14
|
+
quote=quote,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
|
-
def to_function_docstring(self, function: Function) -> str:
|
|
17
|
-
|
|
18
|
-
docstring = [f'"""{DESCRIPTION_PLACEHOLDER}']
|
|
17
|
+
def to_function_docstring(self, function: Function, indent: int) -> str:
|
|
18
|
+
docstring = [DESCRIPTION_PLACEHOLDER]
|
|
19
19
|
|
|
20
20
|
if parameters_section := self.parameters_section(function.parameters):
|
|
21
21
|
docstring.extend(['', parameters_section])
|
|
@@ -23,14 +23,12 @@ class GoogleDocstringConverter(DocstringConverter):
|
|
|
23
23
|
if returns_section := self.returns_section(function.return_type):
|
|
24
24
|
docstring.extend(['', returns_section])
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return sep.join([*docstring, '"""'])
|
|
26
|
+
return self.quote_docstring(docstring, indent=indent)
|
|
29
27
|
|
|
30
28
|
def format_parameter(self, parameter: Parameter) -> str:
|
|
31
29
|
category = f'{f", {parameter.category}" if parameter.category else ""}'
|
|
32
30
|
return (
|
|
33
|
-
f' {parameter.name} ({parameter.type_}{category}): {DESCRIPTION_PLACEHOLDER}
|
|
31
|
+
f' {parameter.name} ({parameter.type_}{category}): {DESCRIPTION_PLACEHOLDER}'
|
|
34
32
|
f'{f" Defaults to {parameter.default}." if parameter.default != NO_DEFAULT else ""}'
|
|
35
33
|
)
|
|
36
34
|
|
|
@@ -40,7 +38,7 @@ class GoogleDocstringConverter(DocstringConverter):
|
|
|
40
38
|
return ''
|
|
41
39
|
|
|
42
40
|
def to_module_docstring(self, module_name: str) -> str:
|
|
43
|
-
return
|
|
41
|
+
return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
|
|
44
42
|
|
|
45
|
-
def to_class_docstring(self, class_name: str) -> str:
|
|
46
|
-
return
|
|
43
|
+
def to_class_docstring(self, class_name: str, indent: int) -> str:
|
|
44
|
+
return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=indent)
|
|
@@ -7,15 +7,15 @@ from .base import DocstringConverter
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class NumpydocDocstringConverter(DocstringConverter):
|
|
10
|
-
def __init__(self) -> None:
|
|
10
|
+
def __init__(self, quote: bool) -> None:
|
|
11
11
|
super().__init__(
|
|
12
12
|
parameters_section_template='Parameters\n----------\n{parameters}',
|
|
13
13
|
returns_section_template='Returns\n-------\n{returns}',
|
|
14
|
+
quote=quote,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
|
-
def to_function_docstring(self, function: Function) -> str:
|
|
17
|
-
|
|
18
|
-
docstring = ['"""', DESCRIPTION_PLACEHOLDER]
|
|
17
|
+
def to_function_docstring(self, function: Function, indent: int) -> str:
|
|
18
|
+
docstring = [DESCRIPTION_PLACEHOLDER]
|
|
19
19
|
|
|
20
20
|
if parameters_section := self.parameters_section(function.parameters):
|
|
21
21
|
docstring.extend(['', parameters_section])
|
|
@@ -23,9 +23,7 @@ class NumpydocDocstringConverter(DocstringConverter):
|
|
|
23
23
|
if returns_section := self.returns_section(function.return_type):
|
|
24
24
|
docstring.extend(['', returns_section])
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return sep.join([*docstring, '"""'])
|
|
26
|
+
return self.quote_docstring(docstring, indent=indent)
|
|
29
27
|
|
|
30
28
|
def format_parameter(self, parameter: Parameter) -> str:
|
|
31
29
|
return (
|
|
@@ -41,7 +39,7 @@ class NumpydocDocstringConverter(DocstringConverter):
|
|
|
41
39
|
return ''
|
|
42
40
|
|
|
43
41
|
def to_module_docstring(self, module_name: str) -> str:
|
|
44
|
-
return
|
|
42
|
+
return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
|
|
45
43
|
|
|
46
|
-
def to_class_docstring(self, class_name: str) -> str:
|
|
47
|
-
return
|
|
44
|
+
def to_class_docstring(self, class_name: str, indent: int) -> str:
|
|
45
|
+
return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=indent)
|
|
@@ -0,0 +1,157 @@
|
|
|
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)
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
docstringify/visitor.py
CHANGED
|
@@ -3,15 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from .
|
|
9
|
-
NO_DEFAULT,
|
|
10
|
-
PARAMETER_TYPE_PLACEHOLDER,
|
|
11
|
-
RETURN_TYPE_PLACEHOLDER,
|
|
12
|
-
Function,
|
|
13
|
-
Parameter,
|
|
14
|
-
)
|
|
8
|
+
from .docstring_generator import DocstringGenerator
|
|
15
9
|
|
|
16
10
|
if TYPE_CHECKING:
|
|
17
11
|
from .converters import DocstringConverter
|
|
@@ -21,7 +15,7 @@ class DocstringVisitor(ast.NodeVisitor):
|
|
|
21
15
|
def __init__(
|
|
22
16
|
self, filename: str, converter: DocstringConverter | None = None
|
|
23
17
|
) -> None:
|
|
24
|
-
self.source_file: Path = Path(filename)
|
|
18
|
+
self.source_file: Path = Path(filename).expanduser().resolve()
|
|
25
19
|
self.source_code: str = self.source_file.read_text()
|
|
26
20
|
|
|
27
21
|
self.docstrings_inspected: int = 0
|
|
@@ -32,154 +26,37 @@ class DocstringVisitor(ast.NodeVisitor):
|
|
|
32
26
|
|
|
33
27
|
self.provide_hints: bool = converter is not None
|
|
34
28
|
if self.provide_hints:
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (not is_keyword_only and default is not NO_DEFAULT) or (
|
|
41
|
-
is_keyword_only and default
|
|
42
|
-
):
|
|
43
|
-
try:
|
|
44
|
-
default_value = default.value
|
|
45
|
-
except AttributeError:
|
|
46
|
-
default_value = f'`{default.id}`'
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
f'"{default_value}"'
|
|
50
|
-
if isinstance(default_value, str) and not default_value.startswith('`')
|
|
51
|
-
else default_value
|
|
29
|
+
self.docstring_generator = DocstringGenerator(
|
|
30
|
+
converter,
|
|
31
|
+
self.module_name,
|
|
32
|
+
self.source_code,
|
|
33
|
+
quote=not issubclass(self.__class__, ast.NodeTransformer),
|
|
52
34
|
)
|
|
53
|
-
return NO_DEFAULT
|
|
54
|
-
|
|
55
|
-
def extract_arguments(
|
|
56
|
-
self, node: ast.AsyncFunctionDef | ast.FunctionDef
|
|
57
|
-
) -> tuple[Parameter, ...]:
|
|
58
|
-
modifiers = {
|
|
59
|
-
'posonlyargs': 'positional-only',
|
|
60
|
-
'kwonlyargs': 'keyword-only',
|
|
61
|
-
}
|
|
62
|
-
params = []
|
|
63
|
-
|
|
64
|
-
positional_arguments_count = len(node.args.posonlyargs) + len(node.args.args)
|
|
65
|
-
if (
|
|
66
|
-
default_count := len(positional_defaults := node.args.defaults)
|
|
67
|
-
) < positional_arguments_count:
|
|
68
|
-
positional_defaults = [NO_DEFAULT] * (
|
|
69
|
-
positional_arguments_count - default_count
|
|
70
|
-
) + positional_defaults
|
|
71
|
-
|
|
72
|
-
keyword_defaults = node.args.kw_defaults
|
|
73
|
-
|
|
74
|
-
processed_positional_args = 0
|
|
75
|
-
for arg_type, args in ast.iter_fields(node.args):
|
|
76
|
-
if arg_type.endswith('defaults'):
|
|
77
|
-
continue
|
|
78
|
-
modifier = modifiers.get(arg_type)
|
|
79
|
-
if arg_type in ['vararg', 'kwarg']:
|
|
80
|
-
if args:
|
|
81
|
-
params.append(
|
|
82
|
-
Parameter(
|
|
83
|
-
name=f'*{args.arg}'
|
|
84
|
-
if arg_type == 'vararg'
|
|
85
|
-
else f'**{args.arg}',
|
|
86
|
-
type_=getattr(
|
|
87
|
-
args.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER
|
|
88
|
-
),
|
|
89
|
-
category=modifier,
|
|
90
|
-
default=NO_DEFAULT,
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
else:
|
|
94
|
-
is_keyword_only = arg_type.startswith('kw')
|
|
95
|
-
params.extend(
|
|
96
|
-
[
|
|
97
|
-
Parameter(
|
|
98
|
-
name=arg.arg,
|
|
99
|
-
type_=getattr(
|
|
100
|
-
arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER
|
|
101
|
-
),
|
|
102
|
-
category=modifier,
|
|
103
|
-
default=self._extract_default_values(
|
|
104
|
-
default, is_keyword_only
|
|
105
|
-
),
|
|
106
|
-
)
|
|
107
|
-
for arg, default in zip(
|
|
108
|
-
args,
|
|
109
|
-
keyword_defaults
|
|
110
|
-
if is_keyword_only
|
|
111
|
-
else positional_defaults[processed_positional_args:],
|
|
112
|
-
)
|
|
113
|
-
]
|
|
114
|
-
)
|
|
115
|
-
if not is_keyword_only:
|
|
116
|
-
processed_positional_args += len(args)
|
|
117
|
-
|
|
118
|
-
params = tuple(params)
|
|
119
|
-
if (
|
|
120
|
-
params
|
|
121
|
-
and isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
122
|
-
and params[0].name.startswith(('self', 'cls'))
|
|
123
|
-
):
|
|
124
|
-
return params[1:]
|
|
125
|
-
return params
|
|
126
|
-
|
|
127
|
-
def extract_returns(
|
|
128
|
-
self, node: ast.AsyncFunctionDef | ast.FunctionDef
|
|
129
|
-
) -> str | None:
|
|
130
|
-
if return_node := node.returns:
|
|
131
|
-
if isinstance(return_node, ast.Constant):
|
|
132
|
-
return return_node.value
|
|
133
|
-
if isinstance(return_node, ast.Name):
|
|
134
|
-
return return_node.id
|
|
135
|
-
return ast.get_source_segment(self.source_code, return_node)
|
|
136
|
-
if (
|
|
137
|
-
return_nodes := [
|
|
138
|
-
body_node
|
|
139
|
-
for body_node in node.body
|
|
140
|
-
if isinstance(body_node, ast.Return)
|
|
141
|
-
]
|
|
142
|
-
) and any(
|
|
143
|
-
not isinstance(return_value := body_return_node.value, ast.Constant)
|
|
144
|
-
or return_value.value
|
|
145
|
-
for body_return_node in return_nodes
|
|
146
|
-
):
|
|
147
|
-
return RETURN_TYPE_PLACEHOLDER
|
|
148
|
-
return return_node
|
|
149
35
|
|
|
150
36
|
def report_missing_docstring(self) -> None:
|
|
151
37
|
self.missing_docstrings += 1
|
|
152
38
|
print(f'{".".join(self.stack)} is missing a docstring', file=sys.stderr)
|
|
153
39
|
|
|
154
|
-
def
|
|
40
|
+
def handle_missing_docstring(
|
|
155
41
|
self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
|
|
156
|
-
) ->
|
|
157
|
-
if
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
if isinstance(node, ast.Module):
|
|
163
|
-
return self.converter.to_module_docstring(self.module_name)
|
|
164
|
-
|
|
165
|
-
# TODO: with the stack, I should be able to let __init__() not have a docstring
|
|
166
|
-
# and suggest those parameters as the docstring for the class
|
|
167
|
-
# (may need to keep the class node in another stack)
|
|
168
|
-
return self.converter.to_class_docstring(node.name)
|
|
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
|
|
169
48
|
|
|
170
49
|
def process_docstring(
|
|
171
50
|
self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
|
|
172
|
-
) ->
|
|
51
|
+
) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
|
|
173
52
|
if not ast.get_docstring(node):
|
|
174
53
|
self.report_missing_docstring()
|
|
175
|
-
|
|
176
|
-
print('Hint:')
|
|
177
|
-
print(self.suggest_docstring(node))
|
|
178
|
-
print()
|
|
54
|
+
node = self.handle_missing_docstring(node)
|
|
179
55
|
|
|
180
56
|
self.docstrings_inspected += 1
|
|
57
|
+
return node
|
|
181
58
|
|
|
182
|
-
def visit(self, node: ast.AST) ->
|
|
59
|
+
def visit(self, node: ast.AST) -> ast.AST:
|
|
183
60
|
if isinstance(
|
|
184
61
|
node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
|
|
185
62
|
):
|
|
@@ -187,10 +64,16 @@ class DocstringVisitor(ast.NodeVisitor):
|
|
|
187
64
|
self.module_name if isinstance(node, ast.Module) else node.name
|
|
188
65
|
)
|
|
189
66
|
|
|
190
|
-
self.process_docstring(node)
|
|
67
|
+
node = self.process_docstring(node)
|
|
191
68
|
|
|
192
69
|
self.generic_visit(node)
|
|
193
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}.')
|
|
194
78
|
|
|
195
|
-
|
|
196
|
-
self.visit(ast.parse(self.source_code))
|
|
79
|
+
return root
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docstringify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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.3.0
|
|
64
64
|
hooks:
|
|
65
65
|
- id: docstringify
|
|
66
66
|
```
|
|
@@ -69,32 +69,34 @@ 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.3.0
|
|
73
73
|
hooks:
|
|
74
74
|
- id: docstringify
|
|
75
75
|
args: [--threshold=0.75]
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
If you would like to see suggested docstring templates (inferred from type annotations for functions and methods), provide the `--suggest-changes` argument.
|
|
78
|
+
If you would like to see suggested docstring templates (inferred from type annotations for functions and methods), provide the `--suggest-changes` argument, along with the docstring style you want to use (options are `google` and `numpydoc`). Here, we ask for [numpydoc-style docstring](https://numpydoc.readthedocs.io/en/latest/format.html#) suggestions:
|
|
79
79
|
|
|
80
80
|
```yaml
|
|
81
81
|
- repo: https://github.com/stefmolin/docstringify
|
|
82
|
-
rev: 0.
|
|
82
|
+
rev: 0.3.0
|
|
83
83
|
hooks:
|
|
84
84
|
- id: docstringify
|
|
85
|
-
args: [--suggest-changes]
|
|
85
|
+
args: [--suggest-changes=numpydoc]
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
Use `--make-changes` to create a copy of each file with docstring templates. Here, we ask for changes using the [Google docstring style](https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html):
|
|
89
89
|
|
|
90
90
|
```yaml
|
|
91
91
|
- repo: https://github.com/stefmolin/docstringify
|
|
92
|
-
rev: 0.
|
|
92
|
+
rev: 0.3.0
|
|
93
93
|
hooks:
|
|
94
94
|
- id: docstringify
|
|
95
|
-
args: [--
|
|
95
|
+
args: [--make-changes=google]
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
If you want the changes to be made in place, change `--make-changes` to `--make-changes-inplace` – make sure you only operate on files that are in version control with this setting. Note that the resulting format of your file may be a little different (spacing, newlines, *etc.*).
|
|
99
|
+
|
|
98
100
|
Be sure to check out the [pre-commit documentation](https://pre-commit.com/#pre-commit-configyaml---hooks) for additional configuration options.
|
|
99
101
|
|
|
100
102
|
### Command line
|
|
@@ -136,8 +138,8 @@ If you would like to see suggested docstring templates (inferred from type annot
|
|
|
136
138
|
```pycon
|
|
137
139
|
>>> from docstringify.converters import NumpydocDocstringConverter
|
|
138
140
|
>>> from docstringify.visitor import DocstringVisitor
|
|
139
|
-
>>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter
|
|
140
|
-
>>> visitor.process_file()
|
|
141
|
+
>>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
|
|
142
|
+
>>> _ = visitor.process_file()
|
|
141
143
|
test is missing a docstring
|
|
142
144
|
Hint:
|
|
143
145
|
"""__description__"""
|
|
@@ -154,3 +156,17 @@ name : str, default="World"
|
|
|
154
156
|
"""
|
|
155
157
|
|
|
156
158
|
```
|
|
159
|
+
|
|
160
|
+
To make changes to your files, you will need to use the `DocstringTransformer` instead. With the `DocstringTransformer`, the converter is required:
|
|
161
|
+
|
|
162
|
+
```pycon
|
|
163
|
+
>>> from docstringify.converters import GoogleDocstringConverter
|
|
164
|
+
>>> from docstringify.transformer import DocstringTransformer
|
|
165
|
+
>>> transformer = DocstringTransformer('test.py', converter=GoogleDocstringConverter)
|
|
166
|
+
>>> _ = transformer.process_file()
|
|
167
|
+
test is missing a docstring
|
|
168
|
+
test.say_hello is missing a docstring
|
|
169
|
+
Docstring templates written to /.../test_docstringify.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
If you want to overwrite the file with the edits, pass `overwrite=True` to `DocstringTransformer()`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
docstringify/__init__.py,sha256=lJ-zemDc-oz26qgkRmOW06LIk6B28i8dKizCqJVlpFc,124
|
|
2
|
+
docstringify/__main__.py,sha256=A5Xq9jCZwHnQVVGklF9ueL-ad-hInsceHQNgze3Yq6E,96
|
|
3
|
+
docstringify/cli.py,sha256=lRBSIEsFLnYYkheKaq7lxu5Kb8zdG9u7FvTA-YpJ7B0,3010
|
|
4
|
+
docstringify/components.py,sha256=7Q__cuBTWNAqLDTil_91Q7T-zIzr7YpbX2TcmXrfph0,471
|
|
5
|
+
docstringify/docstring_generator.py,sha256=v0N9ge_TAwCCGwjO9o2ka9Wux6yIezXn8NUW5EJNlyI,5458
|
|
6
|
+
docstringify/exceptions.py,sha256=lt9p6bUGGqwsKCBk0hv4LrMqOLzNuk-E_3rXgjVKGpc,216
|
|
7
|
+
docstringify/transformer.py,sha256=adU_eY2HspNXAabn8BH_itp0i4x5wKmhQoH02qzbGe4,1836
|
|
8
|
+
docstringify/visitor.py,sha256=W7Zexf4D2Wt3thlDA-e1cWPhlhcaazYkSkp_zbbrotg,2610
|
|
9
|
+
docstringify/converters/__init__.py,sha256=iZXA6m689j3yqpfZjkoAcONXxAR7s-4GUQFEYxDPAyc,267
|
|
10
|
+
docstringify/converters/base.py,sha256=K_oAmVhWl8L6igFxJRluyn9DuZNtJi5UltyBTaTHc90,2268
|
|
11
|
+
docstringify/converters/google.py,sha256=puOCpmCK4InimfjR8TAcvCT_ZdpF_U0z_4E8a5l1UWk,1738
|
|
12
|
+
docstringify/converters/numpydoc.py,sha256=VDQGvknBJMP9o6D9wh3cKsL7-uGNuDP4Ow7C3-Tf08c,1762
|
|
13
|
+
docstringify-0.3.0.dist-info/licenses/LICENSE,sha256=E5RHv_qfCZPwnlJ_ZQb-WKOOy07LaJBA2a160OGtT3U,1071
|
|
14
|
+
docstringify-0.3.0.dist-info/METADATA,sha256=h1yDuJB7d8QoAc82aY9u6iHdSyVFBafsS0CxhhUkZM8,6454
|
|
15
|
+
docstringify-0.3.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
16
|
+
docstringify-0.3.0.dist-info/entry_points.txt,sha256=dJVPsbclB7stLE8g0d4WbO2NaZQLJXSLEVl6oeROAqI,55
|
|
17
|
+
docstringify-0.3.0.dist-info/top_level.txt,sha256=3RFmaFG0rYs4HkqRljatGQtY4BcNN7mqjK4tSznYdnQ,13
|
|
18
|
+
docstringify-0.3.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
docstringify/__init__.py,sha256=F8wAs3yjndnNfOVnCZIiCk86BQIwxiAGx7fXe15mxNE,124
|
|
2
|
-
docstringify/__main__.py,sha256=A5Xq9jCZwHnQVVGklF9ueL-ad-hInsceHQNgze3Yq6E,96
|
|
3
|
-
docstringify/cli.py,sha256=U2G97RihJC2HMR2MCWDNadcU5erBuFDlIDrIOQ4lJSM,2230
|
|
4
|
-
docstringify/components.py,sha256=7Q__cuBTWNAqLDTil_91Q7T-zIzr7YpbX2TcmXrfph0,471
|
|
5
|
-
docstringify/visitor.py,sha256=qF6T6lqbK93RArbkDhqZYOQNAuvi6miMehO0U9tdNJM,7019
|
|
6
|
-
docstringify/converters/__init__.py,sha256=iZXA6m689j3yqpfZjkoAcONXxAR7s-4GUQFEYxDPAyc,267
|
|
7
|
-
docstringify/converters/base.py,sha256=1QWFE5ckS_Rbwi9TyRM-rKCA7MCwHgZ4Ok8ugMyvMgA,1507
|
|
8
|
-
docstringify/converters/google.py,sha256=ytel_pqqUNV5wR8nVV1F8yczFA3Tj1f9_nkfJA4Kq04,1751
|
|
9
|
-
docstringify/converters/numpydoc.py,sha256=iv5smFrHFBc0rHvLhgh66PaV2OEI3va_piRZ3_y2Ig8,1773
|
|
10
|
-
docstringify-0.2.0.dist-info/licenses/LICENSE,sha256=E5RHv_qfCZPwnlJ_ZQb-WKOOy07LaJBA2a160OGtT3U,1071
|
|
11
|
-
docstringify-0.2.0.dist-info/METADATA,sha256=QvPrAwX5TnbG6y7XpK6grHqohENJ2doN3i0WiHipECk,5362
|
|
12
|
-
docstringify-0.2.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
13
|
-
docstringify-0.2.0.dist-info/entry_points.txt,sha256=dJVPsbclB7stLE8g0d4WbO2NaZQLJXSLEVl6oeROAqI,55
|
|
14
|
-
docstringify-0.2.0.dist-info/top_level.txt,sha256=3RFmaFG0rYs4HkqRljatGQtY4BcNN7mqjK4tSznYdnQ,13
|
|
15
|
-
docstringify-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|