Skip to content

Commit fccba31

Browse files
authored
Add verbose mode (#29)
* Add verbose mode * Add docstrings for traversal classes
1 parent 8855332 commit fccba31

4 files changed

Lines changed: 225 additions & 11 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,6 @@ exclude = [ # don't report on checks for these
106106
'\.__repr__$',
107107
'\.__str__$',
108108
]
109+
override_SS05 = [ # override SS05 to allow docstrings starting with these words
110+
'^Process ',
111+
]

src/docstringify/cli.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
import argparse
66
import sys
77
from functools import partial
8-
from typing import Sequence
8+
from typing import TYPE_CHECKING
99

1010
from . import __doc__ as pkg_description
1111
from . import __version__
1212
from .converters import GoogleDocstringConverter, NumpydocDocstringConverter
1313
from .traversal import DocstringTransformer, DocstringVisitor
1414

15+
if TYPE_CHECKING:
16+
from collections.abc import Sequence
17+
1518
PROG = __package__
1619
STYLES = {'google': GoogleDocstringConverter, 'numpydoc': NumpydocDocstringConverter}
1720
CLI_DEFAULTS = {'threshold': 1.0}
@@ -36,6 +39,9 @@ def main(argv: Sequence[str] | None = None) -> int:
3639

3740
parser = argparse.ArgumentParser(prog=PROG, description=pkg_description)
3841
parser.add_argument('filenames', nargs='*', help='Filenames to process')
42+
parser.add_argument(
43+
'-v', '--verbose', action='store_true', help='Run in verbose mode'
44+
)
3945
parser.add_argument(
4046
'--version', action='version', version=f'%(prog)s {__version__}'
4147
)
@@ -79,10 +85,11 @@ def main(argv: Sequence[str] | None = None) -> int:
7985
partial(
8086
DocstringTransformer,
8187
converter=converter,
82-
**{'overwrite': bool(args.make_changes_inplace)},
88+
overwrite=bool(args.make_changes_inplace),
89+
verbose=args.verbose,
8390
)
8491
if args.make_changes or args.make_changes_inplace
85-
else partial(DocstringVisitor, converter=converter)
92+
else partial(DocstringVisitor, converter=converter, verbose=args.verbose)
8693
)
8794

8895
docstrings_processed = missing_docstrings = 0

src/docstringify/traversal/transformer.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""
2+
Traverse the AST with the ability to transform it to add templates for docstrings based
3+
on the source code.
4+
"""
5+
16
from __future__ import annotations
27

38
import ast
@@ -11,16 +16,39 @@
1116

1217

1318
class DocstringTransformer(ast.NodeTransformer, DocstringVisitor):
19+
"""
20+
A class for indicating where docstrings are missing in a single module of source code
21+
and injecting suggested docstring templates based on the AST representation into the
22+
source code.
23+
24+
Parameters
25+
----------
26+
filename : str
27+
The file to process.
28+
converter : type[DocstringConverter]
29+
The converter class determines the docstring style to use for generating the
30+
suggested docstring templates.
31+
overwrite : bool, keyword-only, default=False
32+
Whether to save the modified source code back to the original file.
33+
verbose : bool, keyword-only, default=False
34+
Whether to run in verbose mode.
35+
"""
36+
1437
def __init__(
1538
self,
1639
filename: str,
1740
converter: type[DocstringConverter],
41+
*,
1842
overwrite: bool = False,
43+
verbose: bool = False,
1944
) -> None:
20-
super().__init__(filename, converter)
21-
self.overwrite = overwrite
45+
super().__init__(filename, converter, verbose=verbose)
46+
47+
self.overwrite: bool = overwrite
48+
"""Whether to save the modified source code back to the original file."""
2249

2350
def save(self) -> None:
51+
"""Save the modified AST to a file as source code."""
2452
if self.missing_docstrings:
2553
output = (
2654
self.source_file
@@ -37,6 +65,23 @@ def save(self) -> None:
3765
print(f'Docstring templates written to {output}')
3866

3967
def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
68+
"""
69+
Handle missing docstrings by injecting a suggested docstring template based on
70+
the source code into the AST.
71+
72+
Parameters
73+
----------
74+
docstring_node : DocstringNode
75+
An instance of :class:`.DocstringNode`, which wraps an AST node and adds
76+
additional context relevant for Docstringify.
77+
78+
Returns
79+
-------
80+
DocstringNode
81+
An instance of :class:`.DocstringNode`, which wraps an AST node and adds
82+
additional context relevant for Docstringify. The AST node it contains will
83+
have a new node for the docstring template added to its body.
84+
"""
4085
suggested_docstring = self.docstring_converter.suggest_docstring(
4186
docstring_node,
4287
indent=0
@@ -57,5 +102,9 @@ def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNo
57102
return docstring_node
58103

59104
def process_file(self) -> None:
105+
"""
106+
Process a source code file, handling missing docstrings and saving the modified
107+
AST to a file.
108+
"""
60109
super().process_file()
61110
self.save()

src/docstringify/traversal/visitor.py

Lines changed: 161 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""
2+
Traverse the AST, indicating where docstrings are missing and suggesting templates
3+
based on the AST.
4+
"""
5+
16
from __future__ import annotations
27

38
import ast
@@ -13,37 +18,90 @@
1318

1419

1520
class DocstringVisitor(ast.NodeVisitor):
21+
"""
22+
A class for indicating where docstrings are missing in a single module of source code
23+
and suggesting templates based on the AST representation.
24+
25+
Parameters
26+
----------
27+
filename : str
28+
The file to process.
29+
converter : type[DocstringConverter] | None, optional
30+
When this is ``None``, docstrings will be reported as missing, but when this is
31+
a converter, docstring templates will be generated.
32+
verbose : bool, keyword-only, default=False
33+
Whether to run in verbose mode.
34+
"""
35+
1636
def __init__(
17-
self, filename: str, converter: type[DocstringConverter] | None = None
37+
self,
38+
filename: str,
39+
converter: type[DocstringConverter] | None = None,
40+
*,
41+
verbose: bool = False,
1842
) -> None:
1943
self.source_file: Path = Path(filename).expanduser().resolve()
44+
"""The ``Path`` object for the source file."""
45+
2046
self.source_code: str = self.source_file.read_text()
47+
"""The source code in :attr:`.source_file` as a string."""
48+
2149
self.tree: ast.Module = ast.parse(self.source_code)
50+
"""The AST for the source code in :attr:`.source_code`."""
2251

2352
self.docstrings_inspected: int = 0
53+
"""The number of docstrings inspected."""
54+
2455
self.missing_docstrings: list[DocstringNode] = []
56+
"""The list of docstrings that are missing, each represented as a :class:`.DocstringNode` instance."""
2557

2658
self.module_name: str = self.source_file.stem
59+
"""The name of the module, derived from the source file name (see :attr:`.source_file`)."""
60+
2761
self.stack: list[DocstringNode] = []
62+
"""The stack of docstring nodes, used to track the current context in the AST."""
2863

2964
self.docstring_converter: DocstringConverter | None = (
3065
converter(quote=not issubclass(self.__class__, ast.NodeTransformer))
3166
if converter
3267
else None
3368
)
69+
"""The docstring converter, if templates are to be generated, otherwise ``None``."""
70+
71+
self.verbose: bool = verbose
72+
"""Whether to run in verbose mode."""
3473

3574
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:
75+
"""
76+
Report missing docstrings.
77+
78+
See Also
79+
--------
80+
:meth:`.handle_missing_docstring`
81+
This method is called for each missing docstring, and it defines any actions
82+
that should be taken upon nodes with missing docstrings, such as, suggesting
83+
a docstring template based on the source code.
84+
"""
85+
if self.missing_docstrings:
3986
for docstring_node in self.missing_docstrings:
4087
print(
4188
f'{docstring_node.fully_qualified_name} is missing a docstring',
4289
file=sys.stderr,
4390
)
4491
self.handle_missing_docstring(docstring_node)
92+
elif self.verbose:
93+
print(f'No missing docstrings found in {self.source_file}.')
94+
95+
def handle_missing_docstring(self, docstring_node: DocstringNode) -> None:
96+
"""
97+
Handle missing docstrings by suggesting a template for them when a converter is provided.
4598
46-
def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
99+
Parameters
100+
----------
101+
docstring_node : DocstringNode
102+
An instance of :class:`.DocstringNode`, which wraps an AST node and adds
103+
additional context relevant for Docstringify.
104+
"""
47105
if self.docstring_converter:
48106
print(
49107
'Hint:',
@@ -53,7 +111,23 @@ def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNo
53111
)
54112

55113
def process_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
56-
if docstring_node.docstring_required and not docstring_node.docstring:
114+
"""
115+
Process a docstring node, appending it to :attr:`.missing_docstrings` if a docstring
116+
is required, but there isn't one.
117+
118+
Parameters
119+
----------
120+
docstring_node : DocstringNode
121+
An instance of :class:`.DocstringNode`, which wraps an AST node and adds
122+
additional context relevant for Docstringify.
123+
124+
Returns
125+
-------
126+
DocstringNode
127+
An instance of :class:`.DocstringNode`, which wraps an AST node and adds
128+
additional context relevant for Docstringify.
129+
"""
130+
if docstring_node.docstring_required and (not docstring_node.docstring):
57131
self.missing_docstrings.append(docstring_node)
58132

59133
self.docstrings_inspected += 1
@@ -64,6 +138,21 @@ def visit_docstring(
64138
node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module,
65139
docstring_class: type[DocstringNode],
66140
) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
141+
"""
142+
Visit an AST node by updating the stack and processing the docstring.
143+
144+
Parameters
145+
----------
146+
node : ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
147+
The AST node to visit.
148+
docstring_class : type[DocstringNode]
149+
The class to use for creating the docstring node.
150+
151+
Returns
152+
-------
153+
ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
154+
The AST node that was visited.
155+
"""
67156
docstring_node = docstring_class(
68157
node,
69158
self.module_name,
@@ -79,24 +168,90 @@ def visit_docstring(
79168
return docstring_node.ast_node
80169

81170
def visit_Module(self, node: ast.Module) -> ast.Module: # noqa: N802
171+
"""
172+
Visit an :class:`ast.Module` node.
173+
174+
Parameters
175+
----------
176+
node : ast.Module
177+
The AST node to visit.
178+
179+
Returns
180+
-------
181+
ast.Module
182+
The visited AST node.
183+
"""
82184
return self.visit_docstring(node, DocstringNode)
83185

84186
def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: # noqa: N802
187+
"""
188+
Visit an :class:`ast.ClassDef` node.
189+
190+
Parameters
191+
----------
192+
node : ast.ClassDef
193+
The AST node to visit.
194+
195+
Returns
196+
-------
197+
ast.ClassDef
198+
The visited AST node.
199+
"""
85200
return self.visit_docstring(node, DocstringNode)
86201

87202
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: # noqa: N802
203+
"""
204+
Visit an :class:`ast.FunctionDef` node.
205+
206+
Parameters
207+
----------
208+
node : ast.FunctionDef
209+
The AST node to visit.
210+
211+
Returns
212+
-------
213+
ast.FunctionDef
214+
The visited AST node.
215+
"""
88216
return self.visit_docstring(node, FunctionDocstringNode)
89217

90218
def visit_AsyncFunctionDef( # noqa: N802
91219
self, node: ast.AsyncFunctionDef
92220
) -> ast.AsyncFunctionDef:
221+
"""
222+
Visit an :class:`ast.AsyncFunctionDef` node.
223+
224+
Parameters
225+
----------
226+
node : ast.AsyncFunctionDef
227+
The AST node to visit.
228+
229+
Returns
230+
-------
231+
ast.AsyncFunctionDef
232+
The visited AST node.
233+
"""
93234
return self.visit_docstring(node, FunctionDocstringNode)
94235

95236
def visit_Return(self, node: ast.Return) -> ast.Return: # noqa: N802
237+
"""
238+
Visit an :class:`ast.Return` node.
239+
240+
Parameters
241+
----------
242+
node : ast.Return
243+
The AST node to visit.
244+
245+
Returns
246+
-------
247+
ast.Return
248+
The visited AST node.
249+
"""
96250
if isinstance(self.stack[-1], FunctionDocstringNode):
97251
self.stack[-1].return_statements.append(node)
98252
return node
99253

100254
def process_file(self) -> None:
255+
"""Process a source code file, reporting on the missing docstrings."""
101256
self.visit(self.tree)
102257
self.report_missing_docstrings()

0 commit comments

Comments
 (0)