habit-hooks-python 1.3.0__tar.gz → 1.4.0rc1__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.4.0rc1/PKG-INFO +52 -0
- habit_hooks_python-1.4.0rc1/README.md +28 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/docs/python-plugin.spec.md +15 -14
- habit_hooks_python-1.4.0rc1/pyproject.toml +51 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/src/habit_hooks_python/config.toml +0 -2
- habit_hooks_python-1.4.0rc1/src/habit_hooks_python/sensors/deptry.toml +1 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/src/habit_hooks_python/sensors/deptry_sensor.py +7 -3
- habit_hooks_python-1.4.0rc1/src/habit_hooks_python/sensors/ruff.toml +1 -0
- habit_hooks_python-1.4.0rc1/src/habit_hooks_python/sensors/ruff_sensor.py +126 -0
- habit_hooks_python-1.4.0rc1/src/habit_hooks_python/sensors/tool_spawn.py +77 -0
- habit_hooks_python-1.4.0rc1/tests/conftest.py +20 -0
- habit_hooks_python-1.4.0rc1/tests/test_a_crashing_ruff_fails_the_run.py +55 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/tests/test_a_deptry_nobody_installed_is_named.py +2 -1
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/tests/test_a_project_declaring_no_dependencies_is_clean.py +11 -7
- habit_hooks_python-1.4.0rc1/tests/test_a_ruff_nobody_installed_is_named.py +35 -0
- habit_hooks_python-1.4.0rc1/tests/test_the_ruff_pipeline_maps_codes_to_smells.py +109 -0
- habit_hooks_python-1.4.0rc1/tests/test_the_ruff_sensor_runs_the_real_tool.py +65 -0
- habit_hooks_python-1.3.0/PKG-INFO +0 -5
- habit_hooks_python-1.3.0/pyproject.toml +0 -16
- habit_hooks_python-1.3.0/src/habit_hooks_python/sensors/deptry.toml +0 -1
- habit_hooks_python-1.3.0/src/habit_hooks_python/sensors/ruff.toml +0 -28
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/.gitignore +0 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/src/habit_hooks_python/__init__.py +0 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/src/habit_hooks_python/guides/high-complexity.md +0 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/src/habit_hooks_python/guides/swallowed-exception.md +0 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/src/habit_hooks_python/ruff.toml +0 -0
- {habit_hooks_python-1.3.0 → habit_hooks_python-1.4.0rc1}/tests/test_the_python_files_leave_installed_packages_alone.py +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: habit-hooks-python
|
|
3
|
+
Version: 1.4.0rc1
|
|
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
|
+
|
|
52
|
+
Part of [habit-hooks](https://github.com/habit-hooks/habit-hooks).
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
28
|
+
Part of [habit-hooks](https://github.com/habit-hooks/habit-hooks).
|
|
@@ -12,10 +12,11 @@ plugins = ["python"]
|
|
|
12
12
|
|
|
13
13
|
## ruff adapter maps rule IDs to canonical smells
|
|
14
14
|
|
|
15
|
-
The `ruff` adapter selects `C901,PLR0913,PLR0915,F841,F401,BLE001` and a
|
|
16
|
-
|
|
17
|
-
stamping `source: "ruff:<code>"` on each
|
|
18
|
-
`max-args = 3`, so a four-argument
|
|
15
|
+
The `ruff` adapter selects `C901,PLR0913,PLR0915,F841,F401,BLE001` and a
|
|
16
|
+
Python helper beside the sensor spec (`ruff_sensor.py`) groups the flat
|
|
17
|
+
output into one finding per smell, stamping `source: "ruff:<code>"` on each
|
|
18
|
+
issue. The shipped `ruff.toml` carries `max-args = 3`, so a four-argument
|
|
19
|
+
function trips `PLR0913`.
|
|
19
20
|
|
|
20
21
|
📄ruff.toml @plugins/python/src/habit_hooks_python/ruff.toml
|
|
21
22
|
|
|
@@ -336,17 +337,18 @@ habit-sensors --all 2>&1 >/dev/null | sed -n 1p
|
|
|
336
337
|
|
|
337
338
|
🖥️ ❌ 1
|
|
338
339
|
```text
|
|
339
|
-
habit-sensors: sensor 'deptry' failed: ${python} ${dir}/deptry_sensor.py
|
|
340
|
+
habit-sensors: sensor 'deptry' failed: '${python}' '${dir}/deptry_sensor.py'
|
|
340
341
|
```
|
|
341
342
|
|
|
342
343
|
## A crashing ruff fails the run, never reports clean
|
|
343
344
|
|
|
344
|
-
The `ruff` sensor
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
exits 1, carrying only the reserved
|
|
345
|
+
The `ruff` sensor runs a Python helper (`ruff_sensor.py`) rather than piping
|
|
346
|
+
the tool into `jq`. A crashing `ruff` (here, a malformed `ruff.toml` that makes
|
|
347
|
+
the tool exit non-zero) is a genuine crash, not a violation: ruff's own
|
|
348
|
+
contract is 0 clean, 1 violations found, and the helper treats any other exit
|
|
349
|
+
as broken, exiting 2 itself with nothing on stdout. `habit-sensors` then
|
|
350
|
+
raises, names the sensor on stderr, and exits 1, carrying only the reserved
|
|
351
|
+
`incomplete-run` marker on stdout
|
|
350
352
|
([habit-sensors.spec.md](../../../docs/habit-sensors.spec.md)).
|
|
351
353
|
|
|
352
354
|
📄.habit-hooks/config.toml
|
|
@@ -376,8 +378,7 @@ habit-sensors --all | jq -c '[.[].smell]'
|
|
|
376
378
|
["incomplete-run"]
|
|
377
379
|
```
|
|
378
380
|
|
|
379
|
-
The notice quotes the sensor's whole command
|
|
380
|
-
line shows only where that command starts — and then ruff's own diagnosis, which
|
|
381
|
+
The notice quotes the sensor's whole command, then ruff's own diagnosis, which
|
|
381
382
|
names the absolute path of the config it could not parse. Only the line naming
|
|
382
383
|
the sensor is stable enough to assert.
|
|
383
384
|
|
|
@@ -387,5 +388,5 @@ habit-sensors --all 2>&1 >/dev/null | sed -n 1p
|
|
|
387
388
|
|
|
388
389
|
🖥️ ❌ 1
|
|
389
390
|
```text
|
|
390
|
-
habit-sensors: sensor 'ruff' failed:
|
|
391
|
+
habit-sensors: sensor 'ruff' failed: '${python}' '${dir}/ruff_sensor.py' '${files}'
|
|
391
392
|
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "habit-hooks-python"
|
|
3
|
+
version = "1.4.0rc1"
|
|
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"]
|
|
@@ -8,9 +8,7 @@ files = ["**/*.py", "!**/site-packages/**", "!**/.venv/**", "!**/venv/**"]
|
|
|
8
8
|
sensors = ["ruff", "deptry"]
|
|
9
9
|
transformers = []
|
|
10
10
|
|
|
11
|
-
# The ruff sensor pipes its JSON through jq, so a python-only project needs it.
|
|
12
11
|
detectors = [
|
|
13
12
|
{ name = "ruff", kind = "command", install = "pip install ruff" },
|
|
14
13
|
{ name = "deptry", kind = "command", install = "pip install deptry" },
|
|
15
|
-
{ name = "jq", kind = "command", install = "brew install jq" },
|
|
16
14
|
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
argv = ["${python}", "${dir}/deptry_sensor.py"]
|
|
@@ -13,6 +13,8 @@ import sys
|
|
|
13
13
|
import tempfile
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
+
from tool_spawn import run_tool
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
def run_deptry(report: Path) -> subprocess.CompletedProcess[str]:
|
|
18
20
|
"""What deptry said, or what a shell says about a deptry nobody installed.
|
|
@@ -22,11 +24,13 @@ def run_deptry(report: Path) -> subprocess.CompletedProcess[str]:
|
|
|
22
24
|
absent tool raised a ``FileNotFoundError`` out of here, making twenty lines
|
|
23
25
|
of Python internals the sensor's diagnosis (#114). This wrapper is what looks
|
|
24
26
|
for deptry, so it answers the way the shell would have, and that phrase is
|
|
25
|
-
what the run recognises to name the missing tool.
|
|
27
|
+
what the run recognises to name the missing tool. Looking is ``tool_spawn``'s,
|
|
28
|
+
which names the file — a console script a project's own venv installed, which
|
|
29
|
+
Windows spells ``deptry.exe`` there and habit-hooks' venv may not hold at all.
|
|
26
30
|
"""
|
|
27
31
|
command = ["deptry", ".", "--json-output", str(report)]
|
|
28
32
|
try:
|
|
29
|
-
return
|
|
33
|
+
return run_tool(command)
|
|
30
34
|
except FileNotFoundError:
|
|
31
35
|
return subprocess.CompletedProcess(
|
|
32
36
|
command, 127, "", "deptry: command not found\n"
|
|
@@ -52,7 +56,7 @@ def deptry_found_no_declaration(result: subprocess.CompletedProcess[str]) -> boo
|
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
def unused_dependencies(report: Path) -> list[dict]:
|
|
55
|
-
entries = json.loads(report.read_text())
|
|
59
|
+
entries = json.loads(report.read_text(encoding="utf-8"))
|
|
56
60
|
return [entry for entry in entries if entry["error"]["code"] == "DEP002"]
|
|
57
61
|
|
|
58
62
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
argv = ["${python}", "${dir}/ruff_sensor.py", "${files}"]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Run ruff and print canonical findings, mapped from rule code to smell.
|
|
2
|
+
|
|
3
|
+
ruff's own JSON is one entry per violation; this groups them by mapped smell
|
|
4
|
+
and shapes each into the canonical finding — the job the old
|
|
5
|
+
``ruff | jq`` pipeline did, without needing ``jq`` installed (dropping the
|
|
6
|
+
plugin's only other declared detector, and the shell it took to pipe through
|
|
7
|
+
it).
|
|
8
|
+
|
|
9
|
+
An entry's ``code`` not in :data:`CODE_SMELLS` has no smell to report it
|
|
10
|
+
under (a code outside ``--select`` besides ``invalid-syntax``, which ruff
|
|
11
|
+
reports regardless of ``--select``). It is dropped rather than crashed on or
|
|
12
|
+
forwarded under ruff's own name — the same choice the knip sensor makes for a
|
|
13
|
+
key with no smell mapped (see "A sensor emits vocabulary smells only" in
|
|
14
|
+
CLAUDE.md): a key nobody catalogued has no guide and no severity, so
|
|
15
|
+
forwarding it can only fail a run and decline to explain why. Dropping is
|
|
16
|
+
also safe *by construction* rather than by ``--select`` pinning the set, which
|
|
17
|
+
is what the old jq filter depended on (indexing a jq object with a key that
|
|
18
|
+
maps to nothing is a jq crash, not a miss — issue #83).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
from tool_spawn import run_tool
|
|
28
|
+
|
|
29
|
+
SELECTED_CODES = "C901,PLR0913,PLR0915,F841,F401,BLE001"
|
|
30
|
+
|
|
31
|
+
CODE_SMELLS = {
|
|
32
|
+
"C901": "high-complexity",
|
|
33
|
+
"PLR0913": "too-many-parameters",
|
|
34
|
+
"PLR0915": "oversized-function",
|
|
35
|
+
"F841": "unused-variable",
|
|
36
|
+
"F401": "unused-import",
|
|
37
|
+
"BLE001": "swallowed-exception",
|
|
38
|
+
"invalid-syntax": "parse-error",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ruff's own contract: 0 is clean, 1 is "violations found" — both trustworthy.
|
|
42
|
+
# Anything else is ruff never having produced a real report (bad config, a
|
|
43
|
+
# missing file, an internal error), the same distinction part_output.py's
|
|
44
|
+
# TOOL_EXIT_CODES draws for a part run directly.
|
|
45
|
+
TOOL_EXIT_CODES = (0, 1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_ruff(files: list[str]) -> subprocess.CompletedProcess[str]:
|
|
49
|
+
"""What ruff said, or what a shell says about a ruff nobody installed.
|
|
50
|
+
|
|
51
|
+
``pip install habit-hooks-python`` brings neither wrapped tool with it, so
|
|
52
|
+
this is the ordinary state of a machine that has just enabled the plugin —
|
|
53
|
+
and an absent tool raised a ``FileNotFoundError`` out of here, making
|
|
54
|
+
twenty lines of Python internals the sensor's diagnosis (#114). This
|
|
55
|
+
wrapper is what looks for ruff, so it answers the way the shell would
|
|
56
|
+
have, and that phrase is what the run recognises to name the missing
|
|
57
|
+
tool. Looking is ``tool_spawn``'s, which names the file — ``ruff.exe`` or
|
|
58
|
+
a ``.cmd`` shim on Windows, neither of which a bare ``["ruff", ...]``
|
|
59
|
+
spawn reliably reaches there.
|
|
60
|
+
"""
|
|
61
|
+
command = [
|
|
62
|
+
"ruff",
|
|
63
|
+
"check",
|
|
64
|
+
"--output-format=json",
|
|
65
|
+
f"--select={SELECTED_CODES}",
|
|
66
|
+
*files,
|
|
67
|
+
]
|
|
68
|
+
try:
|
|
69
|
+
return run_tool(command)
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
return subprocess.CompletedProcess(
|
|
72
|
+
command, 127, "", "ruff: command not found\n"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def ruff_crashed(result: subprocess.CompletedProcess[str]) -> bool:
|
|
77
|
+
return result.returncode not in TOOL_EXIT_CODES
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def violations(result: subprocess.CompletedProcess[str]) -> list[dict]:
|
|
81
|
+
text = result.stdout.strip()
|
|
82
|
+
return json.loads(text) if text else []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def issue(entry: dict) -> dict:
|
|
86
|
+
return {
|
|
87
|
+
"key": entry["filename"],
|
|
88
|
+
"details": {
|
|
89
|
+
"file": entry["filename"],
|
|
90
|
+
"line": entry["location"]["row"],
|
|
91
|
+
"column": entry["location"]["column"],
|
|
92
|
+
"message": entry["message"],
|
|
93
|
+
"source": "ruff:" + entry["code"],
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def findings(entries: list[dict]) -> list[dict]:
|
|
99
|
+
by_smell: dict[str, list[dict]] = {}
|
|
100
|
+
for entry in entries:
|
|
101
|
+
smell = CODE_SMELLS.get(entry["code"])
|
|
102
|
+
if smell is None:
|
|
103
|
+
continue
|
|
104
|
+
by_smell.setdefault(smell, []).append(entry)
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
"smell": smell,
|
|
108
|
+
"details": {},
|
|
109
|
+
"issues": [issue(entry) for entry in by_smell[smell]],
|
|
110
|
+
}
|
|
111
|
+
for smell in sorted(by_smell)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main() -> int:
|
|
116
|
+
files = sys.argv[1:]
|
|
117
|
+
result = run_ruff(files)
|
|
118
|
+
if ruff_crashed(result):
|
|
119
|
+
sys.stderr.write(result.stderr)
|
|
120
|
+
return 2
|
|
121
|
+
print(json.dumps(findings(violations(result))))
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
sys.exit(main())
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Spawn the third-party tool a sensor wraps, by the file rather than the name.
|
|
2
|
+
|
|
3
|
+
A local copy of what ``habit_hooks/sensors/spawn.py`` and its ``batch_shell`` do
|
|
4
|
+
for a part's own argv, and it is a copy on purpose: this plugin declares no
|
|
5
|
+
dependency on ``habit-hooks``, so a helper cannot import the core, and four
|
|
6
|
+
small copies beat a dependency that does not exist. The core's modules carry the
|
|
7
|
+
full reasoning; the two rules are Windows' own.
|
|
8
|
+
|
|
9
|
+
**A bare name is looked up first.** Windows' spawn appends ``.exe`` to a name and
|
|
10
|
+
nothing else, so the ``jscpd.CMD`` npm installs and the ``pmd.bat`` PMD ships are
|
|
11
|
+
found by a lookup and then unreachable by a spawn handed the name — the tool the
|
|
12
|
+
setup cleared, reported missing by the run. This is not a second answer to that
|
|
13
|
+
question: habit-hooks hands a helper a ``PATH`` of the project's own bins
|
|
14
|
+
(``sensors/spawn._path_env``), so ``shutil.which`` here asks exactly what cleared
|
|
15
|
+
the tool, along exactly the same path.
|
|
16
|
+
|
|
17
|
+
**A batch file's arguments are then ``cmd.exe``'s syntax.** ``CreateProcess``
|
|
18
|
+
runs a ``.bat`` or ``.cmd`` through that shell, which reads ``&``, ``|``, ``<``,
|
|
19
|
+
``>``, ``^``, ``"``, ``%VAR%`` and a newline as its own (CVE-2024-24576; CPython
|
|
20
|
+
closed its half in 3.11.9, and ``>=3.11`` is supported). A sensor's arguments are
|
|
21
|
+
paths out of the work tree, so one is refused rather than escaped: no tool anyone
|
|
22
|
+
wraps needs those characters, and refusing needs no interpreter version to be
|
|
23
|
+
right.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
|
|
31
|
+
BATCH_SUFFIXES = (".bat", ".cmd")
|
|
32
|
+
|
|
33
|
+
# Everything ``cmd.exe`` reads as other than text: the separators and pipe, both
|
|
34
|
+
# redirections, its own escape character, the quote that ends a quoted run, the
|
|
35
|
+
# ``%`` that opens a variable, and the newline that ends the line it is reading.
|
|
36
|
+
CMD_SYNTAX = frozenset('&|<>^"%\n\r')
|
|
37
|
+
|
|
38
|
+
# Found, and refused rather than run — the code a shell keeps for a command it
|
|
39
|
+
# located and would not execute, and outside every wrapped tool's success set.
|
|
40
|
+
REFUSED_EXIT = 126
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_tool(command: list[str]) -> subprocess.CompletedProcess[str]:
|
|
44
|
+
"""What ``command`` said, spawned as the file this project runs for its name.
|
|
45
|
+
|
|
46
|
+
A name reaching no file is spawned as it stands, so the caller's own
|
|
47
|
+
``FileNotFoundError`` remains the single answer for a tool nobody installed.
|
|
48
|
+
"""
|
|
49
|
+
program = shutil.which(command[0]) or command[0]
|
|
50
|
+
unreadable = cmd_syntax(program, command[1:])
|
|
51
|
+
if unreadable is not None:
|
|
52
|
+
return subprocess.CompletedProcess(
|
|
53
|
+
command,
|
|
54
|
+
REFUSED_EXIT,
|
|
55
|
+
"",
|
|
56
|
+
f"{program}: cannot pass {unreadable!r} to a batch file — cmd.exe "
|
|
57
|
+
"would read it as its own syntax rather than as text\n",
|
|
58
|
+
)
|
|
59
|
+
return subprocess.run(
|
|
60
|
+
[program, *command[1:]],
|
|
61
|
+
capture_output=True,
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
errors="replace", # sensors.spawn's policy
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_syntax(program: str, arguments: list[str]) -> str | None:
|
|
68
|
+
"""The first of ``arguments`` ``cmd.exe`` would read as syntax, if it reads any.
|
|
69
|
+
|
|
70
|
+
``None`` where ``program`` is not a batch file: nothing then stands between
|
|
71
|
+
the spawn and the program, and an ``&`` in a filename is a filename.
|
|
72
|
+
"""
|
|
73
|
+
if not program.lower().endswith(BATCH_SUFFIXES):
|
|
74
|
+
return None
|
|
75
|
+
return next(
|
|
76
|
+
(argument for argument in arguments if CMD_SYNTAX & set(argument)), None
|
|
77
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Load the ruff sensor the way it is actually run — as a loose script.
|
|
2
|
+
|
|
3
|
+
The sensor spec spells ``${python} ${dir}/ruff_sensor.py``, so the
|
|
4
|
+
interpreter puts the helper's own directory first on ``sys.path`` and a unit
|
|
5
|
+
test does the same rather than reaching the code as
|
|
6
|
+
``habit_hooks_python.sensors.ruff_sensor`` — a load path no run ever takes
|
|
7
|
+
(see "A plugin helper imports its neighbours as top-level modules" in
|
|
8
|
+
CLAUDE.md).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
SENSORS = (
|
|
17
|
+
Path(__file__).resolve().parents[1] / "src" / "habit_hooks_python" / "sensors"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(SENSORS))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""A broken ``ruff`` must not read as clean — it must fail the sensor.
|
|
2
|
+
|
|
3
|
+
``ruff_crashed`` trusts exactly ruff's own contract (0 clean, 1 violations
|
|
4
|
+
found); the boundary is exercised directly here, and
|
|
5
|
+
``test_a_broken_ruff_toml_fails_the_run`` proves the whole helper honours it
|
|
6
|
+
against the real tool, mirroring
|
|
7
|
+
``test_deptry_failing_another_way_still_fails_the_run``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from ruff_sensor import ruff_crashed
|
|
18
|
+
|
|
19
|
+
SENSOR = (
|
|
20
|
+
Path(__file__).resolve().parents[1]
|
|
21
|
+
/ "src/habit_hooks_python/sensors/ruff_sensor.py"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _result(returncode: int) -> subprocess.CompletedProcess[str]:
|
|
26
|
+
return subprocess.CompletedProcess(args=[], returncode=returncode, stdout="", stderr="")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize("returncode", [0, 1])
|
|
30
|
+
def test_ruff_s_own_exit_codes_are_trusted(returncode: int) -> None:
|
|
31
|
+
assert ruff_crashed(_result(returncode)) is False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize("returncode", [2, 127, -9])
|
|
35
|
+
def test_any_other_exit_code_is_a_crash(returncode: int) -> None:
|
|
36
|
+
assert ruff_crashed(_result(returncode)) is True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_a_broken_ruff_toml_fails_the_run(tmp_path: Path) -> None:
|
|
40
|
+
(tmp_path / "ruff.toml").write_text(
|
|
41
|
+
"this is not = valid ruff config\n", encoding="utf-8"
|
|
42
|
+
)
|
|
43
|
+
(tmp_path / "app.py").write_text("import os\n", encoding="utf-8")
|
|
44
|
+
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
[sys.executable, str(SENSOR), "app.py"],
|
|
47
|
+
cwd=tmp_path,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
errors="replace",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert result.returncode == 2
|
|
54
|
+
assert result.stdout.strip() == ""
|
|
55
|
+
assert "ruff.toml" in result.stderr
|
|
@@ -25,13 +25,14 @@ SENSOR = (
|
|
|
25
25
|
|
|
26
26
|
|
|
27
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")
|
|
28
|
+
(tmp_path / "app.py").write_text("import os\n", encoding="utf-8")
|
|
29
29
|
|
|
30
30
|
result = subprocess.run(
|
|
31
31
|
[sys.executable, str(SENSOR)],
|
|
32
32
|
cwd=tmp_path,
|
|
33
33
|
capture_output=True,
|
|
34
|
-
|
|
34
|
+
encoding="utf-8",
|
|
35
|
+
errors="replace",
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
assert result.returncode == 0
|
|
@@ -44,13 +45,14 @@ def test_deptry_still_answers_a_missing_declaration_with_that_error(
|
|
|
44
45
|
) -> None:
|
|
45
46
|
"""The sensor recognises deptry's exception by name, so a deptry that stops
|
|
46
47
|
raising it must fail here rather than silently retire that branch."""
|
|
47
|
-
(tmp_path / "app.py").write_text("import os\n")
|
|
48
|
+
(tmp_path / "app.py").write_text("import os\n", encoding="utf-8")
|
|
48
49
|
|
|
49
50
|
result = subprocess.run(
|
|
50
51
|
["deptry", "."],
|
|
51
52
|
cwd=tmp_path,
|
|
52
53
|
capture_output=True,
|
|
53
|
-
|
|
54
|
+
encoding="utf-8",
|
|
55
|
+
errors="replace",
|
|
54
56
|
)
|
|
55
57
|
|
|
56
58
|
assert "DependencySpecificationNotFoundError" in result.stderr
|
|
@@ -59,15 +61,17 @@ def test_deptry_still_answers_a_missing_declaration_with_that_error(
|
|
|
59
61
|
def test_deptry_failing_another_way_still_fails_the_run(tmp_path: Path) -> None:
|
|
60
62
|
(tmp_path / "pyproject.toml").write_text(
|
|
61
63
|
'[project]\nname = "demo"\nversion = "0.0.0"\ndependencies = []\n\n'
|
|
62
|
-
"[tool.deptry]\nnot_a_real_option = true\n"
|
|
64
|
+
"[tool.deptry]\nnot_a_real_option = true\n",
|
|
65
|
+
encoding="utf-8",
|
|
63
66
|
)
|
|
64
|
-
(tmp_path / "app.py").write_text("import os\n")
|
|
67
|
+
(tmp_path / "app.py").write_text("import os\n", encoding="utf-8")
|
|
65
68
|
|
|
66
69
|
result = subprocess.run(
|
|
67
70
|
[sys.executable, str(SENSOR)],
|
|
68
71
|
cwd=tmp_path,
|
|
69
72
|
capture_output=True,
|
|
70
|
-
|
|
73
|
+
encoding="utf-8",
|
|
74
|
+
errors="replace",
|
|
71
75
|
)
|
|
72
76
|
|
|
73
77
|
assert result.returncode == 2
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""A detector this plugin does not bring with it must still answer in one line.
|
|
2
|
+
|
|
3
|
+
``pip install habit-hooks-python`` installs neither ruff nor deptry, so a machine
|
|
4
|
+
that has just enabled the plugin is the ordinary case, not the edge one. ruff is
|
|
5
|
+
spawned from Python now (it used to be a shell recipe, so the shell answered for
|
|
6
|
+
it), and an absent tool is a ``FileNotFoundError`` — twenty lines of internals
|
|
7
|
+
would otherwise become the sensor's diagnosis (#114). Mirrors
|
|
8
|
+
``test_a_deptry_nobody_installed_is_named.py``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
SENSOR = (
|
|
18
|
+
Path(__file__).resolve().parents[1]
|
|
19
|
+
/ "src/habit_hooks_python/sensors/ruff_sensor.py"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_a_ruff_nobody_installed_answers_the_way_a_shell_does(tmp_path: Path) -> None:
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
[sys.executable, str(SENSOR)],
|
|
26
|
+
cwd=tmp_path,
|
|
27
|
+
capture_output=True,
|
|
28
|
+
encoding="utf-8",
|
|
29
|
+
errors="replace",
|
|
30
|
+
env={"PATH": "/nonexistent"},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert result.returncode == 2
|
|
34
|
+
assert result.stdout.strip() == ""
|
|
35
|
+
assert result.stderr.strip() == "ruff: command not found"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""``findings()`` reproduces the old ``ruff | jq`` pipeline exactly: map each
|
|
2
|
+
violation's ``code`` to a smell, group by smell (alphabetically, as jq's
|
|
3
|
+
``group_by`` sorts), and shape each group into the canonical finding.
|
|
4
|
+
|
|
5
|
+
These exercise the pure mapping logic with synthetic ruff-JSON-shaped entries
|
|
6
|
+
— the same style ``plugins/java/tests/test_class_level_metric_violations_are_dropped.py``
|
|
7
|
+
uses for ``pmd_sensor.findings`` — rather than spawning the real tool, which
|
|
8
|
+
``test_the_ruff_sensor_runs_the_real_tool.py`` does instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from ruff_sensor import CODE_SMELLS, findings, violations
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _entry(code: str, filename: str = "a.py", row: int = 1) -> dict:
|
|
20
|
+
return {
|
|
21
|
+
"code": code,
|
|
22
|
+
"filename": filename,
|
|
23
|
+
"location": {"row": row, "column": 1},
|
|
24
|
+
"message": "m",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_zero_entries_is_no_findings() -> None:
|
|
29
|
+
assert findings([]) == []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_one_entry_becomes_one_finding_with_one_issue() -> None:
|
|
33
|
+
entry = {
|
|
34
|
+
"code": "F401",
|
|
35
|
+
"filename": "a.py",
|
|
36
|
+
"location": {"row": 3, "column": 8},
|
|
37
|
+
"message": "`os` unused",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
assert findings([entry]) == [
|
|
41
|
+
{
|
|
42
|
+
"smell": "unused-import",
|
|
43
|
+
"details": {},
|
|
44
|
+
"issues": [
|
|
45
|
+
{
|
|
46
|
+
"key": "a.py",
|
|
47
|
+
"details": {
|
|
48
|
+
"file": "a.py",
|
|
49
|
+
"line": 3,
|
|
50
|
+
"column": 8,
|
|
51
|
+
"message": "`os` unused",
|
|
52
|
+
"source": "ruff:F401",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_many_entries_group_by_smell_sorted_alphabetically() -> None:
|
|
61
|
+
"""Insertion order is deliberately the reverse of the alphabetical group
|
|
62
|
+
order jq's ``group_by`` produces, so a test that merely preserved input
|
|
63
|
+
order would still pass unless the sort is checked."""
|
|
64
|
+
unused_variable = _entry("F841", filename="b.py")
|
|
65
|
+
high_complexity = _entry("C901", filename="a.py")
|
|
66
|
+
|
|
67
|
+
result = findings([unused_variable, high_complexity])
|
|
68
|
+
|
|
69
|
+
assert [finding["smell"] for finding in result] == [
|
|
70
|
+
"high-complexity",
|
|
71
|
+
"unused-variable",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_two_issues_for_the_same_smell_share_one_finding() -> None:
|
|
76
|
+
first = _entry("F401", filename="a.py", row=1)
|
|
77
|
+
second = _entry("F401", filename="a.py", row=2)
|
|
78
|
+
|
|
79
|
+
result = findings([first, second])
|
|
80
|
+
|
|
81
|
+
assert len(result) == 1
|
|
82
|
+
assert [issue["details"]["line"] for issue in result[0]["issues"]] == [1, 2]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.parametrize("code", sorted(CODE_SMELLS))
|
|
86
|
+
def test_every_mapped_code_reaches_its_own_smell(code: str) -> None:
|
|
87
|
+
result = findings([_entry(code)])
|
|
88
|
+
|
|
89
|
+
assert [finding["smell"] for finding in result] == [CODE_SMELLS[code]]
|
|
90
|
+
assert result[0]["issues"][0]["details"]["source"] == f"ruff:{code}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_a_code_with_no_smell_mapped_is_dropped_not_crashed() -> None:
|
|
94
|
+
"""A code outside the plugin's vocabulary has no guide and no catalogue
|
|
95
|
+
severity, so it is dropped rather than forwarded under ruff's own name —
|
|
96
|
+
see "A sensor emits vocabulary smells only" in CLAUDE.md. Indexing a dict
|
|
97
|
+
with ``.get`` rather than jq's ``{...}[.code]`` is what makes this safe by
|
|
98
|
+
construction instead of a crash (issue #83)."""
|
|
99
|
+
mapped = _entry("F401", filename="a.py")
|
|
100
|
+
unmapped = _entry("PLR9999", filename="b.py")
|
|
101
|
+
|
|
102
|
+
assert findings([unmapped]) == []
|
|
103
|
+
assert [f["smell"] for f in findings([mapped, unmapped])] == ["unused-import"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_empty_stdout_is_no_findings() -> None:
|
|
107
|
+
result = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
108
|
+
|
|
109
|
+
assert violations(result) == []
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""The helper wired to the real ``ruff``, mirroring
|
|
2
|
+
``test_a_project_declaring_no_dependencies_is_clean.py``'s real-tool style for
|
|
3
|
+
deptry. The pure mapping is covered by
|
|
4
|
+
``test_the_ruff_pipeline_maps_codes_to_smells.py``; this proves ruff's own
|
|
5
|
+
output still has the shape that mapping expects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
SENSOR = (
|
|
16
|
+
Path(__file__).resolve().parents[1]
|
|
17
|
+
/ "src/habit_hooks_python/sensors/ruff_sensor.py"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run(project: Path, *files: str) -> subprocess.CompletedProcess[str]:
|
|
22
|
+
return subprocess.run(
|
|
23
|
+
[sys.executable, str(SENSOR), *files],
|
|
24
|
+
cwd=project,
|
|
25
|
+
capture_output=True,
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
errors="replace",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_a_clean_file_is_a_clean_run(tmp_path: Path) -> None:
|
|
32
|
+
(tmp_path / "clean.py").write_text("def add(a, b):\n return a + b\n", encoding="utf-8")
|
|
33
|
+
|
|
34
|
+
result = _run(tmp_path, "clean.py")
|
|
35
|
+
|
|
36
|
+
assert result.returncode == 0
|
|
37
|
+
assert result.stderr == ""
|
|
38
|
+
assert json.loads(result.stdout) == []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_a_real_violation_comes_out_as_the_mapped_smell(tmp_path: Path) -> None:
|
|
42
|
+
(tmp_path / "billing.py").write_text(
|
|
43
|
+
"import os\n\n\ndef charge():\n unused = 1\n return 0\n",
|
|
44
|
+
encoding="utf-8",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
result = _run(tmp_path, "billing.py")
|
|
48
|
+
|
|
49
|
+
assert result.returncode == 0
|
|
50
|
+
findings = json.loads(result.stdout)
|
|
51
|
+
assert {finding["smell"] for finding in findings} == {
|
|
52
|
+
"unused-import",
|
|
53
|
+
"unused-variable",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_ruff_maps_a_syntax_error_to_parse_error(tmp_path: Path) -> None:
|
|
58
|
+
(tmp_path / "broken.py").write_text("def broken(:\n return 1\n", encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
result = _run(tmp_path, "broken.py")
|
|
61
|
+
|
|
62
|
+
assert result.returncode == 0
|
|
63
|
+
findings = json.loads(result.stdout)
|
|
64
|
+
assert [finding["smell"] for finding in findings] == ["parse-error"]
|
|
65
|
+
assert findings[0]["issues"][0]["details"]["source"] == "ruff:invalid-syntax"
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "habit-hooks-python"
|
|
3
|
-
version = "1.3.0"
|
|
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"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
command = "${python} ${dir}/deptry_sensor.py"
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|