python-linters 0.2.0__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-linters
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Run ruff, ty and pymodularity on a Python project and print one JSON report grouped by rule.
5
5
  Author: Tilo Himmelsbach
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-linters"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Run ruff, ty and pymodularity on a Python project and print one JSON report grouped by rule."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-linters"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Run ruff, ty and pymodularity on a Python project and print one JSON report grouped by rule."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -169,7 +169,9 @@ def _covers_rule(
169
169
  return False
170
170
  if selector in meta_prefixes:
171
171
  return True
172
- selector_linters = {r["linter"] for r in rules if r["code"] and r["code"].startswith(selector)}
172
+ selector_linters = {
173
+ r["linter"] for r in rules if r["code"] and r["code"].startswith(selector)
174
+ }
173
175
  return selector_linters == {linter}
174
176
 
175
177
 
@@ -221,9 +223,14 @@ def _get_new_categories(
221
223
  for prefix, linter in sorted(categories.items()):
222
224
  if any(_covers(k, prefix, category_linters, meta_prefixes) for k in known):
223
225
  continue
224
- prefix_rules = [r for r in rules if r["code"] and r["code"].rstrip(string.digits) == prefix]
226
+ prefix_rules = [
227
+ r for r in rules if r["code"] and r["code"].rstrip(string.digits) == prefix
228
+ ]
225
229
  if all(
226
- any(_covers_rule(k, r["code"], r["linter"], rules, meta_prefixes) for k in known)
230
+ any(
231
+ _covers_rule(k, r["code"], r["linter"], rules, meta_prefixes)
232
+ for k in known
233
+ )
227
234
  for r in prefix_rules
228
235
  ):
229
236
  continue
@@ -255,9 +262,13 @@ def check_ruff_config(toml_path: Path) -> RuffConfigCheck:
255
262
  commented = _parse_toml_comments(toml_path)
256
263
 
257
264
  known = selected | commented
258
- meta_prefixes = {"PL"} | {p for p, linters in category_linters.items() if len(linters) > 1}
265
+ meta_prefixes = {"PL"} | {
266
+ p for p, linters in category_linters.items() if len(linters) > 1
267
+ }
259
268
 
260
- new_categories = _get_new_categories(rules, categories, category_linters, known, meta_prefixes)
269
+ new_categories = _get_new_categories(
270
+ rules, categories, category_linters, known, meta_prefixes
271
+ )
261
272
  removed = _get_removed(rules, selected)
262
273
 
263
274
  untriaged_codeless = [n for n in _get_codeless_rules(rules) if n not in known]
@@ -293,24 +304,36 @@ def format_ruff_report(toml_path: Path, check: RuffConfigCheck) -> str:
293
304
  if check.new_categories:
294
305
  lines.append(f"NEW categories not yet triaged ({len(check.new_categories)}):")
295
306
  lines.append(" Add to select or comment out with '# excluded' in ruff.toml:\n")
296
- lines.extend(f' "{prefix}",\t# {linter}' for prefix, linter in check.new_categories)
307
+ lines.extend(
308
+ f' "{prefix}",\t# {linter}' for prefix, linter in check.new_categories
309
+ )
297
310
  lines.append("")
298
311
 
299
312
  if check.removed:
300
- lines.append(f"REMOVED/RENAMED categories still in select ({len(check.removed)}):")
313
+ lines.append(
314
+ f"REMOVED/RENAMED categories still in select ({len(check.removed)}):"
315
+ )
301
316
  lines.extend(f" {code}" for code in check.removed)
302
317
  lines.append("")
303
318
 
304
319
  if check.untriaged_codeless:
305
- lines.append(f"Code-less rules not yet triaged ({len(check.untriaged_codeless)}):")
306
- lines.append(" Add to select by name or comment out with '# excluded' in ruff.toml:\n")
320
+ lines.append(
321
+ f"Code-less rules not yet triaged ({len(check.untriaged_codeless)}):"
322
+ )
323
+ lines.append(
324
+ " Add to select by name or comment out with '# excluded' in ruff.toml:\n"
325
+ )
307
326
  lines.extend(f' "{name}",' for name in check.untriaged_codeless)
308
327
  lines.append("")
309
328
 
310
329
  if check.untriaged_preview:
311
330
  lines.append(f"Preview rules not yet triaged ({len(check.untriaged_preview)}):")
312
- lines.append(" Add to select by name or cover with an '# excluded' line in ruff.toml:\n")
313
- lines.extend(f' "{name}",\t# {code}' for code, name in check.untriaged_preview)
331
+ lines.append(
332
+ " Add to select by name or cover with an '# excluded' line in ruff.toml:\n"
333
+ )
334
+ lines.extend(
335
+ f' "{name}",\t# {code}' for code, name in check.untriaged_preview
336
+ )
314
337
  lines.append("")
315
338
 
316
339
  if check.is_clean:
@@ -395,7 +418,9 @@ def format_ty_report(config_path: Path, check: TyConfigCheck) -> str:
395
418
  ]
396
419
 
397
420
  if check.untriaged:
398
- lines.append(f"UNTRIAGED rules ty knows that the config omits ({len(check.untriaged)}):")
421
+ lines.append(
422
+ f"UNTRIAGED rules ty knows that the config omits ({len(check.untriaged)}):"
423
+ )
399
424
  lines.extend(
400
425
  f' {name} = "???" # default: {check.default_levels[name]}'
401
426
  for name in check.untriaged
@@ -403,7 +428,9 @@ def format_ty_report(config_path: Path, check: TyConfigCheck) -> str:
403
428
  lines.append("")
404
429
 
405
430
  if check.stale:
406
- lines.append(f"STALE rules named in config that ty no longer knows ({len(check.stale)}):")
431
+ lines.append(
432
+ f"STALE rules named in config that ty no longer knows ({len(check.stale)}):"
433
+ )
407
434
  lines.extend(f" {name}" for name in check.stale)
408
435
  lines.append("")
409
436
 
@@ -61,7 +61,12 @@ class Location:
61
61
  message: str
62
62
 
63
63
  def to_dict(self) -> dict[str, object]:
64
- return {"file": self.file, "line": self.line, "col": self.col, "message": self.message}
64
+ return {
65
+ "file": self.file,
66
+ "line": self.line,
67
+ "col": self.col,
68
+ "message": self.message,
69
+ }
65
70
 
66
71
 
67
72
  @dataclass
@@ -123,7 +128,16 @@ def run_ruff(project_path: Path) -> dict[str, FindingGroup]:
123
128
  # selector, bad config, ...). Empty stdout on exit 2 must never read
124
129
  # as "0 findings" — `run_checked` raises instead of returning in that case.
125
130
  result = run_checked(
126
- [sys.executable, "-m", "ruff", "check", "--no-cache", "--output-format", "json", "."],
131
+ [
132
+ sys.executable,
133
+ "-m",
134
+ "ruff",
135
+ "check",
136
+ "--no-cache",
137
+ "--output-format",
138
+ "json",
139
+ ".",
140
+ ],
127
141
  project_path,
128
142
  "ruff",
129
143
  ok_codes=(0, 1),
@@ -226,8 +240,15 @@ def run_pymodularity(project_path: Path) -> dict[str, FindingGroup]:
226
240
  # 2 `uv` missing or an uncaught exception (docs/rules.md "JSON output").
227
241
  # A failure must never read as "0 findings".
228
242
  result = run_checked(
229
- [sys.executable, "-m", "pymodularity", "check", "--output-format", "json",
230
- str(project_path)],
243
+ [
244
+ sys.executable,
245
+ "-m",
246
+ "pymodularity",
247
+ "check",
248
+ "--output-format",
249
+ "json",
250
+ str(project_path),
251
+ ],
231
252
  project_path,
232
253
  "pymodularity",
233
254
  ok_codes=(0, 1),
@@ -259,7 +280,11 @@ def build_report(project_path: Path) -> tuple[dict[str, object], int]:
259
280
  sections: dict[str, object] = {}
260
281
  grand_total = 0
261
282
 
262
- for tool, run in (("ruff", run_ruff), ("ty", run_ty), ("pymodularity", run_pymodularity)):
283
+ for tool, run in (
284
+ ("ruff", run_ruff),
285
+ ("ty", run_ty),
286
+ ("pymodularity", run_pymodularity),
287
+ ):
263
288
  print(f"Running {tool} on {project_path} ...", file=sys.stderr)
264
289
  violations = run(project_path)
265
290
  total = sum(group.count for group in violations.values())
@@ -321,9 +321,9 @@ ignore = [
321
321
  # ANN401 (typing.Any disallowed) stays ON: with basedpyright dropped
322
322
  # in favor of ty, ANN401 is the gate's only coverage for explicit
323
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",
324
+ # missing-return-type-special-method stays ON: ty types the target of
325
+ # `with X() as x` as Unknown when `__enter__` has no return type. ruff
326
+ # fixes `__init__ -> None` and `__str__ -> str` itself (safe fix).
327
327
  "missing-type-args", # missing type on *args — typically pass-through,
328
328
  # type depends on caller
329
329
  "missing-type-kwargs", # missing type on **kwargs — same reasoning as
@@ -339,18 +339,18 @@ ignore = [
339
339
  # than max_snr
340
340
 
341
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
342
+ # lambda-assignment stays ON: a lambda cannot annotate its parameters,
343
+ # so ty types `strip = lambda x: x.strip()` as `(x) -> Unknown` and
344
+ # accepts `strip(1)`.
345
345
  "in-dict-keys", # use `key in dict` — `key in dict.keys()` is more
346
346
  # explicit about intent; Python Zen says explicit >
347
347
  # implicit
348
348
  "superfluous-else-return", # the else branch makes the two paths visually
349
349
  # symmetric and easier to read
350
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",
351
+ # super-call-with-parameters stays ON: zero-argument super() fails in a
352
+ # dataclass(slots=True) before Python 3.14, but ruff skips slots
353
+ # dataclasses for this rule (checked on ruff 0.17.3).
354
354
  "compare-to-empty-string", # `s == ""` is more explicit than `not s` (the
355
355
  # latter also matches None, 0, [])
356
356
 
@@ -396,6 +396,11 @@ ignore = [
396
396
  "unused-async", # async test doubles mirror the awaited signature they replace
397
397
  "float-equality-comparison", # asserting an exactly representable result is correct
398
398
  "too-many-locals", # an integration test sets up many real collaborators
399
+ "missing-type-function-argument", # pytest fixtures: the fixture defines the type
400
+ "missing-return-type-private-function", # helpers and fakes local to a test module
401
+ "missing-return-type-special-method", # `__init__` / `__enter__` on test doubles
402
+ "import-outside-top-level", # imports inside a test patch or isolate the import
403
+ "private-member-access", # tests inspect internal state on purpose
399
404
  ]
400
405
  "test_*.py" = [
401
406
  "assert",
@@ -409,6 +414,11 @@ ignore = [
409
414
  "unused-async",
410
415
  "float-equality-comparison",
411
416
  "too-many-locals",
417
+ "missing-type-function-argument",
418
+ "missing-return-type-private-function",
419
+ "missing-return-type-special-method",
420
+ "import-outside-top-level",
421
+ "private-member-access",
412
422
  ]
413
423
 
414
424
  # ── isort ───────────────────────────────────────────────────────────────────
File without changes