docsig 0.82.1__tar.gz → 0.83.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,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docsig
3
- Version: 0.82.1
3
+ Version: 0.83.0
4
4
  Summary: Check signature params for proper documentation
5
5
  License: MIT
6
6
  License-File: LICENSE
7
7
  Keywords: check,docs,docstring,params,signature
8
8
  Author: jshwi
9
- Author-email: stephen@jshwisolutions.com
9
+ Author-email: s.whitlock@live.com
10
10
  Maintainer: jshwi
11
- Maintainer-email: stephen@jshwisolutions.com
11
+ Maintainer-email: s.whitlock@live.com
12
12
  Requires-Python: >=3.10,<4.0
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Programming Language :: Python :: 3
@@ -36,7 +36,7 @@ Description-Content-Type: text/x-rst
36
36
 
37
37
  |
38
38
 
39
- |License| |PyPI| |CI| |CodeQL| |pre-commit.ci status| |codecov.io| |readthedocs.org| |python3.10| |Black| |isort| |pylint| |Security Status| |Known Vulnerabilities|
39
+ |License| |PyPI| |CI| |CodeQL| |pre-commit.ci status| |codecov.io| |readthedocs.org| |python3.10| |Black| |isort| |pylint| |Security Status|
40
40
 
41
41
  .. |License| image:: https://img.shields.io/badge/License-MIT-yellow.svg
42
42
  :target: https://opensource.org/licenses/MIT
@@ -74,9 +74,6 @@ Description-Content-Type: text/x-rst
74
74
  .. |Security Status| image:: https://img.shields.io/badge/security-bandit-yellow.svg
75
75
  :target: https://github.com/PyCQA/bandit
76
76
  :alt: Security Status
77
- .. |Known Vulnerabilities| image:: https://snyk.io/test/github/jshwi/docsig/badge.svg
78
- :target: https://snyk.io/test/github/jshwi/docsig/badge.svg
79
- :alt: Known Vulnerabilities
80
77
 
81
78
  Check Python signature params for proper documentation
82
79
  -------------------------------------------------------
@@ -175,6 +172,20 @@ Options can also be configured with the pyproject.toml file
175
172
  "SIG201",
176
173
  ]
177
174
 
175
+ .. schema-info start
176
+
177
+ .. _here: https://docsig.io/en/latest/usage/configuration/schema.html
178
+
179
+ .. _schema: https://docsig.io/en/latest/usage/configuration/schema.json
180
+
181
+ .. _validate-pyproject: https://github.com/abravalheri/validate-pyproject
182
+
183
+ Configuration options can be found `here`_ or see the full JSON `schema`_
184
+
185
+ You can validate your `docsig` configuration with `validate-pyproject`_
186
+
187
+ .. schema-info end
188
+
178
189
  Flake8
179
190
  ******
180
191
 
@@ -184,7 +195,7 @@ ensure your installation has registered `docsig`
184
195
  .. code-block:: console
185
196
 
186
197
  $ flake8 --version
187
- 7.3.0 (docsig: 0.82.1, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
198
+ 7.3.0 (docsig: 0.83.0, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
188
199
 
189
200
  And now use `flake8` to lint your files
190
201
 
@@ -270,7 +281,7 @@ Standalone
270
281
 
271
282
  repos:
272
283
  - repo: https://github.com/jshwi/docsig
273
- rev: v0.82.1
284
+ rev: v0.83.0
274
285
  hooks:
275
286
  - id: docsig
276
287
  args:
@@ -289,7 +300,7 @@ or integrated with ``flake8``
289
300
  hooks:
290
301
  - id: flake8
291
302
  additional_dependencies:
292
- - docsig==0.82.1
303
+ - docsig==0.83.0
293
304
  args:
294
305
  - "--sig-check-class"
295
306
  - "--sig-check-dunders"
@@ -7,7 +7,7 @@
7
7
 
8
8
  |
9
9
 
10
- |License| |PyPI| |CI| |CodeQL| |pre-commit.ci status| |codecov.io| |readthedocs.org| |python3.10| |Black| |isort| |pylint| |Security Status| |Known Vulnerabilities|
10
+ |License| |PyPI| |CI| |CodeQL| |pre-commit.ci status| |codecov.io| |readthedocs.org| |python3.10| |Black| |isort| |pylint| |Security Status|
11
11
 
12
12
  .. |License| image:: https://img.shields.io/badge/License-MIT-yellow.svg
13
13
  :target: https://opensource.org/licenses/MIT
@@ -45,9 +45,6 @@
45
45
  .. |Security Status| image:: https://img.shields.io/badge/security-bandit-yellow.svg
46
46
  :target: https://github.com/PyCQA/bandit
47
47
  :alt: Security Status
48
- .. |Known Vulnerabilities| image:: https://snyk.io/test/github/jshwi/docsig/badge.svg
49
- :target: https://snyk.io/test/github/jshwi/docsig/badge.svg
50
- :alt: Known Vulnerabilities
51
48
 
52
49
  Check Python signature params for proper documentation
53
50
  -------------------------------------------------------
@@ -146,6 +143,20 @@ Options can also be configured with the pyproject.toml file
146
143
  "SIG201",
147
144
  ]
148
145
 
146
+ .. schema-info start
147
+
148
+ .. _here: https://docsig.io/en/latest/usage/configuration/schema.html
149
+
150
+ .. _schema: https://docsig.io/en/latest/usage/configuration/schema.json
151
+
152
+ .. _validate-pyproject: https://github.com/abravalheri/validate-pyproject
153
+
154
+ Configuration options can be found `here`_ or see the full JSON `schema`_
155
+
156
+ You can validate your `docsig` configuration with `validate-pyproject`_
157
+
158
+ .. schema-info end
159
+
149
160
  Flake8
150
161
  ******
151
162
 
@@ -155,7 +166,7 @@ ensure your installation has registered `docsig`
155
166
  .. code-block:: console
156
167
 
157
168
  $ flake8 --version
158
- 7.3.0 (docsig: 0.82.1, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
169
+ 7.3.0 (docsig: 0.83.0, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
159
170
 
160
171
  And now use `flake8` to lint your files
161
172
 
@@ -241,7 +252,7 @@ Standalone
241
252
 
242
253
  repos:
243
254
  - repo: https://github.com/jshwi/docsig
244
- rev: v0.82.1
255
+ rev: v0.83.0
245
256
  hooks:
246
257
  - id: docsig
247
258
  args:
@@ -260,7 +271,7 @@ or integrated with ``flake8``
260
271
  hooks:
261
272
  - id: flake8
262
273
  additional_dependencies:
263
- - docsig==0.82.1
274
+ - docsig==0.83.0
264
275
  args:
265
276
  - "--sig-check-class"
266
277
  - "--sig-check-dunders"
@@ -97,11 +97,10 @@ class _ArgumentParser(_argparse.ArgumentParser):
97
97
  return namespace, args
98
98
 
99
99
 
100
- def parse_args(args: _t.Sequence[str] | None = None) -> _argparse.Namespace:
101
- """Parse CLI arguments and merge in config from pyproject.toml.
100
+ def build_parser() -> _ArgumentParser:
101
+ """Build and return the argument parser.
102
102
 
103
- :param args: Argument list to parse (defaults to sys.argv).
104
- :return: Parsed namespace with config merged in.
103
+ :return: The argument parser.
105
104
  """
106
105
  parser = _ArgumentParser(
107
106
  description="Check signature params for proper documentation",
@@ -305,6 +304,16 @@ def parse_args(args: _t.Sequence[str] | None = None) -> _argparse.Namespace:
305
304
  metavar="STR",
306
305
  help="string to parse instead of files",
307
306
  )
307
+ return parser
308
+
309
+
310
+ def parse_args(args: _t.Sequence[str] | None = None) -> _argparse.Namespace:
311
+ """Parse CLI arguments and merge in config from pyproject.toml.
312
+
313
+ :param args: Argument list to parse (defaults to sys.argv).
314
+ :return: Parsed namespace with config merged in.
315
+ """
316
+ parser = build_parser()
308
317
  return parser.parse_args(args)
309
318
 
310
319
 
@@ -11,6 +11,7 @@ import logging as _logging
11
11
  import sys as _sys
12
12
  import warnings as _warnings
13
13
  from pathlib import Path as _Path
14
+ from pprint import pformat as _pformat
14
15
 
15
16
  from . import _decorators
16
17
  from ._check import run_checks as _run_checks
@@ -202,6 +203,8 @@ def docsig( # pylint: disable=too-many-locals,too-many-arguments
202
203
  excludes=excludes or [],
203
204
  )
204
205
  setup_logger(config.verbose)
206
+ logger = _logging.getLogger(__package__)
207
+ logger.debug(_pformat(config))
205
208
  if config.list_checks:
206
209
  return int(bool(_print_checks())) # type: ignore
207
210
 
@@ -9,7 +9,6 @@ from __future__ import annotations as _
9
9
 
10
10
  import re as _re
11
11
  import typing as _t
12
- from enum import Enum as _Enum
13
12
  from pathlib import Path as _Path
14
13
 
15
14
  import astroid as _ast
@@ -32,12 +31,12 @@ class _Overloads(dict[str, "Function"]): ...
32
31
  class _Children(list[_t.Union["Parent", "Function"]]): ...
33
32
 
34
33
 
35
- class Error(_Enum):
36
- """Represents an unrecoverable error."""
37
-
38
- SYNTAX = 1
39
- UNICODE = 2
40
- RECURSION = 3
34
+ ERRORS = (
35
+ _ast.AstroidSyntaxError,
36
+ UnicodeDecodeError,
37
+ RecursionError,
38
+ _ast.DuplicateBasesError,
39
+ )
41
40
 
42
41
 
43
42
  class Parent: # pylint: disable=too-many-instance-attributes
@@ -65,7 +64,7 @@ class Parent: # pylint: disable=too-many-instance-attributes
65
64
  file: _Path | None = None,
66
65
  config: _Config | None = None,
67
66
  imports: _Imports | None = None,
68
- error: Error | None = None,
67
+ error: type[BaseException] | None = None,
69
68
  ) -> None:
70
69
  super().__init__()
71
70
  self._error = error
@@ -127,14 +126,18 @@ class Parent: # pylint: disable=too-many-instance-attributes
127
126
  if func.isoverloaded:
128
127
  if (
129
128
  func.name not in self._overloads
130
- or self._overloads[func.name].signature.rettype
129
+ or self._overloads[
130
+ func.name
131
+ ].signature.returns.type
131
132
  == _RetType.NONE
132
133
  ):
133
134
  self._overloads[func.name] = func
134
135
  else:
135
136
  if func.name in self._overloads:
136
137
  func.overload(
137
- self._overloads[func.name].signature.rettype,
138
+ self._overloads[
139
+ func.name
140
+ ].signature.returns.type,
138
141
  )
139
142
 
140
143
  self._children.append(func)
@@ -157,7 +160,7 @@ class Parent: # pylint: disable=too-many-instance-attributes
157
160
  return self._name.startswith("_")
158
161
 
159
162
  @property
160
- def error(self) -> Error | None:
163
+ def error(self) -> type[BaseException] | None:
161
164
  """Unrecoverable error for this scope, if any."""
162
165
  return self._error
163
166
 
@@ -190,7 +193,7 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
190
193
  file: _Path | None = None,
191
194
  config: _Config | None = None,
192
195
  imports: _Imports | None = None,
193
- error: Error | None = None,
196
+ error: type[BaseException] | None = None,
194
197
  ) -> None:
195
198
  super().__init__(
196
199
  node,
@@ -15,7 +15,7 @@ import astroid as _ast
15
15
  from ._config import Config as _Config
16
16
  from ._directives import Directives as _Directives
17
17
  from ._files import FILE_INFO as _FILE_INFO
18
- from ._module import Error as _Error
18
+ from ._module import ERRORS as _ERRORS
19
19
  from ._module import Parent as _Parent
20
20
 
21
21
 
@@ -52,13 +52,14 @@ def parse_from_string(
52
52
  )
53
53
 
54
54
  parent = _Parent(node, directives, file, config)
55
- logger.debug(_FILE_INFO, source_name, "Parsing Python code successful")
56
- except _ast.AstroidSyntaxError as err:
57
- logger.debug(_FILE_INFO, source_name, str(err).replace("\n", " "))
58
- parent = _Parent(error=_Error.SYNTAX)
59
- except RecursionError as err:
60
- logger.debug(_FILE_INFO, source_name, str(err).replace("\n", " "))
61
- parent = _Parent(error=_Error.RECURSION)
55
+ logger.debug(_FILE_INFO, source_name, "parsing python code successful")
56
+ except _ERRORS as err:
57
+ logger.debug(
58
+ _FILE_INFO,
59
+ source_name,
60
+ str(err).replace("\n", " ").lower(),
61
+ )
62
+ parent = _Parent(error=type(err))
62
63
 
63
64
  return parent
64
65
 
@@ -81,7 +82,7 @@ def parse_from_file(file: _Path, config: _Config) -> _Parent:
81
82
  except UnicodeDecodeError as err:
82
83
  logger = _logging.getLogger(__package__)
83
84
  logger.debug(_FILE_INFO, file, str(err).replace("\n", " "))
84
- parent = _Parent(error=_Error.UNICODE)
85
+ parent = _Parent(error=type(err))
85
86
 
86
87
  if parent.error is not None and not file.name.endswith(".py"):
87
88
  parent = _Parent()
@@ -15,10 +15,9 @@ import sys as _sys
15
15
  import typing as _t
16
16
  from warnings import warn as _warn
17
17
 
18
- from astroid.nodes.scoped_nodes import scoped_nodes as _scoped_nodes
18
+ import astroid as _ast
19
19
 
20
20
  from ._config import Config as _Config
21
- from ._module import Error as _Error
22
21
  from ._module import Function as _Function
23
22
  from ._stub import UNNAMED as _UNNAMED
24
23
  from ._stub import VALID_DESCRIPTION as _VALID_DESCRIPTION
@@ -75,7 +74,7 @@ class Failure(list[Failed]):
75
74
  if (
76
75
  self._func.parent is not None
77
76
  and self._func.parent.name
78
- and not isinstance(self._func.parent, _scoped_nodes.Module)
77
+ and not isinstance(self._func.parent, _ast.nodes.Module)
79
78
  ):
80
79
  self._name = f"{self._func.parent.name}.{self._name}"
81
80
 
@@ -288,18 +287,18 @@ class Failure(list[Failed]):
288
287
  self._func.isproperty and not check_property_returns
289
288
  ):
290
289
  # no types, cannot know either way
291
- if self._func.signature.rettype == _RetType.UNTYPED:
290
+ if self._func.signature.returns.type == _RetType.UNTYPED:
292
291
  # confirm-return-needed
293
292
  self._add(_E[501], hint=True)
294
293
  # return-type is none, so no return should be documented
295
- elif self._func.docstring.returns:
296
- if self._func.signature.rettype == _RetType.NONE:
294
+ elif self._func.docstring.returns.returns:
295
+ if self._func.signature.returns.type == _RetType.NONE:
297
296
  # return-documented-for-none
298
297
  self._add(_E[502])
299
- if self._func.docstring.ret_description_missing:
298
+ if self._func.docstring.returns.description_missing:
300
299
  self._add(_E[506])
301
300
  # return-type is some, so return should be documented
302
- elif self._func.signature.returns:
301
+ elif self._func.signature.returns.returns:
303
302
  # return-missing
304
303
  lines = str(self._func.docstring.string).splitlines()
305
304
  self._add(
@@ -310,7 +309,7 @@ class Failure(list[Failed]):
310
309
  and ":param" not in lines[-1]
311
310
  ),
312
311
  )
313
- elif self._func.docstring.returns:
312
+ elif self._func.docstring.returns.returns:
314
313
  # this method is init, so no return should be documented
315
314
  if self._func.isinit:
316
315
  # class-return-documented
@@ -322,13 +321,15 @@ class Failure(list[Failed]):
322
321
 
323
322
  def _sig9xx_error(self) -> None:
324
323
  # invalid-syntax
325
- if self._func.error == _Error.SYNTAX:
324
+ if self._func.error is _ast.AstroidSyntaxError:
326
325
  self._add(_E[901])
327
326
  self._retcode = 123
328
- if self._func.error == _Error.UNICODE:
327
+ if self._func.error is UnicodeDecodeError:
329
328
  self._add(_E[902])
330
- if self._func.error == _Error.RECURSION:
329
+ if self._func.error is RecursionError:
331
330
  self._add(_E[903])
331
+ if self._func.error is _ast.DuplicateBasesError:
332
+ self._add(_E[904])
332
333
 
333
334
  @property
334
335
  def name(self) -> str:
@@ -9,6 +9,7 @@ from __future__ import annotations as _
9
9
 
10
10
  import re as _re
11
11
  import textwrap as _textwrap
12
+ import typing as _t
12
13
  from collections import Counter as _Counter
13
14
  from enum import Enum as _Enum
14
15
 
@@ -80,8 +81,6 @@ class DocType(_Enum):
80
81
  return cls.UNKNOWN
81
82
 
82
83
 
83
- # todo: consider a parent object that can be used for returns that do
84
- # todo: not include the name attribute
85
84
  class Param:
86
85
  """Single parameter from a docstring or function signature.
87
86
 
@@ -152,6 +151,13 @@ class Param:
152
151
  return self._closing_token
153
152
 
154
153
 
154
+ # single return from a docstring or function signature
155
+ class _Return(_t.NamedTuple):
156
+ returns: bool = False
157
+ type: RetType = RetType.UNTYPED
158
+ description_missing: bool = False
159
+
160
+
155
161
  class Params(list[Param]):
156
162
  """A list-like collection of params.
157
163
 
@@ -218,9 +224,13 @@ class Params(list[Param]):
218
224
 
219
225
 
220
226
  class _Stub:
221
- def __init__(self, ignore: _Ignore | None = None) -> None:
227
+ def __init__(
228
+ self,
229
+ returns: _Return | None = None,
230
+ ignore: _Ignore | None = None,
231
+ ) -> None:
222
232
  self._args = Params(ignore or _Ignore())
223
- self._returns = False
233
+ self._returns = returns or _Return()
224
234
 
225
235
  @property
226
236
  def args(self) -> Params:
@@ -228,7 +238,7 @@ class _Stub:
228
238
  return self._args
229
239
 
230
240
  @property
231
- def returns(self) -> bool:
241
+ def returns(self) -> _Return:
232
242
  """True if a return (or yield) is declared or documented."""
233
243
  return self._returns
234
244
 
@@ -236,20 +246,16 @@ class _Stub:
236
246
  class Signature(_Stub):
237
247
  """Parsed function signature (args and return type).
238
248
 
239
- :param rettype: Kind of return (none, some, untyped).
240
249
  :param returns: True if return is declared in the signature.
241
250
  :param ignore: Configuration object for what to ignore.
242
251
  """
243
252
 
244
253
  def __init__(
245
254
  self,
246
- rettype: RetType = RetType.NONE,
247
- returns: bool = False,
255
+ returns: _Return | None = None,
248
256
  ignore: _Ignore | None = None,
249
257
  ) -> None:
250
- super().__init__(ignore or _Ignore())
251
- self._rettype = rettype
252
- self._returns = returns
258
+ super().__init__(returns, ignore or _Ignore())
253
259
 
254
260
  @classmethod
255
261
  def from_ast(
@@ -264,8 +270,8 @@ class Signature(_Stub):
264
270
  :return: Signature with args and return type.
265
271
  """
266
272
  rettype = RetType.from_ast(node.returns)
267
- returns = rettype == RetType.SOME
268
- signature = cls(rettype, returns, ignore)
273
+ returns = _Return(rettype == RetType.SOME, rettype)
274
+ signature = cls(returns, ignore)
269
275
  # noinspection PyUnresolvedReferences
270
276
  for i in [
271
277
  a if isinstance(a, Param) else Param(name=a.name)
@@ -282,18 +288,12 @@ class Signature(_Stub):
282
288
 
283
289
  return signature
284
290
 
285
- @property
286
- def rettype(self) -> RetType:
287
- """Return annotation kind (none, some, or untyped)."""
288
- return self._rettype
289
-
290
291
  def overload(self, rettype: RetType) -> None:
291
292
  """Set this signature's return type (for overloads).
292
293
 
293
294
  :param rettype: Return type for the overloaded variant.
294
295
  """
295
- self._rettype = rettype
296
- self._returns = rettype != RetType.NONE
296
+ self._returns = _Return(rettype != RetType.NONE, rettype)
297
297
 
298
298
 
299
299
  class Docstring(_Stub):
@@ -301,7 +301,6 @@ class Docstring(_Stub):
301
301
 
302
302
  :param string: Raw docstring text after normalization.
303
303
  :param returns: True if a return or yield section is present.
304
- :param ret_description_missing: True if return has no description.
305
304
  """
306
305
 
307
306
  @staticmethod
@@ -345,13 +344,10 @@ class Docstring(_Stub):
345
344
  def __init__(
346
345
  self,
347
346
  string: str | None = None,
348
- returns: bool = False,
349
- ret_description_missing: bool = False,
347
+ returns: _Return | None = None,
350
348
  ) -> None:
351
- super().__init__()
349
+ super().__init__(returns)
352
350
  self._string = string
353
- self._returns = returns
354
- self._ret_description_missing = ret_description_missing
355
351
 
356
352
  @classmethod
357
353
  def from_ast(cls, node: _ast.Const) -> Docstring:
@@ -362,16 +358,16 @@ class Docstring(_Stub):
362
358
  """
363
359
  indent_anomaly = cls._indent_anomaly(node.value)
364
360
  string = cls._normalize_docstring(node.value)
365
- # todo: we can start building return objects for more detailed
366
- # todo: checks that are in common with the params class
367
361
  match = _re.search(
368
362
  r":(?:returns?|yields?):\s*(.*)",
369
363
  string,
370
364
  _re.IGNORECASE,
371
365
  )
372
- returns = bool(match)
373
- ret_description_missing = not match or not match.group(1)
374
- docstring = cls(string, returns, ret_description_missing)
366
+ returns = _Return(
367
+ bool(match),
368
+ description_missing=not match or not match.group(1),
369
+ )
370
+ docstring = cls(string, returns)
375
371
  for match in _re.findall(
376
372
  r":([\w\s]+(?:\s\|\s[\w\s]+|\w+))([^\w\s])((?:.|\n)*?)(?=\n:|$)",
377
373
  string,
@@ -402,9 +398,8 @@ class Docstring(_Stub):
402
398
 
403
399
  Used when the function has a docstring but documents nothing.
404
400
  """
405
- return self._string is not None and not self._args and not self.returns
406
-
407
- @property
408
- def ret_description_missing(self) -> bool:
409
- """True if a return section exists but has no description."""
410
- return self._ret_description_missing
401
+ return (
402
+ self._string is not None
403
+ and not self._args
404
+ and not self.returns.returns
405
+ )
@@ -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.82.1"
11
+ __version__ = "0.83.0"
@@ -198,7 +198,7 @@ E = MessageMap(
198
198
  #: SIG4xx Parameters
199
199
  401: Message(
200
200
  "SIG401",
201
- "param not indented correctly",
201
+ "parameter not indented correctly",
202
202
  "incorrect-indent",
203
203
  ),
204
204
  402: Message(
@@ -267,5 +267,10 @@ E = MessageMap(
267
267
  "maximum recursion depth exceeded",
268
268
  "recursion-error",
269
269
  ),
270
+ 904: Message(
271
+ "SIG904",
272
+ "duplicates found in mros",
273
+ "duplicates-found-in-mros",
274
+ ),
270
275
  },
271
276
  )
@@ -0,0 +1,12 @@
1
+ """
2
+ docsig.plugin
3
+ =============
4
+ """
5
+
6
+ from ._flake8 import Flake8
7
+ from ._validate_pyproject import ValidatePyproject
8
+
9
+ __all__ = [
10
+ "Flake8",
11
+ "ValidatePyproject",
12
+ ]
@@ -1,32 +1,36 @@
1
1
  """
2
- docsig.plugin
3
- =============
2
+ docsig.plugin._flake8
3
+ =====================
4
4
 
5
5
  Flake8 plugin entry point that runs docsig checks and reports as flake8
6
6
  errors.
7
7
  """
8
8
 
9
- import ast
10
- import os
11
- import sys
12
- import typing as t
13
- from argparse import SUPPRESS, Namespace
14
- from pathlib import Path
15
-
16
- from ._config import Check as _Check
17
- from ._config import Config as _Config
18
- from ._config import Ignore as _Ignore
19
- from ._config import get_config as _get_config
20
- from ._config import merge_configs as _merge_configs
21
- from ._core import handle_deprecations, runner, setup_logger
22
- from ._version import __version__
23
- from .messages import FLAKE8, E
24
-
25
- Flake8Error = tuple[int, int, str, type[t.Any]]
26
- sys.path.append(os.path.abspath(os.getcwd()))
27
-
28
-
29
- class Docsig:
9
+ import ast as _ast
10
+ import os as _os
11
+ import sys as _sys
12
+ import typing as _t
13
+ from argparse import SUPPRESS as _SUPPRESS
14
+ from argparse import Namespace as _Namespace
15
+ from pathlib import Path as _Path
16
+
17
+ from .._config import Check as _Check
18
+ from .._config import Config as _Config
19
+ from .._config import Ignore as _Ignore
20
+ from .._config import get_config as _get_config
21
+ from .._config import merge_configs as _merge_configs
22
+ from .._core import handle_deprecations as _handle_deprecations
23
+ from .._core import runner as _runner
24
+ from .._core import setup_logger as _setup_logger
25
+ from .._version import __version__
26
+ from ..messages import FLAKE8 as _FLAKE8
27
+ from ..messages import E as _E
28
+
29
+ _Flake8Error = tuple[int, int, str, type[_t.Any]]
30
+ _sys.path.append(_os.path.abspath(_os.getcwd()))
31
+
32
+
33
+ class Flake8:
30
34
  """Plugin that runs docsig on a file and yields flake8 errors.
31
35
 
32
36
  :param tree: Parsed AST module (unused; flake8 requires it for the
@@ -37,9 +41,9 @@ class Docsig:
37
41
  off_by_default = False
38
42
  name = __package__
39
43
  version = __version__
40
- a = Namespace()
44
+ a = _Namespace()
41
45
 
42
- def __init__(self, tree: ast.Module, filename: str) -> None:
46
+ def __init__(self, tree: _ast.Module, filename: str) -> None:
43
47
  _tree = tree # noqa
44
48
  self.filename = filename
45
49
 
@@ -48,7 +52,7 @@ class Docsig:
48
52
  # might require that flake8 actually be installed, which is not a
49
53
  # requirement for this package
50
54
  @classmethod
51
- def add_options(cls, parser: t.Any) -> None:
55
+ def add_options(cls, parser: _t.Any) -> None:
52
56
  """Register CLI and config opts with the flake8 option parser.
53
57
 
54
58
  :param parser: Flake8 option manager to extend.
@@ -132,18 +136,18 @@ class Docsig:
132
136
  "--sig-ignore-typechecker",
133
137
  action="store_true",
134
138
  parse_from_config=True,
135
- help=SUPPRESS,
139
+ help=_SUPPRESS,
136
140
  )
137
141
 
138
142
  @classmethod
139
- def parse_options(cls, a: Namespace) -> None:
143
+ def parse_options(cls, a: _Namespace) -> None:
140
144
  """Merge parsed options with pyproject config into class state.
141
145
 
142
146
  :param a: Argparse namespace from flake8.
143
147
  """
144
148
  if getattr(a, "sig_ignore_typechecker", False):
145
149
  a.extend_ignore = list(a.extend_ignore or [])
146
- handle_deprecations(
150
+ _handle_deprecations(
147
151
  getattr(a, "sig_ignore_typechecker", False),
148
152
  a.extend_ignore,
149
153
  ["SIG501", "SIG502", "SIG503", "SIG504", "SIG505", "SIG506"],
@@ -155,7 +159,7 @@ class Docsig:
155
159
  _get_config(__package__),
156
160
  )
157
161
 
158
- def run(self) -> t.Generator[Flake8Error, None, None]:
162
+ def run(self) -> _t.Generator[_Flake8Error, None, None]:
159
163
  """Run docsig on the file and yield flake8 errors per failure.
160
164
 
161
165
  If both class and class-constructor checks are enabled,
@@ -167,15 +171,15 @@ class Docsig:
167
171
  """
168
172
  if self.a.check_class and self.a.check_class_constructor:
169
173
  line = "{msg}".format(
170
- msg=FLAKE8.format(
171
- ref=E[5].ref,
172
- description=E[5].description,
173
- symbolic=E[5].symbolic,
174
+ msg=_FLAKE8.format(
175
+ ref=_E[5].ref,
176
+ description=_E[5].description,
177
+ symbolic=_E[5].symbolic,
174
178
  ),
175
179
  )
176
180
  yield 0, 0, line, self.__class__
177
181
  else:
178
- setup_logger(self.a.verbose)
182
+ _setup_logger(self.a.verbose)
179
183
  check = _Check(
180
184
  class_=self.a.check_class,
181
185
  class_constructor=self.a.check_class_constructor,
@@ -196,14 +200,14 @@ class Docsig:
196
200
  ignore=ignore,
197
201
  verbose=self.a.verbose,
198
202
  )
199
- results = runner(Path(self.filename), config)
203
+ results = _runner(_Path(self.filename), config)
200
204
  for result in results:
201
205
  if not result.retcode:
202
206
  continue
203
207
 
204
208
  for info in result:
205
209
  line = "{msg} '{name}'".format(
206
- msg=FLAKE8.format(
210
+ msg=_FLAKE8.format(
207
211
  ref=info.ref,
208
212
  description=info.description,
209
213
  symbolic=info.symbolic,
@@ -0,0 +1,103 @@
1
+ """
2
+ docsig.plugin._validate_pyproject
3
+ =================================
4
+ """
5
+
6
+ # pylint: disable=protected-access
7
+
8
+ import typing as _t
9
+
10
+ # noinspection PyFinal
11
+ from argparse import SUPPRESS as _SUPPRESS
12
+ from argparse import Action as _Action
13
+
14
+ import docsig as _docsig
15
+
16
+ from .._config import build_parser as _build_parser
17
+
18
+ _ID = "https://docsig.io/en/latest/usage/configuration/schema.json"
19
+ _SCHEMA = "http://json-schema.org/draft-07/schema#"
20
+ _DESCRIPTION_EXCLUDE = ("comma separated ",)
21
+ _NARGS = ("+", "*")
22
+ _EXCLUDED_OPTIONS = ("help", "list-checks", "string", "version")
23
+
24
+
25
+ class ValidatePyproject(dict[str, _t.Any]):
26
+ """Schema for the docsig tool section in pyproject.toml.
27
+
28
+ :param tool_name: The tool name to validate.
29
+ """
30
+
31
+ def __init__(self, tool_name: str = "docsig") -> None:
32
+ assert tool_name == "docsig", "Only docsig is supported."
33
+ self._parser = _build_parser()
34
+ super().__init__(
35
+ {
36
+ "$comment": _CMT,
37
+ "$id": _ID,
38
+ "$schema": _SCHEMA,
39
+ "type": "object",
40
+ "additionalProperties": False,
41
+ },
42
+ )
43
+ self._get_properties()
44
+ self._get_mutually_exclusive_options()
45
+
46
+ @staticmethod
47
+ def _skip_action(action: _Action, name: str) -> bool:
48
+ if not action.option_strings:
49
+ return True
50
+
51
+ if action.help == _SUPPRESS:
52
+ return True
53
+
54
+ if name in _EXCLUDED_OPTIONS:
55
+ return True
56
+
57
+ return False
58
+
59
+ def _get_properties(self) -> None:
60
+ self["properties"] = {}
61
+ for action in self._parser._actions:
62
+ name = action.dest.replace("_", "-")
63
+ if self._skip_action(action, name):
64
+ continue
65
+
66
+ self["properties"][name] = {"default": action.default}
67
+ if isinstance(action.default, bool):
68
+ self["properties"][name]["type"] = "boolean"
69
+ elif isinstance(action.default, list) or action.nargs in _NARGS:
70
+ self["properties"][name]["type"] = "array"
71
+ self["properties"][name]["items"] = {"type": "string"}
72
+ elif action.type is None:
73
+ self["properties"][name]["type"] = "string"
74
+
75
+ if action.help:
76
+ description = action.help
77
+ for exclude in _DESCRIPTION_EXCLUDE:
78
+ description = description.replace(exclude, "")
79
+
80
+ self["properties"][name]["description"] = description
81
+
82
+ def _get_mutually_exclusive_options(self) -> None:
83
+ for group in self._parser._mutually_exclusive_groups:
84
+ names = []
85
+ for action in group._group_actions:
86
+ name = action.dest.replace("_", "-")
87
+ if self._skip_action(action, name):
88
+ continue
89
+
90
+ names.append(name)
91
+
92
+ if len(names) > 1:
93
+ if "allOf" not in self:
94
+ self["allOf"] = []
95
+
96
+ self["allOf"].append({"not": {"required": names}})
97
+
98
+
99
+ _CMT = ValidatePyproject.__doc__.splitlines()[0].lower()[:-1] # type: ignore
100
+
101
+ # for
102
+ # $ validate-pyproject --help
103
+ ValidatePyproject.__doc__ = _docsig.__doc__
@@ -11,7 +11,7 @@ line-length = 79
11
11
  allow_dirty = true
12
12
  commit = true
13
13
  commit_args = "-sS"
14
- current_version = "0.82.1"
14
+ current_version = "0.83.0"
15
15
  message = "bump: version {current_version} → {new_version}"
16
16
  sign_tags = true
17
17
  tag = true
@@ -103,7 +103,7 @@ non_interactive = true
103
103
 
104
104
  [tool.poetry]
105
105
  authors = [
106
- "jshwi <stephen@jshwisolutions.com>"
106
+ "jshwi <s.whitlock@live.com>"
107
107
  ]
108
108
  description = "Check signature params for proper documentation"
109
109
  documentation = "https://docsig.io/en/latest"
@@ -117,12 +117,12 @@ keywords = [
117
117
  ]
118
118
  license = "MIT"
119
119
  maintainers = [
120
- "jshwi <stephen@jshwisolutions.com>"
120
+ "jshwi <s.whitlock@live.com>"
121
121
  ]
122
122
  name = "docsig"
123
123
  readme = "README.rst"
124
124
  repository = "https://github.com/jshwi/docsig"
125
- version = "0.82.1"
125
+ version = "0.83.0"
126
126
 
127
127
  [tool.poetry.dependencies]
128
128
  Sphinx = ">=7,<9"
@@ -146,15 +146,17 @@ tomli = "^2.0.1"
146
146
  tomli-w = "^1.0.0"
147
147
  towncrier = ">=23.11,<26.0"
148
148
  tox = "^4.15.0"
149
+ validate-pyproject = {extras = ["all", "store"], version = "^0.25"}
149
150
  vulture = "^2.11"
150
151
 
151
152
  [tool.poetry.group.docs.dependencies]
152
153
  furo = ">=2024.4.27,<2026.0.0"
153
154
  myst-parser = ">=3.0.1,<5.0.0"
154
- pytest = "8.3.5"
155
+ pytest = "9.0.3"
155
156
  pyyaml = "^6.0.1"
156
157
  roman-numerals = "4.1.0"
157
158
  sphinx-copybutton = "^0.5.2"
159
+ sphinx-jsonschema = "^1.19.2"
158
160
  sphinx-markdown-builder = ">=0.5.5,<0.7.0"
159
161
  sphinx-sitemap = "^2.9.0"
160
162
  templatest = ">=0.10.1,<0.13.0"
@@ -163,7 +165,7 @@ templatest = ">=0.10.1,<0.13.0"
163
165
  flake8 = "^7.1.0"
164
166
 
165
167
  [tool.poetry.group.tests.dependencies]
166
- pytest = "^8.2.0"
168
+ pytest = ">=8.2,<10.0"
167
169
  pytest-benchmark = ">=4,<6"
168
170
  pytest-cov = ">=5,<8"
169
171
  pytest-gitignore = "^1.3"
@@ -173,7 +175,10 @@ pytest-xdist = "^3.6.1"
173
175
  templatest = ">=0.10.1,<0.13.0"
174
176
 
175
177
  [tool.poetry.plugins."flake8.extension"]
176
- SIG = "docsig.plugin:Docsig"
178
+ SIG = "docsig.plugin:Flake8"
179
+
180
+ [tool.poetry.plugins."validate_pyproject.tool_schema"]
181
+ docsig = "docsig.plugin:ValidatePyproject"
177
182
 
178
183
  [tool.poetry.scripts]
179
184
  docsig = "docsig.__main__:main"
@@ -225,6 +230,9 @@ name = "Changed"
225
230
  [tool.towncrier.fragment.deprecate]
226
231
  name = "Deprecated"
227
232
 
233
+ [tool.towncrier.fragment.feat]
234
+ name = "Feature"
235
+
228
236
  [tool.towncrier.fragment.fix]
229
237
  name = "Fixed"
230
238
 
@@ -239,7 +247,7 @@ name = "Security"
239
247
 
240
248
  [tool.vulture]
241
249
  exclude = [
242
- "tests/_templates.py",
250
+ "tests/base_test.py",
243
251
  "tests/conftest.py"
244
252
  ]
245
253
  make_whitelist = true
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