docsig 0.86.0__tar.gz → 0.87.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.
- {docsig-0.86.0 → docsig-0.87.0}/PKG-INFO +13 -7
- {docsig-0.86.0 → docsig-0.87.0}/README.rst +11 -5
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_config.py +22 -27
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_core.py +11 -4
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_decorators.py +28 -10
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_files.py +10 -18
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_main.py +4 -1
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_module.py +22 -13
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_report.py +61 -29
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_stub.py +3 -1
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_utils.py +23 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_version.py +1 -1
- {docsig-0.86.0 → docsig-0.87.0}/pyproject.toml +10 -4
- {docsig-0.86.0 → docsig-0.87.0}/LICENSE +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/__init__.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/__main__.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_check.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_directives.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_hooks.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/_parsers.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/messages.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/plugin/__init__.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/plugin/_flake8.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/plugin/_validate_pyproject.py +0 -0
- {docsig-0.86.0 → docsig-0.87.0}/docsig/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docsig
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.87.0
|
|
4
4
|
Summary: Check signature params for proper documentation
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Requires-Dist: Sphinx (>=7,<
|
|
20
|
+
Requires-Dist: Sphinx (>=7,<8.2)
|
|
21
21
|
Requires-Dist: astroid (>=4.0.2,<5.0.0)
|
|
22
22
|
Requires-Dist: pathspec (>=0.12.1,<1.2.0)
|
|
23
23
|
Requires-Dist: tomli (>=2.0.1,<3.0.0)
|
|
@@ -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|
|
|
39
|
+
|License| |PyPI| |Jetbrains Plugin| |CI| |Jetbrains Build| |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,6 +74,12 @@ 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
|
+
.. |Jetbrains Plugin| image:: https://img.shields.io/jetbrains/plugin/v/32129.svg
|
|
78
|
+
:target: https://plugins.jetbrains.com/plugin/32129
|
|
79
|
+
:alt: Jetbrains Plugin
|
|
80
|
+
.. |Jetbrains Build| image:: https://github.com/jshwi/docsig/actions/workflows/build-intellij-plugin.yaml/badge.svg
|
|
81
|
+
:target: https://github.com/jshwi/docsig/actions/workflows/build-intellij-plugin.yaml
|
|
82
|
+
:alt: Build
|
|
77
83
|
|
|
78
84
|
Check Python signature params for proper documentation
|
|
79
85
|
-------------------------------------------------------
|
|
@@ -87,7 +93,7 @@ Maintain accurate and up-to-date Python documentation by automatically checking
|
|
|
87
93
|
that all parameters in function signatures match their docstring documentation.
|
|
88
94
|
Use docsig as a standalone tool, integrate it with ``flake8``, or add it as a
|
|
89
95
|
``pre-commit`` hook to catch documentation issues before they reach your
|
|
90
|
-
repository.
|
|
96
|
+
repository. **docsig** is also available as a ``PyCharm`` plugin
|
|
91
97
|
|
|
92
98
|
Contributing
|
|
93
99
|
------------
|
|
@@ -195,7 +201,7 @@ ensure your installation has registered `docsig`
|
|
|
195
201
|
.. code-block:: console
|
|
196
202
|
|
|
197
203
|
$ flake8 --version
|
|
198
|
-
7.3.0 (docsig: 0.
|
|
204
|
+
7.3.0 (docsig: 0.87.0, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
|
|
199
205
|
|
|
200
206
|
And now use `flake8` to lint your files
|
|
201
207
|
|
|
@@ -281,7 +287,7 @@ Standalone
|
|
|
281
287
|
|
|
282
288
|
repos:
|
|
283
289
|
- repo: https://github.com/jshwi/docsig
|
|
284
|
-
rev: v0.
|
|
290
|
+
rev: v0.87.0
|
|
285
291
|
hooks:
|
|
286
292
|
- id: docsig
|
|
287
293
|
args:
|
|
@@ -300,7 +306,7 @@ or integrated with ``flake8``
|
|
|
300
306
|
hooks:
|
|
301
307
|
- id: flake8
|
|
302
308
|
additional_dependencies:
|
|
303
|
-
- docsig==0.
|
|
309
|
+
- docsig==0.87.0
|
|
304
310
|
args:
|
|
305
311
|
- "--sig-check-class"
|
|
306
312
|
- "--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|
|
|
10
|
+
|License| |PyPI| |Jetbrains Plugin| |CI| |Jetbrains Build| |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,6 +45,12 @@
|
|
|
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
|
+
.. |Jetbrains Plugin| image:: https://img.shields.io/jetbrains/plugin/v/32129.svg
|
|
49
|
+
:target: https://plugins.jetbrains.com/plugin/32129
|
|
50
|
+
:alt: Jetbrains Plugin
|
|
51
|
+
.. |Jetbrains Build| image:: https://github.com/jshwi/docsig/actions/workflows/build-intellij-plugin.yaml/badge.svg
|
|
52
|
+
:target: https://github.com/jshwi/docsig/actions/workflows/build-intellij-plugin.yaml
|
|
53
|
+
:alt: Build
|
|
48
54
|
|
|
49
55
|
Check Python signature params for proper documentation
|
|
50
56
|
-------------------------------------------------------
|
|
@@ -58,7 +64,7 @@ Maintain accurate and up-to-date Python documentation by automatically checking
|
|
|
58
64
|
that all parameters in function signatures match their docstring documentation.
|
|
59
65
|
Use docsig as a standalone tool, integrate it with ``flake8``, or add it as a
|
|
60
66
|
``pre-commit`` hook to catch documentation issues before they reach your
|
|
61
|
-
repository.
|
|
67
|
+
repository. **docsig** is also available as a ``PyCharm`` plugin
|
|
62
68
|
|
|
63
69
|
Contributing
|
|
64
70
|
------------
|
|
@@ -166,7 +172,7 @@ ensure your installation has registered `docsig`
|
|
|
166
172
|
.. code-block:: console
|
|
167
173
|
|
|
168
174
|
$ flake8 --version
|
|
169
|
-
7.3.0 (docsig: 0.
|
|
175
|
+
7.3.0 (docsig: 0.87.0, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
|
|
170
176
|
|
|
171
177
|
And now use `flake8` to lint your files
|
|
172
178
|
|
|
@@ -252,7 +258,7 @@ Standalone
|
|
|
252
258
|
|
|
253
259
|
repos:
|
|
254
260
|
- repo: https://github.com/jshwi/docsig
|
|
255
|
-
rev: v0.
|
|
261
|
+
rev: v0.87.0
|
|
256
262
|
hooks:
|
|
257
263
|
- id: docsig
|
|
258
264
|
args:
|
|
@@ -271,7 +277,7 @@ or integrated with ``flake8``
|
|
|
271
277
|
hooks:
|
|
272
278
|
- id: flake8
|
|
273
279
|
additional_dependencies:
|
|
274
|
-
- docsig==0.
|
|
280
|
+
- docsig==0.87.0
|
|
275
281
|
args:
|
|
276
282
|
- "--sig-check-class"
|
|
277
283
|
- "--sig-check-dunders"
|
|
@@ -6,7 +6,6 @@ Config dataclasses, pyproject.toml loader, and commandline.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import argparse as _argparse
|
|
9
|
-
import os as _os
|
|
10
9
|
import re as _re
|
|
11
10
|
import typing as _t
|
|
12
11
|
from dataclasses import dataclass as _dataclass
|
|
@@ -15,6 +14,7 @@ from pathlib import Path as _Path
|
|
|
15
14
|
|
|
16
15
|
import tomli as _tomli
|
|
17
16
|
|
|
17
|
+
from ._utils import get_parent_that_has as _get_parent_that_has
|
|
18
18
|
from ._version import __version__
|
|
19
19
|
from .messages import Messages as _Messages
|
|
20
20
|
|
|
@@ -26,31 +26,18 @@ def _split_comma(value: str) -> list[str]:
|
|
|
26
26
|
return [i.replace("\\,", ",") for i in _re.split(r"(?<!\\),", value)]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
# attempt to locate a pyproject.toml file if one exists in parents
|
|
30
|
-
def _find_pyproject_toml(path: _Path | None = None) -> _Path | None:
|
|
31
|
-
if not path:
|
|
32
|
-
path = _Path.cwd()
|
|
33
|
-
|
|
34
|
-
pyproject_toml = path / PYPROJECT_TOML
|
|
35
|
-
if pyproject_toml.is_file():
|
|
36
|
-
return pyproject_toml
|
|
37
|
-
|
|
38
|
-
if str(path) == _os.path.abspath(_os.sep):
|
|
39
|
-
return None
|
|
40
|
-
|
|
41
|
-
return _find_pyproject_toml(path.parent)
|
|
42
|
-
|
|
43
|
-
|
|
44
29
|
def get_config(prog: str) -> dict[str, _t.Any]:
|
|
45
30
|
"""Return the program's tool-section config from pyproject.toml.
|
|
46
31
|
|
|
47
32
|
:param prog: Program name.
|
|
48
33
|
:return: Config dict, or empty dict if no config is found.
|
|
49
34
|
"""
|
|
50
|
-
|
|
35
|
+
# attempt to locate a pyproject.toml file if one exists in parents
|
|
36
|
+
pyproject_file = _get_parent_that_has(PYPROJECT_TOML)
|
|
51
37
|
if pyproject_file is None:
|
|
52
38
|
return {}
|
|
53
39
|
|
|
40
|
+
pyproject_file /= PYPROJECT_TOML
|
|
54
41
|
return {
|
|
55
42
|
k.replace("-", "_"): v
|
|
56
43
|
for k, v in _tomli.loads(pyproject_file.read_text())
|
|
@@ -220,17 +207,10 @@ def build_parser() -> _ArgumentParser:
|
|
|
220
207
|
action="store_true",
|
|
221
208
|
help="ignore kwargs prefixed with two asterisks",
|
|
222
209
|
)
|
|
223
|
-
parser.add_argument(
|
|
224
|
-
"-i",
|
|
225
|
-
action="store_true",
|
|
226
|
-
help=_argparse.SUPPRESS,
|
|
227
|
-
dest="ignore_no_params",
|
|
228
|
-
)
|
|
229
210
|
parser.add_argument(
|
|
230
211
|
"--ignore-no-params",
|
|
231
212
|
action="store_true",
|
|
232
213
|
help="ignore docstrings where parameters are not documented",
|
|
233
|
-
dest="ignore_no_params",
|
|
234
214
|
)
|
|
235
215
|
parser.add_argument(
|
|
236
216
|
"--ignore-typechecker",
|
|
@@ -336,6 +316,23 @@ class Ignore:
|
|
|
336
316
|
kwargs: bool = False
|
|
337
317
|
|
|
338
318
|
|
|
319
|
+
@_dataclass
|
|
320
|
+
# pylint: disable-next=too-few-public-methods
|
|
321
|
+
class Filters:
|
|
322
|
+
"""Configuration for which paths to filter.
|
|
323
|
+
|
|
324
|
+
:param exclude: Regular expression of files and dirs to exclude from
|
|
325
|
+
checks.
|
|
326
|
+
:param excludes: Files or dirs to exclude from checks.
|
|
327
|
+
:param include_ignored: Check files even if they match a gitignore
|
|
328
|
+
pattern.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
exclude: list[str] = _field(default_factory=list)
|
|
332
|
+
excludes: list[str] = _field(default_factory=list)
|
|
333
|
+
include_ignored: bool = False
|
|
334
|
+
|
|
335
|
+
|
|
339
336
|
@_dataclass(frozen=True)
|
|
340
337
|
# pylint: disable-next=too-many-instance-attributes,too-few-public-methods
|
|
341
338
|
class Config:
|
|
@@ -347,11 +344,9 @@ class Config:
|
|
|
347
344
|
|
|
348
345
|
check: Check = _field(default_factory=Check)
|
|
349
346
|
ignore: Ignore = _field(default_factory=Ignore)
|
|
347
|
+
filters: Filters = _field(default_factory=Filters)
|
|
350
348
|
target: _Messages = _field(default_factory=_Messages)
|
|
351
349
|
disable: _Messages = _field(default_factory=_Messages)
|
|
352
|
-
exclude: list[str] = _field(default_factory=list)
|
|
353
|
-
excludes: list[str] = _field(default_factory=list)
|
|
354
350
|
list_checks: bool = False
|
|
355
|
-
include_ignored: bool = False
|
|
356
351
|
no_ansi: bool = False
|
|
357
352
|
verbose: bool = False
|
|
@@ -15,12 +15,14 @@ from . import _decorators
|
|
|
15
15
|
from ._check import run_checks as _run_checks
|
|
16
16
|
from ._config import Check as _Check
|
|
17
17
|
from ._config import Config as _Config
|
|
18
|
+
from ._config import Filters as _Filters
|
|
18
19
|
from ._config import Ignore as _Ignore
|
|
19
20
|
from ._files import Files as _Files
|
|
20
21
|
from ._parsers import parse_from_file as _parse_from_file
|
|
21
22
|
from ._parsers import parse_from_string as _parse_from_string
|
|
22
23
|
from ._report import Failures as _Failures
|
|
23
24
|
from ._report import report as _report
|
|
25
|
+
from ._utils import get_parent_that_has as _get_parent_that_has
|
|
24
26
|
from ._utils import print_checks as _print_checks
|
|
25
27
|
from .messages import E as _E
|
|
26
28
|
from .messages import Messages as _Messages
|
|
@@ -191,22 +193,26 @@ def docsig( # pylint: disable=too-many-locals,too-many-arguments
|
|
|
191
193
|
args=ignore_args,
|
|
192
194
|
kwargs=ignore_kwargs,
|
|
193
195
|
)
|
|
196
|
+
filters = _Filters(
|
|
197
|
+
include_ignored=include_ignored,
|
|
198
|
+
exclude=exclude_,
|
|
199
|
+
excludes=excludes or [],
|
|
200
|
+
)
|
|
194
201
|
config = _Config(
|
|
195
202
|
list_checks=list_checks,
|
|
196
|
-
include_ignored=include_ignored,
|
|
197
203
|
check=check,
|
|
198
204
|
ignore=ignore,
|
|
205
|
+
filters=filters,
|
|
199
206
|
no_ansi=no_ansi,
|
|
200
207
|
verbose=verbose,
|
|
201
208
|
target=target or _Messages(),
|
|
202
209
|
disable=disable,
|
|
203
|
-
exclude=exclude_,
|
|
204
|
-
excludes=excludes or [],
|
|
205
210
|
)
|
|
206
211
|
setup_logger(config.verbose)
|
|
207
212
|
logger = _logging.getLogger(__package__)
|
|
208
213
|
logger.debug(_pformat(config))
|
|
209
214
|
if config.list_checks:
|
|
215
|
+
# noinspection PyNoneFunctionAssignment
|
|
210
216
|
return int(bool(_print_checks())) # type: ignore
|
|
211
217
|
|
|
212
218
|
if string:
|
|
@@ -215,7 +221,8 @@ def docsig( # pylint: disable=too-many-locals,too-many-arguments
|
|
|
215
221
|
return _report(failures, config)
|
|
216
222
|
|
|
217
223
|
retcodes = [0]
|
|
218
|
-
|
|
224
|
+
repo = _get_parent_that_has(".git/HEAD")
|
|
225
|
+
files = _Files(path, config.filters, repo)
|
|
219
226
|
for file in files:
|
|
220
227
|
failures = runner(file, config)
|
|
221
228
|
retcode = _report(failures, config, str(file))
|
|
@@ -4,6 +4,8 @@ docsig._decorators
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import functools as _functools
|
|
7
|
+
import json as _json
|
|
8
|
+
import os as _os
|
|
7
9
|
import sys as _sys
|
|
8
10
|
import typing as _t
|
|
9
11
|
from pathlib import Path as _Path
|
|
@@ -11,7 +13,7 @@ from pathlib import Path as _Path
|
|
|
11
13
|
from .messages import E as _E
|
|
12
14
|
|
|
13
15
|
_FuncType = _t.Callable[..., int]
|
|
14
|
-
_WrappedFuncType = _t.Callable[...,
|
|
16
|
+
_WrappedFuncType = _t.Callable[..., int]
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def parse_msgs(func: _WrappedFuncType) -> _WrappedFuncType:
|
|
@@ -25,7 +27,7 @@ def parse_msgs(func: _WrappedFuncType) -> _WrappedFuncType:
|
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
29
|
@_functools.wraps(func)
|
|
28
|
-
def _wrapper(*args: str | _Path, **kwargs: _t.Any) ->
|
|
30
|
+
def _wrapper(*args: str | _Path, **kwargs: _t.Any) -> int:
|
|
29
31
|
disable = _E.from_codes(kwargs.get("disable", [])) or None
|
|
30
32
|
target = _E.from_codes(kwargs.get("target", [])) or None
|
|
31
33
|
kwargs["disable"] = disable
|
|
@@ -35,6 +37,7 @@ def parse_msgs(func: _WrappedFuncType) -> _WrappedFuncType:
|
|
|
35
37
|
return _wrapper
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
# TODO: make report json by default and wrap with a reporter for cli
|
|
38
41
|
def validate_args(func: _FuncType) -> _WrappedFuncType:
|
|
39
42
|
"""Validate arguments before calling the wrapped function.
|
|
40
43
|
|
|
@@ -50,30 +53,32 @@ def validate_args(func: _FuncType) -> _WrappedFuncType:
|
|
|
50
53
|
"""
|
|
51
54
|
|
|
52
55
|
@_functools.wraps(func)
|
|
53
|
-
def _wrapper(*args: str | _Path, **kwargs: _t.Any) ->
|
|
54
|
-
|
|
56
|
+
def _wrapper(*args: str | _Path, **kwargs: _t.Any) -> int:
|
|
57
|
+
format_json = _os.getenv("_DOCSIG_FORMAT_JSON") is not None
|
|
58
|
+
retcode = 2
|
|
59
|
+
errors = []
|
|
55
60
|
if not kwargs.get("list_checks", False):
|
|
56
61
|
if not args and not kwargs.get("string"):
|
|
57
|
-
|
|
62
|
+
errors.append(
|
|
58
63
|
"the following arguments are required: path(s) or string",
|
|
59
64
|
)
|
|
60
65
|
|
|
61
66
|
for message in kwargs.get("disable") or []:
|
|
62
67
|
if not message.isknown:
|
|
63
|
-
|
|
68
|
+
errors.append(
|
|
64
69
|
f"unknown option to disable '{message.description}'",
|
|
65
70
|
)
|
|
66
71
|
|
|
67
72
|
for message in kwargs.get("target") or []:
|
|
68
73
|
if not message.isknown:
|
|
69
|
-
|
|
74
|
+
errors.append(
|
|
70
75
|
f"unknown option to target '{message.description}'",
|
|
71
76
|
)
|
|
72
77
|
|
|
73
78
|
if kwargs.get("check_class") and kwargs.get(
|
|
74
79
|
"check_class_constructor",
|
|
75
80
|
):
|
|
76
|
-
|
|
81
|
+
errors.append(
|
|
77
82
|
"argument to check class constructor not allowed with"
|
|
78
83
|
" argument to check class",
|
|
79
84
|
)
|
|
@@ -84,10 +89,23 @@ def validate_args(func: _FuncType) -> _WrappedFuncType:
|
|
|
84
89
|
# passing both commandline args as argparse won't
|
|
85
90
|
# allow it, therefore, this must be an issue with
|
|
86
91
|
# the pyproject.toml configuration
|
|
87
|
-
|
|
92
|
+
errors.append(
|
|
88
93
|
"please check your pyproject.toml configuration",
|
|
89
94
|
)
|
|
95
|
+
if errors:
|
|
96
|
+
message = "\n".join(errors)
|
|
97
|
+
if format_json:
|
|
98
|
+
obj = [ # pragma: no cover
|
|
99
|
+
{"line": None, "message": message, "exit": retcode},
|
|
100
|
+
]
|
|
101
|
+
if format_json: # pragma: no cover
|
|
102
|
+
print(_json.dumps(obj).strip()) # pragma: no cover
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
else:
|
|
105
|
+
print(message, file=_sys.stderr)
|
|
106
|
+
|
|
107
|
+
return retcode
|
|
108
|
+
|
|
109
|
+
return func(*args, **kwargs)
|
|
92
110
|
|
|
93
111
|
return _wrapper
|
|
@@ -14,24 +14,14 @@ from pathspec import PathSpec as _PathSpec
|
|
|
14
14
|
from pathspec.patterns import GitWildMatchPattern as _GitWildMatchPattern
|
|
15
15
|
from wcmatch.pathlib import Path as _WcPath
|
|
16
16
|
|
|
17
|
-
from ._config import
|
|
17
|
+
from ._config import Filters as _Filters
|
|
18
18
|
|
|
19
19
|
FILE_INFO = "%s: %s"
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class _Gitignore(_PathSpec):
|
|
23
|
-
def
|
|
24
|
-
if (path / ".git" / "HEAD").is_file():
|
|
25
|
-
return path
|
|
26
|
-
|
|
27
|
-
if str(path) == _os.path.abspath(_os.sep):
|
|
28
|
-
return None
|
|
29
|
-
|
|
30
|
-
return self._get_repo_relative_to(path.parent)
|
|
31
|
-
|
|
32
|
-
def __init__(self) -> None:
|
|
23
|
+
def __init__(self, repo: _Path | None = None) -> None:
|
|
33
24
|
patterns = []
|
|
34
|
-
repo = self._get_repo_relative_to(_Path.cwd())
|
|
35
25
|
# only consider gitignore patterns valid if inside a git repo
|
|
36
26
|
# there might be stray gitignore files lying about
|
|
37
27
|
if repo is not None:
|
|
@@ -72,25 +62,27 @@ class Files(list[_Path]):
|
|
|
72
62
|
"""Collect paths to check (gitignore and exclude applied).
|
|
73
63
|
|
|
74
64
|
:param paths: Path(s) to collect (files or directories).
|
|
75
|
-
:param
|
|
65
|
+
:param filters: Filters object.
|
|
66
|
+
:param repo: Path to the repo root.
|
|
76
67
|
"""
|
|
77
68
|
|
|
78
69
|
def __init__(
|
|
79
70
|
self,
|
|
80
71
|
paths: tuple[str | _Path, ...],
|
|
81
|
-
|
|
72
|
+
filters: _Filters,
|
|
73
|
+
repo: _Path | None = None,
|
|
82
74
|
) -> None:
|
|
83
75
|
super().__init__()
|
|
84
|
-
self._include_ignored =
|
|
85
|
-
self._gitignore = _Gitignore()
|
|
76
|
+
self._include_ignored = filters.include_ignored
|
|
77
|
+
self._gitignore = _Gitignore(repo)
|
|
86
78
|
logger = _logging.getLogger(__package__)
|
|
87
79
|
for path in paths:
|
|
88
80
|
self._populate(_Path(path))
|
|
89
81
|
|
|
90
82
|
for path in list(self):
|
|
91
83
|
if str(path) != "." and (
|
|
92
|
-
any(_re.match(i, str(path)) for i in
|
|
93
|
-
or any(_glob(path, i) for i in
|
|
84
|
+
any(_re.match(i, str(path)) for i in filters.exclude)
|
|
85
|
+
or any(_glob(path, i) for i in filters.excludes)
|
|
94
86
|
):
|
|
95
87
|
logger.debug(FILE_INFO, path, "in exclude list, skipping")
|
|
96
88
|
self.remove(path)
|
|
@@ -5,6 +5,7 @@ docsig._main
|
|
|
5
5
|
CLI entry point that parses args and runs docsig.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import os as _os
|
|
8
9
|
import sys as _sys
|
|
9
10
|
import warnings as _warnings
|
|
10
11
|
|
|
@@ -20,7 +21,6 @@ def _warn_on_deprecated_short_flags() -> None:
|
|
|
20
21
|
"-o": "--check-overridden",
|
|
21
22
|
"-p": "--check-protected",
|
|
22
23
|
"-P": "--check-property-returns",
|
|
23
|
-
"-i": "--ignore-no-params",
|
|
24
24
|
}
|
|
25
25
|
raw_args = _sys.argv[1:]
|
|
26
26
|
expanded_flags = []
|
|
@@ -47,6 +47,9 @@ def main() -> str | int:
|
|
|
47
47
|
|
|
48
48
|
:return: Exit code (non-zero if any check failed).
|
|
49
49
|
"""
|
|
50
|
+
if _os.getenv("_DOCSIG_FORMAT_JSON"):
|
|
51
|
+
_warnings.simplefilter("ignore", FutureWarning) # pragma: no cover
|
|
52
|
+
|
|
50
53
|
_warn_on_deprecated_short_flags()
|
|
51
54
|
a = _parse_args()
|
|
52
55
|
_excepthook(a.no_ansi)
|
|
@@ -57,7 +57,6 @@ class Parent: # pylint: disable=too-many-instance-attributes
|
|
|
57
57
|
_ast.nodes.Module
|
|
58
58
|
| _ast.nodes.ClassDef
|
|
59
59
|
| _ast.nodes.FunctionDef
|
|
60
|
-
| _ast.nodes.NodeNG
|
|
61
60
|
| None
|
|
62
61
|
) = None,
|
|
63
62
|
directives: _Directives | None = None,
|
|
@@ -82,7 +81,7 @@ class Parent: # pylint: disable=too-many-instance-attributes
|
|
|
82
81
|
# later inspected to report on the module error (the
|
|
83
82
|
# report doesn't analyze modules or classes - it
|
|
84
83
|
# analyzes functions)
|
|
85
|
-
self._children.append(Function(
|
|
84
|
+
self._children.append(Function(error=error))
|
|
86
85
|
else:
|
|
87
86
|
self._name = node.name
|
|
88
87
|
self._parse_ast(node, file)
|
|
@@ -101,13 +100,13 @@ class Parent: # pylint: disable=too-many-instance-attributes
|
|
|
101
100
|
# resolved in the directive object, they are needed to notify
|
|
102
101
|
# the user in the case that they are invalid
|
|
103
102
|
parent_comments, parent_disabled = self._directives.get(
|
|
104
|
-
node.lineno,
|
|
103
|
+
node.lineno or 0,
|
|
105
104
|
(_Comments(), _Messages()),
|
|
106
105
|
)
|
|
107
106
|
if hasattr(node, "body"):
|
|
108
107
|
for subnode in node.body:
|
|
109
108
|
comments, disabled = self._directives.get(
|
|
110
|
-
subnode.lineno,
|
|
109
|
+
subnode.lineno or 0,
|
|
111
110
|
(_Comments(), _Messages()),
|
|
112
111
|
)
|
|
113
112
|
comments.extend(parent_comments)
|
|
@@ -119,7 +118,7 @@ class Parent: # pylint: disable=too-many-instance-attributes
|
|
|
119
118
|
for name in subnode.names:
|
|
120
119
|
original, alias = name
|
|
121
120
|
self._imports[original] = alias or original
|
|
122
|
-
elif isinstance(subnode, _ast.FunctionDef):
|
|
121
|
+
elif isinstance(subnode, _ast.nodes.FunctionDef):
|
|
123
122
|
func = Function(
|
|
124
123
|
subnode,
|
|
125
124
|
comments,
|
|
@@ -147,7 +146,7 @@ class Parent: # pylint: disable=too-many-instance-attributes
|
|
|
147
146
|
)
|
|
148
147
|
|
|
149
148
|
self._children.append(func)
|
|
150
|
-
elif isinstance(subnode, _ast.ClassDef):
|
|
149
|
+
elif isinstance(subnode, _ast.nodes.ClassDef):
|
|
151
150
|
self._children.append(
|
|
152
151
|
Parent(
|
|
153
152
|
subnode,
|
|
@@ -192,7 +191,7 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
|
|
|
192
191
|
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
|
|
193
192
|
def __init__(
|
|
194
193
|
self,
|
|
195
|
-
node: _ast.nodes.FunctionDef |
|
|
194
|
+
node: _ast.nodes.FunctionDef | None = None,
|
|
196
195
|
comments: _Comments | None = None,
|
|
197
196
|
directives: _Directives | None = None,
|
|
198
197
|
messages: _Messages | None = None,
|
|
@@ -216,7 +215,7 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
|
|
|
216
215
|
self._docstring = _Docstring()
|
|
217
216
|
self._lineno = 0
|
|
218
217
|
self._error = error
|
|
219
|
-
if node is not None:
|
|
218
|
+
if node is not None and node.parent is not None:
|
|
220
219
|
self._parent = node.parent.frame()
|
|
221
220
|
self._decorators = node.decorators
|
|
222
221
|
self._lineno = node.lineno
|
|
@@ -230,7 +229,11 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
|
|
|
230
229
|
node,
|
|
231
230
|
self._config.ignore,
|
|
232
231
|
)
|
|
233
|
-
if
|
|
232
|
+
if (
|
|
233
|
+
self.isinit
|
|
234
|
+
and not self._config.check.class_constructor
|
|
235
|
+
and not isinstance(self._parent, _ast.nodes.Lambda)
|
|
236
|
+
):
|
|
234
237
|
# docstring for __init__ is expected on the class
|
|
235
238
|
# docstring
|
|
236
239
|
relevant_doc_node = self._parent.doc_node
|
|
@@ -259,7 +262,7 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
|
|
|
259
262
|
@property
|
|
260
263
|
def ismethod(self) -> bool:
|
|
261
264
|
"""Whether this function is defined in a class (method)."""
|
|
262
|
-
return isinstance(self._parent, _ast.ClassDef)
|
|
265
|
+
return isinstance(self._parent, _ast.nodes.ClassDef)
|
|
263
266
|
|
|
264
267
|
@property
|
|
265
268
|
def isproperty(self) -> bool:
|
|
@@ -282,11 +285,16 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
|
|
|
282
285
|
@property
|
|
283
286
|
def isoverridden(self) -> bool:
|
|
284
287
|
"""Whether this function overrides a base class method."""
|
|
285
|
-
if
|
|
288
|
+
if (
|
|
289
|
+
self.ismethod
|
|
290
|
+
and not self.isinit
|
|
291
|
+
and self._parent is not None
|
|
292
|
+
and isinstance(self._parent, _ast.nodes.ClassDef)
|
|
293
|
+
):
|
|
286
294
|
for ancestor in self._parent.ancestors():
|
|
287
295
|
if self.name in ancestor and isinstance(
|
|
288
296
|
ancestor[self.name],
|
|
289
|
-
_ast.FunctionDef,
|
|
297
|
+
_ast.nodes.FunctionDef,
|
|
290
298
|
):
|
|
291
299
|
return True
|
|
292
300
|
|
|
@@ -324,6 +332,7 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
|
|
|
324
332
|
| _ast.nodes.Module
|
|
325
333
|
| _ast.nodes.ClassDef
|
|
326
334
|
| _ast.nodes.Lambda
|
|
335
|
+
| None
|
|
327
336
|
):
|
|
328
337
|
"""Function's parent node."""
|
|
329
338
|
return self._parent
|
|
@@ -331,7 +340,7 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
|
|
|
331
340
|
@property
|
|
332
341
|
def lineno(self) -> int:
|
|
333
342
|
"""Line number of function declaration."""
|
|
334
|
-
return self._lineno
|
|
343
|
+
return self._lineno or 0
|
|
335
344
|
|
|
336
345
|
@property
|
|
337
346
|
def signature(self) -> _Signature:
|
|
@@ -9,6 +9,8 @@ return the highest exit code.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import contextlib as _contextlib
|
|
12
|
+
import json as _json
|
|
13
|
+
import os as _os
|
|
12
14
|
import sys as _sys
|
|
13
15
|
import typing as _t
|
|
14
16
|
from warnings import warn as _warn
|
|
@@ -71,12 +73,14 @@ class Failure(list[Failed]):
|
|
|
71
73
|
self._name = self._func.name
|
|
72
74
|
if (
|
|
73
75
|
self._func.parent is not None
|
|
76
|
+
and hasattr(self._func.parent, "name")
|
|
74
77
|
and self._func.parent.name
|
|
75
78
|
and not isinstance(self._func.parent, _ast.nodes.Module)
|
|
76
79
|
):
|
|
77
80
|
self._name = f"{self._func.parent.name}.{self._name}"
|
|
78
81
|
|
|
79
82
|
if self._func.error is not None:
|
|
83
|
+
self._retcode.append(2)
|
|
80
84
|
self._sig9xx_error()
|
|
81
85
|
else:
|
|
82
86
|
self._sig0xx_config()
|
|
@@ -97,7 +101,7 @@ class Failure(list[Failed]):
|
|
|
97
101
|
def _add(
|
|
98
102
|
self,
|
|
99
103
|
value: _Message,
|
|
100
|
-
|
|
104
|
+
include_hint: bool = False,
|
|
101
105
|
**kwargs: _t.Any,
|
|
102
106
|
) -> None:
|
|
103
107
|
self._retcode.append(int(not value.new))
|
|
@@ -107,7 +111,7 @@ class Failure(list[Failed]):
|
|
|
107
111
|
value.description.format(**kwargs),
|
|
108
112
|
value.symbolic,
|
|
109
113
|
self.lineno,
|
|
110
|
-
value.hint if
|
|
114
|
+
value.hint if include_hint else None,
|
|
111
115
|
value.new,
|
|
112
116
|
)
|
|
113
117
|
if value not in self._func.messages and failed not in self:
|
|
@@ -223,9 +227,15 @@ class Failure(list[Failed]):
|
|
|
223
227
|
self._add(_E[203])
|
|
224
228
|
|
|
225
229
|
def _sig3xx_description(self, doc: _Param) -> None:
|
|
226
|
-
|
|
230
|
+
# freeze result as it is a property and PyCharm complains
|
|
231
|
+
# `Member 'None' of 'str | None' does not have attribute
|
|
232
|
+
# 'startswith'` as property could theoretically have different
|
|
233
|
+
# result from doc.description is None to
|
|
234
|
+
# doc.description.startswith
|
|
235
|
+
doc_description = doc.description
|
|
236
|
+
if doc_description is None and doc.name is not None:
|
|
227
237
|
self._add(_E[301])
|
|
228
|
-
elif
|
|
238
|
+
elif doc_description is not None and not doc_description.startswith(
|
|
229
239
|
" ",
|
|
230
240
|
):
|
|
231
241
|
# syntax-error-in-description
|
|
@@ -237,19 +247,19 @@ class Failure(list[Failed]):
|
|
|
237
247
|
self._add(_E[303])
|
|
238
248
|
elif doc.closing_token != ":":
|
|
239
249
|
# bad-closing-token
|
|
240
|
-
self._add(_E[304], token=doc.closing_token,
|
|
241
|
-
if
|
|
250
|
+
self._add(_E[304], token=doc.closing_token, include_hint=True)
|
|
251
|
+
if doc_description is not None and not all(
|
|
242
252
|
stripped[0].isupper()
|
|
243
|
-
for i in _sentence_tokenizer(
|
|
253
|
+
for i in _sentence_tokenizer(doc_description)
|
|
244
254
|
if i and (stripped := i.strip())[0].isalpha()
|
|
245
255
|
):
|
|
246
256
|
# description is not capitalized
|
|
247
257
|
self._add(_E[305])
|
|
248
258
|
# description-missing-period
|
|
249
259
|
if (
|
|
250
|
-
|
|
251
|
-
and
|
|
252
|
-
and
|
|
260
|
+
doc_description
|
|
261
|
+
and doc_description.strip()
|
|
262
|
+
and doc_description.strip()[-1]
|
|
253
263
|
not in (
|
|
254
264
|
"`",
|
|
255
265
|
".",
|
|
@@ -258,22 +268,28 @@ class Failure(list[Failed]):
|
|
|
258
268
|
self._add(_E[306])
|
|
259
269
|
|
|
260
270
|
def _sig4xx_parameters(self, doc: _Param, sig: _Param) -> None:
|
|
271
|
+
# freeze result as it is a property and PyCharm complains
|
|
272
|
+
# `Expected type 'str', got 'str | None' instead ' in
|
|
273
|
+
# _almost_equal as property could theoretically have different
|
|
274
|
+
# result from sig.name is not None and doc.name is not None
|
|
275
|
+
sig_name = sig.name
|
|
276
|
+
doc_name = doc.name
|
|
261
277
|
if doc.indent > 0:
|
|
262
278
|
# incorrect-indent
|
|
263
279
|
self._add(_E[401])
|
|
264
280
|
elif doc != sig:
|
|
265
281
|
if (
|
|
266
|
-
|
|
267
|
-
or
|
|
282
|
+
sig_name in self._func.docstring.args.names
|
|
283
|
+
or doc_name in self._func.signature.args.names
|
|
268
284
|
) and len(self._func.docstring.args) > 1:
|
|
269
285
|
# params-out-of-order
|
|
270
286
|
self._add(_E[402])
|
|
271
287
|
elif (
|
|
272
|
-
|
|
273
|
-
and
|
|
274
|
-
and
|
|
288
|
+
doc_name != _UNNAMED
|
|
289
|
+
and sig_name is not None
|
|
290
|
+
and doc_name is not None
|
|
275
291
|
):
|
|
276
|
-
if _almost_equal(
|
|
292
|
+
if _almost_equal(sig_name, doc_name, _MIN_MATCH, _MAX_MATCH):
|
|
277
293
|
# spelling-error
|
|
278
294
|
self._add(_E[403])
|
|
279
295
|
else:
|
|
@@ -287,7 +303,7 @@ class Failure(list[Failed]):
|
|
|
287
303
|
# no types, cannot know either way
|
|
288
304
|
if self._func.signature.returns.type == _RetType.UNTYPED:
|
|
289
305
|
# confirm-return-needed
|
|
290
|
-
self._add(_E[501],
|
|
306
|
+
self._add(_E[501], include_hint=True)
|
|
291
307
|
# return-type is none, so no return should be documented
|
|
292
308
|
elif self._func.docstring.returns.returns:
|
|
293
309
|
if self._func.signature.returns.type == _RetType.NONE:
|
|
@@ -301,7 +317,7 @@ class Failure(list[Failed]):
|
|
|
301
317
|
lines = str(self._func.docstring.string).splitlines()
|
|
302
318
|
self._add(
|
|
303
319
|
_E[503],
|
|
304
|
-
|
|
320
|
+
include_hint=(
|
|
305
321
|
len(lines) > 1
|
|
306
322
|
and "return" in lines[-1]
|
|
307
323
|
and ":param" not in lines[-1]
|
|
@@ -311,11 +327,11 @@ class Failure(list[Failed]):
|
|
|
311
327
|
# this method is init, so no return should be documented
|
|
312
328
|
if self._func.isinit:
|
|
313
329
|
# class-return-documented
|
|
314
|
-
self._add(_E[504],
|
|
330
|
+
self._add(_E[504], include_hint=True)
|
|
315
331
|
# method is property and not set to document property
|
|
316
332
|
elif self._func.isproperty and not check_property_returns:
|
|
317
333
|
# return-documented-for-property
|
|
318
|
-
self._add(_E[505],
|
|
334
|
+
self._add(_E[505], include_hint=True)
|
|
319
335
|
|
|
320
336
|
def _sig9xx_error(self) -> None:
|
|
321
337
|
# invalid-syntax
|
|
@@ -348,6 +364,7 @@ class Failure(list[Failed]):
|
|
|
348
364
|
return max(self._retcode)
|
|
349
365
|
|
|
350
366
|
|
|
367
|
+
# TODO: make report json by default and wrap with a reporter for cli
|
|
351
368
|
def report(
|
|
352
369
|
failures: Failures,
|
|
353
370
|
config: _Config,
|
|
@@ -363,7 +380,10 @@ def report(
|
|
|
363
380
|
:param file: Module path when failures came from a file (optional).
|
|
364
381
|
:return: Exit code (non-zero if any check failed).
|
|
365
382
|
"""
|
|
383
|
+
format_json = _os.getenv("_DOCSIG_FORMAT_JSON") is not None
|
|
366
384
|
retcodes = [0]
|
|
385
|
+
output = []
|
|
386
|
+
obj = []
|
|
367
387
|
for failure in failures:
|
|
368
388
|
retcodes.append(failure.retcode)
|
|
369
389
|
path_prefix = f"{file}:" if file is not None else ""
|
|
@@ -371,7 +391,7 @@ def report(
|
|
|
371
391
|
if not config.no_ansi and _sys.stdout.isatty():
|
|
372
392
|
header = f"\033[35m{header}\033[0m"
|
|
373
393
|
|
|
374
|
-
|
|
394
|
+
output.append(header)
|
|
375
395
|
for item in failure:
|
|
376
396
|
extra = None
|
|
377
397
|
if item.hint:
|
|
@@ -381,15 +401,27 @@ def report(
|
|
|
381
401
|
extra = "warning: please remember to fix this or disable it"
|
|
382
402
|
_warn(_NEW.format(ref=item.ref), FutureWarning, stacklevel=3)
|
|
383
403
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
description=item.description,
|
|
389
|
-
symbolic=item.symbolic,
|
|
390
|
-
),
|
|
404
|
+
msg = _TEMPLATE.format(
|
|
405
|
+
ref=item.ref,
|
|
406
|
+
description=item.description,
|
|
407
|
+
symbolic=item.symbolic,
|
|
391
408
|
)
|
|
409
|
+
output.append(f" {msg}")
|
|
392
410
|
if extra is not None:
|
|
393
|
-
|
|
411
|
+
output.append(f" {extra}")
|
|
412
|
+
|
|
413
|
+
obj.append(
|
|
414
|
+
{
|
|
415
|
+
"line": None if failure.retcode == 2 else item.lineno,
|
|
416
|
+
"message": msg,
|
|
417
|
+
"exit": failure.retcode,
|
|
418
|
+
},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
if format_json:
|
|
422
|
+
print(_json.dumps(obj).strip()) # pragma: no cover
|
|
423
|
+
|
|
424
|
+
elif output:
|
|
425
|
+
print("\n".join(output))
|
|
394
426
|
|
|
395
427
|
return max(retcodes)
|
|
@@ -260,7 +260,7 @@ class Signature(_Stub):
|
|
|
260
260
|
@classmethod
|
|
261
261
|
def from_ast(
|
|
262
262
|
cls,
|
|
263
|
-
node: _ast.nodes.
|
|
263
|
+
node: _ast.nodes.FunctionDef,
|
|
264
264
|
ignore: _Ignore,
|
|
265
265
|
) -> Signature:
|
|
266
266
|
"""Build Signature from a function or class AST node.
|
|
@@ -368,6 +368,8 @@ class Docstring(_Stub):
|
|
|
368
368
|
description_missing=not match or not match.group(1),
|
|
369
369
|
)
|
|
370
370
|
docstring = cls(string, returns)
|
|
371
|
+
# the suggestion is broken
|
|
372
|
+
# noinspection RegExpSingleCharAlternation
|
|
371
373
|
for match in _re.findall(
|
|
372
374
|
r":([\w\s]+(?:\s\|\s[\w\s]+|\w+))([^\w\s])((?:.|\n)*?)(?=\n:|$)",
|
|
373
375
|
string,
|
|
@@ -8,6 +8,7 @@ Shared helpers.
|
|
|
8
8
|
import re as _re
|
|
9
9
|
import sys as _sys
|
|
10
10
|
from difflib import SequenceMatcher as _SequenceMatcher
|
|
11
|
+
from pathlib import Path as _Path
|
|
11
12
|
|
|
12
13
|
from .messages import TEMPLATE as _TEMPLATE
|
|
13
14
|
from .messages import E as _E
|
|
@@ -84,3 +85,25 @@ def sentence_tokenizer(text: str) -> list[str]:
|
|
|
84
85
|
result.append(text[start:].strip())
|
|
85
86
|
|
|
86
87
|
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_parent_that_has(file: str, start: _Path | None = None) -> _Path | None:
|
|
91
|
+
"""Find the parent directory that contains the given file.
|
|
92
|
+
|
|
93
|
+
Start from the current working directory and walk up to root. If
|
|
94
|
+
no required file is found, return None.
|
|
95
|
+
|
|
96
|
+
:param file: File to find.
|
|
97
|
+
:param start: Starting director.
|
|
98
|
+
:return: Parent directory containing the file or None if not found.
|
|
99
|
+
"""
|
|
100
|
+
if start is None:
|
|
101
|
+
start = _Path.cwd()
|
|
102
|
+
|
|
103
|
+
if (start / file).is_file():
|
|
104
|
+
return start
|
|
105
|
+
|
|
106
|
+
if start.parent == start:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
return get_parent_that_has(file, start.parent)
|
|
@@ -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.
|
|
14
|
+
current_version = "0.87.0"
|
|
15
15
|
message = "bump: version {current_version} → {new_version}"
|
|
16
16
|
sign_tags = true
|
|
17
17
|
tag = true
|
|
@@ -46,7 +46,7 @@ search = "- docsig=={current_version}"
|
|
|
46
46
|
filename = "SECURITY.md"
|
|
47
47
|
|
|
48
48
|
[tool.codespell]
|
|
49
|
-
ignore-words-list = "astroid"
|
|
49
|
+
ignore-words-list = "astroid,atLeast"
|
|
50
50
|
|
|
51
51
|
[tool.coverage.report]
|
|
52
52
|
exclude_lines = [
|
|
@@ -117,10 +117,10 @@ maintainers = [
|
|
|
117
117
|
name = "docsig"
|
|
118
118
|
readme = "README.rst"
|
|
119
119
|
repository = "https://github.com/jshwi/docsig"
|
|
120
|
-
version = "0.
|
|
120
|
+
version = "0.87.0"
|
|
121
121
|
|
|
122
122
|
[tool.poetry.dependencies]
|
|
123
|
-
Sphinx = ">=7,<
|
|
123
|
+
Sphinx = ">=7,<8.2"
|
|
124
124
|
astroid = "^4.0.2"
|
|
125
125
|
pathspec = ">=0.12.1,<1.2.0"
|
|
126
126
|
python = "^3.10"
|
|
@@ -159,6 +159,9 @@ templatest = ">=0.10.1,<0.13.0"
|
|
|
159
159
|
[tool.poetry.group.flake8.dependencies]
|
|
160
160
|
flake8 = "^7.1.0"
|
|
161
161
|
|
|
162
|
+
[tool.poetry.group.plugin.dependencies]
|
|
163
|
+
shiv = "^1.0.8"
|
|
164
|
+
|
|
162
165
|
[tool.poetry.group.tests.dependencies]
|
|
163
166
|
pytest = ">=8.2,<10.0"
|
|
164
167
|
pytest-benchmark = ">=4,<6"
|
|
@@ -175,6 +178,9 @@ SIG = "docsig.plugin:Flake8"
|
|
|
175
178
|
[tool.poetry.plugins."validate_pyproject.tool_schema"]
|
|
176
179
|
docsig = "docsig.plugin:ValidatePyproject"
|
|
177
180
|
|
|
181
|
+
[tool.poetry.requires-plugins]
|
|
182
|
+
poetry-plugin-export = ">=1.8"
|
|
183
|
+
|
|
178
184
|
[tool.poetry.scripts]
|
|
179
185
|
docsig = "docsig.__main__:main"
|
|
180
186
|
|
|
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
|