docstringify 0.6.0__tar.gz → 1.0.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 (41) hide show
  1. {docstringify-0.6.0 → docstringify-1.0.0}/.pre-commit-config.yaml +4 -4
  2. {docstringify-0.6.0 → docstringify-1.0.0}/PKG-INFO +30 -10
  3. {docstringify-0.6.0 → docstringify-1.0.0}/README.md +28 -8
  4. {docstringify-0.6.0 → docstringify-1.0.0}/pyproject.toml +1 -1
  5. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/__init__.py +1 -1
  6. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/cli.py +23 -12
  7. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/converters/__init__.py +2 -0
  8. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/converters/base.py +15 -8
  9. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/converters/google.py +1 -1
  10. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/converters/numpydoc.py +1 -1
  11. docstringify-1.0.0/src/docstringify/converters/stub.py +121 -0
  12. docstringify-1.0.0/src/docstringify/exceptions.py +25 -0
  13. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/nodes/base.py +6 -4
  14. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/nodes/function.py +54 -20
  15. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/traversal/transformer.py +81 -14
  16. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/traversal/visitor.py +37 -9
  17. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify.egg-info/PKG-INFO +30 -10
  18. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify.egg-info/SOURCES.txt +1 -0
  19. docstringify-0.6.0/src/docstringify/exceptions.py +0 -6
  20. {docstringify-0.6.0 → docstringify-1.0.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  21. {docstringify-0.6.0 → docstringify-1.0.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  22. {docstringify-0.6.0 → docstringify-1.0.0}/.github/dependabot.yml +0 -0
  23. {docstringify-0.6.0 → docstringify-1.0.0}/.github/pull_request_template.md +0 -0
  24. {docstringify-0.6.0 → docstringify-1.0.0}/.github/release.yml +0 -0
  25. {docstringify-0.6.0 → docstringify-1.0.0}/.github/workflows/ci.yml +0 -0
  26. {docstringify-0.6.0 → docstringify-1.0.0}/.github/workflows/greetings.yml +0 -0
  27. {docstringify-0.6.0 → docstringify-1.0.0}/.github/workflows/pypi-publish.yml +0 -0
  28. {docstringify-0.6.0 → docstringify-1.0.0}/.gitignore +0 -0
  29. {docstringify-0.6.0 → docstringify-1.0.0}/.pre-commit-hooks.yaml +0 -0
  30. {docstringify-0.6.0 → docstringify-1.0.0}/CODE_OF_CONDUCT.md +0 -0
  31. {docstringify-0.6.0 → docstringify-1.0.0}/CONTRIBUTING.md +0 -0
  32. {docstringify-0.6.0 → docstringify-1.0.0}/LICENSE +0 -0
  33. {docstringify-0.6.0 → docstringify-1.0.0}/setup.cfg +0 -0
  34. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/__main__.py +0 -0
  35. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/components.py +0 -0
  36. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/nodes/__init__.py +0 -0
  37. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify/traversal/__init__.py +0 -0
  38. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify.egg-info/dependency_links.txt +0 -0
  39. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify.egg-info/entry_points.txt +0 -0
  40. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify.egg-info/requires.txt +0 -0
  41. {docstringify-0.6.0 → docstringify-1.0.0}/src/docstringify.egg-info/top_level.txt +0 -0
@@ -12,20 +12,20 @@ repos:
12
12
  - id: trailing-whitespace
13
13
 
14
14
  - repo: https://github.com/stefmolin/docstringify
15
- rev: 0.5.1
15
+ rev: 0.7.0
16
16
  hooks:
17
17
  - id: docstringify
18
18
  args: [--make-changes-inplace=numpydoc]
19
19
 
20
20
  - repo: https://github.com/astral-sh/ruff-pre-commit
21
- rev: v0.11.9
21
+ rev: v0.11.10
22
22
  hooks:
23
- - id: ruff
23
+ - id: ruff-check
24
24
  args: [--fix, --exit-non-zero-on-fix, --show-fixes]
25
25
  - id: ruff-format
26
26
 
27
27
  - repo: https://github.com/tox-dev/pyproject-fmt
28
- rev: v2.5.1
28
+ rev: v2.6.0
29
29
  hooks:
30
30
  - id: pyproject-fmt
31
31
  args: [--keep-full-version, --no-print-diff]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstringify
3
- Version: 0.6.0
3
+ Version: 1.0.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
@@ -29,7 +29,7 @@ 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
31
  Keywords: ast,docstring generation,google-style docstring,numpydoc-style docstring,pre-commit
32
- Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Development Status :: 4 - Beta
33
33
  Classifier: Programming Language :: Python
34
34
  Classifier: Programming Language :: Python :: 3 :: Only
35
35
  Classifier: Programming Language :: Python :: 3.9
@@ -103,11 +103,11 @@ You can use Docstringify in three modes:
103
103
 
104
104
  ### Pre-commit hook
105
105
 
106
- Add the following to your `.pre-commit-config.yaml` file to block commits with missing docstrings:
106
+ Add the following to your `.pre-commit-config.yaml` file to block commits with missing docstrings before any formatters like `ruff`:
107
107
 
108
108
  ```yaml
109
109
  - repo: https://github.com/stefmolin/docstringify
110
- rev: 0.6.0
110
+ rev: 1.0.0
111
111
  hooks:
112
112
  - id: docstringify
113
113
  ```
@@ -116,17 +116,17 @@ By default, all docstrings are required. If you want to be more lenient, you can
116
116
 
117
117
  ```yaml
118
118
  - repo: https://github.com/stefmolin/docstringify
119
- rev: 0.6.0
119
+ rev: 1.0.0
120
120
  hooks:
121
121
  - id: docstringify
122
122
  args: [--threshold=0.75]
123
123
  ```
124
124
 
125
- 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:
125
+ 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`, `numpydoc`, and `stub`). Here, we ask for stub suggestions (just single lines of `"""__description__"""`):
126
126
 
127
127
  ```yaml
128
128
  - repo: https://github.com/stefmolin/docstringify
129
- rev: 0.6.0
129
+ rev: 1.0.0
130
130
  hooks:
131
131
  - id: docstringify
132
132
  args: [--suggest-changes=numpydoc]
@@ -136,13 +136,21 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
136
136
 
137
137
  ```yaml
138
138
  - repo: https://github.com/stefmolin/docstringify
139
- rev: 0.6.0
139
+ rev: 1.0.0
140
140
  hooks:
141
141
  - id: docstringify
142
142
  args: [--make-changes=google]
143
143
  ```
144
144
 
145
- 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.*).
145
+ 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. Here, we ask for [numpydoc-style docstring](https://numpydoc.readthedocs.io/en/latest/format.html#) suggestions:
146
+
147
+ ```yaml
148
+ - repo: https://github.com/stefmolin/docstringify
149
+ rev: 1.0.0
150
+ hooks:
151
+ - id: docstringify
152
+ args: [--make-changes-inplace=numpydoc]
153
+ ```
146
154
 
147
155
  Be sure to check out the [pre-commit documentation](https://pre-commit.com/#pre-commit-configyaml---hooks) for additional configuration options.
148
156
 
@@ -216,7 +224,19 @@ test.say_hello is missing a docstring
216
224
  Docstring templates written to /.../test_docstringify.py
217
225
  ```
218
226
 
219
- If you want to overwrite the file with the edits, pass `overwrite=True` to `DocstringTransformer()`.
227
+ If you want to overwrite the file with the edits, pass `overwrite=True` to `DocstringTransformer()`:
228
+
229
+ ```pycon
230
+ >>> from docstringify.converters import GoogleDocstringConverter
231
+ >>> from docstringify.traversal import DocstringTransformer
232
+ >>> transformer = DocstringTransformer(
233
+ ... 'test.py', converter=GoogleDocstringConverter, overwrite=True
234
+ ... )
235
+ >>> transformer.process_file()
236
+ test is missing a docstring
237
+ test.say_hello is missing a docstring
238
+ Docstring templates written to /.../test.py
239
+ ```
220
240
 
221
241
  ## Contributing
222
242
 
@@ -54,11 +54,11 @@ You can use Docstringify in three modes:
54
54
 
55
55
  ### Pre-commit hook
56
56
 
57
- Add the following to your `.pre-commit-config.yaml` file to block commits with missing docstrings:
57
+ Add the following to your `.pre-commit-config.yaml` file to block commits with missing docstrings before any formatters like `ruff`:
58
58
 
59
59
  ```yaml
60
60
  - repo: https://github.com/stefmolin/docstringify
61
- rev: 0.6.0
61
+ rev: 1.0.0
62
62
  hooks:
63
63
  - id: docstringify
64
64
  ```
@@ -67,17 +67,17 @@ By default, all docstrings are required. If you want to be more lenient, you can
67
67
 
68
68
  ```yaml
69
69
  - repo: https://github.com/stefmolin/docstringify
70
- rev: 0.6.0
70
+ rev: 1.0.0
71
71
  hooks:
72
72
  - id: docstringify
73
73
  args: [--threshold=0.75]
74
74
  ```
75
75
 
76
- 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:
76
+ 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`, `numpydoc`, and `stub`). Here, we ask for stub suggestions (just single lines of `"""__description__"""`):
77
77
 
78
78
  ```yaml
79
79
  - repo: https://github.com/stefmolin/docstringify
80
- rev: 0.6.0
80
+ rev: 1.0.0
81
81
  hooks:
82
82
  - id: docstringify
83
83
  args: [--suggest-changes=numpydoc]
@@ -87,13 +87,21 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
87
87
 
88
88
  ```yaml
89
89
  - repo: https://github.com/stefmolin/docstringify
90
- rev: 0.6.0
90
+ rev: 1.0.0
91
91
  hooks:
92
92
  - id: docstringify
93
93
  args: [--make-changes=google]
94
94
  ```
95
95
 
96
- 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.*).
96
+ 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. Here, we ask for [numpydoc-style docstring](https://numpydoc.readthedocs.io/en/latest/format.html#) suggestions:
97
+
98
+ ```yaml
99
+ - repo: https://github.com/stefmolin/docstringify
100
+ rev: 1.0.0
101
+ hooks:
102
+ - id: docstringify
103
+ args: [--make-changes-inplace=numpydoc]
104
+ ```
97
105
 
98
106
  Be sure to check out the [pre-commit documentation](https://pre-commit.com/#pre-commit-configyaml---hooks) for additional configuration options.
99
107
 
@@ -167,7 +175,19 @@ test.say_hello is missing a docstring
167
175
  Docstring templates written to /.../test_docstringify.py
168
176
  ```
169
177
 
170
- If you want to overwrite the file with the edits, pass `overwrite=True` to `DocstringTransformer()`.
178
+ If you want to overwrite the file with the edits, pass `overwrite=True` to `DocstringTransformer()`:
179
+
180
+ ```pycon
181
+ >>> from docstringify.converters import GoogleDocstringConverter
182
+ >>> from docstringify.traversal import DocstringTransformer
183
+ >>> transformer = DocstringTransformer(
184
+ ... 'test.py', converter=GoogleDocstringConverter, overwrite=True
185
+ ... )
186
+ >>> transformer.process_file()
187
+ test is missing a docstring
188
+ test.say_hello is missing a docstring
189
+ Docstring templates written to /.../test.py
190
+ ```
171
191
 
172
192
  ## Contributing
173
193
 
@@ -23,7 +23,7 @@ authors = [
23
23
  ]
24
24
  requires-python = ">=3.9"
25
25
  classifiers = [
26
- "Development Status :: 3 - Alpha",
26
+ "Development Status :: 4 - Beta",
27
27
  "Programming Language :: Python",
28
28
  "Programming Language :: Python :: 3 :: Only",
29
29
  "Programming Language :: Python :: 3.9",
@@ -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.6.0'
6
+ __version__ = '1.0.0'
@@ -9,14 +9,28 @@ from typing import TYPE_CHECKING
9
9
 
10
10
  from . import __doc__ as pkg_description
11
11
  from . import __version__
12
- from .converters import GoogleDocstringConverter, NumpydocDocstringConverter
12
+ from .converters import (
13
+ GoogleDocstringConverter,
14
+ NumpydocDocstringConverter,
15
+ StubDocstringConverter,
16
+ )
13
17
  from .traversal import DocstringTransformer, DocstringVisitor
14
18
 
15
19
  if TYPE_CHECKING:
16
20
  from collections.abc import Sequence
17
21
 
22
+
18
23
  PROG = __package__
19
- STYLES = {'google': GoogleDocstringConverter, 'numpydoc': NumpydocDocstringConverter}
24
+ STYLES: dict[
25
+ str,
26
+ type[GoogleDocstringConverter]
27
+ | type[NumpydocDocstringConverter]
28
+ | type[StubDocstringConverter],
29
+ ] = {
30
+ 'google': GoogleDocstringConverter,
31
+ 'numpydoc': NumpydocDocstringConverter,
32
+ 'stub': StubDocstringConverter,
33
+ }
20
34
  CLI_DEFAULTS = {'threshold': 1.0}
21
35
 
22
36
 
@@ -74,22 +88,19 @@ def main(argv: Sequence[str] | None = None) -> int:
74
88
  )
75
89
  args = parser.parse_args(argv)
76
90
 
77
- if style := (
78
- args.make_changes or args.make_changes_inplace or args.suggest_changes
79
- ):
80
- converter = STYLES[style]
81
- else:
82
- converter = None
83
-
84
91
  get_docstring_processor = (
85
92
  partial(
86
93
  DocstringTransformer,
87
- converter=converter,
94
+ converter=STYLES[style],
88
95
  overwrite=bool(args.make_changes_inplace),
89
96
  verbose=args.verbose,
90
97
  )
91
- if args.make_changes or args.make_changes_inplace
92
- else partial(DocstringVisitor, converter=converter, verbose=args.verbose)
98
+ if (style := args.make_changes or args.make_changes_inplace)
99
+ else partial(
100
+ DocstringVisitor,
101
+ converter=STYLES.get(args.suggest_changes),
102
+ verbose=args.verbose,
103
+ )
93
104
  )
94
105
 
95
106
  docstrings_processed = missing_docstrings = 0
@@ -3,9 +3,11 @@
3
3
  from .base import DocstringConverter
4
4
  from .google import GoogleDocstringConverter
5
5
  from .numpydoc import NumpydocDocstringConverter
6
+ from .stub import StubDocstringConverter
6
7
 
7
8
  __all__ = [
8
9
  'DocstringConverter',
9
10
  'GoogleDocstringConverter',
10
11
  'NumpydocDocstringConverter',
12
+ 'StubDocstringConverter',
11
13
  ]
@@ -8,11 +8,11 @@ from abc import ABC, abstractmethod
8
8
  from typing import TYPE_CHECKING
9
9
 
10
10
  from ..exceptions import InvalidDocstringError
11
+ from ..nodes.function import FunctionDocstringNode
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from ..components import Parameter
14
15
  from ..nodes.base import DocstringNode
15
- from ..nodes.function import FunctionDocstringNode
16
16
 
17
17
 
18
18
  class DocstringConverter(ABC):
@@ -184,7 +184,9 @@ class DocstringConverter(ABC):
184
184
  return self._returns_section_template.format(returns=return_text)
185
185
  return ''
186
186
 
187
- def format_docstring(self, docstring: str | list[str], indent: int) -> str:
187
+ def format_docstring(
188
+ self, docstring: str | list[str], indent: int, quote: bool | None = None
189
+ ) -> str:
188
190
  """
189
191
  Format the docstring with the requested level of indentation and surrounding
190
192
  triple quotes, if requested.
@@ -195,23 +197,28 @@ class DocstringConverter(ABC):
195
197
  The docstring as a string or list of strings that form lines in the docstring.
196
198
  indent : int
197
199
  The number of spaces by which to indent the docstring.
200
+ quote : bool | None, default=None
201
+ Whether to surround the docstring in triple quotes, passing this overrides
202
+ the ``quote`` setting specified upon initialization.
198
203
 
199
204
  Returns
200
205
  -------
201
206
  str
202
207
  The docstring indented to ``indent`` spaces will be returned either as a
203
- quoted docstring if the converter was initialized with ``quote=True``, or an
204
- unquoted docstring otherwise.
208
+ quoted docstring if the converter was initialized with ``quote=True`` or
209
+ ``quote=True`` was passed when calling this method, or an unquoted
210
+ docstring otherwise.
205
211
  """
206
- quote = '"""' if self._quote else ''
212
+ quote_char = '"""' if quote or self._quote else ''
207
213
  prefix = sep = ''
214
+
208
215
  if isinstance(docstring, str):
209
- docstring = [quote, docstring, quote]
216
+ docstring = [quote_char, docstring, quote_char]
210
217
  elif isinstance(docstring, list):
211
218
  if len(docstring) > 1:
212
219
  prefix = ' ' * indent if indent else ''
213
220
  sep = '\n'
214
- docstring = [quote, *docstring, f'{prefix}{quote}']
221
+ docstring = [quote_char, *docstring, quote_char or f'{prefix}']
215
222
  else:
216
223
  raise InvalidDocstringError(type(docstring).__name__)
217
224
 
@@ -238,7 +245,7 @@ class DocstringConverter(ABC):
238
245
  str | None
239
246
  The suggested docstring.
240
247
  """
241
- if isinstance(docstring_node.ast_node, ast.AsyncFunctionDef | ast.FunctionDef):
248
+ if isinstance(docstring_node, FunctionDocstringNode):
242
249
  return (
243
250
  self.to_function_docstring(docstring_node, indent=indent)
244
251
  if docstring_node.docstring_required
@@ -81,7 +81,7 @@ class GoogleDocstringConverter(DocstringConverter):
81
81
  category = f'{f", {parameter.category}" if parameter.category else ""}'
82
82
  return (
83
83
  f' {parameter.name} ({parameter.type_}{category}): {DESCRIPTION_PLACEHOLDER}'
84
- f'{f" Defaults to {parameter.default}." if parameter.default != NO_DEFAULT else ""}'
84
+ f'{f" Defaults to {parameter.default}." if parameter.default is not NO_DEFAULT else ""}'
85
85
  )
86
86
 
87
87
  def format_return(self, return_type: str | None) -> str:
@@ -81,7 +81,7 @@ class NumpydocDocstringConverter(DocstringConverter):
81
81
  return (
82
82
  f'{parameter.name} : {parameter.type_}'
83
83
  f'{f", {parameter.category}" if parameter.category else ""}'
84
- f'{f", default={parameter.default}" if parameter.default != NO_DEFAULT else ""}'
84
+ f'{f", default={parameter.default}" if parameter.default is not NO_DEFAULT else ""}'
85
85
  f'\n {DESCRIPTION_PLACEHOLDER}'
86
86
  )
87
87
 
@@ -0,0 +1,121 @@
1
+ """Stub docstring converter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..components import DESCRIPTION_PLACEHOLDER, Parameter
8
+ from .base import DocstringConverter
9
+
10
+ if TYPE_CHECKING:
11
+ from ..nodes.base import DocstringNode
12
+ from ..nodes.function import FunctionDocstringNode
13
+
14
+
15
+ class StubDocstringConverter(DocstringConverter):
16
+ """
17
+ Class defining the DocstringConverter API for injecting ``__description__`` stubs only.
18
+
19
+ Parameters
20
+ ----------
21
+ quote : bool
22
+ Whether to surround the generated docstrings in triple quotes.
23
+ """
24
+
25
+ def __init__(self, quote: bool) -> None:
26
+ super().__init__(
27
+ parameters_section_template='', returns_section_template='', quote=quote
28
+ )
29
+
30
+ def to_function_docstring(
31
+ self, docstring_node: FunctionDocstringNode, indent: int
32
+ ) -> str:
33
+ """
34
+ Convert an AST node into a function docstring.
35
+
36
+ Parameters
37
+ ----------
38
+ docstring_node : FunctionDocstringNode
39
+ An instance of :class:`.FunctionDocstringNode`, which wraps an instance of
40
+ :class:`ast.FunctionDef` or :class:`ast.AsyncFunctionDef`, adding additional
41
+ context relevant for Docstringify.
42
+ indent : int
43
+ The number of spaces by which to indent the docstring.
44
+
45
+ Returns
46
+ -------
47
+ str
48
+ The function docstring.
49
+ """
50
+ return self.format_docstring(DESCRIPTION_PLACEHOLDER, indent=indent)
51
+
52
+ def format_parameter(self, parameter: Parameter) -> str:
53
+ """
54
+ Convert a :class:`.Parameter` instance into an entry in the parameters section
55
+ of the docstring.
56
+
57
+ Parameters
58
+ ----------
59
+ parameter : Parameter
60
+ Information on the function parameter, including its name, type, and default
61
+ value (if it has one).
62
+
63
+ Returns
64
+ -------
65
+ str
66
+ An empty string.
67
+ """
68
+ return ''
69
+
70
+ def format_return(self, return_type: str | None) -> str:
71
+ """
72
+ Convert a return type into an entry in the returns section of the docstring.
73
+
74
+ Parameters
75
+ ----------
76
+ return_type : str | None
77
+ The return type name for the function. This will be either a string for the
78
+ name (e.g., ``list[str]``) or ``None``, if ``None`` is returned.
79
+
80
+ Returns
81
+ -------
82
+ str
83
+ An empty string.
84
+ """
85
+ return ''
86
+
87
+ def to_module_docstring(self, docstring_node: DocstringNode) -> str:
88
+ """
89
+ Convert an AST node into a module docstring.
90
+
91
+ Parameters
92
+ ----------
93
+ docstring_node : DocstringNode
94
+ An instance of :class:`.DocstringNode`, which wraps an instance of
95
+ :class:`ast.Module`, adding additional context relevant for Docstringify.
96
+
97
+ Returns
98
+ -------
99
+ str
100
+ The module docstring.
101
+ """
102
+ return self.format_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
103
+
104
+ def to_class_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
105
+ """
106
+ Convert an AST node into a class docstring.
107
+
108
+ Parameters
109
+ ----------
110
+ docstring_node : DocstringNode
111
+ An instance of :class:`.DocstringNode`, which wraps an instance of
112
+ :class:`ast.ClassDef`, adding additional context relevant for Docstringify.
113
+ indent : int
114
+ The number of spaces by which to indent the docstring.
115
+
116
+ Returns
117
+ -------
118
+ str
119
+ The class docstring.
120
+ """
121
+ return self.format_docstring(DESCRIPTION_PLACEHOLDER, indent=indent)
@@ -0,0 +1,25 @@
1
+ """Docstringify exceptions."""
2
+
3
+
4
+ class EmptyDocstringError(ValueError):
5
+ """
6
+ Raised when trying to write the modified source code back to a file if a docstring
7
+ is somehow empty.
8
+ """
9
+
10
+ def __init__(self) -> None:
11
+ super().__init__('Docstring is empty')
12
+
13
+
14
+ class InvalidDocstringError(TypeError):
15
+ """
16
+ Raised when trying to create a docstring with invalid input.
17
+
18
+ Parameters
19
+ ----------
20
+ docstring_class : str
21
+ The class of the docstring object. This is used for the error message.
22
+ """
23
+
24
+ def __init__(self, docstring_class: str) -> None:
25
+ super().__init__(f'Expected str or list[str] docstring, got {docstring_class}')
@@ -40,13 +40,15 @@ class DocstringNode:
40
40
  ) = node
41
41
  self.name: str = getattr(node, 'name', self.module_name)
42
42
 
43
- docstring = ast.get_docstring(node)
44
- self.docstring = docstring if docstring is None else docstring.strip()
45
-
46
- self.get_source_segment: Callable[[ast.AST], str] = partial(
43
+ self.get_source_segment: Callable[[ast.AST], str | None] = partial(
47
44
  ast.get_source_segment, source_code
48
45
  )
49
46
 
47
+ @property
48
+ def docstring(self) -> str | None:
49
+ docstring = ast.get_docstring(self.ast_node)
50
+ return docstring if docstring is None else docstring.strip()
51
+
50
52
  @property
51
53
  def fully_qualified_name(self) -> str:
52
54
  return (
@@ -23,11 +23,17 @@ class FunctionDocstringNode(DocstringNode):
23
23
  parent: DocstringNode,
24
24
  ) -> None:
25
25
  super().__init__(node, module_name, source_code, parent)
26
+
26
27
  self.decorators: list[str] = [
27
- self.get_source_segment(decorator) for decorator in node.decorator_list
28
+ decorator
29
+ for decorator_node in node.decorator_list
30
+ if (decorator := self.get_source_segment(decorator_node))
28
31
  ]
29
32
 
30
- self.is_method: bool = self.parent and isinstance(
33
+ self.is_overload: bool = (
34
+ 'overload' in self.decorators or 'typing.overload' in self.decorators
35
+ )
36
+ self.is_method: bool = self.parent is not None and isinstance(
31
37
  self.parent.ast_node, ast.ClassDef
32
38
  )
33
39
  self.is_abstract_method: bool = self.is_method and (
@@ -39,15 +45,26 @@ class FunctionDocstringNode(DocstringNode):
39
45
  self.is_method and 'staticmethod' in self.decorators
40
46
  )
41
47
  self.is_instance_method: bool = (
42
- self.is_method and not self.is_class_method and not self.is_static_method
48
+ self.is_method
49
+ and (not self.is_class_method)
50
+ and (not self.is_static_method)
43
51
  )
44
52
 
45
53
  # don't require docstring for the __init__ method if the class has a docstring
46
- self.docstring_required: bool = not (
47
- self.is_method and self.name == '__init__' and self.parent.docstring
54
+ # or if the function/method is just a typing.overload signature
55
+ self.docstring_required: bool = (
56
+ not (
57
+ self.is_method
58
+ and self.name == '__init__'
59
+ and self.parent is not None
60
+ and self.parent.docstring
61
+ )
62
+ and not self.is_overload
48
63
  )
49
64
 
50
- self.arguments: ast.arguments | None = getattr(node, 'args', None)
65
+ self.arguments: ast.arguments = node.args
66
+
67
+ self.returns: ast.AST | None = node.returns
51
68
  self.return_annotation: str | None = self._extract_return_annotation()
52
69
  self.return_statements: list[ast.Return] = []
53
70
 
@@ -64,12 +81,23 @@ class FunctionDocstringNode(DocstringNode):
64
81
 
65
82
  return (
66
83
  f'"{default_value}"'
67
- if isinstance(default_value, str) and not default_value.startswith('`')
84
+ if isinstance(default_value, str)
85
+ and (not default_value.startswith('`'))
68
86
  else default_value
69
87
  )
70
88
  return NO_DEFAULT
71
89
 
72
- def _extract_star_args(self) -> list[Parameter | None, Parameter | None]:
90
+ def _extract_star_args(self) -> list[Parameter | None]:
91
+ """
92
+ Extract the function's ``*args`` and ``**kwargs`` arguments.
93
+
94
+ Returns
95
+ -------
96
+ list[Parameter | None]
97
+ A list of the form ``[*args, **kwargs]``, where each entry is either a
98
+ :class:`.Parameter` instance, or ``None`` if that type of argument isn't
99
+ part of the function definition.
100
+ """
73
101
  return [
74
102
  Parameter(
75
103
  name=f'*{arg.arg}' if arg_type == 'vararg' else f'**{arg.arg}',
@@ -83,10 +111,11 @@ class FunctionDocstringNode(DocstringNode):
83
111
  for arg in [getattr(self.arguments, arg_type)]
84
112
  ]
85
113
 
86
- def _extract_positional_args(self) -> list[Parameter, ...]:
114
+ def _extract_positional_args(self) -> list[Parameter]:
87
115
  if (default_count := len(positional_defaults := self.arguments.defaults)) < (
88
- positional_arguments_count := len(self.arguments.posonlyargs)
89
- + len(self.arguments.args)
116
+ positional_arguments_count := (
117
+ len(self.arguments.posonlyargs) + len(self.arguments.args)
118
+ )
90
119
  ):
91
120
  positional_defaults = [NO_DEFAULT] * (
92
121
  positional_arguments_count - default_count
@@ -110,7 +139,7 @@ class FunctionDocstringNode(DocstringNode):
110
139
  )
111
140
  ]
112
141
 
113
- def _extract_keyword_args(self) -> list[Parameter, ...]:
142
+ def _extract_keyword_args(self) -> list[Parameter]:
114
143
  return [
115
144
  Parameter(
116
145
  name=arg.arg,
@@ -145,23 +174,28 @@ class FunctionDocstringNode(DocstringNode):
145
174
  return params
146
175
 
147
176
  def _extract_return_annotation(self) -> str | None:
148
- if return_annotation_node := self.ast_node.returns:
149
- if isinstance(return_annotation_node, ast.Constant):
150
- return return_annotation_node.value
151
- if isinstance(return_annotation_node, ast.Name):
152
- return return_annotation_node.id
153
- return self.get_source_segment(return_annotation_node)
154
- return None
177
+ if self.returns is None:
178
+ return self.returns
179
+
180
+ if isinstance(self.returns, ast.Constant):
181
+ return self.returns.value
182
+
183
+ if isinstance(self.returns, ast.Name):
184
+ return self.returns.id
185
+
186
+ return self.get_source_segment(self.returns)
155
187
 
156
188
  def extract_returns(self) -> str | None:
157
189
  if self.return_annotation:
158
190
  return self.return_annotation
191
+
159
192
  if any(
160
- not isinstance(return_value := return_node.value, ast.Constant)
193
+ not isinstance((return_value := return_node.value), ast.Constant)
161
194
  or return_value.value
162
195
  for return_node in self.return_statements
163
196
  ):
164
197
  return RETURN_TYPE_PLACEHOLDER
198
+
165
199
  return None
166
200
 
167
201
  def to_function(self) -> Function:
@@ -6,8 +6,10 @@ on the source code.
6
6
  from __future__ import annotations
7
7
 
8
8
  import ast
9
+ from textwrap import indent
9
10
  from typing import TYPE_CHECKING
10
11
 
12
+ from ..exceptions import EmptyDocstringError
11
13
  from .visitor import DocstringVisitor
12
14
 
13
15
  if TYPE_CHECKING:
@@ -60,11 +62,80 @@ class DocstringTransformer(ast.NodeTransformer, DocstringVisitor):
60
62
  + ''.join(self.source_file.suffixes)
61
63
  )
62
64
  )
63
- edited_code = ast.unparse(self.tree)
65
+ edited_code = self._convert_to_source_code()
64
66
  output.write_text(edited_code)
65
67
  print(f'Docstring templates written to {output}')
66
68
 
67
- def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
69
+ def _convert_to_source_code(self) -> str:
70
+ """
71
+ Convert the modified AST back to source code, preserving the original format and
72
+ any comments.
73
+
74
+ Returns
75
+ -------
76
+ str
77
+ The original source code with the docstrings injected.
78
+ """
79
+ source_code = self.source_code.splitlines()
80
+ output_lines = []
81
+ write_line = start_line = 0
82
+
83
+ for missing_docstring in self.missing_docstrings:
84
+ docstring_node = missing_docstring.ast_node.body[0]
85
+ prefix = docstring_node.col_offset
86
+ suffix = ''
87
+
88
+ # write everything before docstring
89
+ if not isinstance(missing_docstring.ast_node, ast.Module):
90
+ # line before a code node
91
+ start_line = missing_docstring.ast_node.body[1].lineno - 1
92
+
93
+ if isinstance(missing_docstring.ast_node, ast.ClassDef):
94
+ start_line = docstring_node.lineno
95
+
96
+ if len(body := missing_docstring.ast_node.body) == 2:
97
+ code_node = body[1]
98
+ line_number = code_node.lineno - 1
99
+ line = source_code[line_number]
100
+
101
+ if line.strip() != (function_body := line[code_node.col_offset :]):
102
+ # if the function body is on the same line as the signature, cut it out
103
+ # in order to inject the docstring in the right spot
104
+ source_code[line_number] = source_code[line_number][
105
+ : code_node.col_offset
106
+ ].rstrip()
107
+
108
+ # add the logic under the docstring
109
+ start_line = missing_docstring.ast_node.body[1].lineno
110
+ suffix = function_body
111
+
112
+ output_lines += source_code[write_line:start_line]
113
+
114
+ # write docstring
115
+ if not (docstring := missing_docstring.docstring):
116
+ raise EmptyDocstringError
117
+
118
+ output_lines.append(
119
+ indent(
120
+ self.docstring_converter.format_docstring(
121
+ docstring.splitlines(),
122
+ indent=0,
123
+ quote=True,
124
+ )
125
+ + (f'\n{suffix}' if suffix else ''),
126
+ ' ' * prefix,
127
+ )
128
+ )
129
+
130
+ # update current location in file
131
+ write_line = start_line
132
+
133
+ # write the rest of the file
134
+ output_lines += source_code[write_line:]
135
+
136
+ return '\n'.join(output_lines) + '\n'
137
+
138
+ def handle_missing_docstring(self, docstring_node: DocstringNode) -> None:
68
139
  """
69
140
  Handle missing docstrings by injecting a suggested docstring template based on
70
141
  the source code into the AST.
@@ -74,19 +145,16 @@ class DocstringTransformer(ast.NodeTransformer, DocstringVisitor):
74
145
  docstring_node : DocstringNode
75
146
  An instance of :class:`.DocstringNode`, which wraps an AST node and adds
76
147
  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
148
  """
149
+ indent = (
150
+ 0
151
+ if isinstance(docstring_node.ast_node, ast.Module)
152
+ else docstring_node.ast_node.col_offset + 4
153
+ )
154
+
85
155
  suggested_docstring = self.docstring_converter.suggest_docstring(
86
156
  docstring_node,
87
- indent=0
88
- if isinstance(docstring_node.ast_node, ast.Module)
89
- else docstring_node.ast_node.col_offset + 4,
157
+ indent=indent,
90
158
  )
91
159
  docstring_ast_node = ast.Expr(ast.Constant(suggested_docstring))
92
160
 
@@ -98,8 +166,7 @@ class DocstringTransformer(ast.NodeTransformer, DocstringVisitor):
98
166
  docstring_node.ast_node.body.insert(0, docstring_ast_node)
99
167
 
100
168
  docstring_node.ast_node = ast.fix_missing_locations(docstring_node.ast_node)
101
-
102
- return docstring_node
169
+ docstring_ast_node.col_offset = indent
103
170
 
104
171
  def process_file(self) -> None:
105
172
  """
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
  import ast
9
9
  import sys
10
10
  from pathlib import Path
11
- from typing import TYPE_CHECKING
11
+ from typing import TYPE_CHECKING, overload
12
12
 
13
13
  from ..nodes.base import DocstringNode
14
14
  from ..nodes.function import FunctionDocstringNode
@@ -112,8 +112,8 @@ class DocstringVisitor(ast.NodeVisitor):
112
112
 
113
113
  def process_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
114
114
  """
115
- Process a docstring node, appending it to :attr:`.missing_docstrings` if a docstring
116
- is required, but there isn't one.
115
+ Process a docstring node, appending it to :attr:`.missing_docstrings` if a
116
+ docstring is required, but there isn't one.
117
117
 
118
118
  Parameters
119
119
  ----------
@@ -133,6 +133,34 @@ class DocstringVisitor(ast.NodeVisitor):
133
133
  self.docstrings_inspected += 1
134
134
  return docstring_node
135
135
 
136
+ @overload
137
+ def visit_docstring(
138
+ self,
139
+ node: ast.Module,
140
+ docstring_class: type[DocstringNode],
141
+ ) -> ast.Module: ...
142
+
143
+ @overload
144
+ def visit_docstring(
145
+ self,
146
+ node: ast.ClassDef,
147
+ docstring_class: type[DocstringNode],
148
+ ) -> ast.ClassDef: ...
149
+
150
+ @overload
151
+ def visit_docstring(
152
+ self,
153
+ node: ast.FunctionDef,
154
+ docstring_class: type[FunctionDocstringNode],
155
+ ) -> ast.FunctionDef: ...
156
+
157
+ @overload
158
+ def visit_docstring(
159
+ self,
160
+ node: ast.AsyncFunctionDef,
161
+ docstring_class: type[FunctionDocstringNode],
162
+ ) -> ast.AsyncFunctionDef: ...
163
+
136
164
  def visit_docstring(
137
165
  self,
138
166
  node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module,
@@ -153,12 +181,12 @@ class DocstringVisitor(ast.NodeVisitor):
153
181
  ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
154
182
  The AST node that was visited.
155
183
  """
156
- docstring_node = docstring_class(
157
- node,
158
- self.module_name,
159
- self.source_code,
160
- parent=self.stack[-1] if self.stack else None,
161
- )
184
+ if isinstance(node, ast.Module):
185
+ docstring_node = docstring_class(node, self.module_name, self.source_code)
186
+ else:
187
+ docstring_node = docstring_class(
188
+ node, self.module_name, self.source_code, parent=self.stack[-1]
189
+ )
162
190
 
163
191
  self.stack.append(docstring_node)
164
192
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstringify
3
- Version: 0.6.0
3
+ Version: 1.0.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
@@ -29,7 +29,7 @@ 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
31
  Keywords: ast,docstring generation,google-style docstring,numpydoc-style docstring,pre-commit
32
- Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Development Status :: 4 - Beta
33
33
  Classifier: Programming Language :: Python
34
34
  Classifier: Programming Language :: Python :: 3 :: Only
35
35
  Classifier: Programming Language :: Python :: 3.9
@@ -103,11 +103,11 @@ You can use Docstringify in three modes:
103
103
 
104
104
  ### Pre-commit hook
105
105
 
106
- Add the following to your `.pre-commit-config.yaml` file to block commits with missing docstrings:
106
+ Add the following to your `.pre-commit-config.yaml` file to block commits with missing docstrings before any formatters like `ruff`:
107
107
 
108
108
  ```yaml
109
109
  - repo: https://github.com/stefmolin/docstringify
110
- rev: 0.6.0
110
+ rev: 1.0.0
111
111
  hooks:
112
112
  - id: docstringify
113
113
  ```
@@ -116,17 +116,17 @@ By default, all docstrings are required. If you want to be more lenient, you can
116
116
 
117
117
  ```yaml
118
118
  - repo: https://github.com/stefmolin/docstringify
119
- rev: 0.6.0
119
+ rev: 1.0.0
120
120
  hooks:
121
121
  - id: docstringify
122
122
  args: [--threshold=0.75]
123
123
  ```
124
124
 
125
- 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:
125
+ 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`, `numpydoc`, and `stub`). Here, we ask for stub suggestions (just single lines of `"""__description__"""`):
126
126
 
127
127
  ```yaml
128
128
  - repo: https://github.com/stefmolin/docstringify
129
- rev: 0.6.0
129
+ rev: 1.0.0
130
130
  hooks:
131
131
  - id: docstringify
132
132
  args: [--suggest-changes=numpydoc]
@@ -136,13 +136,21 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
136
136
 
137
137
  ```yaml
138
138
  - repo: https://github.com/stefmolin/docstringify
139
- rev: 0.6.0
139
+ rev: 1.0.0
140
140
  hooks:
141
141
  - id: docstringify
142
142
  args: [--make-changes=google]
143
143
  ```
144
144
 
145
- 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.*).
145
+ 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. Here, we ask for [numpydoc-style docstring](https://numpydoc.readthedocs.io/en/latest/format.html#) suggestions:
146
+
147
+ ```yaml
148
+ - repo: https://github.com/stefmolin/docstringify
149
+ rev: 1.0.0
150
+ hooks:
151
+ - id: docstringify
152
+ args: [--make-changes-inplace=numpydoc]
153
+ ```
146
154
 
147
155
  Be sure to check out the [pre-commit documentation](https://pre-commit.com/#pre-commit-configyaml---hooks) for additional configuration options.
148
156
 
@@ -216,7 +224,19 @@ test.say_hello is missing a docstring
216
224
  Docstring templates written to /.../test_docstringify.py
217
225
  ```
218
226
 
219
- If you want to overwrite the file with the edits, pass `overwrite=True` to `DocstringTransformer()`.
227
+ If you want to overwrite the file with the edits, pass `overwrite=True` to `DocstringTransformer()`:
228
+
229
+ ```pycon
230
+ >>> from docstringify.converters import GoogleDocstringConverter
231
+ >>> from docstringify.traversal import DocstringTransformer
232
+ >>> transformer = DocstringTransformer(
233
+ ... 'test.py', converter=GoogleDocstringConverter, overwrite=True
234
+ ... )
235
+ >>> transformer.process_file()
236
+ test is missing a docstring
237
+ test.say_hello is missing a docstring
238
+ Docstring templates written to /.../test.py
239
+ ```
220
240
 
221
241
  ## Contributing
222
242
 
@@ -29,6 +29,7 @@ src/docstringify/converters/__init__.py
29
29
  src/docstringify/converters/base.py
30
30
  src/docstringify/converters/google.py
31
31
  src/docstringify/converters/numpydoc.py
32
+ src/docstringify/converters/stub.py
32
33
  src/docstringify/nodes/__init__.py
33
34
  src/docstringify/nodes/base.py
34
35
  src/docstringify/nodes/function.py
@@ -1,6 +0,0 @@
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}')
File without changes
File without changes
File without changes