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 CHANGED
@@ -3,4 +3,4 @@ Flag missing docstrings and, optionally, generate them from signatures and
3
3
  type annotations.
4
4
  """
5
5
 
6
- __version__ = '0.2.0'
6
+ __version__ = '0.3.0'
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.add_argument(
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
- action='store_true',
50
+ choices=STYLES.keys(),
28
51
  help='Whether to print out docstring templates for items missing docstrings',
29
52
  )
30
- parser.add_argument(
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=1,
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.style is None or not args.suggest_changes:
45
- converter = None
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
- raise NotImplementedError(f'{args.style} is not a supported option')
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
- visitor = DocstringVisitor(file, converter=converter)
56
- visitor.process_file()
57
- missing_docstrings += visitor.missing_docstrings
58
- docstrings_processed += visitor.docstrings_inspected
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'Missing {missing_percentage:.0%} of docstrings', file=sys.stderr)
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,
@@ -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, parameters_section_template: str, returns_section_template: str
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
- # TODO: the visitor needs the triple quotes but the transformer does not
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
- sep = '' if len(docstring) == 1 else '\n'
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 f'"""{DESCRIPTION_PLACEHOLDER}"""'
41
+ return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
44
42
 
45
- def to_class_docstring(self, class_name: str) -> str:
46
- return f'"""{DESCRIPTION_PLACEHOLDER}"""'
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
- # TODO: the visitor needs the triple quotes but the transformer does not
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
- sep = '' if len(docstring) == 2 else '\n'
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 f'"""{DESCRIPTION_PLACEHOLDER}"""'
42
+ return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
45
43
 
46
- def to_class_docstring(self, class_name: str) -> str:
47
- return f'"""{DESCRIPTION_PLACEHOLDER}"""'
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,6 @@
1
+ """Docstringify exceptions."""
2
+
3
+
4
+ class InvalidDocstringError(ValueError):
5
+ def __init__(self, docstring_class: str) -> None:
6
+ super().__init__(f'Expected str or list[str] docstring, got {docstring_class}')
@@ -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, Literal
6
+ from typing import TYPE_CHECKING
7
7
 
8
- from .components import (
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.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
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 suggest_docstring(
40
+ def handle_missing_docstring(
155
41
  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)
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
- ) -> None:
51
+ ) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
173
52
  if not ast.get_docstring(node):
174
53
  self.report_missing_docstring()
175
- if self.provide_hints:
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) -> None:
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
- def process_file(self) -> None:
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.2.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.2.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.2.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. By default, these will be [numpydoc-style docstrings](https://numpydoc.readthedocs.io/en/latest/format.html#):
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.2.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
- For [Google-style docstrings](https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html), specify `--style google`:
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.2.0
92
+ rev: 0.3.0
93
93
  hooks:
94
94
  - id: docstringify
95
- args: [--suggest-changes, --style=google]
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` &ndash; 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,,