docstringify 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ """
2
+ Flag missing docstrings and, optionally, generate them from signatures and
3
+ type annotations.
4
+ """
5
+
6
+ __version__ = '0.1.0'
@@ -0,0 +1,5 @@
1
+ """Enable running `python -m docstringify`."""
2
+
3
+ from .cli import main
4
+
5
+ raise SystemExit(main())
docstringify/cli.py ADDED
@@ -0,0 +1,71 @@
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())
@@ -0,0 +1,22 @@
1
+ """Docstring components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final, NamedTuple
6
+
7
+ NO_DEFAULT: Final = object()
8
+ DESCRIPTION_PLACEHOLDER = '__description__'
9
+ PARAMETER_TYPE_PLACEHOLDER = '__type__'
10
+ RETURN_TYPE_PLACEHOLDER = '__return_type__'
11
+
12
+
13
+ class Parameter(NamedTuple):
14
+ name: str
15
+ type_: str
16
+ category: str | None
17
+ default: str | NO_DEFAULT
18
+
19
+
20
+ class Function(NamedTuple):
21
+ parameters: tuple[Parameter, ...]
22
+ return_type: str | None
@@ -0,0 +1 @@
1
+ """Docstring converters."""
@@ -0,0 +1,51 @@
1
+ """Base docstring converter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..components import Function, Parameter
10
+
11
+
12
+ class DocstringConverter(ABC):
13
+ def __init__(
14
+ self, parameters_section_template: str, returns_section_template: str
15
+ ) -> None:
16
+ self.parameters_section_template = parameters_section_template
17
+ self.returns_section_template = returns_section_template
18
+
19
+ @abstractmethod
20
+ def to_class_docstring(self, class_name: str) -> str:
21
+ pass
22
+
23
+ @abstractmethod
24
+ def to_function_docstring(self, function: Function) -> str:
25
+ pass
26
+
27
+ @abstractmethod
28
+ def to_module_docstring(self, module_name: str) -> str:
29
+ pass
30
+
31
+ @abstractmethod
32
+ def format_parameter(self, parameter: Parameter) -> str:
33
+ pass
34
+
35
+ def parameters_section(self, parameters: tuple[Parameter, ...]) -> str:
36
+ if parameters:
37
+ return self.parameters_section_template.format(
38
+ parameters='\n'.join(
39
+ self.format_parameter(parameter) for parameter in parameters
40
+ )
41
+ )
42
+ return ''
43
+
44
+ @abstractmethod
45
+ def format_return(self, return_type: str | None) -> str:
46
+ pass
47
+
48
+ def returns_section(self, return_type: str | None) -> str:
49
+ if return_text := self.format_return(return_type):
50
+ return self.returns_section_template.format(returns=return_text)
51
+ return ''
@@ -0,0 +1,47 @@
1
+ """Numpydoc-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 NumpydocDocstringConverter(DocstringConverter):
10
+ def __init__(self) -> None:
11
+ super().__init__(
12
+ parameters_section_template='Parameters\n----------\n{parameters}',
13
+ returns_section_template='Returns\n-------\n{returns}',
14
+ )
15
+
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]
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
+ sep = '' if len(docstring) == 2 else '\n'
27
+
28
+ return sep.join([*docstring, '"""'])
29
+
30
+ def format_parameter(self, parameter: Parameter) -> str:
31
+ return (
32
+ f'{parameter.name} : {parameter.type_}'
33
+ f'{f", {parameter.category}" if parameter.category else ""}'
34
+ f'{f", default={parameter.default}" if parameter.default != NO_DEFAULT else ""}'
35
+ f'\n {DESCRIPTION_PLACEHOLDER}'
36
+ )
37
+
38
+ def format_return(self, return_type: str | None) -> str:
39
+ if return_type:
40
+ return f'{return_type}\n {DESCRIPTION_PLACEHOLDER}'
41
+ return ''
42
+
43
+ def to_module_docstring(self, module_name: str) -> str:
44
+ return f'"""{DESCRIPTION_PLACEHOLDER}"""'
45
+
46
+ def to_class_docstring(self, class_name: str) -> str:
47
+ return f'"""{DESCRIPTION_PLACEHOLDER}"""'
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Literal
7
+
8
+ from .components import (
9
+ NO_DEFAULT,
10
+ PARAMETER_TYPE_PLACEHOLDER,
11
+ RETURN_TYPE_PLACEHOLDER,
12
+ Function,
13
+ Parameter,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from .converters.base import DocstringConverter
18
+
19
+
20
+ class DocstringVisitor(ast.NodeVisitor):
21
+ def __init__(
22
+ self, filename: str, converter: DocstringConverter | None = None
23
+ ) -> 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
36
+
37
+ def _extract_default_values(
38
+ self, default: ast.Constant | None | Literal[NO_DEFAULT], is_keyword_only: bool
39
+ ) -> str | Literal[NO_DEFAULT]:
40
+ if (not is_keyword_only and default is not NO_DEFAULT) or (
41
+ is_keyword_only and default
42
+ ):
43
+ try:
44
+ default_value = default.value
45
+ except AttributeError:
46
+ default_value = f'`{default.id}`'
47
+
48
+ return (
49
+ f'"{default_value}"'
50
+ if isinstance(default_value, str) and not default_value.startswith('`')
51
+ else default_value
52
+ )
53
+ return NO_DEFAULT
54
+
55
+ def extract_arguments(
56
+ self, node: ast.AsyncFunctionDef | ast.FunctionDef
57
+ ) -> tuple[Parameter, ...]:
58
+ modifiers = {
59
+ 'posonlyargs': 'positional-only',
60
+ 'kwonlyargs': 'keyword-only',
61
+ }
62
+ params = []
63
+
64
+ positional_arguments_count = len(node.args.posonlyargs) + len(node.args.args)
65
+ if (
66
+ default_count := len(positional_defaults := node.args.defaults)
67
+ ) < positional_arguments_count:
68
+ positional_defaults = [NO_DEFAULT] * (
69
+ positional_arguments_count - default_count
70
+ ) + positional_defaults
71
+
72
+ keyword_defaults = node.args.kw_defaults
73
+
74
+ processed_positional_args = 0
75
+ for arg_type, args in ast.iter_fields(node.args):
76
+ if arg_type.endswith('defaults'):
77
+ continue
78
+ modifier = modifiers.get(arg_type)
79
+ if arg_type in ['vararg', 'kwarg']:
80
+ if args:
81
+ params.append(
82
+ Parameter(
83
+ name=f'*{args.arg}'
84
+ if arg_type == 'vararg'
85
+ else f'**{args.arg}',
86
+ type_=getattr(
87
+ args.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER
88
+ ),
89
+ category=modifier,
90
+ default=NO_DEFAULT,
91
+ )
92
+ )
93
+ else:
94
+ is_keyword_only = arg_type.startswith('kw')
95
+ params.extend(
96
+ [
97
+ Parameter(
98
+ name=arg.arg,
99
+ type_=getattr(
100
+ arg.annotation, 'id', PARAMETER_TYPE_PLACEHOLDER
101
+ ),
102
+ category=modifier,
103
+ default=self._extract_default_values(
104
+ default, is_keyword_only
105
+ ),
106
+ )
107
+ for arg, default in zip(
108
+ args,
109
+ keyword_defaults
110
+ if is_keyword_only
111
+ else positional_defaults[processed_positional_args:],
112
+ )
113
+ ]
114
+ )
115
+ if not is_keyword_only:
116
+ processed_positional_args += len(args)
117
+
118
+ params = tuple(params)
119
+ if (
120
+ params
121
+ and isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
122
+ and params[0].name.startswith(('self', 'cls'))
123
+ ):
124
+ return params[1:]
125
+ return params
126
+
127
+ def extract_returns(
128
+ self, node: ast.AsyncFunctionDef | ast.FunctionDef
129
+ ) -> str | None:
130
+ if return_node := node.returns:
131
+ if isinstance(return_node, ast.Constant):
132
+ return return_node.value
133
+ if isinstance(return_node, ast.Name):
134
+ return return_node.id
135
+ return ast.get_source_segment(self.source_code, return_node)
136
+ if (
137
+ return_nodes := [
138
+ body_node
139
+ for body_node in node.body
140
+ if isinstance(body_node, ast.Return)
141
+ ]
142
+ ) and any(
143
+ not isinstance(return_value := body_return_node.value, ast.Constant)
144
+ or return_value.value
145
+ for body_return_node in return_nodes
146
+ ):
147
+ return RETURN_TYPE_PLACEHOLDER
148
+ return return_node
149
+
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
+ def suggest_docstring(
155
+ self, node: ast.AsyncFunctionDef | ast.ClassDef | ast.FunctionDef | ast.Module
156
+ ) -> str:
157
+ if isinstance(node, ast.AsyncFunctionDef | ast.FunctionDef):
158
+ return self.converter.to_function_docstring(
159
+ Function(self.extract_arguments(node), self.extract_returns(node))
160
+ )
161
+
162
+ if isinstance(node, ast.Module):
163
+ return self.converter.to_module_docstring(self.module_name)
164
+
165
+ # TODO: with the stack, I should be able to let __init__() not have a docstring
166
+ # and suggest those parameters as the docstring for the class
167
+ # (may need to keep the class node in another stack)
168
+ return self.converter.to_class_docstring(node.name)
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))
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: docstringify
3
+ Version: 0.1.0
4
+ Summary: Flag missing docstrings and, optionally, generate them from signatures and type annotations.
5
+ Author-email: Stefanie Molin <docstringify@stefaniemolin.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Stefanie Molin
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Documentation, https://github.com/stefmolin/docstringify
29
+ Project-URL: Homepage, https://github.com/stefmolin/docstringify
30
+ Project-URL: Source, https://github.com/stefmolin/docstringify
31
+ Keywords: ast,docstring generation,numpydoc-style docstring,pre-commit
32
+ Classifier: Development Status :: 2 - Pre-Alpha
33
+ Classifier: License :: OSI Approved :: Apache Software License
34
+ Classifier: Programming Language :: Python
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.8
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Requires-Python: >=3.8
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Provides-Extra: dev
46
+ Requires-Dist: pre-commit; extra == "dev"
47
+ Requires-Dist: pytest; extra == "dev"
48
+ Requires-Dist: pytest-cov; extra == "dev"
49
+ Requires-Dist: pytest-randomly; extra == "dev"
50
+ Dynamic: license-file
51
+
52
+ # docstringify
53
+ Flag missing docstrings and, optionally, generate them from signatures and type annotations.
54
+
55
+ ## Usage
56
+
57
+ ### Pre-commit hook
58
+
59
+ Add the following to your `.pre-commit-config.yaml` file to block commits with missing docstrings:
60
+
61
+ ```yaml
62
+ - repo: https://github.com/stefmolin/docstringify
63
+ rev: 0.1.0
64
+ hooks:
65
+ - id: docstringify
66
+ ```
67
+
68
+ By default, all docstrings are required. If you want to be more lenient, you can set the threshold, which is the percentage of docstrings that must be present:
69
+
70
+ ```yaml
71
+ - repo: https://github.com/stefmolin/docstringify
72
+ rev: 0.1.0
73
+ hooks:
74
+ - id: docstringify
75
+ args: [--threshold=0.75]
76
+ ```
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:
79
+
80
+ ```yaml
81
+ - repo: https://github.com/stefmolin/docstringify
82
+ rev: 0.1.0
83
+ hooks:
84
+ - id: docstringify
85
+ args: [--suggest-changes]
86
+ ```
87
+
88
+ Be sure to check out the [pre-commit documentation](https://pre-commit.com/#pre-commit-configyaml---hooks) for additional configuration options.
89
+
90
+ ### Command line
91
+
92
+ First, install the `docstringify` package from PyPI:
93
+
94
+ ```shell
95
+ $ python -m pip install docstringify
96
+ ```
97
+
98
+ Then, use the `docstringify` entry point on the file(s) of your choice:
99
+
100
+ ```shell
101
+ $ docstringify /path/to/file [/path/to/another/file]
102
+ ```
103
+
104
+ Run `docstringify --help` for more information.
105
+
106
+ ### Python
107
+
108
+ First, install the `docstringify` package from PyPI:
109
+
110
+ ```shell
111
+ $ python -m pip install docstringify
112
+ ```
113
+
114
+ Then, use the `DocstringVisitor()` class on individual files to see spots where docstrings are missing:
115
+
116
+ ```pycon
117
+ >>> from docstringify.visitor import DocstringVisitor
118
+ >>> visitor = DocstringVisitor('test.py')
119
+ >>> visitor.process_file()
120
+ test is missing a docstring
121
+ test.say_hello is missing a docstring
122
+ ```
123
+
124
+ If you would like to see suggested docstring templates (inferred from type annotations for functions and methods), provide a converter:
125
+
126
+ ```pycon
127
+ >>> from docstringify.converters.numpydoc import NumpydocDocstringConverter
128
+ >>> from docstringify.visitor import DocstringVisitor
129
+ >>> visitor = DocstringVisitor('test.py', converter=NumpydocDocstringConverter())
130
+ >>> visitor.process_file()
131
+ test is missing a docstring
132
+ Hint:
133
+ """__description__"""
134
+
135
+ test.say_hello is missing a docstring
136
+ Hint:
137
+ """
138
+ __description__
139
+
140
+ Parameters
141
+ ----------
142
+ name : str, default="World"
143
+ __description__
144
+ """
145
+
146
+ ```
@@ -0,0 +1,14 @@
1
+ docstringify/__init__.py,sha256=uXJ3o6quBR5skKRbCl3u8jRzjsmj6tGr1qb-3QdFf5Q,124
2
+ docstringify/__main__.py,sha256=A5Xq9jCZwHnQVVGklF9ueL-ad-hInsceHQNgze3Yq6E,96
3
+ docstringify/cli.py,sha256=QrEkav5pGGyCRx6w3cI4SOj4CcBx6YIRT48RVfOgFNo,2121
4
+ docstringify/components.py,sha256=7Q__cuBTWNAqLDTil_91Q7T-zIzr7YpbX2TcmXrfph0,471
5
+ docstringify/visitor.py,sha256=zY0nSAwVyIP1Qa0jkfCKLrKEKlIA0xQ6VtBB5R96V00,7024
6
+ docstringify/converters/__init__.py,sha256=vZE16UrOdY29rAkIH0HcFfr_FDhApua24CJoYM-xL6I,28
7
+ docstringify/converters/base.py,sha256=1QWFE5ckS_Rbwi9TyRM-rKCA7MCwHgZ4Ok8ugMyvMgA,1507
8
+ docstringify/converters/numpydoc.py,sha256=iv5smFrHFBc0rHvLhgh66PaV2OEI3va_piRZ3_y2Ig8,1773
9
+ docstringify-0.1.0.dist-info/licenses/LICENSE,sha256=E5RHv_qfCZPwnlJ_ZQb-WKOOy07LaJBA2a160OGtT3U,1071
10
+ docstringify-0.1.0.dist-info/METADATA,sha256=fsJmCbMOiorPafl732Vwa3y6czXWSdGHmL997u_jsBI,5142
11
+ docstringify-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
12
+ docstringify-0.1.0.dist-info/entry_points.txt,sha256=dJVPsbclB7stLE8g0d4WbO2NaZQLJXSLEVl6oeROAqI,55
13
+ docstringify-0.1.0.dist-info/top_level.txt,sha256=3RFmaFG0rYs4HkqRljatGQtY4BcNN7mqjK4tSznYdnQ,13
14
+ docstringify-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ docstringify = docstringify.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Stefanie Molin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ docstringify