habit-hooks-python 1.0.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.
- habit_hooks_python/__init__.py +1 -0
- habit_hooks_python/config.toml +5 -0
- habit_hooks_python/guides/high-complexity.md +12 -0
- habit_hooks_python/guides/swallowed-exception.md +12 -0
- habit_hooks_python/ruff.toml +6 -0
- habit_hooks_python/sensors/deptry.toml +1 -0
- habit_hooks_python/sensors/deptry_sensor.py +70 -0
- habit_hooks_python/sensors/ruff.toml +28 -0
- habit_hooks_python-1.0.0.dist-info/METADATA +5 -0
- habit_hooks_python-1.0.0.dist-info/RECORD +12 -0
- habit_hooks_python-1.0.0.dist-info/WHEEL +4 -0
- habit_hooks_python-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The python Habit Hooks plugin: package data discovered via the habit_hooks.plugins entry point."""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
High cyclomatic complexity means one function is making too many decisions at once. The smell is not the number, it is that the function has quietly taken on more than one job. The count is the symptom, tangled responsibilities are the cause.
|
|
2
|
+
|
|
3
|
+
{% include "includes/line_level_issues.md" %}
|
|
4
|
+
Work through it in order:
|
|
5
|
+
|
|
6
|
+
1. **Name what each branch is for.** Give every branch a one-sentence description of the responsibility it handles. If two branches describe the same thing, they belong together. If a branch has no clean name, that path probably wants its own function.
|
|
7
|
+
2. **Lift the guards out first.** Turn early-exit conditions into guard clauses (`if not user: return None`) so the happy path stays flat and left-aligned. Most of the count is preconditions wrapped around the real work.
|
|
8
|
+
3. **Change the shape, do not just move it.** When every branch is the same kind of decision, reshape it: a long `if/elif` on a value is often a dict dispatch or polymorphism, a nested loop often a comprehension or a generator. When polymorphism is not the right fit and the branches are genuinely separate jobs, extract one named method per branch, each named for the responsibility it handles. The goal is fewer decisions in one place, not the same tangle split into `_part1` / `_part2` behind worse names.
|
|
9
|
+
|
|
10
|
+
Reducing the number without reducing the tangle is not a fix. Do not merge conditions with `and`/`or` just to drop a branch, and do not rewrite `if` statements as ternaries to slip under the check. ruff's mccabe does not count ternary expressions, so that trick lowers the score while a human still has to hold every condition in their head.
|
|
11
|
+
|
|
12
|
+
The test is not whether the number dropped. It is whether someone reading the function for the first time can hold it in their head at once. If not, it is still doing too much.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
A broad `except` (`except:`, `except Exception`, `except BaseException`) that discards the error silently is hiding a failure you have not understood, not handling one you planned for. Before you write it, name the specific error you expect and why. That one sentence is usually the fix.
|
|
2
|
+
|
|
3
|
+
{% include "includes/line_level_issues.md" %}
|
|
4
|
+
Work through it in order:
|
|
5
|
+
|
|
6
|
+
1. **Catch only what you can name.** `ValueError`, `KeyError`, `TimeoutError`, whatever the call really raises. If you cannot name it, you are guessing, and every other error should stay free to surface where someone can see it.
|
|
7
|
+
2. **Make the decision visible.** Recover from it, add context and re-raise (`raise ... from err`), or, at a boundary that has to stay alive, log the full traceback (`logging.exception(...)`) and continue. Logging and returning a default is a real option, not automatically a swallow.
|
|
8
|
+
3. **The test is not whether you re-raised.** Ask one question: if this fires at 3am, will anyone know it happened, and will they know why? What turns a catch into a swallow is doing it blindly: a wide catch, no named error, nothing logged, the failure gone without a trace. If the answer is no, it is still a swallow, however you dressed it up.
|
|
9
|
+
|
|
10
|
+
Narrowing the type or adding `# noqa` only to quiet ruff is not a fix if the error is still discarded.
|
|
11
|
+
|
|
12
|
+
If you are unsure whether this catch is a real decision or a reflex, check with a human before you keep it.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
command = "python ${dir}/deptry_sensor.py"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Run deptry and print ``unused-dependency`` findings.
|
|
2
|
+
|
|
3
|
+
deptry's stdout is unreliable when piped, so this wrapper runs it against a temp
|
|
4
|
+
JSON report, reads that report, and shapes each ``DEP002`` (a declared but unused
|
|
5
|
+
dependency) into the canonical finding.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_deptry(report: Path) -> subprocess.CompletedProcess[str]:
|
|
18
|
+
return subprocess.run(
|
|
19
|
+
["deptry", ".", "--json-output", str(report)],
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def deptry_crashed(result: subprocess.CompletedProcess[str], report: Path) -> bool:
|
|
26
|
+
return result.returncode not in (0, 1) or not report.is_file()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def unused_dependencies(report: Path) -> list[dict]:
|
|
30
|
+
entries = json.loads(report.read_text())
|
|
31
|
+
return [entry for entry in entries if entry["error"]["code"] == "DEP002"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def issue(entry: dict) -> dict:
|
|
35
|
+
return {
|
|
36
|
+
"key": entry["module"],
|
|
37
|
+
"details": {
|
|
38
|
+
"module": entry["module"],
|
|
39
|
+
"file": entry["location"]["file"],
|
|
40
|
+
"message": entry["error"]["message"],
|
|
41
|
+
"source": "deptry:DEP002",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def findings(entries: list[dict]) -> list[dict]:
|
|
47
|
+
if not entries:
|
|
48
|
+
return []
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
"smell": "unused-dependency",
|
|
52
|
+
"details": {},
|
|
53
|
+
"issues": [issue(entry) for entry in entries],
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main() -> int:
|
|
59
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
60
|
+
report = Path(tmp) / "deptry-report.json"
|
|
61
|
+
result = run_deptry(report)
|
|
62
|
+
if deptry_crashed(result, report):
|
|
63
|
+
sys.stderr.write(result.stderr)
|
|
64
|
+
return 2
|
|
65
|
+
print(json.dumps(findings(unused_dependencies(report))))
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
command = """
|
|
2
|
+
set -o pipefail
|
|
3
|
+
ruff check --output-format=json --select=C901,PLR0913,PLR0915,F841,F401,BLE001 ${files} | jq '
|
|
4
|
+
map(. + {smell: ({
|
|
5
|
+
"C901": "high-complexity",
|
|
6
|
+
"PLR0913": "too-many-parameters",
|
|
7
|
+
"PLR0915": "oversized-function",
|
|
8
|
+
"F841": "unused-variable",
|
|
9
|
+
"F401": "unused-import",
|
|
10
|
+
"BLE001": "swallowed-exception",
|
|
11
|
+
"invalid-syntax": "parse-error"
|
|
12
|
+
}[.code])})
|
|
13
|
+
| group_by(.smell)
|
|
14
|
+
| map({
|
|
15
|
+
smell: .[0].smell,
|
|
16
|
+
details: {},
|
|
17
|
+
issues: map({
|
|
18
|
+
key: .filename,
|
|
19
|
+
details: {
|
|
20
|
+
file: .filename,
|
|
21
|
+
line: .location.row,
|
|
22
|
+
column: .location.column,
|
|
23
|
+
message: .message,
|
|
24
|
+
source: ("ruff:" + .code)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
})'
|
|
28
|
+
"""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
habit_hooks_python/__init__.py,sha256=LGBheP8EM8zRefLDnr_G-R-LLhPj_QxpJ04MSv3hLC8,102
|
|
2
|
+
habit_hooks_python/config.toml,sha256=2ie74-nTqAadJGwi_F4kbAzbByhN_Ziu61toh00mkqw,113
|
|
3
|
+
habit_hooks_python/ruff.toml,sha256=a4ALE0w4pF1pwJVwykQghExpZdlTMAQXB_MkNthvY84,82
|
|
4
|
+
habit_hooks_python/guides/high-complexity.md,sha256=AOgAPBsGRnu8Vk01QHDbpyiAjb3knF9sYzow7DCxmPk,1821
|
|
5
|
+
habit_hooks_python/guides/swallowed-exception.md,sha256=z_Vrfl2i-1k_8MjYSjvaaBrssphWMSL7l9tRyEujtuE,1435
|
|
6
|
+
habit_hooks_python/sensors/deptry.toml,sha256=E94wN-47PGTCehtEQ8eAxA--GQUcjWEA2lQNFXgtjgs,43
|
|
7
|
+
habit_hooks_python/sensors/deptry_sensor.py,sha256=H3UhcMhGbiOWXxbr7-NSDj1A7epXmDIVsZA7DwJqLMs,1856
|
|
8
|
+
habit_hooks_python/sensors/ruff.toml,sha256=yNnsrgrC-3_yiRfZLWrZeX2CmU_ctA-6nJoVlITZnEA,727
|
|
9
|
+
habit_hooks_python-1.0.0.dist-info/METADATA,sha256=AtrzMFDGwJtIGCmydjmS4e62ToAc6brWD2M0GNpy8WM,125
|
|
10
|
+
habit_hooks_python-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
11
|
+
habit_hooks_python-1.0.0.dist-info/entry_points.txt,sha256=FA8Cm73udgU_q3rApY-aLWkYdiGs6X6uvVdxEAYQalc,50
|
|
12
|
+
habit_hooks_python-1.0.0.dist-info/RECORD,,
|