docsig 0.90.3__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 (29) hide show
  1. {docsig-0.90.3 → docsig-0.91.1}/PKG-INFO +8 -8
  2. {docsig-0.90.3 → docsig-0.91.1}/README.rst +7 -7
  3. docsig-0.90.3/docsig/_report.py → docsig-0.91.1/docsig/_checker.py +34 -168
  4. {docsig-0.90.3 → docsig-0.91.1}/docsig/_config.py +8 -7
  5. {docsig-0.90.3 → docsig-0.91.1}/docsig/_core.py +3 -3
  6. {docsig-0.90.3 → docsig-0.91.1}/docsig/_decorators.py +1 -1
  7. docsig-0.91.1/docsig/_diagnostic.py +155 -0
  8. {docsig-0.90.3 → docsig-0.91.1}/docsig/_main.py +0 -1
  9. {docsig-0.90.3 → docsig-0.91.1}/docsig/_parsers.py +3 -3
  10. docsig-0.91.1/docsig/_report.py +94 -0
  11. {docsig-0.90.3 → docsig-0.91.1}/docsig/_stub.py +2 -16
  12. docsig-0.90.3/docsig/_check.py → docsig-0.91.1/docsig/_traverse.py +5 -3
  13. {docsig-0.90.3 → docsig-0.91.1}/docsig/_version.py +1 -1
  14. {docsig-0.90.3 → docsig-0.91.1}/pyproject.toml +3 -3
  15. {docsig-0.90.3 → docsig-0.91.1}/LICENSE +0 -0
  16. {docsig-0.90.3 → docsig-0.91.1}/LICENSES/SPHINX_LICENSE +0 -0
  17. {docsig-0.90.3 → docsig-0.91.1}/docsig/__init__.py +0 -0
  18. {docsig-0.90.3 → docsig-0.91.1}/docsig/__main__.py +0 -0
  19. {docsig-0.90.3 → docsig-0.91.1}/docsig/_directives.py +0 -0
  20. {docsig-0.90.3 → docsig-0.91.1}/docsig/_files.py +0 -0
  21. {docsig-0.90.3 → docsig-0.91.1}/docsig/_hooks.py +0 -0
  22. {docsig-0.90.3 → docsig-0.91.1}/docsig/_module.py +0 -0
  23. {docsig-0.90.3 → docsig-0.91.1}/docsig/_utils.py +0 -0
  24. {docsig-0.90.3 → docsig-0.91.1}/docsig/_vendor/sphinx/ext/napoleon.py +0 -0
  25. {docsig-0.90.3 → docsig-0.91.1}/docsig/messages.py +0 -0
  26. {docsig-0.90.3 → docsig-0.91.1}/docsig/plugin/__init__.py +0 -0
  27. {docsig-0.90.3 → docsig-0.91.1}/docsig/plugin/_flake8.py +0 -0
  28. {docsig-0.90.3 → docsig-0.91.1}/docsig/plugin/_validate_pyproject.py +0 -0
  29. {docsig-0.90.3 → 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.90.3
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.90.3, 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.90.3
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.90.3
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.90.3, 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.90.3
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.90.3
286
+ - docsig==0.91.1
287
287
  args:
288
288
  - "--sig-check-class"
289
289
  - "--sig-check-dunders"
@@ -1,24 +1,19 @@
1
1
  """
2
- docsig._report
3
- ==============
2
+ docsig._checker
3
+ ===============
4
4
 
5
- Collect and format docstring-check failures for reporting. This module
6
- defines Failure (per-function validation results), Failed (one reported
7
- issue), Failures (sequence of failures), and report() to print them and
8
- return the highest exit code.
5
+ Run docstring and signature checks for a single function.
9
6
  """
10
7
 
11
8
  import contextlib as _contextlib
12
- import json as _json
13
- import os as _os
14
- import sys as _sys
15
9
  import typing as _t
16
- from dataclasses import dataclass as _dataclass
17
- from warnings import warn as _warn
18
10
 
19
11
  import astroid as _ast
20
12
 
21
13
  from ._config import Config as _Config
14
+ from ._diagnostic import Collector as _Collector
15
+ from ._diagnostic import Diagnostic as _Diagnostic
16
+ from ._diagnostic import FunctionResult as _FunctionResult
22
17
  from ._module import Function as _Function
23
18
  from ._stub import UNNAMED as _UNNAMED
24
19
  from ._stub import VALID_DESCRIPTION as _VALID_DESCRIPTION
@@ -27,8 +22,6 @@ from ._stub import Params as _Params
27
22
  from ._stub import RetType as _RetType
28
23
  from ._utils import almost_equal as _almost_equal
29
24
  from ._utils import sentence_tokenizer as _sentence_tokenizer
30
- from .messages import NEW as _NEW
31
- from .messages import TEMPLATE as _TEMPLATE
32
25
  from .messages import E as _E
33
26
  from .messages import Message as _Message
34
27
 
@@ -36,56 +29,7 @@ _MIN_MATCH = 0.8
36
29
  _MAX_MATCH = 1.0
37
30
 
38
31
 
39
- def check_function(func: _Function, config: _Config) -> "FunctionChecker":
40
- """Run configured checks for one function and return the result.
41
-
42
- :param func: Function under check.
43
- :param config: Configuration object.
44
- :return: Collected diagnostics for the function.
45
- """
46
- return FunctionChecker(func, config)
47
-
48
-
49
- class RetCode:
50
- """RetCode object.
51
-
52
- :param code: Initial return code, if any, otherwise zero.
53
- """
54
-
55
- def __init__(self, code: int = 0) -> None:
56
- self._data = [code]
57
-
58
- def add(self, code: int) -> None:
59
- """Add a return code.
60
-
61
- :param code: Return code to add.
62
- """
63
- self._data.append(code)
64
-
65
- @property
66
- def result(self) -> int:
67
- """Maximum return code."""
68
- return max(self._data)
69
-
70
-
71
- class Failures(list["FunctionChecker"]):
72
- """Sequence of Failure instances (one per function checked)."""
73
-
74
-
75
- @_dataclass(frozen=True, order=True)
76
- class Diagnostic: # pylint: disable=too-few-public-methods
77
- """Single reported issue."""
78
-
79
- name: str
80
- ref: str
81
- description: str
82
- symbolic: str
83
- lineno: int
84
- hint: str | None = None
85
- new: bool = False
86
-
87
-
88
- class FunctionChecker(list[Diagnostic]):
32
+ class FunctionChecker: # pylint: disable=too-few-public-methods
89
33
  """Collect docstring and signature failures for one function.
90
34
 
91
35
  Runs configured checks and appends Failed entries for each
@@ -96,9 +40,9 @@ class FunctionChecker(list[Diagnostic]):
96
40
  """
97
41
 
98
42
  def __init__(self, func: _Function, config: _Config) -> None:
99
- super().__init__()
100
- self._retcode = RetCode()
101
43
  self._func = func
44
+ self._config = config
45
+ self._diagnostics: list[_Diagnostic] = []
102
46
  if config.target:
103
47
  self._func.messages.extend(
104
48
  i for i in _E.all if i not in config.target
@@ -113,8 +57,15 @@ class FunctionChecker(list[Diagnostic]):
113
57
  ):
114
58
  self._name = f"{self._func.parent.name}.{self._name}"
115
59
 
60
+ self._collector = _Collector(func, self._name, self._func.lineno)
61
+
62
+ def run(self) -> _FunctionResult:
63
+ """Run the function checks and return the result.
64
+
65
+ :return: Function result.
66
+ """
116
67
  if self._func.error is not None:
117
- self._retcode.add(2)
68
+ self._collector.retcode.add(2)
118
69
  self._sig9xx_error()
119
70
  else:
120
71
  self._sig0xx_config()
@@ -128,9 +79,9 @@ class FunctionChecker(list[Diagnostic]):
128
79
  sig = self._func.signature.args.get(index)
129
80
  self._sig4xx_parameters(doc, sig)
130
81
 
131
- self._sig5xx_returns(config.check.property_returns)
82
+ self._sig5xx_returns(self._config.check.property_returns)
132
83
 
133
- self.sort()
84
+ return _FunctionResult(self._name, self._func.lineno, self._collector)
134
85
 
135
86
  def _add(
136
87
  self,
@@ -138,18 +89,7 @@ class FunctionChecker(list[Diagnostic]):
138
89
  include_hint: bool = False,
139
90
  **kwargs: _t.Any,
140
91
  ) -> None:
141
- self._retcode.add(int(not value.new))
142
- failed = Diagnostic(
143
- self._name,
144
- value.ref,
145
- value.description.format(**kwargs),
146
- value.symbolic,
147
- self.lineno,
148
- value.hint if include_hint else None,
149
- value.new,
150
- )
151
- if value not in self._func.messages and failed not in self:
152
- super().append(failed)
92
+ self._collector.add(value, include_hint=include_hint, **kwargs)
153
93
 
154
94
  @staticmethod
155
95
  def _normalize_params(from_: _Params, to: _Params) -> None:
@@ -231,15 +171,17 @@ class FunctionChecker(list[Diagnostic]):
231
171
 
232
172
  def _sig2xx_signature(self) -> None:
233
173
  if self._func.docstring.args.duplicated:
234
- # pop the duplicates so that they are considered a single
235
- # parameter, that way there are no assumptions that the
236
- # parameters must be out of order
237
- for count, arg in enumerate(self._func.docstring.args):
238
- if (
239
- arg in self._func.docstring.args.duplicates
240
- and self._func.docstring.args.count(arg) > 1
241
- ):
242
- 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)
243
185
 
244
186
  # duplicate-params-found
245
187
  self._add(_E[201])
@@ -297,6 +239,8 @@ class FunctionChecker(list[Diagnostic]):
297
239
  not in (
298
240
  "`",
299
241
  ".",
242
+ "!",
243
+ "?",
300
244
  )
301
245
  ):
302
246
  self._add(_E[306])
@@ -371,7 +315,7 @@ class FunctionChecker(list[Diagnostic]):
371
315
  # invalid-syntax
372
316
  if self._func.error is _ast.AstroidSyntaxError:
373
317
  self._add(_E[901])
374
- self._retcode.add(123)
318
+ self._collector.retcode.add(123)
375
319
  # unicode-decode-error
376
320
  if self._func.error is UnicodeDecodeError:
377
321
  self._add(_E[902])
@@ -381,81 +325,3 @@ class FunctionChecker(list[Diagnostic]):
381
325
  # duplicates-found-in-mros
382
326
  if self._func.error is _ast.DuplicateBasesError:
383
327
  self._add(_E[904])
384
-
385
- @property
386
- def name(self) -> str:
387
- """Qualified name (Class.method) when nested, else bare name."""
388
- return self._name
389
-
390
- @property
391
- def lineno(self) -> int:
392
- """Line number of the function in the source."""
393
- return self._func.lineno
394
-
395
- @property
396
- def retcode(self) -> int:
397
- """Exit code (non-zero if any check failed)."""
398
- return self._retcode.result
399
-
400
-
401
- # TODO: make report json by default and wrap with a reporter for cli
402
- def report(
403
- failures: Failures,
404
- config: _Config,
405
- file: str | None = None,
406
- ) -> int:
407
- """Print failures to stdout and return the highest exit code.
408
-
409
- Iterates over failures, prints each with path, line header, and
410
- messages, then returns the maximum retcode (0 or non-zero).
411
-
412
- :param failures: Failures to print (one Failure per function).
413
- :param config: Config for ANSI and formatting.
414
- :param file: Module path when failures came from a file (optional).
415
- :return: Exit code (non-zero if any check failed).
416
- """
417
- format_json = _os.getenv("_DOCSIG_FORMAT_JSON") is not None
418
- retcodes = RetCode()
419
- output = []
420
- obj = []
421
- for result in failures:
422
- retcodes.add(result.retcode)
423
- path_prefix = f"{file}:" if file is not None else ""
424
- header = f"{path_prefix}{result.lineno} in {result.name}"
425
- if not config.no_ansi and _sys.stdout.isatty():
426
- header = f"\033[35m{header}\033[0m"
427
-
428
- output.append(header)
429
- for item in result:
430
- extra = None
431
- if item.hint:
432
- extra = f"hint: {item.hint}"
433
-
434
- if item.new:
435
- extra = "warning: please remember to fix this or disable it"
436
- _warn(_NEW.format(ref=item.ref), FutureWarning, stacklevel=3)
437
-
438
- msg = _TEMPLATE.format(
439
- ref=item.ref,
440
- description=item.description,
441
- symbolic=item.symbolic,
442
- )
443
- output.append(f" {msg}")
444
- if extra is not None:
445
- output.append(f" {extra}")
446
-
447
- obj.append(
448
- {
449
- "line": None if result.retcode == 2 else item.lineno,
450
- "message": msg,
451
- "exit": result.retcode,
452
- },
453
- )
454
-
455
- if format_json:
456
- print(_json.dumps(obj).strip()) # pragma: no cover
457
-
458
- elif output:
459
- print("\n".join(output))
460
-
461
- return retcodes.result
@@ -156,17 +156,10 @@ def build_parser() -> _ArgumentParser:
156
156
  action="store_true",
157
157
  help="check nested functions and classes",
158
158
  )
159
- parser.add_argument(
160
- "-o",
161
- action="store_true",
162
- help=_argparse.SUPPRESS,
163
- dest="check_overridden",
164
- )
165
159
  parser.add_argument(
166
160
  "--check-overridden",
167
161
  action="store_true",
168
162
  help="check overridden methods",
169
- dest="check_overridden",
170
163
  )
171
164
  parser.add_argument(
172
165
  "--check-property-returns",
@@ -332,6 +325,14 @@ class Config:
332
325
 
333
326
  Groups check, ignore settings, and run options so the core runner
334
327
  takes a single config object instead of many parameters.
328
+
329
+ :param check: Configuration for which docstrings and members to
330
+ check.
331
+ :param ignore: Configuration for which docstring elements to ignore.
332
+ :param filters: Configuration for which paths to filter.
333
+ :param target: List of errors to target.
334
+ :param disable: List of errors to disable.
335
+ :param no_ansi: Disable ANSI output.
335
336
  """
336
337
 
337
338
  check: Check = _field(default_factory=Check)
@@ -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)
@@ -19,7 +19,6 @@ def _warn_on_deprecated_short_flags() -> None:
19
19
  "-I": "--include-ignored",
20
20
  "-c": "--check-class",
21
21
  "-D": "--check-dunders",
22
- "-o": "--check-overridden",
23
22
  }
24
23
  raw_args = _sys.argv[1:]
25
24
  expanded_flags = []
@@ -25,9 +25,9 @@ def parse_from_string(
25
25
  ) -> _Parent:
26
26
  """Build a Parent from a string of Python code.
27
27
 
28
- Parses AST and comment directives (AST does not include comments,
29
- so directives are parsed separately). On syntax error, returns a
30
- Parent with an error set.
28
+ Parses AST and comment directives (AST does not include comments, so
29
+ directives are parsed separately). On syntax error, returns a Parent
30
+ with an error set.
31
31
 
32
32
  :param code: Python source to parse.
33
33
  :param config: Configuration object.
@@ -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,12 +1,14 @@
1
1
  """
2
- docsig._check
3
- =============
2
+ docsig._traverse
3
+ ================
4
+
5
+ Traverse modules and run docstring/signature checks per function.
4
6
  """
5
7
 
6
8
  from ._config import Config as _Config
9
+ from ._diagnostic import Failures as _Failures
7
10
  from ._module import Function as _Function
8
11
  from ._module import Parent as _Parent
9
- from ._report import Failures as _Failures
10
12
  from ._report import check_function as _check_function
11
13
 
12
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.90.3"
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.90.3"
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.90.3"
50
+ current_version = "0.91.1"
51
51
  message = "bump: version {current_version} → {new_version}"
52
52
  sign_tags = true
53
53
  tag = true
@@ -146,7 +146,7 @@ black = ">=24.4.2,<27.0.0"
146
146
  bump-my-version = ">=1.4.1,<1.5.0"
147
147
  deptry = ">=0.16.1,<0.26.0"
148
148
  flynt = "^1.0.1"
149
- gitpython = "^3.1.51"
149
+ gitpython = "^3.1.52"
150
150
  isort = ">=5.13.2,<9.0.0"
151
151
  mypy = ">=2.3.0,<3.0"
152
152
  pre-commit = ">=3.3.3,<5.0.0"
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