habit-hooks-python 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (15) hide show
  1. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/PKG-INFO +1 -1
  2. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/docs/python-plugin.spec.md +49 -8
  3. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/pyproject.toml +1 -1
  4. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/src/habit_hooks_python/sensors/deptry_sensor.py +17 -0
  5. habit_hooks_python-1.3.0/tests/test_a_project_declaring_no_dependencies_is_clean.py +75 -0
  6. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/.gitignore +0 -0
  7. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/src/habit_hooks_python/__init__.py +0 -0
  8. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/src/habit_hooks_python/config.toml +0 -0
  9. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/src/habit_hooks_python/guides/high-complexity.md +0 -0
  10. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/src/habit_hooks_python/guides/swallowed-exception.md +0 -0
  11. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/src/habit_hooks_python/ruff.toml +0 -0
  12. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/src/habit_hooks_python/sensors/deptry.toml +0 -0
  13. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/src/habit_hooks_python/sensors/ruff.toml +0 -0
  14. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/tests/test_a_deptry_nobody_installed_is_named.py +0 -0
  15. {habit_hooks_python-1.2.0 → habit_hooks_python-1.3.0}/tests/test_the_python_files_leave_installed_packages_alone.py +0 -0
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: habit-hooks-python
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: The Python Habit Hooks plugin
5
5
  Requires-Python: >=3.11
@@ -252,19 +252,50 @@ habit-sensors --all | jq '.[] | {smell, language, key: .issues[0].key, file: .is
252
252
  }
253
253
  ```
254
254
 
255
+ ## A project with no pyproject.toml or requirements.txt runs clean
256
+
257
+ deptry needs a dependency declaration to check against — a `pyproject.toml`
258
+ carrying `[project]`/`[tool.poetry.dependencies]`/`[tool.pdm]`, or one of its
259
+ requirements filenames. A project with neither has nothing to check, and that
260
+ is not the same as a crash: the sensor reports a clean, empty run instead of
261
+ the `incomplete-run` the case below reports for a genuine one.
262
+
263
+ 📄.habit-hooks/config.toml
264
+ ```toml
265
+ plugins = ["python"]
266
+
267
+ [sensors.ruff]
268
+ disabled = true
269
+ ```
270
+
271
+ 📄app.py
272
+ ```python
273
+ def fetch(url):
274
+ return url
275
+ ```
276
+
277
+ ```bash
278
+ habit-sensors --all
279
+ ```
280
+
281
+ 🖥️ ✅
282
+ ```json
283
+ []
284
+ ```
285
+
255
286
  ## A crashing deptry fails the run, never reports clean
256
287
 
257
- deptry needs a `pyproject.toml` to analyse; without one it exits non-zero
258
- instead of emitting findings. The sensor must surface that as a failure a
259
- crashed tool is never a clean run. The sensor exits with a code outside the
260
- findings range, so `habit-sensors` raises, names the sensor on stderr, and exits
261
- 1 rather than printing an empty (false-clean) result. The failed run carries only
262
- the reserved `incomplete-run` marker on stdout
288
+ A `[tool.deptry]` option deptry does not recognise is a genuine crash — unlike
289
+ the case above, this is not deptry answering "nothing here to check" but the
290
+ tool itself breaking. The sensor must surface that as a failure a broken
291
+ tool is never a clean run. The sensor exits with a code outside the findings
292
+ range, so `habit-sensors` raises, names the sensor on stderr, and exits 1
293
+ rather than printing an empty (false-clean) result. The failed run carries
294
+ only the reserved `incomplete-run` marker on stdout
263
295
  ([habit-sensors.spec.md](../../../docs/habit-sensors.spec.md)).
264
296
 
265
297
  The notice carries deptry's own diagnosis after that first line. That text is
266
- deptry's to word and names absolute paths, so only the line naming the sensor is
267
- asserted here.
298
+ deptry's to word, so only the line naming the sensor is asserted here.
268
299
 
269
300
  📄.habit-hooks/config.toml
270
301
  ```toml
@@ -274,6 +305,16 @@ plugins = ["python"]
274
305
  disabled = true
275
306
  ```
276
307
 
308
+ 📄pyproject.toml
309
+ ```toml
310
+ [project]
311
+ name = "demo"
312
+ version = "0.0.0"
313
+
314
+ [tool.deptry]
315
+ not_a_real_option = true
316
+ ```
317
+
277
318
  📄app.py
278
319
  ```python
279
320
  def fetch(url):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "habit-hooks-python"
3
- version = "1.2.0"
3
+ version = "1.3.0"
4
4
  description = "The Python Habit Hooks plugin"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = []
@@ -37,6 +37,20 @@ def deptry_crashed(result: subprocess.CompletedProcess[str], report: Path) -> bo
37
37
  return result.returncode not in (0, 1) or not report.is_file()
38
38
 
39
39
 
40
+ def deptry_found_no_declaration(result: subprocess.CompletedProcess[str]) -> bool:
41
+ """Whether deptry crashed because it found no dependency declaration to check.
42
+
43
+ deptry raises ``DependencySpecificationNotFoundError`` when it finds no
44
+ dependency declaration to check. Asking the tool this way, instead of
45
+ reimplementing its PEP 621/poetry/pdm search, keeps the answer from
46
+ drifting off deptry's own. A project with none declared genuinely has
47
+ zero declared-but-unused dependencies — a clean result, not a swallowed
48
+ failure (#88). Matching on the exception's class name, only inside the
49
+ already-crashed branch, keeps every other crash failing loud.
50
+ """
51
+ return "DependencySpecificationNotFoundError" in result.stderr
52
+
53
+
40
54
  def unused_dependencies(report: Path) -> list[dict]:
41
55
  entries = json.loads(report.read_text())
42
56
  return [entry for entry in entries if entry["error"]["code"] == "DEP002"]
@@ -71,6 +85,9 @@ def main() -> int:
71
85
  report = Path(tmp) / "deptry-report.json"
72
86
  result = run_deptry(report)
73
87
  if deptry_crashed(result, report):
88
+ if deptry_found_no_declaration(result):
89
+ print(json.dumps([]))
90
+ return 0
74
91
  sys.stderr.write(result.stderr)
75
92
  return 2
76
93
  print(json.dumps(findings(unused_dependencies(report))))
@@ -0,0 +1,75 @@
1
+ """A project that declares no dependencies has none that go unused.
2
+
3
+ deptry raises ``DependencySpecificationNotFoundError`` when it finds no
4
+ ``pyproject.toml`` carrying a ``[project]``/``[tool.poetry.dependencies]``/
5
+ ``[tool.pdm]`` section and none of its requirements filenames either — its own
6
+ search across PEP 621, poetry, pdm and deptry's configurable requirements
7
+ names, in one place, so this sensor recognises that answer rather than
8
+ re-implementing the search (see "A wrapped tool's own config wins" in
9
+ CLAUDE.md). A project with nothing declared has, honestly, zero declared but
10
+ unused dependencies, and that must read as a clean run, not a broken one —
11
+ kept distinct here from every other way deptry can fail, which still has to
12
+ fail loud (#88).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import subprocess
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ SENSOR = (
22
+ Path(__file__).resolve().parents[1]
23
+ / "src/habit_hooks_python/sensors/deptry_sensor.py"
24
+ )
25
+
26
+
27
+ def test_a_project_declaring_no_dependencies_is_a_clean_run(tmp_path: Path) -> None:
28
+ (tmp_path / "app.py").write_text("import os\n")
29
+
30
+ result = subprocess.run(
31
+ [sys.executable, str(SENSOR)],
32
+ cwd=tmp_path,
33
+ capture_output=True,
34
+ text=True,
35
+ )
36
+
37
+ assert result.returncode == 0
38
+ assert result.stdout.strip() == "[]"
39
+ assert result.stderr.strip() == ""
40
+
41
+
42
+ def test_deptry_still_answers_a_missing_declaration_with_that_error(
43
+ tmp_path: Path,
44
+ ) -> None:
45
+ """The sensor recognises deptry's exception by name, so a deptry that stops
46
+ raising it must fail here rather than silently retire that branch."""
47
+ (tmp_path / "app.py").write_text("import os\n")
48
+
49
+ result = subprocess.run(
50
+ ["deptry", "."],
51
+ cwd=tmp_path,
52
+ capture_output=True,
53
+ text=True,
54
+ )
55
+
56
+ assert "DependencySpecificationNotFoundError" in result.stderr
57
+
58
+
59
+ def test_deptry_failing_another_way_still_fails_the_run(tmp_path: Path) -> None:
60
+ (tmp_path / "pyproject.toml").write_text(
61
+ '[project]\nname = "demo"\nversion = "0.0.0"\ndependencies = []\n\n'
62
+ "[tool.deptry]\nnot_a_real_option = true\n"
63
+ )
64
+ (tmp_path / "app.py").write_text("import os\n")
65
+
66
+ result = subprocess.run(
67
+ [sys.executable, str(SENSOR)],
68
+ cwd=tmp_path,
69
+ capture_output=True,
70
+ text=True,
71
+ )
72
+
73
+ assert result.returncode == 2
74
+ assert result.stdout.strip() == ""
75
+ assert "not_a_real_option" in result.stderr