docsig 0.57.0__tar.gz → 0.59.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: docsig
3
- Version: 0.57.0
3
+ Version: 0.59.0
4
4
  Summary: Check signature params for proper documentation
5
5
  Home-page: https://pypi.org/project/docsig/
6
6
  License: MIT
@@ -19,7 +19,6 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Requires-Dist: Sphinx (>=7.0.0,<8.0.0)
20
20
  Requires-Dist: arcon (>=0.4.0)
21
21
  Requires-Dist: astroid (>=3.0.1,<4.0.0)
22
- Requires-Dist: click (>=8.1.7,<9.0.0)
23
22
  Requires-Dist: pathspec (>=0.12.1,<0.13.0)
24
23
  Project-URL: Documentation, https://docsig.readthedocs.io/en/latest
25
24
  Project-URL: Repository, https://github.com/jshwi/docsig
@@ -170,7 +169,7 @@ ensure your installation has registered `docsig`
170
169
  .. code-block:: console
171
170
 
172
171
  $ flake8 --version
173
- 7.1.0 (docsig: 0.57.0, mccabe: 0.7.0, pycodestyle: 2.12.0, pyflakes: 3.2.0) CPython 3.8.13 on Darwin
172
+ 7.1.0 (docsig: 0.59.0, mccabe: 0.7.0, pycodestyle: 2.12.0, pyflakes: 3.2.0) CPython 3.8.13 on Darwin
174
173
 
175
174
  And now use `flake8` to lint your files
176
175
 
@@ -253,11 +252,13 @@ pre-commit
253
252
 
254
253
  It can be added to your .pre-commit-config.yaml as follows:
255
254
 
255
+ Standalone
256
+
256
257
  .. code-block:: yaml
257
258
 
258
259
  repos:
259
260
  - repo: https://github.com/jshwi/docsig
260
- rev: v0.57.0
261
+ rev: v0.59.0
261
262
  hooks:
262
263
  - id: docsig
263
264
  args:
@@ -266,3 +267,20 @@ It can be added to your .pre-commit-config.yaml as follows:
266
267
  - "--check-overridden"
267
268
  - "--check-protected"
268
269
 
270
+ or integrated with ``flake8``
271
+
272
+ .. code-block:: yaml
273
+
274
+ repos:
275
+ - repo: https://github.com/PyCQA/flake8
276
+ rev: "7.1.0"
277
+ hooks:
278
+ - id: flake8
279
+ additional_dependencies:
280
+ - docsig==0.59.0
281
+ args:
282
+ - "--sig-check-class"
283
+ - "--sig-check-dunders"
284
+ - "--sig-check-overridden"
285
+ - "--sig-check-protected"
286
+
@@ -143,7 +143,7 @@ ensure your installation has registered `docsig`
143
143
  .. code-block:: console
144
144
 
145
145
  $ flake8 --version
146
- 7.1.0 (docsig: 0.57.0, mccabe: 0.7.0, pycodestyle: 2.12.0, pyflakes: 3.2.0) CPython 3.8.13 on Darwin
146
+ 7.1.0 (docsig: 0.59.0, mccabe: 0.7.0, pycodestyle: 2.12.0, pyflakes: 3.2.0) CPython 3.8.13 on Darwin
147
147
 
148
148
  And now use `flake8` to lint your files
149
149
 
@@ -226,11 +226,13 @@ pre-commit
226
226
 
227
227
  It can be added to your .pre-commit-config.yaml as follows:
228
228
 
229
+ Standalone
230
+
229
231
  .. code-block:: yaml
230
232
 
231
233
  repos:
232
234
  - repo: https://github.com/jshwi/docsig
233
- rev: v0.57.0
235
+ rev: v0.59.0
234
236
  hooks:
235
237
  - id: docsig
236
238
  args:
@@ -238,3 +240,20 @@ It can be added to your .pre-commit-config.yaml as follows:
238
240
  - "--check-dunders"
239
241
  - "--check-overridden"
240
242
  - "--check-protected"
243
+
244
+ or integrated with ``flake8``
245
+
246
+ .. code-block:: yaml
247
+
248
+ repos:
249
+ - repo: https://github.com/PyCQA/flake8
250
+ rev: "7.1.0"
251
+ hooks:
252
+ - id: flake8
253
+ additional_dependencies:
254
+ - docsig==0.59.0
255
+ args:
256
+ - "--sig-check-class"
257
+ - "--sig-check-dunders"
258
+ - "--sig-check-overridden"
259
+ - "--sig-check-protected"
@@ -0,0 +1,419 @@
1
+ """
2
+ docsig._core
3
+ ============
4
+ """
5
+
6
+ from __future__ import annotations as _
7
+
8
+ import sys as _sys
9
+ from pathlib import Path as _Path
10
+
11
+ import astroid as _ast
12
+
13
+ from . import _decorators
14
+ from ._directives import Directives as _Directives
15
+ from ._files import FILE_INFO as _FILE_INFO
16
+ from ._files import Paths as _Paths
17
+ from ._module import Ast as _Ast
18
+ from ._module import Function as _Function
19
+ from ._module import Parent as _Parent
20
+ from ._report import Failure as _Failure
21
+ from ._report import Failures as _Failures
22
+ from ._utils import pretty_print_error as _pretty_print_error
23
+ from ._utils import print_checks as _print_checks
24
+ from ._utils import vprint as _vprint
25
+ from .messages import TEMPLATE as _TEMPLATE
26
+ from .messages import Messages as _Messages
27
+
28
+ _DEFAULT_EXCLUDES = """\
29
+ (?x)^(
30
+ |\\.?venv
31
+ |\\.git
32
+ |\\.hg
33
+ |\\.idea
34
+ |\\.mypy_cache
35
+ |\\.nox
36
+ |\\.pytest_cache
37
+ |\\.svn
38
+ |\\.tox
39
+ |\\.vscode
40
+ |_?build
41
+ |__pycache__
42
+ |dist
43
+ |node_modules
44
+ )$
45
+ """
46
+
47
+
48
+ def _run_check( # pylint: disable=too-many-arguments,too-many-locals
49
+ child: _Parent,
50
+ parent: _Parent,
51
+ check_class: bool,
52
+ check_class_constructor: bool,
53
+ check_dunders: bool,
54
+ check_nested: bool,
55
+ check_overridden: bool,
56
+ check_protected: bool,
57
+ check_property_returns: bool,
58
+ ignore_no_params: bool,
59
+ ignore_typechecker: bool,
60
+ no_ansi: bool,
61
+ target: _Messages,
62
+ failures: _Failures,
63
+ ) -> None:
64
+ if isinstance(child, _Function):
65
+ if not (child.isoverridden and not check_overridden) and (
66
+ not (child.isprotected and not check_protected)
67
+ and not (
68
+ child.isinit
69
+ and not (
70
+ (check_class or check_class_constructor)
71
+ and not (parent.isprotected and not check_protected)
72
+ )
73
+ )
74
+ and not (child.isdunder and not check_dunders)
75
+ and not (child.docstring.bare and ignore_no_params)
76
+ ):
77
+ failure = _Failure(
78
+ child, target, check_property_returns, ignore_typechecker
79
+ )
80
+ if failure:
81
+ failures.append(failure)
82
+
83
+ if check_nested:
84
+ for func in child:
85
+ _run_check(
86
+ func,
87
+ child,
88
+ check_class,
89
+ check_class_constructor,
90
+ check_dunders,
91
+ check_nested,
92
+ check_overridden,
93
+ check_protected,
94
+ check_property_returns,
95
+ ignore_no_params,
96
+ ignore_typechecker,
97
+ no_ansi,
98
+ target,
99
+ failures,
100
+ )
101
+ else:
102
+ # this is a class
103
+ for func in child:
104
+ _run_check(
105
+ func,
106
+ child,
107
+ check_class,
108
+ check_class_constructor,
109
+ check_dunders,
110
+ check_nested,
111
+ check_overridden,
112
+ check_protected,
113
+ check_property_returns,
114
+ ignore_no_params,
115
+ ignore_typechecker,
116
+ no_ansi,
117
+ target,
118
+ failures,
119
+ )
120
+
121
+
122
+ def _get_failures( # pylint: disable=too-many-locals,too-many-arguments
123
+ module: _Parent,
124
+ check_class: bool,
125
+ check_class_constructor: bool,
126
+ check_dunders: bool,
127
+ check_nested: bool,
128
+ check_overridden: bool,
129
+ check_protected: bool,
130
+ check_property_returns: bool,
131
+ ignore_no_params: bool,
132
+ ignore_typechecker: bool,
133
+ check_protected_class_methods: bool,
134
+ no_ansi: bool,
135
+ target: _Messages,
136
+ ) -> _Failures:
137
+ failures = _Failures()
138
+ for top_level in module:
139
+ if (
140
+ not top_level.isprotected
141
+ or check_protected
142
+ or check_protected_class_methods
143
+ ):
144
+ _run_check(
145
+ top_level,
146
+ module,
147
+ check_class,
148
+ check_class_constructor,
149
+ check_dunders,
150
+ check_nested,
151
+ check_overridden,
152
+ check_protected,
153
+ check_property_returns,
154
+ ignore_no_params,
155
+ ignore_typechecker,
156
+ no_ansi,
157
+ target or _Messages(),
158
+ failures,
159
+ )
160
+
161
+ return failures
162
+
163
+
164
+ def _report(
165
+ failures: _Failures, path: str | None = None, no_ansi: bool = False
166
+ ) -> None:
167
+ for failure in failures:
168
+ module = f"{path}:" if path is not None else ""
169
+ header = f"{module}{failure.lineno} in {failure.name}"
170
+ if not no_ansi and _sys.stdout.isatty():
171
+ header = f"\033[35m{header}\033[0m"
172
+
173
+ print(header)
174
+ for item in failure:
175
+ print(
176
+ " "
177
+ + _TEMPLATE.format(
178
+ ref=item.ref,
179
+ description=item.description,
180
+ symbolic=item.symbolic,
181
+ )
182
+ )
183
+ if item.hint:
184
+ print(f" hint: {item.hint}")
185
+
186
+
187
+ def runner( # pylint: disable=too-many-locals,too-many-arguments
188
+ file: str | _Path,
189
+ disable: _Messages | None = None,
190
+ check_class: bool = False,
191
+ check_class_constructor: bool = False,
192
+ check_dunders: bool = False,
193
+ check_nested: bool = False,
194
+ check_overridden: bool = False,
195
+ check_protected: bool = False,
196
+ check_property_returns: bool = False,
197
+ ignore_no_params: bool = False,
198
+ ignore_args: bool = False,
199
+ ignore_kwargs: bool = False,
200
+ ignore_typechecker: bool = False,
201
+ check_protected_class_methods: bool = False,
202
+ no_ansi: bool = False,
203
+ verbose: bool = False,
204
+ target: _Messages | None = None,
205
+ ) -> tuple[_Failures, int]:
206
+ """Per path runner.
207
+
208
+ :param file: Path to check.
209
+ :param disable: Messages to disable.
210
+ :param check_class: Check class docstrings.
211
+ :param check_class_constructor: Check ``__init__`` methods. Note that this
212
+ is mutually incompatible with check_class.
213
+ :param check_dunders: Check dunder methods
214
+ :param check_nested: Check nested functions and classes.
215
+ :param check_overridden: Check overridden methods
216
+ :param check_protected: Check protected functions and classes.
217
+ :param check_property_returns: Run return checks on properties.
218
+ :param ignore_no_params: Ignore docstrings where parameters are not
219
+ documented
220
+ :param ignore_args: Ignore args prefixed with an asterisk.
221
+ :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
222
+ :param ignore_typechecker: Ignore checking return values.
223
+ :param check_protected_class_methods: Check public methods belonging
224
+ to protected classes.
225
+ :param no_ansi: Disable ANSI output.
226
+ :param verbose: increase output verbosity.
227
+ :param target: List of errors to target.
228
+ :return: Exit status for whether test failed or not.
229
+ """
230
+ failures = _Failures()
231
+ path = _Path(file)
232
+ string = path.read_text(encoding="utf-8")
233
+ ast = _Ast.parse(string)
234
+ if ast.success:
235
+ _vprint(
236
+ _FILE_INFO.format(path=path, msg="Parsing Python code successful"),
237
+ verbose,
238
+ )
239
+ module = _Parent(
240
+ ast.module,
241
+ _Directives(string, disable or _Messages()),
242
+ path,
243
+ ignore_args,
244
+ ignore_kwargs,
245
+ check_class_constructor,
246
+ )
247
+ failures = _get_failures(
248
+ module,
249
+ check_class,
250
+ check_class_constructor,
251
+ check_dunders,
252
+ check_nested,
253
+ check_overridden,
254
+ check_protected,
255
+ check_property_returns,
256
+ ignore_no_params,
257
+ ignore_typechecker,
258
+ check_protected_class_methods,
259
+ no_ansi,
260
+ target or _Messages(),
261
+ )
262
+ return failures, 0
263
+
264
+ if path.name.endswith(".py"):
265
+ # pass by silently for files that do not end with .py, may
266
+ # result in a 123 syntax error exit status in the future
267
+ print(path, file=_sys.stderr)
268
+ _pretty_print_error(type(ast.err), str(ast.err), no_ansi=no_ansi)
269
+ return failures, 1
270
+
271
+ _vprint(
272
+ _FILE_INFO.format(path=path, msg=str(ast.err)),
273
+ verbose,
274
+ )
275
+ return failures, 0
276
+
277
+
278
+ @_decorators.parse_msgs
279
+ @_decorators.handle_deprecations
280
+ @_decorators.validate_args
281
+ def docsig( # pylint: disable=too-many-locals,too-many-arguments
282
+ *path: str | _Path,
283
+ string: str | None = None,
284
+ list_checks: bool = False,
285
+ check_class: bool = False,
286
+ check_class_constructor: bool = False,
287
+ check_dunders: bool = False,
288
+ check_protected_class_methods: bool = False,
289
+ check_nested: bool = False,
290
+ check_overridden: bool = False,
291
+ check_protected: bool = False,
292
+ check_property_returns: bool = False,
293
+ include_ignored: bool = False,
294
+ ignore_no_params: bool = False,
295
+ ignore_args: bool = False,
296
+ ignore_kwargs: bool = False,
297
+ ignore_typechecker: bool = False,
298
+ no_ansi: bool = False,
299
+ verbose: bool = False,
300
+ target: _Messages | None = None,
301
+ disable: _Messages | None = None,
302
+ exclude: str | None = None,
303
+ ) -> int:
304
+ """Package's core functionality.
305
+
306
+ Populate a sequence of module objects before iterating over their
307
+ top-level functions and classes.
308
+
309
+ If any of the functions within the module - and methods within its
310
+ classes - fail, print the resulting function string representation
311
+ and report.
312
+
313
+ :param path: Path(s) to check.
314
+ :param string: String to check.
315
+ :param list_checks: Display a list of all checks and their messages.
316
+ :param check_class: Check class docstrings.
317
+ :param check_class_constructor: Check ``__init__`` methods. Note that this
318
+ is mutually incompatible with check_class.
319
+ :param check_dunders: Check dunder methods
320
+ :param check_protected_class_methods: Check public methods belonging
321
+ to protected classes.
322
+ :param check_nested: Check nested functions and classes.
323
+ :param check_overridden: Check overridden methods
324
+ :param check_protected: Check protected functions and classes.
325
+ :param check_property_returns: Run return checks on properties.
326
+ :param include_ignored: Check files even if they match a gitignore
327
+ pattern.
328
+ :param ignore_no_params: Ignore docstrings where parameters are not
329
+ documented
330
+ :param ignore_args: Ignore args prefixed with an asterisk.
331
+ :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
332
+ :param ignore_typechecker: Ignore checking return values.
333
+ :param no_ansi: Disable ANSI output.
334
+ :param verbose: increase output verbosity.
335
+ :param target: List of errors to target.
336
+ :param disable: List of errors to disable.
337
+ :param exclude: Regular expression of files and dirs to exclude from
338
+ checks.
339
+ :return: Exit status for whether test failed or not.
340
+ """
341
+ if list_checks:
342
+ return int(bool(_print_checks())) # type: ignore
343
+
344
+ excludes = [_DEFAULT_EXCLUDES]
345
+ if exclude is not None:
346
+ excludes.append(exclude)
347
+
348
+ if string is None:
349
+ retcode = 0
350
+ paths = _Paths(
351
+ *tuple(_Path(i) for i in path),
352
+ excludes=excludes,
353
+ include_ignored=include_ignored,
354
+ verbose=verbose,
355
+ )
356
+ for file in paths:
357
+ failures, parse_retcode = runner(
358
+ file,
359
+ disable,
360
+ check_class,
361
+ check_class_constructor,
362
+ check_dunders,
363
+ check_nested,
364
+ check_overridden,
365
+ check_protected,
366
+ check_property_returns,
367
+ ignore_no_params,
368
+ ignore_args,
369
+ ignore_kwargs,
370
+ ignore_typechecker,
371
+ check_protected_class_methods,
372
+ no_ansi,
373
+ verbose,
374
+ target,
375
+ )
376
+ retcode = retcode or parse_retcode
377
+ if failures:
378
+ _report(failures, str(file), no_ansi)
379
+ retcode = 1
380
+
381
+ return retcode
382
+
383
+ ast = _Ast.parse(string)
384
+ if ast.success:
385
+ _vprint(
386
+ _FILE_INFO.format(
387
+ path="stdin", msg="Parsing Python code successful"
388
+ ),
389
+ verbose,
390
+ )
391
+ module = _Parent(
392
+ _ast.parse(string),
393
+ _Directives(string, messages=disable or _Messages()),
394
+ ignore_args=ignore_args,
395
+ ignore_kwargs=ignore_kwargs,
396
+ check_class_constructor=check_class_constructor,
397
+ )
398
+ failures = _get_failures(
399
+ module,
400
+ check_class,
401
+ check_class_constructor,
402
+ check_dunders,
403
+ check_nested,
404
+ check_overridden,
405
+ check_protected,
406
+ check_property_returns,
407
+ ignore_no_params,
408
+ ignore_typechecker,
409
+ check_protected_class_methods,
410
+ no_ansi,
411
+ target or _Messages(),
412
+ )
413
+ if failures:
414
+ _report(failures, no_ansi=no_ansi)
415
+ return 1
416
+
417
+ _vprint(_FILE_INFO.format(path="stdin", msg=str(ast.err)), verbose)
418
+
419
+ return 0
@@ -0,0 +1,118 @@
1
+ """
2
+ docsig._files
3
+ =============
4
+ """
5
+
6
+ from __future__ import annotations as _
7
+
8
+ import os as _os
9
+ import re as _re
10
+ import typing as _t
11
+ from pathlib import Path as _Path
12
+
13
+ from pathspec import PathSpec as _PathSpec
14
+ from pathspec.patterns import GitWildMatchPattern as _GitWildMatchPattern
15
+
16
+ from ._utils import vprint as _vprint
17
+
18
+ FILE_INFO = "{path}: {msg}"
19
+
20
+
21
+ class _Gitignore(_PathSpec):
22
+ def _get_repo_relative_to(self, path: _Path) -> _Path | None:
23
+ if (path / ".git" / "HEAD").is_file():
24
+ return path
25
+
26
+ if str(path) == _os.path.abspath(_os.sep):
27
+ return None
28
+
29
+ return self._get_repo_relative_to(path.parent)
30
+
31
+ def __init__(self) -> None:
32
+ patterns = []
33
+ repo = self._get_repo_relative_to(_Path.cwd())
34
+ # only consider gitignore patterns valid if inside a git repo
35
+ # there might be stray gitignore files lying about
36
+ if repo is not None:
37
+ # add patterns from all gitignore files
38
+ # adjust patterns to account for their relative paths
39
+ for file in repo.rglob(".gitignore"):
40
+ for pattern in file.read_text(encoding="utf-8").splitlines():
41
+ if pattern.startswith("#"):
42
+ continue
43
+
44
+ # if pattern starts with "/" then os.path.join will
45
+ # consider it in the filesystem root, and it will
46
+ # only ever return /pattern
47
+ if pattern.startswith("/"):
48
+ pattern = pattern[1:]
49
+
50
+ # use os.path.dirname, so it joins without a leading
51
+ # "./", like it does with pathlib parent
52
+ # use os.path.join so trailing slash is preserved
53
+ # replace sep with "/" as, even on windows,
54
+ # gitignore patterns only ever use "/"
55
+ patterns.append(
56
+ _os.path.join(
57
+ _os.path.dirname(file.relative_to(repo)),
58
+ pattern.strip(),
59
+ ).replace(_os.sep, "/")
60
+ )
61
+
62
+ super().__init__(map(_GitWildMatchPattern, patterns))
63
+
64
+
65
+ class Paths(_t.List[_Path]): # pylint: disable=too-many-instance-attributes
66
+ """Collect a list of valid paths.
67
+
68
+ :param paths: Path(s) to parse ``Module``(s) from.
69
+ :param excludes: List pf regular expression of files and dirs to
70
+ exclude from checks.
71
+ :param include_ignored: Check files even if they match a gitignore
72
+ pattern.
73
+ :param verbose: increase output verbosity.
74
+ """
75
+
76
+ def __init__( # pylint: disable=too-many-arguments
77
+ self,
78
+ *paths: _Path,
79
+ excludes: list[str],
80
+ include_ignored: bool = False,
81
+ verbose: bool = False,
82
+ ) -> None:
83
+ super().__init__()
84
+ self._excludes = excludes
85
+ self._include_ignored = include_ignored
86
+ self._verbose = verbose
87
+ self._gitignore = _Gitignore()
88
+ for path in paths:
89
+ self._populate(path)
90
+
91
+ self.sort()
92
+
93
+ def _populate(self, root: _Path) -> None:
94
+ if not root.exists():
95
+ raise FileNotFoundError(root)
96
+
97
+ if str(root) != "." and any(
98
+ _re.match(i, root.name) for i in self._excludes
99
+ ):
100
+ _vprint(
101
+ FILE_INFO.format(path=root, msg="in exclude list, skipping"),
102
+ self._verbose,
103
+ )
104
+ return
105
+
106
+ if not self._include_ignored and self._gitignore.match_file(root):
107
+ _vprint(
108
+ FILE_INFO.format(path=root, msg="in gitignore, skipping"),
109
+ self._verbose,
110
+ )
111
+ return
112
+
113
+ if root.is_file():
114
+ self.append(root)
115
+
116
+ if root.is_dir():
117
+ for path in root.iterdir():
118
+ self._populate(path)