docstringify 0.3.0__tar.gz → 0.5.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 (39) hide show
  1. docstringify-0.5.0/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
  2. docstringify-0.5.0/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  3. docstringify-0.5.0/.github/workflows/greetings.yml +16 -0
  4. {docstringify-0.3.0 → docstringify-0.5.0}/PKG-INFO +10 -10
  5. {docstringify-0.3.0 → docstringify-0.5.0}/README.md +9 -9
  6. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/__init__.py +1 -1
  7. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/cli.py +3 -4
  8. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/converters/base.py +23 -4
  9. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/converters/google.py +14 -4
  10. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/converters/numpydoc.py +14 -4
  11. docstringify-0.5.0/src/docstringify/nodes/__init__.py +1 -0
  12. docstringify-0.5.0/src/docstringify/nodes/base.py +56 -0
  13. docstringify-0.5.0/src/docstringify/nodes/function.py +168 -0
  14. docstringify-0.5.0/src/docstringify/traversal/__init__.py +9 -0
  15. docstringify-0.5.0/src/docstringify/traversal/transformer.py +61 -0
  16. docstringify-0.5.0/src/docstringify/traversal/visitor.py +99 -0
  17. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/PKG-INFO +10 -10
  18. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/SOURCES.txt +10 -4
  19. docstringify-0.3.0/src/docstringify/docstring_generator.py +0 -157
  20. docstringify-0.3.0/src/docstringify/transformer.py +0 -53
  21. docstringify-0.3.0/src/docstringify/visitor.py +0 -79
  22. {docstringify-0.3.0 → docstringify-0.5.0}/.github/dependabot.yml +0 -0
  23. {docstringify-0.3.0 → docstringify-0.5.0}/.github/release.yml +0 -0
  24. {docstringify-0.3.0 → docstringify-0.5.0}/.github/workflows/ci.yml +0 -0
  25. {docstringify-0.3.0 → docstringify-0.5.0}/.github/workflows/pypi-publish.yml +0 -0
  26. {docstringify-0.3.0 → docstringify-0.5.0}/.gitignore +0 -0
  27. {docstringify-0.3.0 → docstringify-0.5.0}/.pre-commit-config.yaml +0 -0
  28. {docstringify-0.3.0 → docstringify-0.5.0}/.pre-commit-hooks.yaml +0 -0
  29. {docstringify-0.3.0 → docstringify-0.5.0}/LICENSE +0 -0
  30. {docstringify-0.3.0 → docstringify-0.5.0}/pyproject.toml +0 -0
  31. {docstringify-0.3.0 → docstringify-0.5.0}/setup.cfg +0 -0
  32. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/__main__.py +0 -0
  33. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/components.py +0 -0
  34. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/converters/__init__.py +0 -0
  35. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify/exceptions.py +0 -0
  36. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/dependency_links.txt +0 -0
  37. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/entry_points.txt +0 -0
  38. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/requires.txt +0 -0
  39. {docstringify-0.3.0 → docstringify-0.5.0}/src/docstringify.egg-info/top_level.txt +0 -0
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something isn't working
4
+ title: "[BUG] "
5
+ labels: bug
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Required attestation**
11
+ - [ ] I have checked that this issue has not already been reported.
12
+ - [ ] I have confirmed this bug exists on the latest version of docstringify.
13
+ - [ ] I have confirmed this bug exists on the `main` branch of docstringify.
14
+
15
+ **Describe the bug**
16
+ A clear and concise description of what the bug is.
17
+
18
+ **To Reproduce**
19
+ Steps to reproduce the behavior.
20
+
21
+ **Expected behavior**
22
+ A clear and concise description of what you expected to happen.
23
+
24
+ **Screenshots**
25
+ If applicable, add screenshots to help explain your problem.
26
+
27
+ **Environment**
28
+ - OS: [e.g. iOS]
29
+ - Python Version [e.g. 3.12]
30
+
31
+ **Additional context**
32
+ Add any other context about the problem here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ Add any other context or screenshots about the feature request here.
@@ -0,0 +1,16 @@
1
+ name: Greetings
2
+
3
+ on: [pull_request_target, issues]
4
+
5
+ jobs:
6
+ greeting:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ issues: write
10
+ pull-requests: write
11
+ steps:
12
+ - uses: actions/first-interaction@34f15e814fe48ac9312ccf29db4e74fa767cbab7 # v1.3.0
13
+ with:
14
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
15
+ issue-message: "It looks like this is your first issue here – welcome! Please familiarize yourself with the [contributing guidelines](https://github.com/stefmolin/docstringify/blob/main/CONTRIBUTING.md), if you haven't already."
16
+ pr-message: "Congratulations on making your first pull request to `docstringify`! Please familiarize yourself with the [contributing guidelines](https://github.com/stefmolin/docstringify/blob/main/CONTRIBUTING.md), if you haven't already."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstringify
3
- Version: 0.3.0
3
+ Version: 0.5.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.3.0
63
+ rev: 0.5.0
64
64
  hooks:
65
65
  - id: docstringify
66
66
  ```
@@ -69,7 +69,7 @@ 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.3.0
72
+ rev: 0.5.0
73
73
  hooks:
74
74
  - id: docstringify
75
75
  args: [--threshold=0.75]
@@ -79,7 +79,7 @@ If you would like to see suggested docstring templates (inferred from type annot
79
79
 
80
80
  ```yaml
81
81
  - repo: https://github.com/stefmolin/docstringify
82
- rev: 0.3.0
82
+ rev: 0.5.0
83
83
  hooks:
84
84
  - id: docstringify
85
85
  args: [--suggest-changes=numpydoc]
@@ -89,7 +89,7 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
89
89
 
90
90
  ```yaml
91
91
  - repo: https://github.com/stefmolin/docstringify
92
- rev: 0.3.0
92
+ rev: 0.5.0
93
93
  hooks:
94
94
  - id: docstringify
95
95
  args: [--make-changes=google]
@@ -126,7 +126,7 @@ $ python -m pip install docstringify
126
126
  Then, use the `DocstringVisitor()` class on individual files to see spots where docstrings are missing:
127
127
 
128
128
  ```pycon
129
- >>> from docstringify.visitor import DocstringVisitor
129
+ >>> from docstringify.traversal import DocstringVisitor
130
130
  >>> visitor = DocstringVisitor('test.py')
131
131
  >>> visitor.process_file()
132
132
  test is missing a docstring
@@ -137,9 +137,9 @@ If you would like to see suggested docstring templates (inferred from type annot
137
137
 
138
138
  ```pycon
139
139
  >>> from docstringify.converters import NumpydocDocstringConverter
140
- >>> from docstringify.visitor import DocstringVisitor
140
+ >>> from docstringify.traversal import DocstringVisitor
141
141
  >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
142
- >>> _ = visitor.process_file()
142
+ >>> visitor.process_file()
143
143
  test is missing a docstring
144
144
  Hint:
145
145
  """__description__"""
@@ -161,9 +161,9 @@ To make changes to your files, you will need to use the `DocstringTransformer` i
161
161
 
162
162
  ```pycon
163
163
  >>> from docstringify.converters import GoogleDocstringConverter
164
- >>> from docstringify.transformer import DocstringTransformer
164
+ >>> from docstringify.traversal import DocstringTransformer
165
165
  >>> transformer = DocstringTransformer('test.py', converter=GoogleDocstringConverter)
166
- >>> _ = transformer.process_file()
166
+ >>> transformer.process_file()
167
167
  test is missing a docstring
168
168
  test.say_hello is missing a docstring
169
169
  Docstring templates written to /.../test_docstringify.py
@@ -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.3.0
12
+ rev: 0.5.0
13
13
  hooks:
14
14
  - id: docstringify
15
15
  ```
@@ -18,7 +18,7 @@ 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.3.0
21
+ rev: 0.5.0
22
22
  hooks:
23
23
  - id: docstringify
24
24
  args: [--threshold=0.75]
@@ -28,7 +28,7 @@ If you would like to see suggested docstring templates (inferred from type annot
28
28
 
29
29
  ```yaml
30
30
  - repo: https://github.com/stefmolin/docstringify
31
- rev: 0.3.0
31
+ rev: 0.5.0
32
32
  hooks:
33
33
  - id: docstringify
34
34
  args: [--suggest-changes=numpydoc]
@@ -38,7 +38,7 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
38
38
 
39
39
  ```yaml
40
40
  - repo: https://github.com/stefmolin/docstringify
41
- rev: 0.3.0
41
+ rev: 0.5.0
42
42
  hooks:
43
43
  - id: docstringify
44
44
  args: [--make-changes=google]
@@ -75,7 +75,7 @@ $ python -m pip install docstringify
75
75
  Then, use the `DocstringVisitor()` class on individual files to see spots where docstrings are missing:
76
76
 
77
77
  ```pycon
78
- >>> from docstringify.visitor import DocstringVisitor
78
+ >>> from docstringify.traversal import DocstringVisitor
79
79
  >>> visitor = DocstringVisitor('test.py')
80
80
  >>> visitor.process_file()
81
81
  test is missing a docstring
@@ -86,9 +86,9 @@ If you would like to see suggested docstring templates (inferred from type annot
86
86
 
87
87
  ```pycon
88
88
  >>> from docstringify.converters import NumpydocDocstringConverter
89
- >>> from docstringify.visitor import DocstringVisitor
89
+ >>> from docstringify.traversal import DocstringVisitor
90
90
  >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
91
- >>> _ = visitor.process_file()
91
+ >>> visitor.process_file()
92
92
  test is missing a docstring
93
93
  Hint:
94
94
  """__description__"""
@@ -110,9 +110,9 @@ To make changes to your files, you will need to use the `DocstringTransformer` i
110
110
 
111
111
  ```pycon
112
112
  >>> from docstringify.converters import GoogleDocstringConverter
113
- >>> from docstringify.transformer import DocstringTransformer
113
+ >>> from docstringify.traversal import DocstringTransformer
114
114
  >>> transformer = DocstringTransformer('test.py', converter=GoogleDocstringConverter)
115
- >>> _ = transformer.process_file()
115
+ >>> transformer.process_file()
116
116
  test is missing a docstring
117
117
  test.say_hello is missing a docstring
118
118
  Docstring templates written to /.../test_docstringify.py
@@ -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.3.0'
6
+ __version__ = '0.5.0'
@@ -7,8 +7,7 @@ from typing import Sequence
7
7
 
8
8
  from . import __version__
9
9
  from .converters import GoogleDocstringConverter, NumpydocDocstringConverter
10
- from .transformer import DocstringTransformer
11
- from .visitor import DocstringVisitor
10
+ from .traversal import DocstringTransformer, DocstringVisitor
12
11
 
13
12
  PROG = __package__
14
13
 
@@ -77,13 +76,13 @@ def main(argv: Sequence[str] | None = None) -> int:
77
76
  for file in args.filenames:
78
77
  processor = get_docstring_processor(file)
79
78
  processor.process_file()
80
- missing_docstrings += processor.missing_docstrings
79
+ missing_docstrings += len(processor.missing_docstrings)
81
80
  docstrings_processed += processor.docstrings_inspected
82
81
 
83
82
  if docstrings_processed and (
84
83
  missing_percentage := (missing_docstrings / docstrings_processed)
85
84
  ) > (1 - args.threshold):
86
- print(f'\nMissing {missing_percentage:.0%} of docstrings', file=sys.stderr)
85
+ print(f'Missing {missing_percentage:.0%} of docstrings', file=sys.stderr)
87
86
  print(
88
87
  f'Your settings require {args.threshold:.0%} of docstrings to be present',
89
88
  file=sys.stderr,
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import ast
5
6
  import textwrap
6
7
  from abc import ABC, abstractmethod
7
8
  from typing import TYPE_CHECKING
@@ -9,7 +10,8 @@ from typing import TYPE_CHECKING
9
10
  from ..exceptions import InvalidDocstringError
10
11
 
11
12
  if TYPE_CHECKING:
12
- from ..components import Function, Parameter
13
+ from ..components import Parameter
14
+ from ..nodes.base import DocstringNode
13
15
 
14
16
 
15
17
  class DocstringConverter(ABC):
@@ -24,15 +26,15 @@ class DocstringConverter(ABC):
24
26
  self._quote = quote
25
27
 
26
28
  @abstractmethod
27
- def to_class_docstring(self, class_name: str, indent: int) -> str:
29
+ def to_class_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
28
30
  pass
29
31
 
30
32
  @abstractmethod
31
- def to_function_docstring(self, function: Function, indent: int) -> str:
33
+ def to_function_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
32
34
  pass
33
35
 
34
36
  @abstractmethod
35
- def to_module_docstring(self, module_name: str) -> str:
37
+ def to_module_docstring(self, docstring_node: DocstringNode) -> str:
36
38
  pass
37
39
 
38
40
  @abstractmethod
@@ -71,3 +73,20 @@ class DocstringConverter(ABC):
71
73
  raise InvalidDocstringError(type(docstring).__name__)
72
74
 
73
75
  return textwrap.indent(sep.join(docstring), prefix)
76
+
77
+ def suggest_docstring(
78
+ self,
79
+ docstring_node: DocstringNode,
80
+ indent: int = 0,
81
+ ) -> str | None:
82
+ if isinstance(docstring_node.ast_node, ast.AsyncFunctionDef | ast.FunctionDef):
83
+ return (
84
+ self.to_function_docstring(docstring_node, indent=indent)
85
+ if docstring_node.docstring_required
86
+ else None
87
+ )
88
+
89
+ if isinstance(docstring_node.ast_node, ast.Module):
90
+ return self.to_module_docstring(docstring_node)
91
+
92
+ return self.to_class_docstring(docstring_node, indent=indent)
@@ -2,9 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from ..components import DESCRIPTION_PLACEHOLDER, NO_DEFAULT, Function, Parameter
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..components import DESCRIPTION_PLACEHOLDER, NO_DEFAULT, Parameter
6
8
  from .base import DocstringConverter
7
9
 
10
+ if TYPE_CHECKING:
11
+ from ..nodes.base import DocstringNode
12
+ from ..nodes.function import FunctionDocstringNode
13
+
8
14
 
9
15
  class GoogleDocstringConverter(DocstringConverter):
10
16
  def __init__(self, quote: bool) -> None:
@@ -14,7 +20,11 @@ class GoogleDocstringConverter(DocstringConverter):
14
20
  quote=quote,
15
21
  )
16
22
 
17
- def to_function_docstring(self, function: Function, indent: int) -> str:
23
+ def to_function_docstring(
24
+ self, docstring_node: FunctionDocstringNode, indent: int
25
+ ) -> str:
26
+ function = docstring_node.to_function()
27
+
18
28
  docstring = [DESCRIPTION_PLACEHOLDER]
19
29
 
20
30
  if parameters_section := self.parameters_section(function.parameters):
@@ -37,8 +47,8 @@ class GoogleDocstringConverter(DocstringConverter):
37
47
  return f'{return_type}: {DESCRIPTION_PLACEHOLDER}'
38
48
  return ''
39
49
 
40
- def to_module_docstring(self, module_name: str) -> str:
50
+ def to_module_docstring(self, docstring_node: DocstringNode) -> str:
41
51
  return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
42
52
 
43
- def to_class_docstring(self, class_name: str, indent: int) -> str:
53
+ def to_class_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
44
54
  return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=indent)
@@ -2,9 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from ..components import DESCRIPTION_PLACEHOLDER, NO_DEFAULT, Function, Parameter
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..components import DESCRIPTION_PLACEHOLDER, NO_DEFAULT, Parameter
6
8
  from .base import DocstringConverter
7
9
 
10
+ if TYPE_CHECKING:
11
+ from ..nodes.base import DocstringNode
12
+ from ..nodes.function import FunctionDocstringNode
13
+
8
14
 
9
15
  class NumpydocDocstringConverter(DocstringConverter):
10
16
  def __init__(self, quote: bool) -> None:
@@ -14,7 +20,11 @@ class NumpydocDocstringConverter(DocstringConverter):
14
20
  quote=quote,
15
21
  )
16
22
 
17
- def to_function_docstring(self, function: Function, indent: int) -> str:
23
+ def to_function_docstring(
24
+ self, docstring_node: FunctionDocstringNode, indent: int
25
+ ) -> str:
26
+ function = docstring_node.to_function()
27
+
18
28
  docstring = [DESCRIPTION_PLACEHOLDER]
19
29
 
20
30
  if parameters_section := self.parameters_section(function.parameters):
@@ -38,8 +48,8 @@ class NumpydocDocstringConverter(DocstringConverter):
38
48
  return f'{return_type}\n {DESCRIPTION_PLACEHOLDER}'
39
49
  return ''
40
50
 
41
- def to_module_docstring(self, module_name: str) -> str:
51
+ def to_module_docstring(self, docstring_node: DocstringNode) -> str:
42
52
  return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=0)
43
53
 
44
- def to_class_docstring(self, class_name: str, indent: int) -> str:
54
+ def to_class_docstring(self, docstring_node: DocstringNode, indent: int) -> str:
45
55
  return self.quote_docstring(DESCRIPTION_PLACEHOLDER, indent=indent)
@@ -0,0 +1 @@
1
+ """Docstringify nodes."""
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from functools import partial
5
+ from typing import Callable, overload
6
+
7
+
8
+ class DocstringNode:
9
+ @overload
10
+ def __init__(
11
+ self,
12
+ node: ast.Module,
13
+ module_name: str,
14
+ source_code: str,
15
+ parent: None = None,
16
+ ) -> None: ...
17
+
18
+ @overload
19
+ def __init__(
20
+ self,
21
+ node: ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef,
22
+ module_name: str,
23
+ source_code: str,
24
+ parent: DocstringNode,
25
+ ) -> None: ...
26
+
27
+ def __init__(
28
+ self,
29
+ node: ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef,
30
+ module_name: str,
31
+ source_code: str,
32
+ parent: DocstringNode | None = None,
33
+ ) -> None:
34
+ self.module_name: str = module_name
35
+ self.parent: DocstringNode | None = parent
36
+ self.docstring_required: bool = True
37
+
38
+ self.ast_node: (
39
+ ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
40
+ ) = node
41
+ self.name: str = getattr(node, 'name', self.module_name)
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(
47
+ ast.get_source_segment, source_code
48
+ )
49
+
50
+ @property
51
+ def fully_qualified_name(self) -> str:
52
+ return (
53
+ f'{self.parent.fully_qualified_name}.{self.name}'
54
+ if self.parent
55
+ else self.name
56
+ )
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import itertools
5
+ from typing import Literal
6
+
7
+ from ..components import (
8
+ NO_DEFAULT,
9
+ PARAMETER_TYPE_PLACEHOLDER,
10
+ RETURN_TYPE_PLACEHOLDER,
11
+ Function,
12
+ Parameter,
13
+ )
14
+ from .base import DocstringNode
15
+
16
+
17
+ class FunctionDocstringNode(DocstringNode):
18
+ def __init__(
19
+ self,
20
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
21
+ module_name: str,
22
+ source_code: str,
23
+ parent: DocstringNode,
24
+ ) -> None:
25
+ super().__init__(node, module_name, source_code, parent)
26
+ self.decorators: list[str] = [
27
+ self.get_source_segment(decorator) for decorator in node.decorator_list
28
+ ]
29
+
30
+ self.is_method: bool = self.parent and isinstance(
31
+ self.parent.ast_node, ast.ClassDef
32
+ )
33
+ self.is_abstract_method: bool = self.is_method and (
34
+ 'abstractmethod' in self.decorators
35
+ or 'abc.abstractmethod' in self.decorators
36
+ )
37
+ self.is_class_method: bool = self.is_method and 'classmethod' in self.decorators
38
+ self.is_static_method: bool = (
39
+ self.is_method and 'staticmethod' in self.decorators
40
+ )
41
+ self.is_instance_method: bool = (
42
+ self.is_method and not self.is_class_method and not self.is_static_method
43
+ )
44
+
45
+ # 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
48
+ )
49
+
50
+ self.arguments: ast.arguments | None = getattr(node, 'args', None)
51
+ self.return_annotation: str | None = self._extract_return_annotation()
52
+ self.return_statements: list[ast.Return] = []
53
+
54
+ def _extract_default_values(
55
+ self, default: ast.Constant | None | Literal[NO_DEFAULT], is_keyword_only: bool
56
+ ) -> str | Literal[NO_DEFAULT]:
57
+ if (not is_keyword_only and default is not NO_DEFAULT) or (
58
+ is_keyword_only and default
59
+ ):
60
+ try:
61
+ default_value = default.value
62
+ except AttributeError:
63
+ default_value = f'`{default.id}`'
64
+
65
+ return (
66
+ f'"{default_value}"'
67
+ if isinstance(default_value, str) and not default_value.startswith('`')
68
+ else default_value
69
+ )
70
+ return NO_DEFAULT
71
+
72
+ def _extract_star_args(self) -> list[Parameter | None, Parameter | None]:
73
+ return [
74
+ Parameter(
75
+ name=f'*{arg.arg}' if arg_type == 'vararg' else f'**{arg.arg}',
76
+ type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
77
+ category=None,
78
+ default=NO_DEFAULT,
79
+ )
80
+ if arg
81
+ else None
82
+ for arg_type in ['vararg', 'kwarg']
83
+ for arg in [getattr(self.arguments, arg_type)]
84
+ ]
85
+
86
+ def _extract_positional_args(self) -> list[Parameter, ...]:
87
+ if (default_count := len(positional_defaults := self.arguments.defaults)) < (
88
+ positional_arguments_count := len(self.arguments.posonlyargs)
89
+ + len(self.arguments.args)
90
+ ):
91
+ positional_defaults = [NO_DEFAULT] * (
92
+ positional_arguments_count - default_count
93
+ ) + positional_defaults
94
+
95
+ return [
96
+ Parameter(
97
+ name=arg.arg,
98
+ type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
99
+ category=category,
100
+ default=self._extract_default_values(default, False),
101
+ )
102
+ for (arg, category), default in zip(
103
+ itertools.chain(
104
+ zip(
105
+ self.arguments.posonlyargs, itertools.repeat('positional-only')
106
+ ),
107
+ zip(self.arguments.args, itertools.repeat(None)),
108
+ ),
109
+ positional_defaults,
110
+ )
111
+ ]
112
+
113
+ def _extract_keyword_args(self) -> list[Parameter, ...]:
114
+ return [
115
+ Parameter(
116
+ name=arg.arg,
117
+ type_=getattr(arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER),
118
+ category='keyword-only',
119
+ default=self._extract_default_values(default, True),
120
+ )
121
+ for arg, default in zip(
122
+ self.arguments.kwonlyargs, self.arguments.kw_defaults
123
+ )
124
+ ]
125
+
126
+ def extract_arguments(self) -> tuple[Parameter, ...]:
127
+ params = self._extract_positional_args()
128
+
129
+ varargs, kwargs = self._extract_star_args()
130
+
131
+ if varargs:
132
+ params.append(varargs)
133
+
134
+ params.extend(self._extract_keyword_args())
135
+
136
+ if kwargs:
137
+ params.append(kwargs)
138
+
139
+ params = tuple(params)
140
+ if params and (
141
+ (self.is_class_method and params[0].name == 'cls')
142
+ or (self.is_instance_method and params[0].name == 'self')
143
+ ):
144
+ return params[1:]
145
+ return params
146
+
147
+ 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
155
+
156
+ def extract_returns(self) -> str | None:
157
+ if self.return_annotation:
158
+ return self.return_annotation
159
+ if any(
160
+ not isinstance(return_value := return_node.value, ast.Constant)
161
+ or return_value.value
162
+ for return_node in self.return_statements
163
+ ):
164
+ return RETURN_TYPE_PLACEHOLDER
165
+ return None
166
+
167
+ def to_function(self) -> Function:
168
+ return Function(self.extract_arguments(), self.extract_returns())
@@ -0,0 +1,9 @@
1
+ """AST traversal for docstrings."""
2
+
3
+ from .transformer import DocstringTransformer
4
+ from .visitor import DocstringVisitor
5
+
6
+ __all__ = [
7
+ 'DocstringTransformer',
8
+ 'DocstringVisitor',
9
+ ]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .visitor import DocstringVisitor
7
+
8
+ if TYPE_CHECKING:
9
+ from ..converters import DocstringConverter
10
+ from ..nodes.base import DocstringNode
11
+
12
+
13
+ class DocstringTransformer(ast.NodeTransformer, DocstringVisitor):
14
+ def __init__(
15
+ self,
16
+ filename: str,
17
+ converter: type[DocstringConverter],
18
+ overwrite: bool = False,
19
+ ) -> None:
20
+ super().__init__(filename, converter)
21
+ self.overwrite = overwrite
22
+
23
+ def save(self) -> None:
24
+ if self.missing_docstrings:
25
+ output = (
26
+ self.source_file
27
+ if self.overwrite
28
+ else self.source_file.parent
29
+ / (
30
+ self.source_file.stem
31
+ + '_docstringify'
32
+ + ''.join(self.source_file.suffixes)
33
+ )
34
+ )
35
+ edited_code = ast.unparse(self.tree)
36
+ output.write_text(edited_code)
37
+ print(f'Docstring templates written to {output}')
38
+
39
+ def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
40
+ suggested_docstring = self.docstring_converter.suggest_docstring(
41
+ docstring_node,
42
+ indent=0
43
+ if isinstance(docstring_node.ast_node, ast.Module)
44
+ else docstring_node.ast_node.col_offset + 4,
45
+ )
46
+ docstring_ast_node = ast.Expr(ast.Constant(suggested_docstring))
47
+
48
+ if docstring_node.docstring is not None:
49
+ # If the docstring is empty, we replace it with the suggested docstring
50
+ docstring_node.ast_node.body[0] = docstring_ast_node
51
+ else:
52
+ # If the docstring is missing, we insert the suggested docstring
53
+ docstring_node.ast_node.body.insert(0, docstring_ast_node)
54
+
55
+ docstring_node.ast_node = ast.fix_missing_locations(docstring_node.ast_node)
56
+
57
+ return docstring_node
58
+
59
+ def process_file(self) -> None:
60
+ super().process_file()
61
+ self.save()
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ from ..nodes.base import DocstringNode
9
+ from ..nodes.function import FunctionDocstringNode
10
+
11
+ if TYPE_CHECKING:
12
+ from ..converters import DocstringConverter
13
+
14
+
15
+ class DocstringVisitor(ast.NodeVisitor):
16
+ def __init__(
17
+ self, filename: str, converter: type[DocstringConverter] | None = None
18
+ ) -> None:
19
+ self.source_file: Path = Path(filename).expanduser().resolve()
20
+ self.source_code: str = self.source_file.read_text()
21
+ self.tree: ast.Module = ast.parse(self.source_code)
22
+
23
+ self.docstrings_inspected: int = 0
24
+ self.missing_docstrings: list[DocstringNode] = []
25
+
26
+ self.module_name: str = self.source_file.stem
27
+ self.stack: list[DocstringNode] = []
28
+
29
+ self.docstring_converter: DocstringConverter | None = (
30
+ converter(quote=not issubclass(self.__class__, ast.NodeTransformer))
31
+ if converter
32
+ else None
33
+ )
34
+
35
+ def report_missing_docstrings(self) -> None:
36
+ if not self.missing_docstrings:
37
+ print(f'No missing docstrings found in {self.source_file}.')
38
+ else:
39
+ for docstring_node in self.missing_docstrings:
40
+ print(
41
+ f'{docstring_node.fully_qualified_name} is missing a docstring',
42
+ file=sys.stderr,
43
+ )
44
+ self.handle_missing_docstring(docstring_node)
45
+
46
+ def handle_missing_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
47
+ if self.docstring_converter:
48
+ print('Hint:')
49
+ print(self.docstring_converter.suggest_docstring(docstring_node))
50
+ print()
51
+
52
+ def process_docstring(self, docstring_node: DocstringNode) -> DocstringNode:
53
+ if docstring_node.docstring_required and not docstring_node.docstring:
54
+ self.missing_docstrings.append(docstring_node)
55
+
56
+ self.docstrings_inspected += 1
57
+ return docstring_node
58
+
59
+ def visit_docstring(
60
+ self,
61
+ node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module,
62
+ docstring_class: type[DocstringNode],
63
+ ) -> ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module:
64
+ docstring_node = docstring_class(
65
+ node,
66
+ self.module_name,
67
+ self.source_code,
68
+ parent=self.stack[-1] if self.stack else None,
69
+ )
70
+
71
+ self.stack.append(docstring_node)
72
+
73
+ docstring_node = self.process_docstring(docstring_node)
74
+ self.generic_visit(docstring_node.ast_node)
75
+ self.stack.pop()
76
+ return docstring_node.ast_node
77
+
78
+ def visit_Module(self, node: ast.Module) -> ast.Module: # noqa: N802
79
+ return self.visit_docstring(node, DocstringNode)
80
+
81
+ def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: # noqa: N802
82
+ return self.visit_docstring(node, DocstringNode)
83
+
84
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: # noqa: N802
85
+ return self.visit_docstring(node, FunctionDocstringNode)
86
+
87
+ def visit_AsyncFunctionDef( # noqa: N802
88
+ self, node: ast.AsyncFunctionDef
89
+ ) -> ast.AsyncFunctionDef:
90
+ return self.visit_docstring(node, FunctionDocstringNode)
91
+
92
+ def visit_Return(self, node: ast.Return) -> ast.Return: # noqa: N802
93
+ if isinstance(self.stack[-1], FunctionDocstringNode):
94
+ self.stack[-1].return_statements.append(node)
95
+ return node
96
+
97
+ def process_file(self) -> None:
98
+ self.visit(self.tree)
99
+ self.report_missing_docstrings()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstringify
3
- Version: 0.3.0
3
+ Version: 0.5.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.3.0
63
+ rev: 0.5.0
64
64
  hooks:
65
65
  - id: docstringify
66
66
  ```
@@ -69,7 +69,7 @@ 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.3.0
72
+ rev: 0.5.0
73
73
  hooks:
74
74
  - id: docstringify
75
75
  args: [--threshold=0.75]
@@ -79,7 +79,7 @@ If you would like to see suggested docstring templates (inferred from type annot
79
79
 
80
80
  ```yaml
81
81
  - repo: https://github.com/stefmolin/docstringify
82
- rev: 0.3.0
82
+ rev: 0.5.0
83
83
  hooks:
84
84
  - id: docstringify
85
85
  args: [--suggest-changes=numpydoc]
@@ -89,7 +89,7 @@ Use `--make-changes` to create a copy of each file with docstring templates. Her
89
89
 
90
90
  ```yaml
91
91
  - repo: https://github.com/stefmolin/docstringify
92
- rev: 0.3.0
92
+ rev: 0.5.0
93
93
  hooks:
94
94
  - id: docstringify
95
95
  args: [--make-changes=google]
@@ -126,7 +126,7 @@ $ python -m pip install docstringify
126
126
  Then, use the `DocstringVisitor()` class on individual files to see spots where docstrings are missing:
127
127
 
128
128
  ```pycon
129
- >>> from docstringify.visitor import DocstringVisitor
129
+ >>> from docstringify.traversal import DocstringVisitor
130
130
  >>> visitor = DocstringVisitor('test.py')
131
131
  >>> visitor.process_file()
132
132
  test is missing a docstring
@@ -137,9 +137,9 @@ If you would like to see suggested docstring templates (inferred from type annot
137
137
 
138
138
  ```pycon
139
139
  >>> from docstringify.converters import NumpydocDocstringConverter
140
- >>> from docstringify.visitor import DocstringVisitor
140
+ >>> from docstringify.traversal import DocstringVisitor
141
141
  >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter)
142
- >>> _ = visitor.process_file()
142
+ >>> visitor.process_file()
143
143
  test is missing a docstring
144
144
  Hint:
145
145
  """__description__"""
@@ -161,9 +161,9 @@ To make changes to your files, you will need to use the `DocstringTransformer` i
161
161
 
162
162
  ```pycon
163
163
  >>> from docstringify.converters import GoogleDocstringConverter
164
- >>> from docstringify.transformer import DocstringTransformer
164
+ >>> from docstringify.traversal import DocstringTransformer
165
165
  >>> transformer = DocstringTransformer('test.py', converter=GoogleDocstringConverter)
166
- >>> _ = transformer.process_file()
166
+ >>> transformer.process_file()
167
167
  test is missing a docstring
168
168
  test.say_hello is missing a docstring
169
169
  Docstring templates written to /.../test_docstringify.py
@@ -6,16 +6,16 @@ README.md
6
6
  pyproject.toml
7
7
  .github/dependabot.yml
8
8
  .github/release.yml
9
+ .github/ISSUE_TEMPLATE/bug_report.md
10
+ .github/ISSUE_TEMPLATE/feature_request.md
9
11
  .github/workflows/ci.yml
12
+ .github/workflows/greetings.yml
10
13
  .github/workflows/pypi-publish.yml
11
14
  src/docstringify/__init__.py
12
15
  src/docstringify/__main__.py
13
16
  src/docstringify/cli.py
14
17
  src/docstringify/components.py
15
- src/docstringify/docstring_generator.py
16
18
  src/docstringify/exceptions.py
17
- src/docstringify/transformer.py
18
- src/docstringify/visitor.py
19
19
  src/docstringify.egg-info/PKG-INFO
20
20
  src/docstringify.egg-info/SOURCES.txt
21
21
  src/docstringify.egg-info/dependency_links.txt
@@ -25,4 +25,10 @@ src/docstringify.egg-info/top_level.txt
25
25
  src/docstringify/converters/__init__.py
26
26
  src/docstringify/converters/base.py
27
27
  src/docstringify/converters/google.py
28
- src/docstringify/converters/numpydoc.py
28
+ src/docstringify/converters/numpydoc.py
29
+ src/docstringify/nodes/__init__.py
30
+ src/docstringify/nodes/base.py
31
+ src/docstringify/nodes/function.py
32
+ src/docstringify/traversal/__init__.py
33
+ src/docstringify/traversal/transformer.py
34
+ src/docstringify/traversal/visitor.py
@@ -1,157 +0,0 @@
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)
@@ -1,53 +0,0 @@
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
@@ -1,79 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import ast
4
- import sys
5
- from pathlib import Path
6
- from typing import TYPE_CHECKING
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
File without changes
File without changes
File without changes