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.
Files changed (62) hide show
  1. npdlint-1/.github/workflows/ci.yml +38 -0
  2. npdlint-1/.github/workflows/numpydoc-lint.yml +29 -0
  3. npdlint-1/.github/workflows/publish-PyPI.yml +70 -0
  4. npdlint-1/.gitignore +9 -0
  5. npdlint-1/.pre-commit-hooks.yaml +7 -0
  6. npdlint-1/PKG-INFO +469 -0
  7. npdlint-1/README.md +448 -0
  8. npdlint-1/pyproject.toml +66 -0
  9. npdlint-1/src/npdlint/__init__.py +12 -0
  10. npdlint-1/src/npdlint/_vendor/LICENSE-numpydoc.txt +24 -0
  11. npdlint-1/src/npdlint/_vendor/__init__.py +10 -0
  12. npdlint-1/src/npdlint/_vendor/docscrape.py +780 -0
  13. npdlint-1/src/npdlint/cli.py +588 -0
  14. npdlint-1/src/npdlint/compat.py +220 -0
  15. npdlint-1/src/npdlint/config.py +816 -0
  16. npdlint-1/src/npdlint/diagnostics.py +50 -0
  17. npdlint-1/src/npdlint/discovery.py +338 -0
  18. npdlint-1/src/npdlint/docstring.py +404 -0
  19. npdlint-1/src/npdlint/output.py +226 -0
  20. npdlint-1/src/npdlint/plugins.py +142 -0
  21. npdlint-1/src/npdlint/rules/__init__.py +33 -0
  22. npdlint-1/src/npdlint/rules/base.py +356 -0
  23. npdlint-1/src/npdlint/rules/general.py +346 -0
  24. npdlint-1/src/npdlint/rules/messages.py +127 -0
  25. npdlint-1/src/npdlint/rules/parameters.py +377 -0
  26. npdlint-1/src/npdlint/rules/properties.py +438 -0
  27. npdlint-1/src/npdlint/rules/returns.py +160 -0
  28. npdlint-1/src/npdlint/rules/see_also.py +145 -0
  29. npdlint-1/src/npdlint/rules/summary.py +233 -0
  30. npdlint-1/src/npdlint/rules/suppressions.py +178 -0
  31. npdlint-1/src/npdlint/runner.py +479 -0
  32. npdlint-1/src/npdlint/scope.py +284 -0
  33. npdlint-1/src/npdlint/selection.py +164 -0
  34. npdlint-1/src/npdlint/source.py +477 -0
  35. npdlint-1/src/npdlint/targets.py +1093 -0
  36. npdlint-1/tests/conftest.py +59 -0
  37. npdlint-1/tests/fixtures/gl.py +88 -0
  38. npdlint-1/tests/fixtures/nq.py +100 -0
  39. npdlint-1/tests/fixtures/nq.toml +1 -0
  40. npdlint-1/tests/fixtures/pr.py +115 -0
  41. npdlint-1/tests/fixtures/pt.py +66 -0
  42. npdlint-1/tests/fixtures/pt.toml +2 -0
  43. npdlint-1/tests/fixtures/rt_yd.py +73 -0
  44. npdlint-1/tests/fixtures/sa_ex.py +58 -0
  45. npdlint-1/tests/fixtures/ss_es.py +54 -0
  46. npdlint-1/tests/fixtures/structural.py +42 -0
  47. npdlint-1/tests/fixtures/structural.toml +13 -0
  48. npdlint-1/tests/test_cli.py +238 -0
  49. npdlint-1/tests/test_compat.py +296 -0
  50. npdlint-1/tests/test_config.py +156 -0
  51. npdlint-1/tests/test_dataclasses.py +467 -0
  52. npdlint-1/tests/test_discovery.py +145 -0
  53. npdlint-1/tests/test_parity.py +295 -0
  54. npdlint-1/tests/test_performance.py +74 -0
  55. npdlint-1/tests/test_plugins.py +142 -0
  56. npdlint-1/tests/test_properties.py +195 -0
  57. npdlint-1/tests/test_rules.py +89 -0
  58. npdlint-1/tests/test_scope.py +191 -0
  59. npdlint-1/tests/test_selection.py +85 -0
  60. npdlint-1/tests/test_suppression.py +266 -0
  61. npdlint-1/tests/test_targets.py +231 -0
  62. 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
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ build/
5
+ dist/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
@@ -0,0 +1,7 @@
1
+ - id: npdlint
2
+ name: npdlint
3
+ description: Lint numpydoc-style docstrings.
4
+ entry: npdlint check
5
+ language: python
6
+ types: [python]
7
+ require_serial: false
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.