python-linters 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,505 @@
1
+ # ============================================================================
2
+ # Ruff Reference Configuration
3
+ # ============================================================================
4
+ #
5
+ # Canonical starting point for new Python projects. Copy this file as
6
+ # `ruff.toml` into your project root and adjust:
7
+ #
8
+ # 1. target-version — match your project's minimum Python version
9
+ # 2. known-first-party — list your project's own packages
10
+ # 3. exclude — add project-specific paths if needed
11
+ # 4. ignore — add project-specific ignores WITH justification
12
+ # 5. flake8-type-checking — list your runtime-evaluated base classes and
13
+ # decorators (pydantic, FastAPI preset)
14
+ #
15
+ # Last triaged against: ruff 0.17.3
16
+ # Rule names in `select`/`ignore` and in `# ruff: ignore[...]` comments need
17
+ # `preview = true`: without it ruff rejects the config and treats name-based
18
+ # suppression comments as inert.
19
+ # ============================================================================
20
+
21
+ # Older ruff fails on the first rule name it does not know, with a cryptic
22
+ # `Unknown rule selector`. This guard names the version instead.
23
+ required-version = ">=0.17.3"
24
+
25
+ target-version = "py312"
26
+
27
+ # Ruff/Black default: 88 ("~10% over 80 — the wise choice" per Black docs).
28
+ # Common alternatives:
29
+ # 79 — PEP 8 stdlib standard
30
+ # 88 — Black/Ruff default (this config)
31
+ # 100 — PEP 8 allows up to 99 for teams; popular with type-heavy codebases
32
+ # 119 — Django project standard (79 for docstrings)
33
+ # Pick one for the project and stick with it. 88 is the safe default;
34
+ # 100 is reasonable if long signatures or type annotations cause excessive wrapping.
35
+ line-length = 88
36
+
37
+ # ── Lint settings ───────────────────────────────────────────────────────────
38
+
39
+ [lint]
40
+ preview = true
41
+ # A preview rule runs only when `select` names it. A category select brings its
42
+ # stable rules alone, so a ruff bump cannot switch on an unreviewed preview rule.
43
+ explicit-preview-rules = true
44
+ # No `extend-safe-fixes`: an unsafe fix guesses intent and silences the rule, so the finding must be resolved by a decision.
45
+
46
+ # `RUF100` (unused-noqa) stays enabled: it is the only automated check that a
47
+ # `# ruff: ignore[<rule-name>]` cites the rule that actually fires there, and
48
+ # a suppression naming the wrong rule is inert — it silently suppresses
49
+ # nothing while reading as justified. Enable it and it also flags every
50
+ # suppression made redundant by a broader `ignore` / `per-file-ignores` entry.
51
+ #
52
+ # It does NOT inspect other tools' suppressions: `# type: ignore` (mypy),
53
+ # `# ty: ignore` (ty), and `# nosemgrep` use their own comment syntax. A
54
+ # `# noqa` carrying a code of a tool ruff does not implement (vulture's `V…`)
55
+ # trips RUF100 and noqa-comments (RUF105) — list that prefix here rather than
56
+ # ignoring either rule:
57
+ #
58
+ # external = ["V"] # keep vulture's `# noqa: V…` directives
59
+
60
+ # ── Rule categories ─────────────────────────────────────────────────────────
61
+ #
62
+ # Every ruff category must appear below — either enabled in `select` or
63
+ # commented out with `# excluded` and a reason. A rule without a code (ruff
64
+ # ships new rules without one since 0.16.5) has no prefix to inherit, so it
65
+ # appears by name under the same convention. This makes it easy to
66
+ # audit coverage and triage new categories when ruff is updated.
67
+ #
68
+ # Categories are grouped: core → style → security → complexity → framework.
69
+
70
+ select = [
71
+ # ── Core correctness ──
72
+ "F", # Pyflakes — undefined names, unused imports, unreachable code
73
+ "E", # pycodestyle errors — basic PEP 8 violations
74
+ "W", # pycodestyle warnings — whitespace, deprecation warnings
75
+ "B", # flake8-bugbear — common pitfalls (mutable defaults, assert misuse, …)
76
+ "PLE", # Pylint errors — actual bugs (bad super(), invalid __all__, …)
77
+ "UP", # pyupgrade — modernize syntax for target-version (f-strings, type unions, …)
78
+
79
+ # ── Imports ──
80
+ "I", # isort — consistent import ordering
81
+ "FA", # flake8-future-annotations — fires only for target-version <= py39
82
+ "ICN", # flake8-import-conventions — `import numpy as np` etc.
83
+ "TID", # flake8-tidy-imports — TID252 catches parent-relative `from ..x` by default; sibling `from .x` needs [lint.flake8-tidy-imports] ban-relative-imports = "all". Also: banned-api markers.
84
+ "TC", # flake8-type-checking — move typing-only imports behind TYPE_CHECKING
85
+
86
+ # ── Type annotations ──
87
+ "ANN", # flake8-annotations — enforce type annotations on public API
88
+ # Noisy on first adoption. Consider ignoring ANN201 temporarily,
89
+ # but aim to enable it — return types are the highest-value annotations.
90
+
91
+ # ── Naming ──
92
+ "N", # pep8-naming — PEP 8 naming conventions (classes, functions, constants)
93
+ # Watch out for N815 on Pydantic models with camelCase JSON fields.
94
+
95
+ # ── Security ──
96
+ "S", # flake8-bandit — security antipatterns (hardcoded passwords, exec, …)
97
+ "EXE", # flake8-executable — shebang correctness
98
+
99
+ # ── Error handling ──
100
+ "BLE", # flake8-blind-except — no bare `except:` or `except Exception:`
101
+ "EM", # flake8-errmsg — don't embed f-strings directly in exception constructors
102
+ "RSE", # flake8-raise — correct raise usage
103
+ "TRY", # tryceratops — try/except hygiene (too broad except, reraise properly, …)
104
+
105
+ # ── Code style & simplification ──
106
+ "SIM", # flake8-simplify — simplifiable if/else, context managers, boolean ops
107
+ "PIE", # flake8-pie — misc. simplifications (no-op pass, unnecessary spread, …)
108
+ "RET", # flake8-return — unnecessary else-after-return, implicit return values
109
+ "C4", # flake8-comprehensions — prefer list/dict/set comprehensions over map/filter
110
+ "FLY", # flynt — prefer f-strings over .format() and % formatting
111
+ "Q", # flake8-quotes — consistent quote style
112
+ "ISC", # flake8-implicit-str-concat — catch accidental string concatenation
113
+ "SLOT", # flake8-slots — suggest __slots__ on simple classes
114
+
115
+ # ── Unused code & arguments ──
116
+ "ARG", # flake8-unused-arguments — flag unused function/method arguments
117
+
118
+ # ── Pathlib & modernization ──
119
+ "PTH", # flake8-use-pathlib — prefer pathlib over os.path
120
+ "FURB", # refurb — more modernization suggestions
121
+
122
+ # ── Logging ──
123
+ "LOG", # flake8-logging — correct logging module usage
124
+ "G", # flake8-logging-format — logging format string issues
125
+
126
+ # ── Async ──
127
+ "ASYNC", # flake8-async — async pitfalls (blocking calls in async, …)
128
+
129
+ # ── Testing ──
130
+ "PT", # flake8-pytest-style — pytest best practices
131
+ "pytest-fixture-autouse", # code-less rule, no prefix selects it: an autouse
132
+ # fixture injects hidden setup into every test in scope
133
+ "shared-clock-mock", # code-less rule: patching `worker.time.sleep` patches the
134
+ # process-wide `time.sleep`, which other threads and tasks
135
+ # share. Inject a clock or patch `worker.time` instead
136
+
137
+ # ── Complexity ──
138
+ "unnecessary-regular-expression-compile", # code-less rule:
139
+ # `re.compile(p).match(s)` used once is `re.match(p, s)`
140
+ "C90", # mccabe — cyclomatic complexity threshold
141
+ "PLC", # Pylint convention — import placement, naming, …
142
+ "PLR", # Pylint refactor — too many args/branches/returns, magic values, …
143
+ "PLW", # Pylint warnings — global statements, unreachable code, …
144
+ "PERF", # Perflint — performance antipatterns
145
+
146
+ # ── Framework-specific ──
147
+ "FAST", # FastAPI — common FastAPI mistakes (missing Annotated, …)
148
+ "DTZ", # flake8-datetimez — require timezone-aware datetimes
149
+ "PYI", # flake8-pyi — type stub (.pyi) correctness
150
+ "INP", # flake8-no-pep420 — implicit namespace packages
151
+ "INT", # flake8-gettext — i18n correctness
152
+
153
+ # ── Self-access ──
154
+ "SLF", # flake8-self — flag access to private members from outside
155
+
156
+ # ── Builtins ──
157
+ "A", # flake8-builtins — don't shadow Python builtins
158
+
159
+ # ── Debugger ──
160
+ "T10", # flake8-debugger — no leftover breakpoint() / pdb calls
161
+
162
+ # ── Ruff-specific ──
163
+ "RUF", # Ruff-specific rules — parenthesized generators, ambiguous unicode, …
164
+ "PGH", # pygrep-hooks — blanket noqa, type: ignore without code
165
+
166
+ # ── Numeric / data libraries (harmless if unused — rules only fire on relevant code) ──
167
+ "NPY", # NumPy-specific rules — deprecated NumPy aliases
168
+ "PD", # pandas-vet — common pandas antipatterns
169
+ "YTT", # flake8-2020 — outdated sys.version checks
170
+
171
+ # ── Preview rules, selected by name ──────────────────────────────────
172
+ # `explicit-preview-rules = true`: a category select above turns on only its
173
+ # stable rules. Each preview rule below was triaged on ruff 0.17.3.
174
+ # A preview rule in a selected category that is neither listed
175
+ # here nor covered by an `# excluded` line below is untriaged.
176
+
177
+ # Ruff-specific (25)
178
+ # bugs: the code runs but does the wrong thing
179
+ "missing-f-string-syntax", # `"{x}"` without `f` prints the braces
180
+ "unraw-re-pattern", # `"\b"` in a non-raw pattern is a backspace, not a word boundary, and nothing else warns
181
+ "implicit-class-var-in-dataclass", # an unannotated field is a class var shared by all instances
182
+ "property-without-return", # the property silently returns None
183
+ "float-equality-comparison", # `==` on floats depends on rounding; tests ignore it
184
+ "f-string-percent-format", # `%` on an f-string mixes two interpolations
185
+ "incorrect-decorator-order", # decorators apply bottom-up, the wrong order breaks at runtime
186
+ "fallible-context-manager", # cleanup after an unguarded `yield` skips on exception
187
+ "method-receiver-default", # a default on `self`/`cls` applies only to `A.method()` calls, almost always a mistake
188
+ "unused-async", # forces callers into async and hides a missing await; tests ignore it
189
+ # dead code: removing it changes no behavior, mostly auto-fixable
190
+ "needless-else",
191
+ "unnecessary-if",
192
+ "useless-finally",
193
+ "unnecessary-assign-before-yield",
194
+ "falsy-dict-get-fallback",
195
+ "redundant-bool-literal",
196
+ "unnecessary-regular-expression", # a str method replaces the regex
197
+ "logging-eager-conversion", # `%s` already calls str(), and `repr(x)` with `%s` is `%r`
198
+ # readability
199
+ "used-dummy-variable", # an `_x` that is read lies about being unused
200
+ "non-empty-init-module", # code in `__init__.py` runs on import and gets overlooked
201
+ "incorrectly-parenthesized-tuple-in-subscript", # one tuple style in subscripts
202
+ "indented-form-feed", # the language reference leaves a form feed mid-indentation undefined
203
+ # rule names over codes: a name says what it checks, a code needs a lookup
204
+ "noqa-comments",
205
+ "rule-codes-in-suppression-comments",
206
+ "rule-codes-in-selectors",
207
+
208
+ # Pylint (18)
209
+ # bugs: runtime errors or silently lost behavior
210
+ "dict-iter-missing-items", # `for k, v in d` unpacks keys, not items
211
+ "modified-iterating-set", # RuntimeError
212
+ "redefined-slots-in-subclass", # makes the base class slot unreachable
213
+ "bad-dunder-method-name", # a misspelled dunder is never called
214
+ "unspecified-encoding", # `open()` without encoding uses the platform locale
215
+ # size and nesting limits, every threshold set under [lint.pylint]
216
+ "too-many-public-methods",
217
+ "too-many-locals",
218
+ "too-many-boolean-expressions",
219
+ "too-many-nested-blocks", # the nesting axis C901 cannot see
220
+ "too-many-statements-in-try-clause", # the except also catches failures from statements nobody expected to raise
221
+ # design: private names stay private (as SLF), functions over methods
222
+ "import-private-name", # `_name` is private to its module
223
+ "no-self-use", # a method without `self` is a function; tests ignore it
224
+ # idioms, mostly auto-fixable
225
+ "unnecessary-dunder-call",
226
+ "no-classmethod-decorator",
227
+ "no-staticmethod-decorator",
228
+ "swap-with-temporary-variable",
229
+ "non-augmented-assignment",
230
+ "literal-membership",
231
+
232
+ # flake8-bandit (4): each flags an insecure library or protocol at import,
233
+ # because no stable call-level rule covers these
234
+ "suspicious-xmlrpc-import", # XML attacks over the network
235
+ "suspicious-httpoxy-import", # CGI handlers, httpoxy
236
+ "suspicious-pycrypto-import", # pycrypto: unmaintained, known buffer overflow. Maintained
237
+ # pycryptodome shares the `Crypto` namespace and also fires:
238
+ # check the distribution, then suppress with a reason
239
+ "suspicious-pyghmi-import", # IPMI is unencrypted
240
+
241
+ # refurb (10): stdlib idioms that say the intent, mostly auto-fixable.
242
+ # Unlike the 3 excluded refurb rules, no fix here loses a type or breaks isinstance
243
+ "read-whole-file", # Path.read_text(), same direction as PTH
244
+ "write-whole-file", # Path.write_text(), same direction as PTH
245
+ "delete-full-slice",
246
+ "for-loop-set-mutations",
247
+ "slice-copy",
248
+ "unnecessary-enumerate",
249
+ "math-constant", # catches a truncated pi
250
+ "repeated-global",
251
+ "hardcoded-string-charset",
252
+ "meta-class-abc-meta",
253
+
254
+ # flake8-bugbear (4)
255
+ "return-in-generator", # a for loop drops the returned value
256
+ "loop-iterator-mutation", # mutating the iterable skips elements or loops forever
257
+ "class-as-data-structure", # an `__init__`-only class is a dataclass
258
+ "del-attr-with-constant", # `delattr(o, "x")` is `del o.x`, auto-fixable
259
+
260
+ # pyupgrade (3): legacy spellings, mostly auto-fixable
261
+ "while-one", # Python 2 idiom
262
+ "deprecated-abc-decorator", # deprecated since 3.3
263
+ "context-manager-iterator", # @contextmanager needs Generator, Iterator lacks `throw`
264
+
265
+ # flake8-async (1): a context manager's cleanup is delayed until the
266
+ # generator closes, when `await` is no longer allowed
267
+ "yield-in-context-manager-in-async-generator",
268
+
269
+ # flake8-pytest-style (1): `pytest.warns()` without a warning fails at runtime
270
+ "pytest-warns-without-warning",
271
+
272
+ # flake8-type-checking (1): quotes in an explicit type alias are needless. A quoted
273
+ # `TypeAlias` is a str at runtime, so `Alias | None` raises TypeError
274
+ "quoted-type-alias",
275
+
276
+ # ── Excluded categories ──────────────────────────────────────────────
277
+ # Each must have a reason.
278
+
279
+ # excluded "AIR", # Airflow — not relevant outside Airflow projects
280
+ # excluded "COM", # flake8-commas — conflicts with `ruff format` (COM812)
281
+ # excluded "CPY", # flake8-copyright — copyright headers not enforced
282
+ # excluded "D", # pydocstyle — docstrings rarely improve well-named code;
283
+ # enforce selectively via per-file if needed
284
+ # excluded "DJ", # flake8-django — only for Django projects
285
+ # excluded "DOC", # pydoclint — docstring param/return validation; enable
286
+ # once D is adopted
287
+ # excluded "ERA", # eradicate — commented-out code is sometimes intentional
288
+ # (bookmarks, alternative approaches being tested)
289
+ # excluded "FBT", # flake8-boolean-trap — philosophically sound but hard to
290
+ # retrofit; `def foo(*, verbose: bool)` is fine
291
+ # excluded "FIX", # flake8-fixme — TODOs/FIXMEs in code are legitimate
292
+ # bookmarks during development
293
+ # excluded "T20", # flake8-print — print() is fine in scripts, CLIs, and
294
+ # debugging; logging.info isn't always better
295
+ # excluded "TD", # flake8-todos — enforces TODO format (author, link);
296
+ # too bureaucratic for most projects
297
+
298
+ # ── Excluded preview rules ───────────────────────────────────────────
299
+ # Preview rules stay off unless selected by name. Each prefix needs a reason.
300
+
301
+ # excluded "E11", "E2", "E3", "E502", "W391", # pycodestyle preview (42): `ruff format` settles all but E262/E265/E266, which only police comment prefixes (`#:` Sphinx, `##` banners), 0 findings outside generated `*_pb2.py`, E111/E114/E117 conflict with it
302
+ # excluded "S401", "S402", "S403", "S404", "S405", "S406", "S407", "S408", "S409", # bandit import bans (9): importing is not the risk, stable S3xx/S60x rules flag the risky calls
303
+ # excluded "FURB113", "FURB118", "FURB189", # refurb (3): fix erases types (itemgetter -> Any), reads worse (extend), or breaks isinstance (UserList/UserDict)
304
+ ]
305
+
306
+ # ── Ignored rules ───────────────────────────────────────────────────────────
307
+ #
308
+ # Every ignore must have a justification. Group by theme.
309
+ # Rules listed here are GLOBALLY disabled. Prefer per-file-ignores for
310
+ # rules that only need suppression in specific contexts (tests, scripts).
311
+
312
+ ignore = [
313
+ # ── Line length ──
314
+ "line-too-long", # the formatter handles wrappable lines; remaining
315
+ # violations are long strings/URLs that would be worse
316
+ # if broken up
317
+
318
+ # ── Type annotations ──
319
+ # We enforce annotations broadly but exempt specific patterns that
320
+ # add noise without value.
321
+ # ANN401 (typing.Any disallowed) stays ON: with basedpyright dropped
322
+ # in favor of ty, ANN401 is the gate's only coverage for explicit
323
+ # Any (basedpyright's reportExplicitAny): ty has no equivalent rule.
324
+ # missing return type on __init__, __enter__, __exit__ — always obvious
325
+ # from context
326
+ "missing-return-type-special-method",
327
+ "missing-type-args", # missing type on *args — typically pass-through,
328
+ # type depends on caller
329
+ "missing-type-kwargs", # missing type on **kwargs — same reasoning as
330
+ # missing-type-args
331
+
332
+ # ── Naming ──
333
+ # prevents CONSTANT_IN_FUNCTION convention, which is useful for thresholds
334
+ # and config values inside functions
335
+ "non-lowercase-variable-in-function",
336
+ "invalid-function-name", # test names like `test_ASRDecoder` embed class
337
+ # names for grep-ability
338
+ "invalid-argument-name", # abbreviations like max_SNR are more readable
339
+ # than max_snr
340
+
341
+ # ── Code style ──
342
+ "lambda-assignment", # `transform = lambda x: x.strip()` is sometimes
343
+ # clearer than a named function for one-off local
344
+ # transforms
345
+ "in-dict-keys", # use `key in dict` — `key in dict.keys()` is more
346
+ # explicit about intent; Python Zen says explicit >
347
+ # implicit
348
+ "superfluous-else-return", # the else branch makes the two paths visually
349
+ # symmetric and easier to read
350
+ "superfluous-else-raise", # same reasoning as superfluous-else-return
351
+ # super() issues with dataclass(slots=True), see
352
+ # https://stackoverflow.com/questions/73268995
353
+ "super-call-with-parameters",
354
+ "compare-to-empty-string", # `s == ""` is more explicit than `not s` (the
355
+ # latter also matches None, 0, [])
356
+
357
+ # ── Security ──
358
+ "hardcoded-bind-all-interfaces", # hardcoded 0.0.0.0 binding —
359
+ # intentional for dev servers
360
+ # non-cryptographic random — not relevant when random is used for jitter,
361
+ # sampling, or test data (not tokens/keys)
362
+ "suspicious-non-cryptographic-random-usage",
363
+
364
+ # ── Logging ──
365
+ "logging-f-string", # f-string in logging — f-strings in log calls are
366
+ # fine; the lazy-% argument only matters at high log
367
+ # volume where the string wouldn't be emitted anyway
368
+
369
+ # ── Performance ──
370
+ "reimplemented-starmap", # starmap slower than comprehension — confirmed
371
+ # regression in Python 3.12, comprehensions are
372
+ # faster
373
+
374
+ # ── Complexity / false positives ──
375
+ "open-file-with-context-handler", # false positives with lambdas and
376
+ # callback-based APIs
377
+ ]
378
+
379
+ # ── Per-file ignores ────────────────────────────────────────────────────────
380
+ #
381
+ # Rules that should only be suppressed in specific file patterns.
382
+ # Prefer this over global ignore when the rule IS useful in production code.
383
+
384
+ [lint.per-file-ignores]
385
+ "**/tests/**/*.py" = [ # any depth: monorepos nest tests per package
386
+ "assert", # pytest's testing idiom
387
+ "hardcoded-temp-file", # hardcoded /tmp — fine in tests
388
+ "magic-value-comparison", # `assert len(x) == 3` is clear
389
+ "no-self-use", # pytest classes group tests, methods don't need self
390
+ "too-many-public-methods", # a pytest class groups tests and has no
391
+ # callers, so its method count is not an
392
+ # interface
393
+ # missing return type — test functions always return None
394
+ "missing-return-type-undocumented-public-function",
395
+ "import-private-name", # testing internal functions is legitimate
396
+ "unused-async", # async test doubles mirror the awaited signature they replace
397
+ "float-equality-comparison", # asserting an exactly representable result is correct
398
+ "too-many-locals", # an integration test sets up many real collaborators
399
+ ]
400
+ "test_*.py" = [
401
+ "assert",
402
+ "hardcoded-temp-file",
403
+ "magic-value-comparison",
404
+ "no-self-use",
405
+ "too-many-public-methods",
406
+ "missing-return-type-undocumented-public-function",
407
+ "implicit-namespace-package", # standalone test scripts
408
+ "import-private-name",
409
+ "unused-async",
410
+ "float-equality-comparison",
411
+ "too-many-locals",
412
+ ]
413
+
414
+ # ── isort ───────────────────────────────────────────────────────────────────
415
+
416
+ [lint.isort]
417
+ known-first-party = [] # ← EDIT: add your project's packages
418
+
419
+ # ── Type checking imports ───────────────────────────────────────────────────
420
+
421
+ [lint.flake8-type-checking]
422
+ # TC001-003 move a typing-only import behind TYPE_CHECKING. Classes and
423
+ # functions whose annotations a library reads at runtime are exempt only if
424
+ # listed here. Both lists default to empty.
425
+ # ← EDIT: add your own BaseModel subclasses and runtime-evaluating decorators.
426
+ runtime-evaluated-base-classes = [
427
+ "pydantic.BaseModel",
428
+ "pydantic_settings.BaseSettings",
429
+ ]
430
+ # ruff resolves `@app.get` / `@router.get` on instances in the same module.
431
+ runtime-evaluated-decorators = [
432
+ "fastapi.FastAPI.get", "fastapi.FastAPI.post", "fastapi.FastAPI.put",
433
+ "fastapi.FastAPI.patch", "fastapi.FastAPI.delete",
434
+ "fastapi.FastAPI.websocket",
435
+ "fastapi.APIRouter.get", "fastapi.APIRouter.post", "fastapi.APIRouter.put",
436
+ "fastapi.APIRouter.patch", "fastapi.APIRouter.delete",
437
+ "fastapi.APIRouter.websocket",
438
+ ]
439
+ # Blind spots: ruff sees declarations, never the call site that reads
440
+ # annotations. With deferred annotations (py314+ or `from __future__ import
441
+ # annotations`), it still flags a parameter type of a `Depends(dep)` function
442
+ # and a field type of a dataclass passed to `TypeAdapter`. The fix is unsafe,
443
+ # so check each move for these before applying it.
444
+
445
+ # ── Complexity thresholds ───────────────────────────────────────────────────
446
+
447
+ [lint.mccabe]
448
+ # Cyclomatic complexity. Radon: A ≤ 5, B ≤ 10. Ruff/flake8 default: 10.
449
+ # Counts branches, and is *blind to nesting*: a 4-deep nest of `if`s and the
450
+ # flat guard-clause rewrite of the same logic both measure 5, so C901 cannot
451
+ # distinguish them at any threshold. Nesting depth is a separate axis —
452
+ # see `max-nested-blocks` below (PLR1702), the complement to this rule.
453
+ max-complexity = 10
454
+
455
+ [lint.pylint]
456
+ # pydantic hooks, not typos: bad-dunder-method-name (PLW3201) flags them otherwise.
457
+ allow-dunder-method-names = ["__get_pydantic_core_schema__", "__get_pydantic_json_schema__"]
458
+ # max-args: default 5 — raise only if your domain genuinely needs it
459
+ max-args = 5
460
+ # max-branches: default 12
461
+ max-branches = 12
462
+ # max-returns: ruff/pylint default 6, wemake default 5.
463
+ # Keep at default 6, but strive for fewer — each return is an exit path
464
+ # the reader must track. Prefer early-return guard clauses over deep nesting,
465
+ # but consolidate where possible.
466
+ max-returns = 6
467
+ # max-statements: ruff default 50 — rarely hit because C901 triggers first
468
+ max-statements = 50
469
+ # max-nested-blocks (PLR1702, preview — selected by name in `select`).
470
+ # Pylint default 5 is lax enough to be effectively dead; 3 is the enforced cap.
471
+ # This is the nesting axis C901 cannot see (see the note under [lint.mccabe]).
472
+ # Counts *every* block type — `if`/`for`/`while`/`with`/`try` — so a plain
473
+ # `for` + `with` + `try` + `if` I/O function already measures 4 and must be
474
+ # extracted. That is the intended pressure: the cure is a named helper (or
475
+ # guard clauses), not a raised threshold.
476
+ # Flattening only fixes the *shape*. When the levels correspond to genuinely
477
+ # different input worlds, the depth was un-modeled state — model it upstream.
478
+ max-nested-blocks = 3
479
+ # max-public-methods (PLR0904, preview): ruff default 20, enforced cap 10.
480
+ # A class is a smaller unit than a file, so its interface gets no more room
481
+ # than a file's: 10 public names.
482
+ # A ceiling, not a target: below 10, a narrower interface is still better.
483
+ # No decorator exception: a pydantic Settings class with many validators can
484
+ # trip it — `# ruff: ignore[too-many-public-methods]` with a reason there, not a raised cap.
485
+ max-public-methods = 10
486
+ # max-locals (PLR0914, preview): ruff default 15. Tests are exempt: an
487
+ # integration test sets up many real collaborators.
488
+ max-locals = 15
489
+ # max-bool-expr (PLR0916, preview): ruff default 5. Past 5 terms, name the
490
+ # condition or its parts as variables.
491
+ max-bool-expr = 5
492
+ # max-statements-in-try (PLW0717, preview): ruff default 5. A `try` without
493
+ # `except` is not counted, so cleanup in `finally` stays free.
494
+ max-statements-in-try = 5
495
+
496
+ # ── Format settings ─────────────────────────────────────────────────────────
497
+
498
+ [format]
499
+ # Format code examples in docstrings (`>>>` doctests, Markdown and reST code
500
+ # blocks) like the code around them, so the examples cannot drift from the
501
+ # project's style. Ruff skips an example that is not valid Python.
502
+ # Cost: a hand-aligned layout inside an example, such as a table of `case`
503
+ # arms with lined-up comments, gets reflowed. Write that example as a plain
504
+ # text block instead.
505
+ docstring-code-format = true