habit-hooks-python 1.2.1__tar.gz → 1.3.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.
- habit_hooks_python-1.3.1/PKG-INFO +53 -0
- habit_hooks_python-1.3.1/README.md +29 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/docs/python-plugin.spec.md +49 -8
- habit_hooks_python-1.3.1/pyproject.toml +51 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/sensors/deptry_sensor.py +17 -0
- habit_hooks_python-1.3.1/tests/test_a_project_declaring_no_dependencies_is_clean.py +75 -0
- habit_hooks_python-1.2.1/PKG-INFO +0 -5
- habit_hooks_python-1.2.1/pyproject.toml +0 -16
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/.gitignore +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/__init__.py +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/config.toml +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/guides/high-complexity.md +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/guides/swallowed-exception.md +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/ruff.toml +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/sensors/deptry.toml +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/sensors/ruff.toml +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/tests/test_a_deptry_nobody_installed_is_named.py +0 -0
- {habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/tests/test_the_python_files_leave_installed_packages_alone.py +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: habit-hooks-python
|
|
3
|
+
Version: 1.3.1
|
|
4
|
+
Summary: The Python Habit Hooks plugin
|
|
5
|
+
Project-URL: Homepage, https://habit-hooks.com
|
|
6
|
+
Project-URL: Repository, https://github.com/habit-hooks/habit-hooks
|
|
7
|
+
Project-URL: Issues, https://github.com/habit-hooks/habit-hooks/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/habit-hooks/habit-hooks/blob/main/CHANGELOG.md
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai-coding-agents,ci,code-quality,code-review,code-smells,coding-agents,developer-tools,linter,python,refactoring,static-analysis,technical-debt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# habit-hooks-python
|
|
26
|
+
|
|
27
|
+
The Python Habit Hooks plugin: wraps [`ruff`](https://docs.astral.sh/ruff/) for
|
|
28
|
+
structural smells (complexity, too many parameters, swallowed exceptions, …)
|
|
29
|
+
and [`deptry`](https://github.com/fpgmaas/deptry) for dependency issues.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install "habit-hooks[python]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Enable
|
|
38
|
+
|
|
39
|
+
```toml
|
|
40
|
+
# .habit-hooks/config.toml
|
|
41
|
+
plugins = ["python", "generic"]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Installing a plugin does not switch it on — it has to be named in
|
|
45
|
+
`plugins` before habit-hooks runs it.
|
|
46
|
+
|
|
47
|
+
## Detectors
|
|
48
|
+
|
|
49
|
+
- [`ruff`](https://docs.astral.sh/ruff/), [`deptry`](https://github.com/fpgmaas/deptry) —
|
|
50
|
+
`pip install ruff deptry`
|
|
51
|
+
- `jq` — from your system package manager (e.g. `brew install jq`)
|
|
52
|
+
|
|
53
|
+
Part of [habit-hooks](https://github.com/habit-hooks/habit-hooks).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# habit-hooks-python
|
|
2
|
+
|
|
3
|
+
The Python Habit Hooks plugin: wraps [`ruff`](https://docs.astral.sh/ruff/) for
|
|
4
|
+
structural smells (complexity, too many parameters, swallowed exceptions, …)
|
|
5
|
+
and [`deptry`](https://github.com/fpgmaas/deptry) for dependency issues.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install "habit-hooks[python]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Enable
|
|
14
|
+
|
|
15
|
+
```toml
|
|
16
|
+
# .habit-hooks/config.toml
|
|
17
|
+
plugins = ["python", "generic"]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Installing a plugin does not switch it on — it has to be named in
|
|
21
|
+
`plugins` before habit-hooks runs it.
|
|
22
|
+
|
|
23
|
+
## Detectors
|
|
24
|
+
|
|
25
|
+
- [`ruff`](https://docs.astral.sh/ruff/), [`deptry`](https://github.com/fpgmaas/deptry) —
|
|
26
|
+
`pip install ruff deptry`
|
|
27
|
+
- `jq` — from your system package manager (e.g. `brew install jq`)
|
|
28
|
+
|
|
29
|
+
Part of [habit-hooks](https://github.com/habit-hooks/habit-hooks).
|
|
@@ -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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
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):
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "habit-hooks-python"
|
|
3
|
+
version = "1.3.1"
|
|
4
|
+
description = "The Python Habit Hooks plugin"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
keywords = [
|
|
8
|
+
"code-quality",
|
|
9
|
+
"code-smells",
|
|
10
|
+
"static-analysis",
|
|
11
|
+
"linter",
|
|
12
|
+
"ci",
|
|
13
|
+
"code-review",
|
|
14
|
+
"technical-debt",
|
|
15
|
+
"refactoring",
|
|
16
|
+
"ai-coding-agents",
|
|
17
|
+
"coding-agents",
|
|
18
|
+
"developer-tools",
|
|
19
|
+
"python",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Environment :: Console",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
31
|
+
"Topic :: Software Development :: Testing",
|
|
32
|
+
"Topic :: Utilities",
|
|
33
|
+
]
|
|
34
|
+
requires-python = ">=3.11"
|
|
35
|
+
dependencies = []
|
|
36
|
+
|
|
37
|
+
[project.entry-points."habit_hooks.plugins"]
|
|
38
|
+
python = "habit_hooks_python"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://habit-hooks.com"
|
|
42
|
+
Repository = "https://github.com/habit-hooks/habit-hooks"
|
|
43
|
+
Issues = "https://github.com/habit-hooks/habit-hooks/issues"
|
|
44
|
+
Changelog = "https://github.com/habit-hooks/habit-hooks/blob/main/CHANGELOG.md"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/habit_hooks_python"]
|
|
@@ -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
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "habit-hooks-python"
|
|
3
|
-
version = "1.2.1"
|
|
4
|
-
description = "The Python Habit Hooks plugin"
|
|
5
|
-
requires-python = ">=3.11"
|
|
6
|
-
dependencies = []
|
|
7
|
-
|
|
8
|
-
[project.entry-points."habit_hooks.plugins"]
|
|
9
|
-
python = "habit_hooks_python"
|
|
10
|
-
|
|
11
|
-
[build-system]
|
|
12
|
-
requires = ["hatchling"]
|
|
13
|
-
build-backend = "hatchling.build"
|
|
14
|
-
|
|
15
|
-
[tool.hatch.build.targets.wheel]
|
|
16
|
-
packages = ["src/habit_hooks_python"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/sensors/deptry.toml
RENAMED
|
File without changes
|
{habit_hooks_python-1.2.1 → habit_hooks_python-1.3.1}/src/habit_hooks_python/sensors/ruff.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|