docstringify 0.2.0__py3-none-any.whl → 0.4.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 +44 -22
- docstringify/converters/base.py +47 -4
- docstringify/converters/google.py +9 -11
- docstringify/converters/numpydoc.py +8 -10
- docstringify/exceptions.py +6 -0
- docstringify/nodes/__init__.py +1 -0
- docstringify/nodes/base.py +53 -0
- docstringify/nodes/function.py +159 -0
- docstringify/traversal/__init__.py +9 -0
- docstringify/traversal/transformer.py +64 -0
- docstringify/traversal/visitor.py +100 -0
- {docstringify-0.2.0.dist-info → docstringify-0.4.0.dist-info}/METADATA +28 -12
- docstringify-0.4.0.dist-info/RECORD +21 -0
- docstringify/visitor.py +0 -196
- docstringify-0.2.0.dist-info/RECORD +0 -15
- {docstringify-0.2.0.dist-info → docstringify-0.4.0.dist-info}/WHEEL +0 -0
- {docstringify-0.2.0.dist-info → docstringify-0.4.0.dist-info}/entry_points.txt +0 -0
- {docstringify-0.2.0.dist-info → docstringify-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {docstringify-0.2.0.dist-info → docstringify-0.4.0.dist-info}/top_level.txt +0 -0
docstringify/__init__.py
CHANGED
docstringify/cli.py
CHANGED
|
@@ -2,14 +2,21 @@ 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
|
|
9
|
-
from .
|
|
10
|
+
from .traversal import DocstringTransformer, DocstringVisitor
|
|
10
11
|
|
|
11
12
|
PROG = __package__
|
|
12
13
|
|
|
14
|
+
STYLES = {
|
|
15
|
+
'google': GoogleDocstringConverter,
|
|
16
|
+
'numpydoc': NumpydocDocstringConverter,
|
|
17
|
+
}
|
|
18
|
+
CLI_DEFAULTS = {'threshold': 1.0}
|
|
19
|
+
|
|
13
20
|
|
|
14
21
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
15
22
|
parser = argparse.ArgumentParser(prog=PROG)
|
|
@@ -22,40 +29,55 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
22
29
|
'--version', action='version', version=f'%(prog)s {__version__}'
|
|
23
30
|
)
|
|
24
31
|
|
|
25
|
-
parser.
|
|
32
|
+
run_group = parser.add_argument_group('Run options')
|
|
33
|
+
handle_missing_docstring = run_group.add_mutually_exclusive_group(required=False)
|
|
34
|
+
handle_missing_docstring.add_argument(
|
|
35
|
+
'--make-changes',
|
|
36
|
+
choices=STYLES.keys(),
|
|
37
|
+
help='Whether to insert docstring templates for items missing docstrings',
|
|
38
|
+
)
|
|
39
|
+
handle_missing_docstring.add_argument(
|
|
40
|
+
'--make-changes-inplace',
|
|
41
|
+
choices=STYLES.keys(),
|
|
42
|
+
help=(
|
|
43
|
+
'Whether to insert docstring templates for items missing docstrings, '
|
|
44
|
+
'overwriting the original file'
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
handle_missing_docstring.add_argument(
|
|
26
48
|
'--suggest-changes',
|
|
27
|
-
|
|
49
|
+
choices=STYLES.keys(),
|
|
28
50
|
help='Whether to print out docstring templates for items missing docstrings',
|
|
29
51
|
)
|
|
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(
|
|
52
|
+
run_group.add_argument(
|
|
37
53
|
'--threshold',
|
|
38
54
|
type=float,
|
|
39
|
-
default=
|
|
55
|
+
default=CLI_DEFAULTS['threshold'],
|
|
40
56
|
help='The percentage of docstrings that must be present to pass',
|
|
41
57
|
)
|
|
42
58
|
args = parser.parse_args(argv)
|
|
43
59
|
|
|
44
|
-
if args.
|
|
45
|
-
converter =
|
|
46
|
-
elif args.style == 'google':
|
|
47
|
-
converter = GoogleDocstringConverter()
|
|
48
|
-
elif args.style == 'numpydoc':
|
|
49
|
-
converter = NumpydocDocstringConverter()
|
|
60
|
+
if style := args.make_changes or args.make_changes_inplace or args.suggest_changes:
|
|
61
|
+
converter = STYLES[style]
|
|
50
62
|
else:
|
|
51
|
-
|
|
63
|
+
converter = None
|
|
64
|
+
|
|
65
|
+
get_docstring_processor = (
|
|
66
|
+
partial(
|
|
67
|
+
DocstringTransformer,
|
|
68
|
+
converter=converter,
|
|
69
|
+
**{'overwrite': bool(args.make_changes_inplace)},
|
|
70
|
+
)
|
|
71
|
+
if args.make_changes or args.make_changes_inplace
|
|
72
|
+
else partial(DocstringVisitor, converter=converter)
|
|
73
|
+
)
|
|
52
74
|
|
|
53
75
|
docstrings_processed = missing_docstrings = 0
|
|
54
76
|
for file in args.filenames:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
missing_docstrings +=
|
|
58
|
-
docstrings_processed +=
|
|
77
|
+
processor = get_docstring_processor(file)
|
|
78
|
+
processor.process_file()
|
|
79
|
+
missing_docstrings += processor.missing_docstrings
|
|
80
|
+
docstrings_processed += processor.docstrings_inspected
|
|
59
81
|
|
|
60
82
|
if docstrings_processed and (
|
|
61
83
|
missing_percentage := (missing_docstrings / docstrings_processed)
|
docstringify/converters/base.py
CHANGED
|
@@ -2,26 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import ast
|
|
6
|
+
import textwrap
|
|
5
7
|
from abc import ABC, abstractmethod
|
|
6
8
|
from typing import TYPE_CHECKING
|
|
7
9
|
|
|
10
|
+
from ..components import Function
|
|
11
|
+
from ..exceptions import InvalidDocstringError
|
|
12
|
+
|
|
8
13
|
if TYPE_CHECKING:
|
|
9
|
-
from ..components import
|
|
14
|
+
from ..components import Parameter
|
|
15
|
+
from ..nodes.base import DocstringNode
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
class DocstringConverter(ABC):
|
|
13
19
|
def __init__(
|
|
14
|
-
self,
|
|
20
|
+
self,
|
|
21
|
+
parameters_section_template: str,
|
|
22
|
+
returns_section_template: str,
|
|
23
|
+
quote: bool,
|
|
15
24
|
) -> None:
|
|
16
25
|
self.parameters_section_template = parameters_section_template
|
|
17
26
|
self.returns_section_template = returns_section_template
|
|
27
|
+
self._quote = quote
|
|
18
28
|
|
|
19
29
|
@abstractmethod
|
|
20
|
-
def to_class_docstring(self, class_name: str) -> str:
|
|
30
|
+
def to_class_docstring(self, class_name: str, indent: int) -> str:
|
|
21
31
|
pass
|
|
22
32
|
|
|
23
33
|
@abstractmethod
|
|
24
|
-
def to_function_docstring(self, function: Function) -> str:
|
|
34
|
+
def to_function_docstring(self, function: Function, indent: int) -> str:
|
|
25
35
|
pass
|
|
26
36
|
|
|
27
37
|
@abstractmethod
|
|
@@ -49,3 +59,36 @@ class DocstringConverter(ABC):
|
|
|
49
59
|
if return_text := self.format_return(return_type):
|
|
50
60
|
return self.returns_section_template.format(returns=return_text)
|
|
51
61
|
return ''
|
|
62
|
+
|
|
63
|
+
def quote_docstring(self, docstring: str | list[str], indent: int) -> str:
|
|
64
|
+
quote = '"""' if self._quote else ''
|
|
65
|
+
prefix = sep = ''
|
|
66
|
+
if isinstance(docstring, str):
|
|
67
|
+
docstring = [quote, docstring, quote]
|
|
68
|
+
elif isinstance(docstring, list):
|
|
69
|
+
if len(docstring) > 1:
|
|
70
|
+
prefix = ' ' * indent if indent else ''
|
|
71
|
+
sep = '\n'
|
|
72
|
+
docstring = [quote, *docstring, f'{prefix}{quote}']
|
|
73
|
+
else:
|
|
74
|
+
raise InvalidDocstringError(type(docstring).__name__)
|
|
75
|
+
|
|
76
|
+
return textwrap.indent(sep.join(docstring), prefix)
|
|
77
|
+
|
|
78
|
+
def suggest_docstring(
|
|
79
|
+
self,
|
|
80
|
+
docstring_node: DocstringNode,
|
|
81
|
+
indent: int = 0,
|
|
82
|
+
) -> str:
|
|
83
|
+
if isinstance(docstring_node.ast_node, ast.AsyncFunctionDef | ast.FunctionDef):
|
|
84
|
+
return self.to_function_docstring(
|
|
85
|
+
Function(
|
|
86
|
+
docstring_node.extract_arguments(), docstring_node.extract_returns()
|
|
87
|
+
),
|
|
88
|
+
indent=indent,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if isinstance(docstring_node.ast_node, ast.Module):
|
|
92
|
+
return self.to_module_docstring(docstring_node.module_name)
|
|
93
|
+
|
|
94
|
+
return self.to_class_docstring(docstring_node.name, indent=indent)
|
|
@@ -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 @@
|
|
|
1
|
+
"""Docstringify nodes."""
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
|
|
37
|
+
self.ast_node: (
|
|
38
|
+
ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
|
|
39
|
+
) = node
|
|
40
|
+
self.name: str = getattr(node, 'name', self.module_name)
|
|
41
|
+
|
|
42
|
+
self.docstring = ast.get_docstring(node)
|
|
43
|
+
self.get_source_segment: Callable[[ast.AST], str] = partial(
|
|
44
|
+
ast.get_source_segment, source_code
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def fully_qualified_name(self) -> str:
|
|
49
|
+
return (
|
|
50
|
+
f'{self.parent.fully_qualified_name}.{self.name}'
|
|
51
|
+
if self.parent
|
|
52
|
+
else self.name
|
|
53
|
+
)
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
Parameter,
|
|
12
|
+
)
|
|
13
|
+
from .base import DocstringNode
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FunctionDocstringNode(DocstringNode):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
20
|
+
module_name: str,
|
|
21
|
+
source_code: str,
|
|
22
|
+
parent: DocstringNode,
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__(node, module_name, source_code, parent)
|
|
25
|
+
self.decorators: list[str] = [
|
|
26
|
+
self.get_source_segment(decorator) for decorator in node.decorator_list
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
self.is_method: bool = self.parent and isinstance(
|
|
30
|
+
self.parent.ast_node, ast.ClassDef
|
|
31
|
+
)
|
|
32
|
+
self.is_abstract_method: bool = self.is_method and (
|
|
33
|
+
'abstractmethod' in self.decorators
|
|
34
|
+
or 'abc.abstractmethod' in self.decorators
|
|
35
|
+
)
|
|
36
|
+
self.is_class_method: bool = self.is_method and 'classmethod' in self.decorators
|
|
37
|
+
self.is_static_method: bool = (
|
|
38
|
+
self.is_method and 'staticmethod' in self.decorators
|
|
39
|
+
)
|
|
40
|
+
self.is_instance_method: bool = (
|
|
41
|
+
self.is_method and not self.is_class_method and not self.is_static_method
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.arguments: ast.arguments | None = getattr(node, 'args', None)
|
|
45
|
+
self.return_annotation: str | None = self._extract_return_annotation()
|
|
46
|
+
self.return_statements: list[ast.Return] = []
|
|
47
|
+
|
|
48
|
+
def _extract_default_values(
|
|
49
|
+
self, default: ast.Constant | None | Literal[NO_DEFAULT], is_keyword_only: bool
|
|
50
|
+
) -> str | Literal[NO_DEFAULT]:
|
|
51
|
+
if (not is_keyword_only and default is not NO_DEFAULT) or (
|
|
52
|
+
is_keyword_only and default
|
|
53
|
+
):
|
|
54
|
+
try:
|
|
55
|
+
default_value = default.value
|
|
56
|
+
except AttributeError:
|
|
57
|
+
default_value = f'`{default.id}`'
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
f'"{default_value}"'
|
|
61
|
+
if isinstance(default_value, str) and not default_value.startswith('`')
|
|
62
|
+
else default_value
|
|
63
|
+
)
|
|
64
|
+
return NO_DEFAULT
|
|
65
|
+
|
|
66
|
+
def _extract_star_args(self) -> list[Parameter | None, Parameter | None]:
|
|
67
|
+
return [
|
|
68
|
+
Parameter(
|
|
69
|
+
name=f'*{arg.arg}' if arg_type == 'vararg' else f'**{arg.arg}',
|
|
70
|
+
type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
|
|
71
|
+
category=None,
|
|
72
|
+
default=NO_DEFAULT,
|
|
73
|
+
)
|
|
74
|
+
if arg
|
|
75
|
+
else None
|
|
76
|
+
for arg_type in ['vararg', 'kwarg']
|
|
77
|
+
for arg in [getattr(self.arguments, arg_type)]
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def _extract_positional_args(self) -> list[Parameter, ...]:
|
|
81
|
+
if (default_count := len(positional_defaults := self.arguments.defaults)) < (
|
|
82
|
+
positional_arguments_count := len(self.arguments.posonlyargs)
|
|
83
|
+
+ len(self.arguments.args)
|
|
84
|
+
):
|
|
85
|
+
positional_defaults = [NO_DEFAULT] * (
|
|
86
|
+
positional_arguments_count - default_count
|
|
87
|
+
) + positional_defaults
|
|
88
|
+
|
|
89
|
+
return [
|
|
90
|
+
Parameter(
|
|
91
|
+
name=arg.arg,
|
|
92
|
+
type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
|
|
93
|
+
category=category,
|
|
94
|
+
default=self._extract_default_values(default, False),
|
|
95
|
+
)
|
|
96
|
+
for (arg, category), default in zip(
|
|
97
|
+
itertools.chain(
|
|
98
|
+
zip(
|
|
99
|
+
self.arguments.posonlyargs, itertools.repeat('positional-only')
|
|
100
|
+
),
|
|
101
|
+
zip(self.arguments.args, itertools.repeat(None)),
|
|
102
|
+
),
|
|
103
|
+
positional_defaults,
|
|
104
|
+
)
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
def _extract_keyword_args(self) -> list[Parameter, ...]:
|
|
108
|
+
return [
|
|
109
|
+
Parameter(
|
|
110
|
+
name=arg.arg,
|
|
111
|
+
type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
|
|
112
|
+
category='keyword-only',
|
|
113
|
+
default=self._extract_default_values(default, True),
|
|
114
|
+
)
|
|
115
|
+
for arg, default in zip(
|
|
116
|
+
self.arguments.kwonlyargs, self.arguments.kw_defaults
|
|
117
|
+
)
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
def extract_arguments(self) -> tuple[Parameter, ...]:
|
|
121
|
+
params = self._extract_positional_args()
|
|
122
|
+
|
|
123
|
+
varargs, kwargs = self._extract_star_args()
|
|
124
|
+
|
|
125
|
+
if varargs:
|
|
126
|
+
params.append(varargs)
|
|
127
|
+
|
|
128
|
+
params.extend(self._extract_keyword_args())
|
|
129
|
+
|
|
130
|
+
if kwargs:
|
|
131
|
+
params.append(kwargs)
|
|
132
|
+
|
|
133
|
+
params = tuple(params)
|
|
134
|
+
if params and (
|
|
135
|
+
(self.is_class_method and params[0].name == 'cls')
|
|
136
|
+
or (self.is_instance_method and params[0].name == 'self')
|
|
137
|
+
):
|
|
138
|
+
return params[1:]
|
|
139
|
+
return params
|
|
140
|
+
|
|
141
|
+
def _extract_return_annotation(self) -> str | None:
|
|
142
|
+
if return_annotation_node := self.ast_node.returns:
|
|
143
|
+
if isinstance(return_annotation_node, ast.Constant):
|
|
144
|
+
return return_annotation_node.value
|
|
145
|
+
if isinstance(return_annotation_node, ast.Name):
|
|
146
|
+
return return_annotation_node.id
|
|
147
|
+
return self.get_source_segment(return_annotation_node)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
def extract_returns(self) -> str | None:
|
|
151
|
+
if self.return_annotation:
|
|
152
|
+
return self.return_annotation
|
|
153
|
+
if any(
|
|
154
|
+
not isinstance(return_value := return_node.value, ast.Constant)
|
|
155
|
+
or return_value.value
|
|
156
|
+
for return_node in self.return_statements
|
|
157
|
+
):
|
|
158
|
+
return RETURN_TYPE_PLACEHOLDER
|
|
159
|
+
return None
|
|
@@ -0,0 +1,64 @@
|
|
|
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 (
|
|
49
|
+
docstring_node.docstring is not None
|
|
50
|
+
and docstring_node.docstring.strip() == ''
|
|
51
|
+
):
|
|
52
|
+
# If the docstring is empty, we replace it with the suggested docstring
|
|
53
|
+
docstring_node.ast_node.body[0] = docstring_ast_node
|
|
54
|
+
else:
|
|
55
|
+
# If the docstring is missing, we insert the suggested docstring
|
|
56
|
+
docstring_node.ast_node.body.insert(0, docstring_ast_node)
|
|
57
|
+
|
|
58
|
+
docstring_node.ast_node = ast.fix_missing_locations(docstring_node.ast_node)
|
|
59
|
+
|
|
60
|
+
return docstring_node
|
|
61
|
+
|
|
62
|
+
def process_file(self) -> None:
|
|
63
|
+
super().process_file()
|
|
64
|
+
self.save()
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
# TODO: need to print full path for node name
|
|
41
|
+
print(
|
|
42
|
+
f'{docstring_node.fully_qualified_name} is missing a docstring',
|
|
43
|
+
file=sys.stderr,
|
|
44
|
+
)
|
|
45
|
+
self.handle_missing_docstring(docstring_node)
|
|
46
|
+
|
|
47
|
+
def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
|
|
48
|
+
if self.docstring_converter:
|
|
49
|
+
print('Hint:')
|
|
50
|
+
print(self.docstring_converter.suggest_docstring(docstring_node))
|
|
51
|
+
print()
|
|
52
|
+
|
|
53
|
+
def process_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
|
|
54
|
+
if docstring_node.docstring is None or docstring_node.docstring.strip() == '':
|
|
55
|
+
self.missing_docstrings.append(docstring_node)
|
|
56
|
+
|
|
57
|
+
self.docstrings_inspected += 1
|
|
58
|
+
return docstring_node
|
|
59
|
+
|
|
60
|
+
def visit_docstring(
|
|
61
|
+
self,
|
|
62
|
+
node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module,
|
|
63
|
+
docstring_class: type[DocstringNode],
|
|
64
|
+
) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
|
|
65
|
+
docstring_node = docstring_class(
|
|
66
|
+
node,
|
|
67
|
+
self.module_name,
|
|
68
|
+
self.source_code,
|
|
69
|
+
parent=self.stack[-1] if self.stack else None,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.stack.append(docstring_node)
|
|
73
|
+
|
|
74
|
+
docstring_node = self.process_docstring(docstring_node)
|
|
75
|
+
self.generic_visit(docstring_node.ast_node)
|
|
76
|
+
self.stack.pop()
|
|
77
|
+
return docstring_node.ast_node
|
|
78
|
+
|
|
79
|
+
def visit_Module(self, node: ast.Module) -> ast.Module: # noqa: N802
|
|
80
|
+
return self.visit_docstring(node, DocstringNode)
|
|
81
|
+
|
|
82
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: # noqa: N802
|
|
83
|
+
return self.visit_docstring(node, DocstringNode)
|
|
84
|
+
|
|
85
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: # noqa: N802
|
|
86
|
+
return self.visit_docstring(node, FunctionDocstringNode)
|
|
87
|
+
|
|
88
|
+
def visit_AsyncFunctionDef( # noqa: N802
|
|
89
|
+
self, node: ast.AsyncFunctionDef
|
|
90
|
+
) -> ast.AsyncFunctionDef:
|
|
91
|
+
return self.visit_docstring(node, FunctionDocstringNode)
|
|
92
|
+
|
|
93
|
+
def visit_Return(self, node: ast.Return) -> ast.Return: # noqa: N802
|
|
94
|
+
if isinstance(self.stack[-1], FunctionDocstringNode):
|
|
95
|
+
self.stack[-1].return_statements.append(node)
|
|
96
|
+
return node
|
|
97
|
+
|
|
98
|
+
def process_file(self) -> None:
|
|
99
|
+
self.visit(self.tree)
|
|
100
|
+
self.report_missing_docstrings()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docstringify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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.4.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.4.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.4.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.4.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
|
|
@@ -124,7 +126,7 @@ $ python -m pip install docstringify
|
|
|
124
126
|
Then, use the `DocstringVisitor()` class on individual files to see spots where docstrings are missing:
|
|
125
127
|
|
|
126
128
|
```pycon
|
|
127
|
-
>>> from docstringify.
|
|
129
|
+
>>> from docstringify.traversal import DocstringVisitor
|
|
128
130
|
>>> visitor = DocstringVisitor('test.py')
|
|
129
131
|
>>> visitor.process_file()
|
|
130
132
|
test is missing a docstring
|
|
@@ -135,8 +137,8 @@ If you would like to see suggested docstring templates (inferred from type annot
|
|
|
135
137
|
|
|
136
138
|
```pycon
|
|
137
139
|
>>> from docstringify.converters import NumpydocDocstringConverter
|
|
138
|
-
>>> from docstringify.
|
|
139
|
-
>>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter
|
|
140
|
+
>>> from docstringify.traversal import DocstringVisitor
|
|
141
|
+
>>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
|
|
140
142
|
>>> visitor.process_file()
|
|
141
143
|
test is missing a docstring
|
|
142
144
|
Hint:
|
|
@@ -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.traversal 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,21 @@
|
|
|
1
|
+
docstringify/__init__.py,sha256=ORdK5paDVN7Lbf_kfyo-3tlnIQvFidCXMqyn70WhisI,124
|
|
2
|
+
docstringify/__main__.py,sha256=A5Xq9jCZwHnQVVGklF9ueL-ad-hInsceHQNgze3Yq6E,96
|
|
3
|
+
docstringify/cli.py,sha256=0zUoDDj8f60NDw5cnL_otSiymorMIwq5XfaJbRCrEHQ,2986
|
|
4
|
+
docstringify/components.py,sha256=7Q__cuBTWNAqLDTil_91Q7T-zIzr7YpbX2TcmXrfph0,471
|
|
5
|
+
docstringify/exceptions.py,sha256=lt9p6bUGGqwsKCBk0hv4LrMqOLzNuk-E_3rXgjVKGpc,216
|
|
6
|
+
docstringify/converters/__init__.py,sha256=iZXA6m689j3yqpfZjkoAcONXxAR7s-4GUQFEYxDPAyc,267
|
|
7
|
+
docstringify/converters/base.py,sha256=nIUgfGTjwFSXqcGHfEm3iljo4YcsteCs7iL5o5BNceY,2989
|
|
8
|
+
docstringify/converters/google.py,sha256=puOCpmCK4InimfjR8TAcvCT_ZdpF_U0z_4E8a5l1UWk,1738
|
|
9
|
+
docstringify/converters/numpydoc.py,sha256=VDQGvknBJMP9o6D9wh3cKsL7-uGNuDP4Ow7C3-Tf08c,1762
|
|
10
|
+
docstringify/nodes/__init__.py,sha256=nNCajaWkQJGRv5cKdWyo43JDfSsBUrlnCL8JRTaW4cI,26
|
|
11
|
+
docstringify/nodes/base.py,sha256=JMAHvJm7FRl-NmP6hbO8QzgTMHqT-5DnVo8C5-xSLeo,1415
|
|
12
|
+
docstringify/nodes/function.py,sha256=HfkAHy5Mb6--mxtR-6Swpd9iw_qfdzdH-wkN2lJy5T0,5593
|
|
13
|
+
docstringify/traversal/__init__.py,sha256=fy9DEvmXm4sJyf4dAJI-patQPoL8_zPgPpda84q0a-4,188
|
|
14
|
+
docstringify/traversal/transformer.py,sha256=SvyLQsQ3WrOKFSzIPYegA6vacp7Tp1gjNdPAFwoH8jg,2130
|
|
15
|
+
docstringify/traversal/visitor.py,sha256=Uewzh4esB9XY5tQ9QfRt1TbwDEpIq4kW7Kdqm8E4IDA,3688
|
|
16
|
+
docstringify-0.4.0.dist-info/licenses/LICENSE,sha256=E5RHv_qfCZPwnlJ_ZQb-WKOOy07LaJBA2a160OGtT3U,1071
|
|
17
|
+
docstringify-0.4.0.dist-info/METADATA,sha256=wUOlXNPNwUyszzYWhPvtZZqn7thccQvN77PIjiVrlzA,6448
|
|
18
|
+
docstringify-0.4.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
19
|
+
docstringify-0.4.0.dist-info/entry_points.txt,sha256=dJVPsbclB7stLE8g0d4WbO2NaZQLJXSLEVl6oeROAqI,55
|
|
20
|
+
docstringify-0.4.0.dist-info/top_level.txt,sha256=3RFmaFG0rYs4HkqRljatGQtY4BcNN7mqjK4tSznYdnQ,13
|
|
21
|
+
docstringify-0.4.0.dist-info/RECORD,,
|
docstringify/visitor.py
DELETED
|
@@ -1,196 +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, Literal
|
|
7
|
-
|
|
8
|
-
from .components import (
|
|
9
|
-
NO_DEFAULT,
|
|
10
|
-
PARAMETER_TYPE_PLACEHOLDER,
|
|
11
|
-
RETURN_TYPE_PLACEHOLDER,
|
|
12
|
-
Function,
|
|
13
|
-
Parameter,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
from .converters import DocstringConverter
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class DocstringVisitor(ast.NodeVisitor):
|
|
21
|
-
def __init__(
|
|
22
|
-
self, filename: str, converter: DocstringConverter | None = None
|
|
23
|
-
) -> None:
|
|
24
|
-
self.source_file: Path = Path(filename)
|
|
25
|
-
self.source_code: str = self.source_file.read_text()
|
|
26
|
-
|
|
27
|
-
self.docstrings_inspected: int = 0
|
|
28
|
-
self.missing_docstrings: int = 0
|
|
29
|
-
|
|
30
|
-
self.module_name: str = self.source_file.stem
|
|
31
|
-
self.stack: list[str] = []
|
|
32
|
-
|
|
33
|
-
self.provide_hints: bool = converter is not None
|
|
34
|
-
if self.provide_hints:
|
|
35
|
-
self.converter: DocstringVisitor = converter # TODO: consider whether this should be instantiated here instead of outside
|
|
36
|
-
|
|
37
|
-
def _extract_default_values(
|
|
38
|
-
self, default: ast.Constant | None | Literal[NO_DEFAULT], is_keyword_only: bool
|
|
39
|
-
) -> str | Literal[NO_DEFAULT]:
|
|
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
|
|
52
|
-
)
|
|
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
|
-
|
|
150
|
-
def report_missing_docstring(self) -> None:
|
|
151
|
-
self.missing_docstrings += 1
|
|
152
|
-
print(f'{".".join(self.stack)} is missing a docstring', file=sys.stderr)
|
|
153
|
-
|
|
154
|
-
def suggest_docstring(
|
|
155
|
-
self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
|
|
156
|
-
) -> str:
|
|
157
|
-
if isinstance(node, ast.AsyncFunctionDef | ast.FunctionDef):
|
|
158
|
-
return self.converter.to_function_docstring(
|
|
159
|
-
Function(self.extract_arguments(node), self.extract_returns(node))
|
|
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)
|
|
169
|
-
|
|
170
|
-
def process_docstring(
|
|
171
|
-
self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
|
|
172
|
-
) -> None:
|
|
173
|
-
if not ast.get_docstring(node):
|
|
174
|
-
self.report_missing_docstring()
|
|
175
|
-
if self.provide_hints:
|
|
176
|
-
print('Hint:')
|
|
177
|
-
print(self.suggest_docstring(node))
|
|
178
|
-
print()
|
|
179
|
-
|
|
180
|
-
self.docstrings_inspected += 1
|
|
181
|
-
|
|
182
|
-
def visit(self, node: ast.AST) -> None:
|
|
183
|
-
if isinstance(
|
|
184
|
-
node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
|
|
185
|
-
):
|
|
186
|
-
self.stack.append(
|
|
187
|
-
self.module_name if isinstance(node, ast.Module) else node.name
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
self.process_docstring(node)
|
|
191
|
-
|
|
192
|
-
self.generic_visit(node)
|
|
193
|
-
_ = self.stack.pop()
|
|
194
|
-
|
|
195
|
-
def process_file(self) -> None:
|
|
196
|
-
self.visit(ast.parse(self.source_code))
|
|
@@ -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
|