docsig 0.91.0__tar.gz → 0.91.1__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 (30) hide show
  1. {docsig-0.91.0 → docsig-0.91.1}/PKG-INFO +8 -8
  2. {docsig-0.91.0 → docsig-0.91.1}/README.rst +7 -7
  3. docsig-0.91.0/docsig/_report.py → docsig-0.91.1/docsig/_checker.py +19 -207
  4. {docsig-0.91.0 → docsig-0.91.1}/docsig/_core.py +3 -3
  5. {docsig-0.91.0 → docsig-0.91.1}/docsig/_decorators.py +1 -1
  6. docsig-0.91.1/docsig/_diagnostic.py +155 -0
  7. docsig-0.91.1/docsig/_report.py +94 -0
  8. {docsig-0.91.0 → docsig-0.91.1}/docsig/_stub.py +2 -16
  9. docsig-0.91.0/docsig/_check.py → docsig-0.91.1/docsig/_traverse.py +3 -3
  10. {docsig-0.91.0 → docsig-0.91.1}/docsig/_version.py +1 -1
  11. {docsig-0.91.0 → docsig-0.91.1}/pyproject.toml +2 -2
  12. docsig-0.91.0/docsig/_diagnostic.py +0 -21
  13. {docsig-0.91.0 → docsig-0.91.1}/LICENSE +0 -0
  14. {docsig-0.91.0 → docsig-0.91.1}/LICENSES/SPHINX_LICENSE +0 -0
  15. {docsig-0.91.0 → docsig-0.91.1}/docsig/__init__.py +0 -0
  16. {docsig-0.91.0 → docsig-0.91.1}/docsig/__main__.py +0 -0
  17. {docsig-0.91.0 → docsig-0.91.1}/docsig/_config.py +0 -0
  18. {docsig-0.91.0 → docsig-0.91.1}/docsig/_directives.py +0 -0
  19. {docsig-0.91.0 → docsig-0.91.1}/docsig/_files.py +0 -0
  20. {docsig-0.91.0 → docsig-0.91.1}/docsig/_hooks.py +0 -0
  21. {docsig-0.91.0 → docsig-0.91.1}/docsig/_main.py +0 -0
  22. {docsig-0.91.0 → docsig-0.91.1}/docsig/_module.py +0 -0
  23. {docsig-0.91.0 → docsig-0.91.1}/docsig/_parsers.py +0 -0
  24. {docsig-0.91.0 → docsig-0.91.1}/docsig/_utils.py +0 -0
  25. {docsig-0.91.0 → docsig-0.91.1}/docsig/_vendor/sphinx/ext/napoleon.py +0 -0
  26. {docsig-0.91.0 → docsig-0.91.1}/docsig/messages.py +0 -0
  27. {docsig-0.91.0 → docsig-0.91.1}/docsig/plugin/__init__.py +0 -0
  28. {docsig-0.91.0 → docsig-0.91.1}/docsig/plugin/_flake8.py +0 -0
  29. {docsig-0.91.0 → docsig-0.91.1}/docsig/plugin/_validate_pyproject.py +0 -0
  30. {docsig-0.91.0 → docsig-0.91.1}/docsig/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docsig
3
- Version: 0.91.0
3
+ Version: 0.91.1
4
4
  Summary: Check signature params for proper documentation
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -59,7 +59,7 @@ Description-Content-Type: text/x-rst
59
59
  :target: https://docsig.io/en/latest/?badge=latest
60
60
  :alt: readthedocs.org
61
61
  .. |python3.10| image:: https://img.shields.io/badge/python-3.10-blue.svg
62
- :target: https://www.python.org/downloads/release/python-390
62
+ :target: https://www.python.org/downloads/release/python-310
63
63
  :alt: python3.10
64
64
  .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
65
65
  :target: https://github.com/psf/black
@@ -80,7 +80,7 @@ Description-Content-Type: text/x-rst
80
80
  :target: https://github.com/jshwi/docsig/actions/workflows/build-intellij-plugin.yaml
81
81
  :alt: Build
82
82
  .. |VS Code Build| image:: https://github.com/jshwi/docsig/actions/workflows/build-vscode-extension.yaml/badge.svg
83
- :target: https://github.com/jshwi/docsig/actions/workflows/build-vscode-extension.yaml.yaml
83
+ :target: https://github.com/jshwi/docsig/actions/workflows/build-vscode-extension.yaml
84
84
  :alt: Build
85
85
  .. |VS Code Extension| image:: https://img.shields.io/badge/vscode%20extension-v1.0.1-007ACC
86
86
  :target: https://marketplace.visualstudio.com/items?itemName=jshwi.docsig
@@ -125,14 +125,14 @@ Commandline
125
125
  [--check-protected-class-methods] [--ignore-args] [--ignore-kwargs]
126
126
  [--ignore-no-params] [-d LIST] [-t LIST] [-e PATTERN] [-E PATH [PATH ...]]
127
127
  [-i] [-s STR]
128
- [path [path ...]]
128
+ [path ...]
129
129
 
130
130
  Check signature params for proper documentation
131
131
 
132
132
  positional arguments:
133
133
  path directories or files to check
134
134
 
135
- optional arguments:
135
+ options:
136
136
  -h, --help show this help message and exit
137
137
  -V, --version show program's version number and exit
138
138
  -l, --list-checks display a list of all checks and their messages
@@ -206,7 +206,7 @@ ensure your installation has registered `docsig`
206
206
  .. code-block:: console
207
207
 
208
208
  $ flake8 --version
209
- 7.3.0 (docsig: 0.91.0, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
209
+ 7.3.0 (docsig: 0.91.1, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
210
210
 
211
211
  And now use `flake8` to lint your files
212
212
 
@@ -292,7 +292,7 @@ Standalone
292
292
 
293
293
  repos:
294
294
  - repo: https://github.com/jshwi/docsig
295
- rev: v0.91.0
295
+ rev: v0.91.1
296
296
  hooks:
297
297
  - id: docsig
298
298
  args:
@@ -311,7 +311,7 @@ or integrated with ``flake8``
311
311
  hooks:
312
312
  - id: flake8
313
313
  additional_dependencies:
314
- - docsig==0.91.0
314
+ - docsig==0.91.1
315
315
  args:
316
316
  - "--sig-check-class"
317
317
  - "--sig-check-dunders"
@@ -31,7 +31,7 @@
31
31
  :target: https://docsig.io/en/latest/?badge=latest
32
32
  :alt: readthedocs.org
33
33
  .. |python3.10| image:: https://img.shields.io/badge/python-3.10-blue.svg
34
- :target: https://www.python.org/downloads/release/python-390
34
+ :target: https://www.python.org/downloads/release/python-310
35
35
  :alt: python3.10
36
36
  .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
37
37
  :target: https://github.com/psf/black
@@ -52,7 +52,7 @@
52
52
  :target: https://github.com/jshwi/docsig/actions/workflows/build-intellij-plugin.yaml
53
53
  :alt: Build
54
54
  .. |VS Code Build| image:: https://github.com/jshwi/docsig/actions/workflows/build-vscode-extension.yaml/badge.svg
55
- :target: https://github.com/jshwi/docsig/actions/workflows/build-vscode-extension.yaml.yaml
55
+ :target: https://github.com/jshwi/docsig/actions/workflows/build-vscode-extension.yaml
56
56
  :alt: Build
57
57
  .. |VS Code Extension| image:: https://img.shields.io/badge/vscode%20extension-v1.0.1-007ACC
58
58
  :target: https://marketplace.visualstudio.com/items?itemName=jshwi.docsig
@@ -97,14 +97,14 @@ Commandline
97
97
  [--check-protected-class-methods] [--ignore-args] [--ignore-kwargs]
98
98
  [--ignore-no-params] [-d LIST] [-t LIST] [-e PATTERN] [-E PATH [PATH ...]]
99
99
  [-i] [-s STR]
100
- [path [path ...]]
100
+ [path ...]
101
101
 
102
102
  Check signature params for proper documentation
103
103
 
104
104
  positional arguments:
105
105
  path directories or files to check
106
106
 
107
- optional arguments:
107
+ options:
108
108
  -h, --help show this help message and exit
109
109
  -V, --version show program's version number and exit
110
110
  -l, --list-checks display a list of all checks and their messages
@@ -178,7 +178,7 @@ ensure your installation has registered `docsig`
178
178
  .. code-block:: console
179
179
 
180
180
  $ flake8 --version
181
- 7.3.0 (docsig: 0.91.0, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
181
+ 7.3.0 (docsig: 0.91.1, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
182
182
 
183
183
  And now use `flake8` to lint your files
184
184
 
@@ -264,7 +264,7 @@ Standalone
264
264
 
265
265
  repos:
266
266
  - repo: https://github.com/jshwi/docsig
267
- rev: v0.91.0
267
+ rev: v0.91.1
268
268
  hooks:
269
269
  - id: docsig
270
270
  args:
@@ -283,7 +283,7 @@ or integrated with ``flake8``
283
283
  hooks:
284
284
  - id: flake8
285
285
  additional_dependencies:
286
- - docsig==0.91.0
286
+ - docsig==0.91.1
287
287
  args:
288
288
  - "--sig-check-class"
289
289
  - "--sig-check-dunders"
@@ -1,21 +1,19 @@
1
1
  """
2
- docsig._report
3
- ==============
2
+ docsig._checker
3
+ ===============
4
4
 
5
- Format and print docstring-check diagnostics for CLI and tooling.
5
+ Run docstring and signature checks for a single function.
6
6
  """
7
7
 
8
8
  import contextlib as _contextlib
9
- import json as _json
10
- import os as _os
11
- import sys as _sys
12
9
  import typing as _t
13
- from warnings import warn as _warn
14
10
 
15
11
  import astroid as _ast
16
12
 
17
13
  from ._config import Config as _Config
14
+ from ._diagnostic import Collector as _Collector
18
15
  from ._diagnostic import Diagnostic as _Diagnostic
16
+ from ._diagnostic import FunctionResult as _FunctionResult
19
17
  from ._module import Function as _Function
20
18
  from ._stub import UNNAMED as _UNNAMED
21
19
  from ._stub import VALID_DESCRIPTION as _VALID_DESCRIPTION
@@ -24,8 +22,6 @@ from ._stub import Params as _Params
24
22
  from ._stub import RetType as _RetType
25
23
  from ._utils import almost_equal as _almost_equal
26
24
  from ._utils import sentence_tokenizer as _sentence_tokenizer
27
- from .messages import NEW as _NEW
28
- from .messages import TEMPLATE as _TEMPLATE
29
25
  from .messages import E as _E
30
26
  from .messages import Message as _Message
31
27
 
@@ -33,42 +29,6 @@ _MIN_MATCH = 0.8
33
29
  _MAX_MATCH = 1.0
34
30
 
35
31
 
36
- def check_function(func: _Function, config: _Config) -> "_FunctionResult":
37
- """Run configured checks for one function and return the result.
38
-
39
- :param func: Function under check.
40
- :param config: Configuration object.
41
- :return: Collected diagnostics for the function.
42
- """
43
- return FunctionChecker(func, config).run()
44
-
45
-
46
- class RetCode:
47
- """RetCode object.
48
-
49
- :param code: Initial return code, if any, otherwise zero.
50
- """
51
-
52
- def __init__(self, code: int = 0) -> None:
53
- self._data = [code]
54
-
55
- def add(self, code: int) -> None:
56
- """Add a return code.
57
-
58
- :param code: Return code to add.
59
- """
60
- self._data.append(code)
61
-
62
- @property
63
- def result(self) -> int:
64
- """Maximum return code."""
65
- return max(self._data)
66
-
67
-
68
- class Failures(list["_FunctionResult"]):
69
- """Sequence of result instances (one per function checked)."""
70
-
71
-
72
32
  class FunctionChecker: # pylint: disable=too-few-public-methods
73
33
  """Collect docstring and signature failures for one function.
74
34
 
@@ -99,7 +59,7 @@ class FunctionChecker: # pylint: disable=too-few-public-methods
99
59
 
100
60
  self._collector = _Collector(func, self._name, self._func.lineno)
101
61
 
102
- def run(self) -> "_FunctionResult":
62
+ def run(self) -> _FunctionResult:
103
63
  """Run the function checks and return the result.
104
64
 
105
65
  :return: Function result.
@@ -211,15 +171,17 @@ class FunctionChecker: # pylint: disable=too-few-public-methods
211
171
 
212
172
  def _sig2xx_signature(self) -> None:
213
173
  if self._func.docstring.args.duplicated:
214
- # pop the duplicates so that they are considered a single
215
- # parameter, that way there are no assumptions that the
216
- # parameters must be out of order
217
- for count, arg in enumerate(self._func.docstring.args):
218
- if (
219
- arg in self._func.docstring.args.duplicates
220
- and self._func.docstring.args.count(arg) > 1
221
- ):
222
- self._func.docstring.args.pop(count)
174
+ # reduce each duplicate group to one representative without
175
+ # mutating the list during iteration (which skips items)
176
+ seen: set[str | None] = set()
177
+ deduped: list[_Param] = []
178
+ for arg in self._func.docstring.args:
179
+ if arg.name not in seen:
180
+ seen.add(arg.name)
181
+ deduped.append(arg)
182
+
183
+ list.clear(self._func.docstring.args)
184
+ list.extend(self._func.docstring.args, deduped)
223
185
 
224
186
  # duplicate-params-found
225
187
  self._add(_E[201])
@@ -277,6 +239,8 @@ class FunctionChecker: # pylint: disable=too-few-public-methods
277
239
  not in (
278
240
  "`",
279
241
  ".",
242
+ "!",
243
+ "?",
280
244
  )
281
245
  ):
282
246
  self._add(_E[306])
@@ -361,155 +325,3 @@ class FunctionChecker: # pylint: disable=too-few-public-methods
361
325
  # duplicates-found-in-mros
362
326
  if self._func.error is _ast.DuplicateBasesError:
363
327
  self._add(_E[904])
364
-
365
-
366
- class _Collector:
367
- def __init__(
368
- self,
369
- func: _Function,
370
- qualified_name: str,
371
- lineno: int,
372
- ) -> None:
373
- self._func = func
374
- self._qualified_name = qualified_name
375
- self._lineno = lineno
376
- self._diagnostics: list[_Diagnostic] = []
377
- self._retcode = RetCode()
378
-
379
- def add(
380
- self,
381
- value: _Message,
382
- include_hint: bool = False,
383
- **kwargs: _t.Any,
384
- ) -> None:
385
- """Add a diagnostic message.
386
-
387
- :param value: Message to add.
388
- :param include_hint: Whether to include the hint.
389
- :param kwargs: Additional arguments to format the description.
390
- """
391
- self._retcode.add(int(not value.new))
392
- diagnostic = _Diagnostic(
393
- self._qualified_name,
394
- value.ref,
395
- value.description.format(**kwargs),
396
- value.symbolic,
397
- self._lineno,
398
- value.hint if include_hint else None,
399
- value.new,
400
- )
401
- if (
402
- value not in self._func.messages
403
- and diagnostic not in self._diagnostics
404
- ):
405
- self._diagnostics.append(diagnostic)
406
-
407
- @property
408
- def diagnostics(self) -> list[_Diagnostic]:
409
- """Diagnostics sorted for stable output."""
410
- return sorted(self._diagnostics)
411
-
412
- @property
413
- def retcode(self) -> RetCode:
414
- """Exit code (non-zero if any check failed)."""
415
- return self._retcode
416
-
417
- def __bool__(self) -> bool:
418
- return bool(self._diagnostics)
419
-
420
-
421
- class _FunctionResult:
422
- def __init__(
423
- self,
424
- name: str,
425
- lineno: int,
426
- collector: _Collector,
427
- ) -> None:
428
- self._name = name
429
- self._lineno = lineno
430
- self._collector = collector
431
-
432
- @property
433
- def name(self) -> str:
434
- """Qualified name (Class.method) when nested, else bare name."""
435
- return self._name
436
-
437
- @property
438
- def lineno(self) -> int:
439
- """Line number of the function in the source."""
440
- return self._lineno
441
-
442
- @property
443
- def retcode(self) -> int:
444
- """Exit code (non-zero if any check failed)."""
445
- return self._collector.retcode.result
446
-
447
- def __iter__(self) -> _t.Iterator[_Diagnostic]:
448
- return iter(self._collector.diagnostics)
449
-
450
- def __bool__(self) -> bool:
451
- return bool(self._collector)
452
-
453
-
454
- # TODO: make report json by default and wrap with a reporter for cli
455
- def report(
456
- failures: Failures,
457
- config: _Config,
458
- file: str | None = None,
459
- ) -> int:
460
- """Print failures and return the highest exit code.
461
-
462
- Iterates over failures, prints each with path, line header, and
463
- messages, then returns the maximum retcode (0 or non-zero).
464
-
465
- :param failures: Failures to print (one FunctionResult per
466
- function).
467
- :param config: Config for ANSI and formatting.
468
- :param file: Module path when failures came from a file (optional).
469
- :return: Exit code (non-zero if any check failed).
470
- """
471
- format_json = _os.getenv("_DOCSIG_FORMAT_JSON") is not None
472
- retcodes = RetCode()
473
- output = []
474
- obj = []
475
- for result in failures:
476
- retcodes.add(result.retcode)
477
- path_prefix = f"{file}:" if file is not None else ""
478
- header = f"{path_prefix}{result.lineno} in {result.name}"
479
- if not config.no_ansi and _sys.stdout.isatty():
480
- header = f"\033[35m{header}\033[0m"
481
-
482
- output.append(header)
483
- for item in result:
484
- extra = None
485
- if item.hint:
486
- extra = f"hint: {item.hint}"
487
-
488
- if item.new:
489
- extra = "warning: please remember to fix this or disable it"
490
- _warn(_NEW.format(ref=item.ref), FutureWarning, stacklevel=3)
491
-
492
- msg = _TEMPLATE.format(
493
- ref=item.ref,
494
- description=item.description,
495
- symbolic=item.symbolic,
496
- )
497
- output.append(f" {msg}")
498
- if extra is not None:
499
- output.append(f" {extra}")
500
-
501
- obj.append(
502
- {
503
- "line": None if result.retcode == 2 else item.lineno,
504
- "message": msg,
505
- "exit": result.retcode,
506
- },
507
- )
508
-
509
- if format_json:
510
- print(_json.dumps(obj).strip()) # pragma: no cover
511
-
512
- elif output:
513
- print("\n".join(output))
514
-
515
- return retcodes.result
@@ -12,17 +12,17 @@ from pathlib import Path as _Path
12
12
  from pprint import pformat as _pformat
13
13
 
14
14
  from . import _decorators
15
- from ._check import run_checks as _run_checks
16
15
  from ._config import Check as _Check
17
16
  from ._config import Config as _Config
18
17
  from ._config import Filters as _Filters
19
18
  from ._config import Ignore as _Ignore
19
+ from ._diagnostic import Failures as _Failures
20
+ from ._diagnostic import RetCode as _RetCode
20
21
  from ._files import Files as _Files
21
22
  from ._parsers import parse_from_file as _parse_from_file
22
23
  from ._parsers import parse_from_string as _parse_from_string
23
- from ._report import Failures as _Failures
24
- from ._report import RetCode as _RetCode
25
24
  from ._report import report as _report
25
+ from ._traverse import run_checks as _run_checks
26
26
  from ._utils import get_parent_that_has as _get_parent_that_has
27
27
  from ._utils import print_checks as _print_checks
28
28
  from .messages import E as _E
@@ -10,7 +10,7 @@ import sys as _sys
10
10
  import typing as _t
11
11
  from pathlib import Path as _Path
12
12
 
13
- from ._report import RetCode as _RetCode
13
+ from ._diagnostic import RetCode as _RetCode
14
14
  from .messages import E as _E
15
15
 
16
16
  _FuncType = _t.Callable[..., int]
@@ -0,0 +1,155 @@
1
+ """
2
+ docsig._diagnostic
3
+ ==================
4
+
5
+ Diagnostic records and per-function check results for docsig runs.
6
+ """
7
+
8
+ import typing as _t
9
+ from dataclasses import dataclass as _dataclass
10
+
11
+ from ._module import Function as _Function
12
+ from .messages import Message as _Message
13
+
14
+
15
+ class Failures(list["FunctionResult"]):
16
+ """Sequence of result instances (one per function checked)."""
17
+
18
+
19
+ class RetCode:
20
+ """RetCode object.
21
+
22
+ :param code: Initial return code, if any, otherwise zero.
23
+ """
24
+
25
+ def __init__(self, code: int = 0) -> None:
26
+ self._data = [code]
27
+
28
+ def add(self, code: int) -> None:
29
+ """Add a return code.
30
+
31
+ :param code: Return code to add.
32
+ """
33
+ self._data.append(code)
34
+
35
+ @property
36
+ def result(self) -> int:
37
+ """Maximum return code."""
38
+ return max(self._data)
39
+
40
+
41
+ @_dataclass(frozen=True, order=True)
42
+ class Diagnostic: # pylint: disable=too-few-public-methods
43
+ """Single reported issue for one function."""
44
+
45
+ name: str
46
+ ref: str
47
+ description: str
48
+ symbolic: str
49
+ lineno: int
50
+ hint: str | None = None
51
+ new: bool = False
52
+
53
+
54
+ class Collector:
55
+ """Collect diagnostics and exit weights for one function.
56
+
57
+ :param func: Function to collect diagnostics for.
58
+ :param qualified_name: Qualified name (Class.method) when nested,
59
+ else bare name.
60
+ :param lineno: Line number of the function in the source.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ func: _Function,
66
+ qualified_name: str,
67
+ lineno: int,
68
+ ) -> None:
69
+ self._func = func
70
+ self._qualified_name = qualified_name
71
+ self._lineno = lineno
72
+ self._diagnostics: list[Diagnostic] = []
73
+ self._retcode = RetCode()
74
+
75
+ def add(
76
+ self,
77
+ value: _Message,
78
+ include_hint: bool = False,
79
+ **kwargs: _t.Any,
80
+ ) -> None:
81
+ """Add a diagnostic message.
82
+
83
+ :param value: Message to add.
84
+ :param include_hint: Whether to include the hint.
85
+ :param kwargs: Additional arguments to format the description.
86
+ """
87
+ self._retcode.add(int(not value.new))
88
+ diagnostic = Diagnostic(
89
+ self._qualified_name,
90
+ value.ref,
91
+ value.description.format(**kwargs),
92
+ value.symbolic,
93
+ self._lineno,
94
+ value.hint if include_hint else None,
95
+ value.new,
96
+ )
97
+ if (
98
+ value not in self._func.messages
99
+ and diagnostic not in self._diagnostics
100
+ ):
101
+ self._diagnostics.append(diagnostic)
102
+
103
+ @property
104
+ def diagnostics(self) -> list[Diagnostic]:
105
+ """Diagnostics sorted for stable output."""
106
+ return sorted(self._diagnostics)
107
+
108
+ @property
109
+ def retcode(self) -> RetCode:
110
+ """Exit code (non-zero if any check failed)."""
111
+ return self._retcode
112
+
113
+ def __bool__(self) -> bool:
114
+ return bool(self._diagnostics)
115
+
116
+
117
+ class FunctionResult:
118
+ """Diagnostics and exit code for one checked function.
119
+
120
+ :param name: Qualified name (Class.method) when nested, else bare
121
+ name.
122
+ :param lineno: Line number of the function in the source.
123
+ :param collector: Collector for the function.
124
+ """
125
+
126
+ def __init__(
127
+ self,
128
+ name: str,
129
+ lineno: int,
130
+ collector: Collector,
131
+ ) -> None:
132
+ self._name = name
133
+ self._lineno = lineno
134
+ self._collector = collector
135
+
136
+ @property
137
+ def name(self) -> str:
138
+ """Qualified name (Class.method) when nested, else bare name."""
139
+ return self._name
140
+
141
+ @property
142
+ def lineno(self) -> int:
143
+ """Line number of the function in the source."""
144
+ return self._lineno
145
+
146
+ @property
147
+ def retcode(self) -> int:
148
+ """Exit code (non-zero if any check failed)."""
149
+ return self._collector.retcode.result
150
+
151
+ def __iter__(self) -> _t.Iterator[Diagnostic]:
152
+ return iter(self._collector.diagnostics)
153
+
154
+ def __bool__(self) -> bool:
155
+ return bool(self._collector)
@@ -0,0 +1,94 @@
1
+ """
2
+ docsig._report
3
+ ==============
4
+
5
+ Format and print docstring-check diagnostics for CLI and tooling.
6
+ """
7
+
8
+ import json as _json
9
+ import os as _os
10
+ import sys as _sys
11
+ from warnings import warn as _warn
12
+
13
+ from ._checker import FunctionChecker as _FunctionChecker
14
+ from ._config import Config as _Config
15
+ from ._diagnostic import Failures as _Failures
16
+ from ._diagnostic import FunctionResult as _FunctionResult
17
+ from ._diagnostic import RetCode as _RetCode
18
+ from ._module import Function as _Function
19
+ from .messages import NEW as _NEW
20
+ from .messages import TEMPLATE as _TEMPLATE
21
+
22
+
23
+ def check_function(func: _Function, config: _Config) -> _FunctionResult:
24
+ """Run configured checks for one function and return the result.
25
+
26
+ :param func: Function under check.
27
+ :param config: Configuration object.
28
+ :return: Collected diagnostics for the function.
29
+ """
30
+ return _FunctionChecker(func, config).run()
31
+
32
+
33
+ # TODO: make report json by default and wrap with a reporter for cli
34
+ def report(
35
+ failures: _Failures,
36
+ config: _Config,
37
+ file: str | None = None,
38
+ ) -> int:
39
+ """Print failures and return the highest exit code.
40
+
41
+ Iterates over failures, prints each with path, line header, and
42
+ messages, then returns the maximum retcode (0 or non-zero).
43
+
44
+ :param failures: Failures to print (one FunctionResult per
45
+ function).
46
+ :param config: Config for ANSI and formatting.
47
+ :param file: Module path when failures came from a file (optional).
48
+ :return: Exit code (non-zero if any check failed).
49
+ """
50
+ format_json = _os.getenv("_DOCSIG_FORMAT_JSON") is not None
51
+ retcodes = _RetCode()
52
+ output = []
53
+ obj = []
54
+ for result in failures:
55
+ retcodes.add(result.retcode)
56
+ path_prefix = f"{file}:" if file is not None else ""
57
+ header = f"{path_prefix}{result.lineno} in {result.name}"
58
+ if not config.no_ansi and _sys.stdout.isatty():
59
+ header = f"\033[35m{header}\033[0m"
60
+
61
+ output.append(header)
62
+ for item in result:
63
+ extra = None
64
+ if item.hint:
65
+ extra = f"hint: {item.hint}"
66
+
67
+ if item.new:
68
+ extra = "warning: please remember to fix this or disable it"
69
+ _warn(_NEW.format(ref=item.ref), FutureWarning, stacklevel=3)
70
+
71
+ msg = _TEMPLATE.format(
72
+ ref=item.ref,
73
+ description=item.description,
74
+ symbolic=item.symbolic,
75
+ )
76
+ output.append(f" {msg}")
77
+ if extra is not None:
78
+ output.append(f" {extra}")
79
+
80
+ obj.append(
81
+ {
82
+ "line": None if result.retcode == 2 else item.lineno,
83
+ "message": msg,
84
+ "exit": result.retcode,
85
+ },
86
+ )
87
+
88
+ if format_json:
89
+ print(_json.dumps(obj).strip()) # pragma: no cover
90
+
91
+ elif output:
92
+ print("\n".join(output))
93
+
94
+ return retcodes.result
@@ -179,7 +179,6 @@ class Params(list[Param]):
179
179
  def __init__(self, ignore: _Ignore) -> None:
180
180
  super().__init__()
181
181
  self._ignore = ignore
182
- self._duplicates: list[Param] = []
183
182
 
184
183
  def append(self, value: Param) -> None:
185
184
  if not value.isprotected and any(
@@ -219,20 +218,7 @@ class Params(list[Param]):
219
218
 
220
219
  Only names are compared; descriptions are ignored.
221
220
  """
222
- for k, v in _Counter(i.name for i in self).items():
223
- if v > 1:
224
- for i in self:
225
- if i.name == k:
226
- # record the duplicates for later analysis
227
- self._duplicates.append(i)
228
- return True
229
-
230
- return False
231
-
232
- @property
233
- def duplicates(self) -> list[Param]:
234
- """Params that share a name."""
235
- return self._duplicates
221
+ return any(v > 1 for v in _Counter(i.name for i in self).values())
236
222
 
237
223
 
238
224
  class _Stub:
@@ -394,7 +380,7 @@ class Docstring(_Stub):
394
380
  indent_anomaly = cls._indent_anomaly(node.value)
395
381
  string = cls._normalize_docstring(node.value)
396
382
  match = _re.search(
397
- r"^[ \t]*:(?:returns?|yields?):\s*(.*)",
383
+ r"^[ \t]*:(?:returns?|yields?|rtype):\s*(.*)",
398
384
  string,
399
385
  _re.IGNORECASE | _re.MULTILINE,
400
386
  )
@@ -1,14 +1,14 @@
1
1
  """
2
- docsig._check
3
- =============
2
+ docsig._traverse
3
+ ================
4
4
 
5
5
  Traverse modules and run docstring/signature checks per function.
6
6
  """
7
7
 
8
8
  from ._config import Config as _Config
9
+ from ._diagnostic import Failures as _Failures
9
10
  from ._module import Function as _Function
10
11
  from ._module import Parent as _Parent
11
- from ._report import Failures as _Failures
12
12
  from ._report import check_function as _check_function
13
13
 
14
14
 
@@ -8,4 +8,4 @@ Allows for access to the version internally without cyclic imports
8
8
  caused by accessing it through __init__.
9
9
  """
10
10
 
11
- __version__ = "0.91.0"
11
+ __version__ = "0.91.1"
@@ -29,7 +29,7 @@ maintainers = [
29
29
  name = "docsig"
30
30
  readme = "README.rst"
31
31
  requires-python = ">=3.10"
32
- version = "0.91.0"
32
+ version = "0.91.1"
33
33
 
34
34
  [project.entry-points."flake8.extension"]
35
35
  SIG = "docsig.plugin:Flake8"
@@ -47,7 +47,7 @@ line-length = 79
47
47
  allow_dirty = true
48
48
  commit = true
49
49
  commit_args = "-sS"
50
- current_version = "0.91.0"
50
+ current_version = "0.91.1"
51
51
  message = "bump: version {current_version} → {new_version}"
52
52
  sign_tags = true
53
53
  tag = true
@@ -1,21 +0,0 @@
1
- """
2
- docsig._diagnostic
3
- ==================
4
-
5
- Diagnostic records and per-function check results for docsig runs.
6
- """
7
-
8
- from dataclasses import dataclass as _dataclass
9
-
10
-
11
- @_dataclass(frozen=True, order=True)
12
- class Diagnostic: # pylint: disable=too-few-public-methods
13
- """Single reported issue for one function."""
14
-
15
- name: str
16
- ref: str
17
- description: str
18
- symbolic: str
19
- lineno: int
20
- hint: str | None = None
21
- new: bool = False
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes