docstringify 0.1.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {docstringify-0.1.1 → docstringify-0.3.0}/PKG-INFO +36 -10
  2. {docstringify-0.1.1 → docstringify-0.3.0}/README.md +34 -8
  3. {docstringify-0.1.1 → docstringify-0.3.0}/pyproject.toml +1 -0
  4. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify/__init__.py +1 -1
  5. docstringify-0.3.0/src/docstringify/cli.py +96 -0
  6. docstringify-0.3.0/src/docstringify/converters/__init__.py +11 -0
  7. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify/converters/base.py +25 -3
  8. docstringify-0.3.0/src/docstringify/converters/google.py +44 -0
  9. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify/converters/numpydoc.py +8 -10
  10. docstringify-0.1.1/src/docstringify/visitor.py → docstringify-0.3.0/src/docstringify/docstring_generator.py +16 -55
  11. docstringify-0.3.0/src/docstringify/exceptions.py +6 -0
  12. docstringify-0.3.0/src/docstringify/transformer.py +53 -0
  13. docstringify-0.3.0/src/docstringify/visitor.py +79 -0
  14. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify.egg-info/PKG-INFO +36 -10
  15. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify.egg-info/SOURCES.txt +4 -0
  16. docstringify-0.1.1/src/docstringify/cli.py +0 -71
  17. docstringify-0.1.1/src/docstringify/converters/__init__.py +0 -1
  18. {docstringify-0.1.1 → docstringify-0.3.0}/.github/dependabot.yml +0 -0
  19. {docstringify-0.1.1 → docstringify-0.3.0}/.github/release.yml +0 -0
  20. {docstringify-0.1.1 → docstringify-0.3.0}/.github/workflows/ci.yml +0 -0
  21. {docstringify-0.1.1 → docstringify-0.3.0}/.github/workflows/pypi-publish.yml +0 -0
  22. {docstringify-0.1.1 → docstringify-0.3.0}/.gitignore +0 -0
  23. {docstringify-0.1.1 → docstringify-0.3.0}/.pre-commit-config.yaml +0 -0
  24. {docstringify-0.1.1 → docstringify-0.3.0}/.pre-commit-hooks.yaml +0 -0
  25. {docstringify-0.1.1 → docstringify-0.3.0}/LICENSE +0 -0
  26. {docstringify-0.1.1 → docstringify-0.3.0}/setup.cfg +0 -0
  27. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify/__main__.py +0 -0
  28. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify/components.py +0 -0
  29. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify.egg-info/dependency_links.txt +0 -0
  30. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify.egg-info/entry_points.txt +0 -0
  31. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify.egg-info/requires.txt +0 -0
  32. {docstringify-0.1.1 → docstringify-0.3.0}/src/docstringify.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstringify
3
- Version: 0.1.1
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
@@ -28,7 +28,7 @@ License: MIT License
28
28
  Project-URL: Documentation, https://github.com/stefmolin/docstringify
29
29
  Project-URL: Homepage, https://github.com/stefmolin/docstringify
30
30
  Project-URL: Source, https://github.com/stefmolin/docstringify
31
- Keywords: ast,docstring generation,numpydoc-style docstring,pre-commit
31
+ Keywords: ast,docstring generation,google-style docstring,numpydoc-style docstring,pre-commit
32
32
  Classifier: Development Status :: 3 - Alpha
33
33
  Classifier: License :: OSI Approved :: MIT License
34
34
  Classifier: Programming Language :: Python
@@ -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.1.1
63
+ rev: 0.3.0
64
64
  hooks:
65
65
  - id: docstringify
66
66
  ```
@@ -69,22 +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.1.1
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#); support for Google-style docstrings will come in a future release:
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.1.1
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
+ 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
+
90
+ ```yaml
91
+ - repo: https://github.com/stefmolin/docstringify
92
+ rev: 0.3.0
93
+ hooks:
94
+ - id: docstringify
95
+ args: [--make-changes=google]
96
+ ```
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
+
88
100
  Be sure to check out the [pre-commit documentation](https://pre-commit.com/#pre-commit-configyaml---hooks) for additional configuration options.
89
101
 
90
102
  ### Command line
@@ -124,10 +136,10 @@ test.say_hello is missing a docstring
124
136
  If you would like to see suggested docstring templates (inferred from type annotations for functions and methods), provide a converter:
125
137
 
126
138
  ```pycon
127
- >>> from docstringify.converters.numpydoc import NumpydocDocstringConverter
139
+ >>> from docstringify.converters import NumpydocDocstringConverter
128
140
  >>> from docstringify.visitor import DocstringVisitor
129
- >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter())
130
- >>> visitor.process_file()
141
+ >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
142
+ >>> _ = visitor.process_file()
131
143
  test is missing a docstring
132
144
  Hint:
133
145
  """__description__"""
@@ -144,3 +156,17 @@ name : str, default="World"
144
156
  """
145
157
 
146
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()`.
@@ -9,7 +9,7 @@ Add the following to your `.pre-commit-config.yaml` file to block commits with m
9
9
 
10
10
  ```yaml
11
11
  - repo: https://github.com/stefmolin/docstringify
12
- rev: 0.1.1
12
+ rev: 0.3.0
13
13
  hooks:
14
14
  - id: docstringify
15
15
  ```
@@ -18,22 +18,34 @@ By default, all docstrings are required. If you want to be more lenient, you can
18
18
 
19
19
  ```yaml
20
20
  - repo: https://github.com/stefmolin/docstringify
21
- rev: 0.1.1
21
+ rev: 0.3.0
22
22
  hooks:
23
23
  - id: docstringify
24
24
  args: [--threshold=0.75]
25
25
  ```
26
26
 
27
- 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#); support for Google-style docstrings will come in a future release:
27
+ 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:
28
28
 
29
29
  ```yaml
30
30
  - repo: https://github.com/stefmolin/docstringify
31
- rev: 0.1.1
31
+ rev: 0.3.0
32
32
  hooks:
33
33
  - id: docstringify
34
- args: [--suggest-changes]
34
+ args: [--suggest-changes=numpydoc]
35
35
  ```
36
36
 
37
+ 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):
38
+
39
+ ```yaml
40
+ - repo: https://github.com/stefmolin/docstringify
41
+ rev: 0.3.0
42
+ hooks:
43
+ - id: docstringify
44
+ args: [--make-changes=google]
45
+ ```
46
+
47
+ 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.*).
48
+
37
49
  Be sure to check out the [pre-commit documentation](https://pre-commit.com/#pre-commit-configyaml---hooks) for additional configuration options.
38
50
 
39
51
  ### Command line
@@ -73,10 +85,10 @@ test.say_hello is missing a docstring
73
85
  If you would like to see suggested docstring templates (inferred from type annotations for functions and methods), provide a converter:
74
86
 
75
87
  ```pycon
76
- >>> from docstringify.converters.numpydoc import NumpydocDocstringConverter
88
+ >>> from docstringify.converters import NumpydocDocstringConverter
77
89
  >>> from docstringify.visitor import DocstringVisitor
78
- >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter())
79
- >>> visitor.process_file()
90
+ >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
91
+ >>> _ = visitor.process_file()
80
92
  test is missing a docstring
81
93
  Hint:
82
94
  """__description__"""
@@ -93,3 +105,17 @@ name : str, default="World"
93
105
  """
94
106
 
95
107
  ```
108
+
109
+ To make changes to your files, you will need to use the `DocstringTransformer` instead. With the `DocstringTransformer`, the converter is required:
110
+
111
+ ```pycon
112
+ >>> from docstringify.converters import GoogleDocstringConverter
113
+ >>> from docstringify.transformer import DocstringTransformer
114
+ >>> transformer = DocstringTransformer('test.py', converter=GoogleDocstringConverter)
115
+ >>> _ = transformer.process_file()
116
+ test is missing a docstring
117
+ test.say_hello is missing a docstring
118
+ Docstring templates written to /.../test_docstringify.py
119
+ ```
120
+
121
+ If you want to overwrite the file with the edits, pass `overwrite=True` to `DocstringTransformer()`.
@@ -13,6 +13,7 @@ readme = "README.md"
13
13
  keywords = [
14
14
  "ast",
15
15
  "docstring generation",
16
+ "google-style docstring",
16
17
  "numpydoc-style docstring",
17
18
  "pre-commit",
18
19
  ]
@@ -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.1.1'
6
+ __version__ = '0.3.0'
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from functools import partial
6
+ from typing import Sequence
7
+
8
+ from . import __version__
9
+ from .converters import GoogleDocstringConverter, NumpydocDocstringConverter
10
+ from .transformer import DocstringTransformer
11
+ from .visitor import DocstringVisitor
12
+
13
+ PROG = __package__
14
+
15
+ STYLES = {
16
+ 'google': GoogleDocstringConverter,
17
+ 'numpydoc': NumpydocDocstringConverter,
18
+ }
19
+ CLI_DEFAULTS = {'threshold': 1.0}
20
+
21
+
22
+ def main(argv: Sequence[str] | None = None) -> int:
23
+ parser = argparse.ArgumentParser(prog=PROG)
24
+ parser.add_argument(
25
+ 'filenames',
26
+ nargs='*',
27
+ help='Filenames to process',
28
+ )
29
+ parser.add_argument(
30
+ '--version', action='version', version=f'%(prog)s {__version__}'
31
+ )
32
+
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(
49
+ '--suggest-changes',
50
+ choices=STYLES.keys(),
51
+ help='Whether to print out docstring templates for items missing docstrings',
52
+ )
53
+ run_group.add_argument(
54
+ '--threshold',
55
+ type=float,
56
+ default=CLI_DEFAULTS['threshold'],
57
+ help='The percentage of docstrings that must be present to pass',
58
+ )
59
+ args = parser.parse_args(argv)
60
+
61
+ if style := args.make_changes or args.make_changes_inplace or args.suggest_changes:
62
+ converter = STYLES[style]
63
+ else:
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
+ )
75
+
76
+ docstrings_processed = missing_docstrings = 0
77
+ for file in args.filenames:
78
+ processor = get_docstring_processor(file)
79
+ processor.process_file()
80
+ missing_docstrings += processor.missing_docstrings
81
+ docstrings_processed += processor.docstrings_inspected
82
+
83
+ if docstrings_processed and (
84
+ missing_percentage := (missing_docstrings / docstrings_processed)
85
+ ) > (1 - args.threshold):
86
+ print(f'\nMissing {missing_percentage:.0%} of docstrings', file=sys.stderr)
87
+ print(
88
+ f'Your settings require {args.threshold:.0%} of docstrings to be present',
89
+ file=sys.stderr,
90
+ )
91
+ return 1
92
+ return 0
93
+
94
+
95
+ if __name__ == '__main__':
96
+ raise SystemExit(main())
@@ -0,0 +1,11 @@
1
+ """Docstring converters."""
2
+
3
+ from .base import DocstringConverter
4
+ from .google import GoogleDocstringConverter
5
+ from .numpydoc import NumpydocDocstringConverter
6
+
7
+ __all__ = [
8
+ 'DocstringConverter',
9
+ 'GoogleDocstringConverter',
10
+ 'NumpydocDocstringConverter',
11
+ ]
@@ -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)
@@ -0,0 +1,44 @@
1
+ """Google-style docstring converter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..components import DESCRIPTION_PLACEHOLDER, NO_DEFAULT, Function, Parameter
6
+ from .base import DocstringConverter
7
+
8
+
9
+ class GoogleDocstringConverter(DocstringConverter):
10
+ def __init__(self, quote: bool) -> None:
11
+ super().__init__(
12
+ parameters_section_template='Args:\n{parameters}',
13
+ returns_section_template='Returns:\n {returns}',
14
+ quote=quote,
15
+ )
16
+
17
+ def to_function_docstring(self, function: Function, indent: int) -> str:
18
+ docstring = [DESCRIPTION_PLACEHOLDER]
19
+
20
+ if parameters_section := self.parameters_section(function.parameters):
21
+ docstring.extend(['', parameters_section])
22
+
23
+ if returns_section := self.returns_section(function.return_type):
24
+ docstring.extend(['', returns_section])
25
+
26
+ return self.quote_docstring(docstring, indent=indent)
27
+
28
+ def format_parameter(self, parameter: Parameter) -> str:
29
+ category = f'{f", {parameter.category}" if parameter.category else ""}'
30
+ return (
31
+ f' {parameter.name} ({parameter.type_}{category}): {DESCRIPTION_PLACEHOLDER}'
32
+ f'{f" Defaults to {parameter.default}." if parameter.default != NO_DEFAULT else ""}'
33
+ )
34
+
35
+ def format_return(self, return_type: str | None) -> str:
36
+ if return_type:
37
+ return f'{return_type}: {DESCRIPTION_PLACEHOLDER}'
38
+ return ''
39
+
40
+ def to_module_docstring(self, module_name: str) -> str:
41
+ return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
42
+
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)
@@ -1,8 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
- import sys
5
- from pathlib import Path
6
4
  from typing import TYPE_CHECKING, Literal
7
5
 
8
6
  from .components import (
@@ -14,25 +12,20 @@ from .components import (
14
12
  )
15
13
 
16
14
  if TYPE_CHECKING:
17
- from .converters.base import DocstringConverter
15
+ from .converters import DocstringConverter
18
16
 
19
17
 
20
- class DocstringVisitor(ast.NodeVisitor):
18
+ class DocstringGenerator:
21
19
  def __init__(
22
- self, filename: str, converter: DocstringConverter | None = None
20
+ self,
21
+ converter: type[DocstringConverter],
22
+ module_name: str,
23
+ source_code: str,
24
+ quote: bool,
23
25
  ) -> 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
26
+ self.converter = converter(quote=quote)
27
+ self.module_name = module_name
28
+ self.source_code = source_code
36
29
 
37
30
  def _extract_default_values(
38
31
  self, default: ast.Constant | None | Literal[NO_DEFAULT], is_keyword_only: bool
@@ -147,50 +140,18 @@ class DocstringVisitor(ast.NodeVisitor):
147
140
  return RETURN_TYPE_PLACEHOLDER
148
141
  return return_node
149
142
 
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
143
  def suggest_docstring(
155
- self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
144
+ self,
145
+ node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module,
146
+ indent: int = 0,
156
147
  ) -> str:
157
148
  if isinstance(node, ast.AsyncFunctionDef | ast.FunctionDef):
158
149
  return self.converter.to_function_docstring(
159
- Function(self.extract_arguments(node), self.extract_returns(node))
150
+ Function(self.extract_arguments(node), self.extract_returns(node)),
151
+ indent=indent,
160
152
  )
161
153
 
162
154
  if isinstance(node, ast.Module):
163
155
  return self.converter.to_module_docstring(self.module_name)
164
156
 
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))
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
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ from .docstring_generator import DocstringGenerator
9
+
10
+ if TYPE_CHECKING:
11
+ from .converters import DocstringConverter
12
+
13
+
14
+ class DocstringVisitor(ast.NodeVisitor):
15
+ def __init__(
16
+ self, filename: str, converter: DocstringConverter | None = None
17
+ ) -> None:
18
+ self.source_file: Path = Path(filename).expanduser().resolve()
19
+ self.source_code: str = self.source_file.read_text()
20
+
21
+ self.docstrings_inspected: int = 0
22
+ self.missing_docstrings: int = 0
23
+
24
+ self.module_name: str = self.source_file.stem
25
+ self.stack: list[str] = []
26
+
27
+ self.provide_hints: bool = converter is not None
28
+ if self.provide_hints:
29
+ self.docstring_generator = DocstringGenerator(
30
+ converter,
31
+ self.module_name,
32
+ self.source_code,
33
+ quote=not issubclass(self.__class__, ast.NodeTransformer),
34
+ )
35
+
36
+ def report_missing_docstring(self) -> None:
37
+ self.missing_docstrings += 1
38
+ print(f'{".".join(self.stack)} is missing a docstring', file=sys.stderr)
39
+
40
+ def handle_missing_docstring(
41
+ self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
42
+ ) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
43
+ if self.provide_hints:
44
+ print('Hint:')
45
+ print(self.docstring_generator.suggest_docstring(node))
46
+ print()
47
+ return node
48
+
49
+ def process_docstring(
50
+ self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
51
+ ) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
52
+ if not ast.get_docstring(node):
53
+ self.report_missing_docstring()
54
+ node = self.handle_missing_docstring(node)
55
+
56
+ self.docstrings_inspected += 1
57
+ return node
58
+
59
+ def visit(self, node: ast.AST) -> ast.AST:
60
+ if isinstance(
61
+ node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
62
+ ):
63
+ self.stack.append(
64
+ self.module_name if isinstance(node, ast.Module) else node.name
65
+ )
66
+
67
+ node = self.process_docstring(node)
68
+
69
+ self.generic_visit(node)
70
+ _ = self.stack.pop()
71
+ return node
72
+
73
+ def process_file(self) -> ast.Module:
74
+ root = self.visit(ast.parse(self.source_code))
75
+
76
+ if not self.missing_docstrings:
77
+ print(f'No missing docstrings found in {self.source_file}.')
78
+
79
+ return root
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstringify
3
- Version: 0.1.1
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
@@ -28,7 +28,7 @@ License: MIT License
28
28
  Project-URL: Documentation, https://github.com/stefmolin/docstringify
29
29
  Project-URL: Homepage, https://github.com/stefmolin/docstringify
30
30
  Project-URL: Source, https://github.com/stefmolin/docstringify
31
- Keywords: ast,docstring generation,numpydoc-style docstring,pre-commit
31
+ Keywords: ast,docstring generation,google-style docstring,numpydoc-style docstring,pre-commit
32
32
  Classifier: Development Status :: 3 - Alpha
33
33
  Classifier: License :: OSI Approved :: MIT License
34
34
  Classifier: Programming Language :: Python
@@ -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.1.1
63
+ rev: 0.3.0
64
64
  hooks:
65
65
  - id: docstringify
66
66
  ```
@@ -69,22 +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.1.1
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#); support for Google-style docstrings will come in a future release:
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.1.1
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
+ 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
+
90
+ ```yaml
91
+ - repo: https://github.com/stefmolin/docstringify
92
+ rev: 0.3.0
93
+ hooks:
94
+ - id: docstringify
95
+ args: [--make-changes=google]
96
+ ```
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
+
88
100
  Be sure to check out the [pre-commit documentation](https://pre-commit.com/#pre-commit-configyaml---hooks) for additional configuration options.
89
101
 
90
102
  ### Command line
@@ -124,10 +136,10 @@ test.say_hello is missing a docstring
124
136
  If you would like to see suggested docstring templates (inferred from type annotations for functions and methods), provide a converter:
125
137
 
126
138
  ```pycon
127
- >>> from docstringify.converters.numpydoc import NumpydocDocstringConverter
139
+ >>> from docstringify.converters import NumpydocDocstringConverter
128
140
  >>> from docstringify.visitor import DocstringVisitor
129
- >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter())
130
- >>> visitor.process_file()
141
+ >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
142
+ >>> _ = visitor.process_file()
131
143
  test is missing a docstring
132
144
  Hint:
133
145
  """__description__"""
@@ -144,3 +156,17 @@ name : str, default="World"
144
156
  """
145
157
 
146
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()`.
@@ -12,6 +12,9 @@ src/docstringify/__init__.py
12
12
  src/docstringify/__main__.py
13
13
  src/docstringify/cli.py
14
14
  src/docstringify/components.py
15
+ src/docstringify/docstring_generator.py
16
+ src/docstringify/exceptions.py
17
+ src/docstringify/transformer.py
15
18
  src/docstringify/visitor.py
16
19
  src/docstringify.egg-info/PKG-INFO
17
20
  src/docstringify.egg-info/SOURCES.txt
@@ -21,4 +24,5 @@ src/docstringify.egg-info/requires.txt
21
24
  src/docstringify.egg-info/top_level.txt
22
25
  src/docstringify/converters/__init__.py
23
26
  src/docstringify/converters/base.py
27
+ src/docstringify/converters/google.py
24
28
  src/docstringify/converters/numpydoc.py
@@ -1,71 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import sys
5
- from typing import Sequence
6
-
7
- from . import __version__
8
- from .converters.numpydoc import NumpydocDocstringConverter
9
- from .visitor import DocstringVisitor
10
-
11
- PROG = __package__
12
-
13
-
14
- def main(argv: Sequence[str] | None = None) -> int:
15
- parser = argparse.ArgumentParser(prog=PROG)
16
- parser.add_argument(
17
- 'filenames',
18
- nargs='*',
19
- help='Filenames to process',
20
- )
21
- parser.add_argument(
22
- '--version', action='version', version=f'%(prog)s {__version__}'
23
- )
24
-
25
- parser.add_argument(
26
- '--suggest-changes',
27
- action='store_true',
28
- help='Whether to provide docstring templates for items missing docstrings',
29
- )
30
- parser.add_argument(
31
- '--style',
32
- choices=['numpydoc'],
33
- default='numpydoc',
34
- help='The style of docstring to use (only used when --suggest-changes is passed)',
35
- )
36
- parser.add_argument(
37
- '--threshold',
38
- type=float,
39
- default=1,
40
- help='The percentage of docstrings that must be present to pass',
41
- )
42
- args = parser.parse_args(argv)
43
-
44
- if args.style is None or not args.suggest_changes:
45
- converter = None
46
- elif args.style == 'numpydoc':
47
- converter = NumpydocDocstringConverter()
48
- else:
49
- raise NotImplementedError(f'{args.style} is not a supported option')
50
-
51
- docstrings_processed = missing_docstrings = 0
52
- for file in args.filenames:
53
- visitor = DocstringVisitor(file, converter=converter)
54
- visitor.process_file()
55
- missing_docstrings += visitor.missing_docstrings
56
- docstrings_processed += visitor.docstrings_inspected
57
-
58
- if docstrings_processed and (
59
- missing_percentage := (missing_docstrings / docstrings_processed)
60
- ) > (1 - args.threshold):
61
- print(f'Missing {missing_percentage:.0%} of docstrings', file=sys.stderr)
62
- print(
63
- f'Your settings require {args.threshold:.0%} of docstrings to be present',
64
- file=sys.stderr,
65
- )
66
- return 1
67
- return 0
68
-
69
-
70
- if __name__ == '__main__':
71
- raise SystemExit(main())
@@ -1 +0,0 @@
1
- """Docstring converters."""
File without changes
File without changes
File without changes