npdlint 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.
- npdlint-1/.github/workflows/ci.yml +38 -0
- npdlint-1/.github/workflows/numpydoc-lint.yml +29 -0
- npdlint-1/.github/workflows/publish-PyPI.yml +70 -0
- npdlint-1/.gitignore +9 -0
- npdlint-1/.pre-commit-hooks.yaml +7 -0
- npdlint-1/PKG-INFO +469 -0
- npdlint-1/README.md +448 -0
- npdlint-1/pyproject.toml +66 -0
- npdlint-1/src/npdlint/__init__.py +12 -0
- npdlint-1/src/npdlint/_vendor/LICENSE-numpydoc.txt +24 -0
- npdlint-1/src/npdlint/_vendor/__init__.py +10 -0
- npdlint-1/src/npdlint/_vendor/docscrape.py +780 -0
- npdlint-1/src/npdlint/cli.py +588 -0
- npdlint-1/src/npdlint/compat.py +220 -0
- npdlint-1/src/npdlint/config.py +816 -0
- npdlint-1/src/npdlint/diagnostics.py +50 -0
- npdlint-1/src/npdlint/discovery.py +338 -0
- npdlint-1/src/npdlint/docstring.py +404 -0
- npdlint-1/src/npdlint/output.py +226 -0
- npdlint-1/src/npdlint/plugins.py +142 -0
- npdlint-1/src/npdlint/rules/__init__.py +33 -0
- npdlint-1/src/npdlint/rules/base.py +356 -0
- npdlint-1/src/npdlint/rules/general.py +346 -0
- npdlint-1/src/npdlint/rules/messages.py +127 -0
- npdlint-1/src/npdlint/rules/parameters.py +377 -0
- npdlint-1/src/npdlint/rules/properties.py +438 -0
- npdlint-1/src/npdlint/rules/returns.py +160 -0
- npdlint-1/src/npdlint/rules/see_also.py +145 -0
- npdlint-1/src/npdlint/rules/summary.py +233 -0
- npdlint-1/src/npdlint/rules/suppressions.py +178 -0
- npdlint-1/src/npdlint/runner.py +479 -0
- npdlint-1/src/npdlint/scope.py +284 -0
- npdlint-1/src/npdlint/selection.py +164 -0
- npdlint-1/src/npdlint/source.py +477 -0
- npdlint-1/src/npdlint/targets.py +1093 -0
- npdlint-1/tests/conftest.py +59 -0
- npdlint-1/tests/fixtures/gl.py +88 -0
- npdlint-1/tests/fixtures/nq.py +100 -0
- npdlint-1/tests/fixtures/nq.toml +1 -0
- npdlint-1/tests/fixtures/pr.py +115 -0
- npdlint-1/tests/fixtures/pt.py +66 -0
- npdlint-1/tests/fixtures/pt.toml +2 -0
- npdlint-1/tests/fixtures/rt_yd.py +73 -0
- npdlint-1/tests/fixtures/sa_ex.py +58 -0
- npdlint-1/tests/fixtures/ss_es.py +54 -0
- npdlint-1/tests/fixtures/structural.py +42 -0
- npdlint-1/tests/fixtures/structural.toml +13 -0
- npdlint-1/tests/test_cli.py +238 -0
- npdlint-1/tests/test_compat.py +296 -0
- npdlint-1/tests/test_config.py +156 -0
- npdlint-1/tests/test_dataclasses.py +467 -0
- npdlint-1/tests/test_discovery.py +145 -0
- npdlint-1/tests/test_parity.py +295 -0
- npdlint-1/tests/test_performance.py +74 -0
- npdlint-1/tests/test_plugins.py +142 -0
- npdlint-1/tests/test_properties.py +195 -0
- npdlint-1/tests/test_rules.py +89 -0
- npdlint-1/tests/test_scope.py +191 -0
- npdlint-1/tests/test_selection.py +85 -0
- npdlint-1/tests/test_suppression.py +266 -0
- npdlint-1/tests/test_targets.py +231 -0
- npdlint-1/uv.lock +583 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- run: uv pip install --system -e ".[dev]"
|
|
22
|
+
- name: Test
|
|
23
|
+
run: python -m pytest -q
|
|
24
|
+
- name: Lint its own docstrings
|
|
25
|
+
run: npdlint check --output-format github
|
|
26
|
+
- name: Lint its own code
|
|
27
|
+
run: ruff check .
|
|
28
|
+
|
|
29
|
+
performance:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: astral-sh/setup-uv@v5
|
|
34
|
+
- uses: actions/setup-python@v5
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
- run: uv pip install --system -e ".[dev]"
|
|
38
|
+
- run: python -m pytest -q -m performance --run-performance
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Reusable workflow replacing the fd-find + xargs + numpydoc pipeline.
|
|
2
|
+
#
|
|
3
|
+
# The linter discovers its own files and reads its rules from pyproject.toml,
|
|
4
|
+
# so this job takes no inputs beyond the Python version. What used to be
|
|
5
|
+
# workflow inputs (package-dir, extra excludes, the __init__.py exemption)
|
|
6
|
+
# now live in [tool.npdlint] in the repository being checked.
|
|
7
|
+
name: numpydoc-lint
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
workflow_call:
|
|
11
|
+
inputs:
|
|
12
|
+
python-version:
|
|
13
|
+
type: string
|
|
14
|
+
default: "3.12"
|
|
15
|
+
paths:
|
|
16
|
+
description: Optional paths to lint, overriding the configured include.
|
|
17
|
+
type: string
|
|
18
|
+
default: ""
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
numpydoc-lint:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ inputs.python-version }}
|
|
28
|
+
- run: pip install npdlint
|
|
29
|
+
- run: npdlint check ${{ inputs.paths }} --output-format github --statistics
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
run-name: Publish Python Package to PyPI for release ${{ github.event.release.tag_name || inputs.tag_name || github.ref_name }}
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
tag_name:
|
|
10
|
+
description: 'Git tag to checkout and publish'
|
|
11
|
+
required: false
|
|
12
|
+
type: string
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
name: Build distribution 📦
|
|
17
|
+
# Pure Python, so one py3-none-any wheel serves every interpreter and
|
|
18
|
+
# platform: no build matrix and no cibuildwheel.
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout code
|
|
23
|
+
uses: actions/checkout@v4.1.0
|
|
24
|
+
with:
|
|
25
|
+
ref: ${{ inputs.tag_name || github.ref }}
|
|
26
|
+
# hatch-vcs derives the version from the tag, so it needs the full
|
|
27
|
+
# history rather than a shallow clone.
|
|
28
|
+
fetch-depth: 0
|
|
29
|
+
|
|
30
|
+
- name: Set up Python
|
|
31
|
+
uses: actions/setup-python@v5
|
|
32
|
+
with:
|
|
33
|
+
python-version: "3.13"
|
|
34
|
+
|
|
35
|
+
- name: Build wheel and source distribution
|
|
36
|
+
run: |
|
|
37
|
+
python3 -m pip install build
|
|
38
|
+
python3 -m build
|
|
39
|
+
|
|
40
|
+
- name: Store the distribution packages
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: python-package-distributions
|
|
44
|
+
path: dist/
|
|
45
|
+
if-no-files-found: error
|
|
46
|
+
|
|
47
|
+
publish-to-pypi:
|
|
48
|
+
name: Publish to PyPI
|
|
49
|
+
needs:
|
|
50
|
+
- build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
|
|
53
|
+
environment:
|
|
54
|
+
name: pypi
|
|
55
|
+
url: https://pypi.org/p/npdlint
|
|
56
|
+
|
|
57
|
+
permissions:
|
|
58
|
+
id-token: write
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- name: Download the dists
|
|
62
|
+
uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: python-package-distributions
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
- name: Publish distribution to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
with:
|
|
70
|
+
packages-dir: dist
|
npdlint-1/.gitignore
ADDED
npdlint-1/PKG-INFO
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: npdlint
|
|
3
|
+
Version: 1
|
|
4
|
+
Summary: A fast, modular, structurally-aware linter for numpydoc-style docstrings.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ucgmsim/numpydoc-linter
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Keywords: docstring,documentation,lint,linter,numpydoc
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: pathspec>=0.12
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: numpydoc>=1.9; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# npdlint
|
|
23
|
+
|
|
24
|
+
A fast, modular, structurally-aware linter for numpydoc-style docstrings.
|
|
25
|
+
|
|
26
|
+
It finds its own files, keeps every check as a separate rule, and lets you vary
|
|
27
|
+
the rules by the *kind* of thing being documented, so a `@property`, a private
|
|
28
|
+
helper and a public function can each be held to a different standard.
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
$ npdlint check
|
|
32
|
+
src/shapes.py:12:5: PT01 Property docstring does not match the type-line form (<type>: <summary>)
|
|
33
|
+
src/shapes.py:40:1: PR04 Parameter "radius" has no type
|
|
34
|
+
Found 2 issues in 18 files
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Why
|
|
38
|
+
|
|
39
|
+
`numpydoc lint` has the right checks but an awkward shape: it takes a list of
|
|
40
|
+
files rather than a project, every check lives inside one function, and the only
|
|
41
|
+
way to vary behaviour is a regex over dotted names. That pushes projects into
|
|
42
|
+
shell pipelines like `fd . src/ -E __init__.py --extension py | xargs numpydoc lint`.
|
|
43
|
+
|
|
44
|
+
This tool keeps numpydoc's checks and error codes, and adds:
|
|
45
|
+
|
|
46
|
+
- **Discovery.** Point it at a directory, or nothing at all. It honours
|
|
47
|
+
`.gitignore`.
|
|
48
|
+
- **Rules as objects.** Each check is a class with a code, a name, and the kinds
|
|
49
|
+
of object it applies to. Third-party rules load from entry points or a local file.
|
|
50
|
+
- **Scope blocks.** Select and ignore rules by kind, decorator, visibility,
|
|
51
|
+
qualified name, path, and more.
|
|
52
|
+
- **Property conventions.** Built-in support for the `type: summary` form that
|
|
53
|
+
numpydoc cannot express.
|
|
54
|
+
- **A drop-in migration.** An existing `[tool.numpydoc_validation]` table is
|
|
55
|
+
read as-is, so adopting it changes no configuration on day one.
|
|
56
|
+
- **Dataclass awareness.** Fields of a `@dataclass` are the constructor's
|
|
57
|
+
parameters, inherited ones included, so documenting them is not an error.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```console
|
|
62
|
+
pip install npdlint
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
npdlint check [PATHS...] # lint; PATHS defaults to the configured include
|
|
69
|
+
npdlint rule PR # describe rules, by code or prefix
|
|
70
|
+
npdlint explain src/x.py:42 # show which rules apply there, and why
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`check` is the default command, so `npdlint src` works too.
|
|
74
|
+
|
|
75
|
+
Exit codes are 0 when clean, 1 when violations were found, and 2 when the tool
|
|
76
|
+
could not run.
|
|
77
|
+
|
|
78
|
+
### Output formats
|
|
79
|
+
|
|
80
|
+
`--output-format` takes `concise` (default), `full`, `json`, `github`, or
|
|
81
|
+
`pylint`. Use `github` in CI to get inline annotations on the pull request.
|
|
82
|
+
`--statistics` prints counts per rule, which is the quickest way to decide what
|
|
83
|
+
to ignore when adopting the tool on an existing codebase.
|
|
84
|
+
|
|
85
|
+
## Migrating from numpydoc
|
|
86
|
+
|
|
87
|
+
An existing `[tool.numpydoc_validation]` table is read as-is, so a project
|
|
88
|
+
already configured for `numpydoc lint` runs without touching its configuration:
|
|
89
|
+
|
|
90
|
+
```console
|
|
91
|
+
$ npdlint check
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The table is interpreted with numpydoc's semantics, not this tool's.
|
|
95
|
+
|
|
96
|
+
| numpydoc setting | Behaviour |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `checks` | An allow-list, unless it contains `"all"`, in which case it is every numpydoc check minus the ones listed |
|
|
99
|
+
| `exclude` | Regular expressions searched against the object's numpydoc name, which is rooted at the file stem rather than the package path |
|
|
100
|
+
| `exclude_files` | Regular expressions anchored at the start of the file path |
|
|
101
|
+
| `override_<CODE>` | Suppresses one check for an object when the pattern is found in its docstring |
|
|
102
|
+
|
|
103
|
+
`setup.cfg` with a `[tool:numpydoc_validation]` section works too, and as in
|
|
104
|
+
numpydoc it is only consulted when there is no `pyproject.toml`.
|
|
105
|
+
|
|
106
|
+
`checks = ["all"]` means the 37 checks numpydoc defines and no more, so
|
|
107
|
+
adopting this tool never silently turns on rules a project has not asked for.
|
|
108
|
+
The `PT` and `DS` rules are opt-in.
|
|
109
|
+
|
|
110
|
+
Anything written in `[tool.npdlint]` wins over the legacy table, so a
|
|
111
|
+
project can migrate one setting at a time. Set `numpydoc-compat = false` to
|
|
112
|
+
ignore the legacy table entirely.
|
|
113
|
+
|
|
114
|
+
The same three features exist natively, under clearer names:
|
|
115
|
+
|
|
116
|
+
```toml
|
|
117
|
+
[tool.npdlint]
|
|
118
|
+
exclude-object-patterns = ['\.__repr__$'] # regex on the object name
|
|
119
|
+
exclude-file-patterns = ['^generated/'] # regex on the path, anchored
|
|
120
|
+
|
|
121
|
+
[tool.npdlint.overrides]
|
|
122
|
+
SS05 = ['^Process ', '^Access '] # regex found in the docstring
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Note that `exclude` in `[tool.npdlint]` is a gitignore-style glob, not
|
|
126
|
+
a regular expression. `exclude-file-patterns` is the regex equivalent.
|
|
127
|
+
|
|
128
|
+
### What is left in the workflow
|
|
129
|
+
|
|
130
|
+
File selection that used to live in the shell moves into configuration:
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
[tool.npdlint]
|
|
134
|
+
include = ["mypackage"]
|
|
135
|
+
extend-exclude = ["mypackage/vendored.py"]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Then the CI step is one line with no project-specific data in it.
|
|
139
|
+
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
Everything lives in `pyproject.toml` under `[tool.npdlint]`. The tool was
|
|
143
|
+
called `numpydoc-linter` to begin with, and `[tool.numpydoc-linter]` is still
|
|
144
|
+
read when no `[tool.npdlint]` table is present, with a warning on stderr
|
|
145
|
+
naming the file. Where both are present the current name wins outright, since
|
|
146
|
+
merging the two would make the effective configuration impossible to read off
|
|
147
|
+
the file.
|
|
148
|
+
|
|
149
|
+
```toml
|
|
150
|
+
[tool.npdlint]
|
|
151
|
+
include = ["src"]
|
|
152
|
+
extend-exclude = ["**/tests/**"]
|
|
153
|
+
select = ["ALL"]
|
|
154
|
+
ignore = ["ES01", "SA01", "EX01"]
|
|
155
|
+
|
|
156
|
+
[tool.npdlint.per-file-ignores]
|
|
157
|
+
"**/__init__.py" = ["GL08"]
|
|
158
|
+
|
|
159
|
+
# Private helpers are not part of the public API, so they need no docstring.
|
|
160
|
+
[[tool.npdlint.scope]]
|
|
161
|
+
match = { private = true }
|
|
162
|
+
skip = true
|
|
163
|
+
|
|
164
|
+
# Properties read as attributes, so document them as "type: summary".
|
|
165
|
+
[[tool.npdlint.scope]]
|
|
166
|
+
match = { kind = "property" }
|
|
167
|
+
extend-ignore = ["PR", "RT", "ES01"]
|
|
168
|
+
extend-select = ["PT"]
|
|
169
|
+
property-form = "type-line"
|
|
170
|
+
|
|
171
|
+
# Dunder methods only need to exist.
|
|
172
|
+
[[tool.npdlint.scope]]
|
|
173
|
+
match = { kind = "any-method", dunder = true }
|
|
174
|
+
select = ["GL08"]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Excluding files
|
|
178
|
+
|
|
179
|
+
Directories like `.git`, `.venv`, `build` and `__pycache__` are excluded by
|
|
180
|
+
default, and `.gitignore` is honoured. Following ruff, `extend-exclude` adds to
|
|
181
|
+
the built-in list while `exclude` replaces it, so reach for `extend-exclude`
|
|
182
|
+
unless you really mean to walk everything.
|
|
183
|
+
|
|
184
|
+
### Selecting rules
|
|
185
|
+
|
|
186
|
+
Selectors follow ruff. `ALL` matches everything, `PR` matches a family, `PR0` a
|
|
187
|
+
partial code, and `PR04` one rule. Where two selectors both match, the more
|
|
188
|
+
specific wins; at equal specificity an ignore beats a select.
|
|
189
|
+
|
|
190
|
+
### Scope blocks
|
|
191
|
+
|
|
192
|
+
Blocks are evaluated top to bottom. Each matching block layers its overrides on
|
|
193
|
+
the result of the previous one, so ordering is explicit rather than a precedence
|
|
194
|
+
puzzle. Within a block the order is: `skip`, `select`, `ignore`, `extend-select`,
|
|
195
|
+
`extend-ignore`.
|
|
196
|
+
|
|
197
|
+
A `match` table may combine any of these, and all of them must hold:
|
|
198
|
+
|
|
199
|
+
| Key | Type | Matches on |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `kind` | string or list | `module`, `class`, `function`, `method`, `property`, `setter`, `classmethod`, `staticmethod`, or the groups `callable`, `any-method`, `any` |
|
|
202
|
+
| `name` | regex | the bare name |
|
|
203
|
+
| `qualname` | regex | the dotted name from the module root |
|
|
204
|
+
| `path` | glob or list | the file, relative to the project root |
|
|
205
|
+
| `decorator` | string or list | any decorator, by dotted name |
|
|
206
|
+
| `parent-kind` | string or list | the kind of the enclosing object |
|
|
207
|
+
| `in-all` | bool | membership of the module's `__all__` |
|
|
208
|
+
| `private` | bool | name starts with one underscore |
|
|
209
|
+
| `dunder` | bool | name is `__like_this__` |
|
|
210
|
+
| `abstract` | bool | decorated `@abstractmethod` |
|
|
211
|
+
| `stub` | bool | body is only `pass`, `...`, or `raise NotImplementedError` |
|
|
212
|
+
| `overload` | bool | decorated `@overload` |
|
|
213
|
+
| `override` | bool | decorated `@override` |
|
|
214
|
+
| `async` | bool | declared `async def` |
|
|
215
|
+
| `generator` | bool | contains a `yield` |
|
|
216
|
+
| `returns-value` | bool | contains a `return <expr>` |
|
|
217
|
+
| `has-docstring` | bool | has a docstring at all |
|
|
218
|
+
| `dataclass` | bool | a decorator synthesises the constructor |
|
|
219
|
+
| `nested` | bool | defined inside a function body |
|
|
220
|
+
|
|
221
|
+
Run `npdlint explain path.py:42` when a rule fires and you expected it not to.
|
|
222
|
+
It prints the target's flags and every layer of the resolution.
|
|
223
|
+
|
|
224
|
+
### Property forms
|
|
225
|
+
|
|
226
|
+
`property-form` selects what a `@property` docstring should look like.
|
|
227
|
+
|
|
228
|
+
| Form | Shape |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `returns-section` | plain numpydoc, with a Returns section; the default |
|
|
231
|
+
| `type-line` | `float: The radius in metres.` |
|
|
232
|
+
| `summary-only` | a summary with no type |
|
|
233
|
+
| `{ regex = "..." }` | your own, with optional `type` and `summary` groups |
|
|
234
|
+
|
|
235
|
+
`PT02` compares a captured `type` group against the return annotation, so
|
|
236
|
+
`int: ...` on a method annotated `-> str` is reported.
|
|
237
|
+
|
|
238
|
+
### Dataclasses
|
|
239
|
+
|
|
240
|
+
A `@dataclass` has no `__init__` in the syntax tree, so numpydoc sees an empty
|
|
241
|
+
signature and calls every documented field an unknown parameter. This linter
|
|
242
|
+
derives the constructor the decorator will synthesise: annotated attributes in
|
|
243
|
+
order, `ClassVar` and `field(init=False)` excluded, `InitVar` unwrapped, and
|
|
244
|
+
fields inherited from a base class in the same file merged in first. Where a
|
|
245
|
+
base class lives in another module its fields cannot be seen, so documented
|
|
246
|
+
names that are not accounted for are left alone rather than reported.
|
|
247
|
+
|
|
248
|
+
A field is a constructor parameter and an attribute at the same time, and real
|
|
249
|
+
code documents it in any of three places. All three count: the `Parameters`
|
|
250
|
+
section, the `Attributes` section, and an inline attribute docstring, which is
|
|
251
|
+
a bare string literal directly below the field.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
@dataclass
|
|
255
|
+
class Point:
|
|
256
|
+
"""
|
|
257
|
+
A point.
|
|
258
|
+
|
|
259
|
+
Attributes
|
|
260
|
+
----------
|
|
261
|
+
x : int
|
|
262
|
+
The x.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
x: int
|
|
266
|
+
y: int
|
|
267
|
+
"""The y."""
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Set `dataclass-fields-section = "parameters"` to accept only the `Parameters`
|
|
271
|
+
section, as numpydoc would. Names documented under `Attributes` are not treated
|
|
272
|
+
as declared parameters, so a property or `ClassVar` described there is not
|
|
273
|
+
reported as an unknown parameter and has no position to be out of order.
|
|
274
|
+
|
|
275
|
+
`attrs` and pydantic dataclasses are recognised too; add more with
|
|
276
|
+
`dataclass-decorators`.
|
|
277
|
+
|
|
278
|
+
### Stub files
|
|
279
|
+
|
|
280
|
+
`.pyi` files are not linted by default: a stub carries the signatures while the
|
|
281
|
+
implementation carries the docstrings. Set `include-stubs = true` to lint them.
|
|
282
|
+
|
|
283
|
+
### Private parameters
|
|
284
|
+
|
|
285
|
+
By default every parameter needs documenting, as numpydoc requires. Set
|
|
286
|
+
`private-parameters = "ignore"` to exempt underscore-prefixed ones, which is
|
|
287
|
+
useful for dataclasses that carry private caches. The exemption is symmetric:
|
|
288
|
+
an exempt parameter is neither required nor rejected.
|
|
289
|
+
|
|
290
|
+
### Suppressing inline
|
|
291
|
+
|
|
292
|
+
`# noqa: PR04,RT01` suppresses those rules for the object it sits on, and a bare
|
|
293
|
+
`# noqa` suppresses everything. numpydoc's `# numpydoc ignore=PR04` spelling is
|
|
294
|
+
accepted too, so existing suppressions keep working.
|
|
295
|
+
|
|
296
|
+
The comment may go anywhere in the signature, which matters because the natural
|
|
297
|
+
place for it is rarely the `def` line:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
@overload
|
|
301
|
+
def f(
|
|
302
|
+
x: int,
|
|
303
|
+
) -> int: # numpydoc ignore=GL08
|
|
304
|
+
...
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The docstring's closing line works as well. A comment inside the body does not
|
|
308
|
+
count, which is where this differs from numpydoc: numpydoc attaches a
|
|
309
|
+
suppression to the most recent `def` it has seen, so a comment buried deep in a
|
|
310
|
+
function body silently suppresses that function's checks.
|
|
311
|
+
|
|
312
|
+
### Suppressions that suppress nothing
|
|
313
|
+
|
|
314
|
+
A suppression outlives the problem it was written for. The docstring gets its
|
|
315
|
+
`Returns` section, nobody deletes the `# numpydoc ignore=RT01` above it, and
|
|
316
|
+
from then on the comment is both useless and misleading: the next reader takes
|
|
317
|
+
it as evidence that the rule still fires there, and it hides the rule if the
|
|
318
|
+
docstring later regresses.
|
|
319
|
+
|
|
320
|
+
`NQ01` reports a comment that did no work, and `NQ02` one that names a rule
|
|
321
|
+
code no rule answers to:
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
src/geometry.py:41:23: NQ01 Unnecessary suppression comment: RT01 was not reported for this object
|
|
325
|
+
src/geometry.py:88:19: NQ01 Unnecessary suppression comment: it applies to no documented object
|
|
326
|
+
src/geometry.py:96:19: NQ02 Suppression comment names unknown rule code RT09
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Both are on by default, and both are ordinary rules: `ignore = ["NQ"]` in
|
|
330
|
+
`[tool.npdlint]`, a `per-file-ignores` entry, or `--ignore NQ` on the
|
|
331
|
+
command line turns them off. They are the only rules that judge the file
|
|
332
|
+
rather than an object in it, so scope blocks do not apply to them.
|
|
333
|
+
|
|
334
|
+
What counts as unnecessary is deliberately conservative, because the cost of a
|
|
335
|
+
false positive is a user deleting a comment they needed:
|
|
336
|
+
|
|
337
|
+
- **A rule your configuration already disabled is never judged.** Running with
|
|
338
|
+
a narrowed `--select`, a `skip`ping scope block, `exclude-object-patterns`,
|
|
339
|
+
or an `overrides` pattern leaves the comments for those rules alone, so a
|
|
340
|
+
partial run cannot condemn them.
|
|
341
|
+
- **`# noqa` is shared with other linters, so only codes registered here are
|
|
342
|
+
judged in one.** `# noqa: F401` is ruff's business, and a bare `# noqa` is
|
|
343
|
+
left alone entirely. `# numpydoc ignore` is addressed to this tool alone, so
|
|
344
|
+
everything in one is judged, a bare `# numpydoc ignore` included.
|
|
345
|
+
- **Only the unused codes are named.** `# numpydoc ignore=GL08,RT01` on an
|
|
346
|
+
undocumented function reports `RT01` and says nothing about `GL08`.
|
|
347
|
+
- **A comment can exempt itself**, by naming the rule: `# numpydoc
|
|
348
|
+
ignore=RT01,NQ01` keeps a suppression you want to hold on to.
|
|
349
|
+
|
|
350
|
+
A comment that applies to no documented object is reported too. That is the
|
|
351
|
+
other half of the divergence above: numpydoc would attach a suppression
|
|
352
|
+
written in a function body to the function, and this linter does not, so
|
|
353
|
+
`NQ01` names the comments that stopped working when you migrated rather than
|
|
354
|
+
letting them fail quietly.
|
|
355
|
+
|
|
356
|
+
## Continuous integration
|
|
357
|
+
|
|
358
|
+
Replacing a `fd | xargs numpydoc lint` pipeline:
|
|
359
|
+
|
|
360
|
+
```yaml
|
|
361
|
+
- uses: actions/setup-python@v5
|
|
362
|
+
- run: pip install npdlint
|
|
363
|
+
- run: npdlint check --output-format github
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
What used to be workflow inputs, such as the package directory, extra
|
|
367
|
+
excludes, and the `__init__.py` exemption, move into `pyproject.toml`, so the
|
|
368
|
+
workflow step carries no project-specific data.
|
|
369
|
+
|
|
370
|
+
Or as a pre-commit hook:
|
|
371
|
+
|
|
372
|
+
```yaml
|
|
373
|
+
repos:
|
|
374
|
+
- repo: https://github.com/ucgmsim/numpydoc-linter
|
|
375
|
+
rev: v0.1.0
|
|
376
|
+
hooks:
|
|
377
|
+
- id: npdlint
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Rules
|
|
381
|
+
|
|
382
|
+
Codes `GL`, `SS`, `ES`, `PR`, `RT`, `YD`, `SA` and `EX` are ports of numpydoc's
|
|
383
|
+
checks and keep its codes and wording, so existing ignore lists carry over
|
|
384
|
+
unchanged. `PT` covers property forms, `DS01` reports a docstring the parser
|
|
385
|
+
cannot read, and `NQ` reports suppression comments that are not doing anything.
|
|
386
|
+
Run `npdlint rule` for the full list.
|
|
387
|
+
|
|
388
|
+
### Differences from numpydoc
|
|
389
|
+
|
|
390
|
+
A differential test lints a shared corpus with both tools and requires them to
|
|
391
|
+
agree, so every difference below is deliberate and pinned down by a test.
|
|
392
|
+
|
|
393
|
+
1. **`GL08` on `__init__`.** numpydoc documents that a properly formatted class
|
|
394
|
+
docstring silences this for an undocumented constructor. Its AST hook gets
|
|
395
|
+
this wrong in both directions, reporting it when the class does document the
|
|
396
|
+
parameters and skipping it when the class has no docstring at all. This
|
|
397
|
+
linter implements the documented behaviour.
|
|
398
|
+
2. **`PR01` and `PR02` on dataclasses.** See above. numpydoc's own test suite
|
|
399
|
+
contains a dataclass annotated "As param1 is not documented this class should
|
|
400
|
+
also raise PR01", which numpydoc does not raise and this linter does.
|
|
401
|
+
3. **`YD01` on `yield from`.** numpydoc's generator check looks only for a bare
|
|
402
|
+
`yield` statement among a function's direct children, so it misses
|
|
403
|
+
`yield from` and any yield inside a branch or loop.
|
|
404
|
+
4. **Nested objects.** numpydoc's visitor stops at anything that is not a
|
|
405
|
+
module, class or function, so a function defined inside an `if` or a `for` is
|
|
406
|
+
never checked while one defined directly in a body is. This linter is
|
|
407
|
+
consistent. To keep numpydoc's effective behaviour, skip them:
|
|
408
|
+
|
|
409
|
+
```toml
|
|
410
|
+
[[tool.npdlint.scope]]
|
|
411
|
+
match = { nested = true }
|
|
412
|
+
skip = true
|
|
413
|
+
```
|
|
414
|
+
5. **Malformed docstrings.** A docstring that breaks the parser makes numpydoc
|
|
415
|
+
abandon the whole file. This linter reports `DS01` and carries on.
|
|
416
|
+
6. **Suppression syntax.** A bare `# noqa` or `# numpydoc ignore`, with no
|
|
417
|
+
codes, suppresses everything on its line here; numpydoc recognises only
|
|
418
|
+
`# numpydoc ignore=CODE`. The `NQ` rules above have no numpydoc equivalent,
|
|
419
|
+
and a `[tool.numpydoc_validation]` table never enables them.
|
|
420
|
+
|
|
421
|
+
### Performance
|
|
422
|
+
|
|
423
|
+
On 1076 files of third-party Python, with every rule enabled:
|
|
424
|
+
|
|
425
|
+
| Tool | Wall clock |
|
|
426
|
+
|---|---|
|
|
427
|
+
| `numpydoc lint`, 1 process | 30.1 s |
|
|
428
|
+
| `npdlint`, 1 process | 7.5 s |
|
|
429
|
+
| `npdlint`, 8 processes | 3.0 s |
|
|
430
|
+
|
|
431
|
+
numpydoc also failed to parse 4 of those files.
|
|
432
|
+
|
|
433
|
+
## Writing your own rules
|
|
434
|
+
|
|
435
|
+
Point at a local file, or ship a package that advertises the
|
|
436
|
+
`npdlint.rules` entry point.
|
|
437
|
+
|
|
438
|
+
```toml
|
|
439
|
+
[tool.npdlint]
|
|
440
|
+
plugins = ["tools/doc_rules.py"]
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
from npdlint.rules import BaseRule, registry
|
|
445
|
+
from npdlint.targets import Kind
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@registry.register
|
|
449
|
+
class SummaryMentionsUnits(BaseRule):
|
|
450
|
+
code = "X001"
|
|
451
|
+
name = "summary-mentions-units"
|
|
452
|
+
summary = "Physical quantities should state their units."
|
|
453
|
+
kinds = frozenset({Kind.PROPERTY})
|
|
454
|
+
|
|
455
|
+
def check(self, target, ctx):
|
|
456
|
+
doc = target.docstring
|
|
457
|
+
if "metres" not in doc.summary and target.name.endswith("_m"):
|
|
458
|
+
yield self.diagnostic(target, "summary should state the units")
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Subclass `BaseFileRule` instead for a check on the file as a whole. It runs
|
|
462
|
+
once, after every object has been checked, and implements `check_file(ctx)`
|
|
463
|
+
rather than `check`; `ctx.source` is the parsed file and `self.at(ctx, line,
|
|
464
|
+
col, message)` builds the diagnostic. The `NQ` rules are written this way.
|
|
465
|
+
|
|
466
|
+
## Licence
|
|
467
|
+
|
|
468
|
+
BSD 3-Clause. `src/npdlint/_vendor/docscrape.py` is vendored from
|
|
469
|
+
numpydoc under the same licence; its copyright notice is kept alongside it.
|