claude-dev-env 2.2.1 → 2.4.0
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.
- package/CLAUDE.md +3 -2
- package/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
- package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +4 -4
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +24 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -810
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +118 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/CLAUDE.md +2 -1
- package/scripts/claude-chain.example.json +15 -3
- package/scripts/claude_chain_runner.py +130 -27
- package/scripts/claude_chain_usage.py +346 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -0
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/orchestrator/SKILL.md +1 -20
- package/skills/orchestrator-refresh/SKILL.md +1 -1
- package/skills/pr-converge/SKILL.md +3 -3
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +3 -3
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +5 -4
|
@@ -4,19 +4,33 @@ import contextlib
|
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
6
|
import tempfile
|
|
7
|
-
import tomllib
|
|
8
7
|
from collections.abc import Iterator
|
|
9
8
|
from dataclasses import dataclass
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
|
|
11
|
+
_validators_directory = str(Path(__file__).resolve().parent)
|
|
12
12
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
13
13
|
|
|
14
|
+
try:
|
|
15
|
+
from pyproject_config_discovery import (
|
|
16
|
+
ancestor_directories,
|
|
17
|
+
find_pyproject_configuring_tool,
|
|
18
|
+
)
|
|
19
|
+
except ModuleNotFoundError:
|
|
20
|
+
if _validators_directory not in sys.path:
|
|
21
|
+
sys.path.insert(0, _validators_directory)
|
|
22
|
+
from pyproject_config_discovery import (
|
|
23
|
+
ancestor_directories,
|
|
24
|
+
find_pyproject_configuring_tool,
|
|
25
|
+
)
|
|
26
|
+
|
|
14
27
|
try:
|
|
15
28
|
from hooks_constants.mypy_integration_constants import (
|
|
16
29
|
GIT_DIRECTORY_NAME,
|
|
17
30
|
PYPROJECT_FILENAME,
|
|
18
31
|
PYTHON_SOURCE_SUFFIX,
|
|
19
32
|
)
|
|
33
|
+
from hooks_constants.pyproject_config_discovery_constants import MYPY_TOOL_TABLE_NAME
|
|
20
34
|
except ModuleNotFoundError:
|
|
21
35
|
if _hooks_directory not in sys.path:
|
|
22
36
|
sys.path.insert(0, _hooks_directory)
|
|
@@ -25,6 +39,7 @@ except ModuleNotFoundError:
|
|
|
25
39
|
PYPROJECT_FILENAME,
|
|
26
40
|
PYTHON_SOURCE_SUFFIX,
|
|
27
41
|
)
|
|
42
|
+
from hooks_constants.pyproject_config_discovery_constants import MYPY_TOOL_TABLE_NAME
|
|
28
43
|
|
|
29
44
|
|
|
30
45
|
@dataclass
|
|
@@ -48,38 +63,6 @@ def check_mypy_available() -> bool:
|
|
|
48
63
|
return False
|
|
49
64
|
|
|
50
65
|
|
|
51
|
-
def _pyproject_contains_tool_mypy(pyproject_path: Path) -> bool:
|
|
52
|
-
try:
|
|
53
|
-
with pyproject_path.open("rb") as readable_handle:
|
|
54
|
-
parsed_toml = tomllib.load(readable_handle)
|
|
55
|
-
except (OSError, tomllib.TOMLDecodeError):
|
|
56
|
-
return False
|
|
57
|
-
tool_table = parsed_toml.get("tool", {})
|
|
58
|
-
return isinstance(tool_table, dict) and "mypy" in tool_table
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _ancestor_directories(starting_file: Path) -> list[Path]:
|
|
62
|
-
"""Return *starting_file*'s directory and every parent, nearest first.
|
|
63
|
-
|
|
64
|
-
::
|
|
65
|
-
|
|
66
|
-
repo/pkg/mod.py -> [repo/pkg, repo, ... , filesystem root]
|
|
67
|
-
repo/pkg/ -> [repo/pkg, repo, ... , filesystem root]
|
|
68
|
-
|
|
69
|
-
A file resolves to its containing directory; a directory resolves to
|
|
70
|
-
itself, so both ancestor walks below start from the same first candidate.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
starting_file: The file (or directory) the walk begins from.
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
The resolved starting directory followed by each of its parents.
|
|
77
|
-
"""
|
|
78
|
-
resolved_starting_file = starting_file.resolve()
|
|
79
|
-
walk_origin = resolved_starting_file.parent if resolved_starting_file.is_file() else resolved_starting_file
|
|
80
|
-
return [walk_origin, *walk_origin.parents]
|
|
81
|
-
|
|
82
|
-
|
|
83
66
|
def find_pyproject_with_mypy_config(starting_file: Path) -> Path | None:
|
|
84
67
|
"""Walk up from a starting file to locate a pyproject.toml that configures mypy.
|
|
85
68
|
|
|
@@ -97,12 +80,7 @@ def find_pyproject_with_mypy_config(starting_file: Path) -> Path | None:
|
|
|
97
80
|
table, or ``None`` when no such file exists between ``starting_file``
|
|
98
81
|
and the filesystem root.
|
|
99
82
|
"""
|
|
100
|
-
|
|
101
|
-
for each_candidate_directory in _ancestor_directories(starting_file):
|
|
102
|
-
candidate_pyproject = each_candidate_directory / pyproject_filename_for_lookup
|
|
103
|
-
if candidate_pyproject.is_file() and _pyproject_contains_tool_mypy(candidate_pyproject):
|
|
104
|
-
return candidate_pyproject
|
|
105
|
-
return None
|
|
83
|
+
return find_pyproject_configuring_tool(starting_file, MYPY_TOOL_TABLE_NAME)
|
|
106
84
|
|
|
107
85
|
|
|
108
86
|
def find_module_resolution_root(starting_file: Path) -> Path | None:
|
|
@@ -127,7 +105,7 @@ def find_module_resolution_root(starting_file: Path) -> Path | None:
|
|
|
127
105
|
"""
|
|
128
106
|
git_entry_name = GIT_DIRECTORY_NAME
|
|
129
107
|
pyproject_filename = PYPROJECT_FILENAME
|
|
130
|
-
for each_candidate_directory in
|
|
108
|
+
for each_candidate_directory in ancestor_directories(starting_file):
|
|
131
109
|
has_git_entry = (each_candidate_directory / git_entry_name).exists()
|
|
132
110
|
has_pyproject = (each_candidate_directory / pyproject_filename).is_file()
|
|
133
111
|
if has_git_entry or has_pyproject:
|
|
@@ -171,7 +149,7 @@ def mypy_working_directory(all_py_files: list[str]) -> Iterator[str]:
|
|
|
171
149
|
yield isolated_directory
|
|
172
150
|
|
|
173
151
|
|
|
174
|
-
def
|
|
152
|
+
def _native_mypy_config_argument(all_py_files: list[str]) -> list[str]:
|
|
175
153
|
"""Return the ``--config-file`` argument for the first file with a mypy config."""
|
|
176
154
|
for each_py_file in all_py_files:
|
|
177
155
|
discovered_pyproject = find_pyproject_with_mypy_config(Path(each_py_file))
|
|
@@ -180,9 +158,39 @@ def _mypy_config_argument(all_py_files: list[str]) -> list[str]:
|
|
|
180
158
|
return []
|
|
181
159
|
|
|
182
160
|
|
|
183
|
-
def
|
|
161
|
+
def _mypy_config_argument(
|
|
162
|
+
all_py_files: list[str], config_source_path: Path | None
|
|
163
|
+
) -> list[str]:
|
|
164
|
+
"""Return the ``--config-file`` argument, resolved from the original path when given.
|
|
165
|
+
|
|
166
|
+
::
|
|
167
|
+
|
|
168
|
+
config_source_path resolves .../hooks/pyproject.toml
|
|
169
|
+
-> ["--config-file", ".../hooks/pyproject.toml"]
|
|
170
|
+
config_source_path given, no [tool.mypy] up-tree -> [] (native discovery)
|
|
171
|
+
config_source_path None -> native per-file discovery over all_py_files
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
all_py_files: The resolved paths mypy will check.
|
|
175
|
+
config_source_path: The original target path the staged copy stands in
|
|
176
|
+
for, or ``None`` for a native multi-file run.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The ``--config-file`` argument vector, empty when no config resolves.
|
|
180
|
+
"""
|
|
181
|
+
if config_source_path is None:
|
|
182
|
+
return _native_mypy_config_argument(all_py_files)
|
|
183
|
+
resolved_pyproject = find_pyproject_with_mypy_config(config_source_path)
|
|
184
|
+
if resolved_pyproject is not None:
|
|
185
|
+
return ["--config-file", str(resolved_pyproject)]
|
|
186
|
+
return _native_mypy_config_argument(all_py_files)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run_mypy_subprocess(
|
|
190
|
+
all_py_files: list[str], config_source_path: Path | None
|
|
191
|
+
) -> subprocess.CompletedProcess[str]:
|
|
184
192
|
"""Run mypy over *all_py_files* from each file's own project root."""
|
|
185
|
-
config_argument = _mypy_config_argument(all_py_files)
|
|
193
|
+
config_argument = _mypy_config_argument(all_py_files, config_source_path)
|
|
186
194
|
with mypy_working_directory(all_py_files) as working_directory:
|
|
187
195
|
return subprocess.run(
|
|
188
196
|
["mypy", *config_argument, "--ignore-missing-imports", "--no-error-summary"]
|
|
@@ -194,24 +202,29 @@ def _run_mypy_subprocess(all_py_files: list[str]) -> subprocess.CompletedProcess
|
|
|
194
202
|
)
|
|
195
203
|
|
|
196
204
|
|
|
197
|
-
def run_mypy_check(
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
def run_mypy_check(
|
|
206
|
+
all_files: list[Path], config_source_path: Path | None = None
|
|
207
|
+
) -> MypyResult:
|
|
208
|
+
"""Run mypy on files, resolving config from *config_source_path* when given.
|
|
209
|
+
|
|
210
|
+
A given ``config_source_path`` walks ``--config-file`` up from the original
|
|
211
|
+
target rather than the staged copy's own ancestors.
|
|
212
|
+
"""
|
|
213
|
+
if not all_files:
|
|
200
214
|
return MypyResult(passed=True, output="No files to check", error_count=0)
|
|
201
215
|
|
|
202
216
|
if not check_mypy_available():
|
|
203
217
|
return MypyResult(passed=True, output="Mypy not installed - skipping", error_count=0)
|
|
204
218
|
|
|
205
|
-
python_source_suffix = PYTHON_SOURCE_SUFFIX
|
|
206
219
|
all_py_files = [
|
|
207
220
|
str(each_file.resolve())
|
|
208
|
-
for each_file in
|
|
209
|
-
if each_file.suffix ==
|
|
221
|
+
for each_file in all_files
|
|
222
|
+
if each_file.suffix == PYTHON_SOURCE_SUFFIX
|
|
210
223
|
]
|
|
211
224
|
if not all_py_files:
|
|
212
225
|
return MypyResult(passed=True, output="No Python files", error_count=0)
|
|
213
226
|
|
|
214
|
-
completed_process = _run_mypy_subprocess(all_py_files)
|
|
227
|
+
completed_process = _run_mypy_subprocess(all_py_files, config_source_path)
|
|
215
228
|
error_count = completed_process.stdout.count(": error:")
|
|
216
229
|
|
|
217
230
|
return MypyResult(
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Resolve a validator's config file by walking up from an original path.
|
|
2
|
+
|
|
3
|
+
A staged temp copy has no config in its own ancestors, so ruff and mypy would
|
|
4
|
+
discover nothing there. This walk starts from the ORIGINAL target path and
|
|
5
|
+
returns the first pyproject.toml whose ``[tool.<name>]`` table exists.
|
|
6
|
+
|
|
7
|
+
::
|
|
8
|
+
|
|
9
|
+
tool_table_name="ruff", start .../hooks/validators/module.py
|
|
10
|
+
-> climbs to .../hooks/pyproject.toml, which declares [tool.ruff] -> that path
|
|
11
|
+
tool_table_name="ruff", start .../advisor/scripts/module.py
|
|
12
|
+
-> that pyproject declares [tool.mypy] only, no [tool.ruff] -> None
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
import tomllib
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from hooks_constants.mypy_integration_constants import PYPROJECT_FILENAME
|
|
23
|
+
from hooks_constants.pyproject_config_discovery_constants import TOOL_TABLE_KEY
|
|
24
|
+
except ModuleNotFoundError:
|
|
25
|
+
if _hooks_directory not in sys.path:
|
|
26
|
+
sys.path.insert(0, _hooks_directory)
|
|
27
|
+
from hooks_constants.mypy_integration_constants import PYPROJECT_FILENAME
|
|
28
|
+
from hooks_constants.pyproject_config_discovery_constants import TOOL_TABLE_KEY
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ancestor_directories(starting_file: Path) -> list[Path]:
|
|
32
|
+
"""Return *starting_file*'s directory and every parent, nearest first.
|
|
33
|
+
|
|
34
|
+
::
|
|
35
|
+
|
|
36
|
+
repo/pkg/mod.py -> [repo/pkg, repo, ... , filesystem root]
|
|
37
|
+
repo/pkg/ -> [repo/pkg, repo, ... , filesystem root]
|
|
38
|
+
|
|
39
|
+
A file resolves to its containing directory; a directory resolves to itself,
|
|
40
|
+
so both callers start the walk from the same first candidate.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
starting_file: The file (or directory) the walk begins from.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The resolved starting directory followed by each of its parents.
|
|
47
|
+
"""
|
|
48
|
+
resolved_starting_file = starting_file.resolve()
|
|
49
|
+
walk_origin = (
|
|
50
|
+
resolved_starting_file.parent
|
|
51
|
+
if resolved_starting_file.is_file()
|
|
52
|
+
else resolved_starting_file
|
|
53
|
+
)
|
|
54
|
+
return [walk_origin, *walk_origin.parents]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _pyproject_configures_tool(pyproject_path: Path, tool_table_name: str) -> bool:
|
|
58
|
+
"""Return whether *pyproject_path* declares a ``[tool.<tool_table_name>]`` table.
|
|
59
|
+
|
|
60
|
+
A malformed or unreadable file counts as no match, so a broken pyproject
|
|
61
|
+
never shadows a valid config further up the tree.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
pyproject_path: The candidate pyproject.toml to parse.
|
|
65
|
+
tool_table_name: The tool's table name below ``[tool]`` (for example ``ruff``).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True when the parsed file holds ``[tool.<tool_table_name>]``, else False.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
with pyproject_path.open("rb") as readable_handle:
|
|
72
|
+
parsed_toml = tomllib.load(readable_handle)
|
|
73
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
74
|
+
return False
|
|
75
|
+
tool_table = parsed_toml.get(TOOL_TABLE_KEY, {})
|
|
76
|
+
return isinstance(tool_table, dict) and tool_table_name in tool_table
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def find_pyproject_configuring_tool(starting_file: Path, tool_table_name: str) -> Path | None:
|
|
80
|
+
"""Walk up from *starting_file* to the first pyproject.toml configuring a tool.
|
|
81
|
+
|
|
82
|
+
The walk skips a pyproject.toml that does not declare the tool's table, so an
|
|
83
|
+
unrelated package config does not shadow a config that actually configures
|
|
84
|
+
the tool further up the tree.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
starting_file: The file (or directory) the walk begins from — the
|
|
88
|
+
original target path, not the staged temp copy.
|
|
89
|
+
tool_table_name: The tool's table name below ``[tool]`` (for example ``ruff``).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The first ``pyproject.toml`` Path that declares ``[tool.<tool_table_name>]``,
|
|
93
|
+
or ``None`` when no such file exists up to the filesystem root.
|
|
94
|
+
"""
|
|
95
|
+
for each_candidate_directory in ancestor_directories(starting_file):
|
|
96
|
+
candidate_pyproject = each_candidate_directory / PYPROJECT_FILENAME
|
|
97
|
+
if candidate_pyproject.is_file() and _pyproject_configures_tool(
|
|
98
|
+
candidate_pyproject, tool_table_name
|
|
99
|
+
):
|
|
100
|
+
return candidate_pyproject
|
|
101
|
+
return None
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
"""Ruff integration for fast Python linting."""
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
|
+
import sys
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
8
|
+
_validators_directory = str(Path(__file__).resolve().parent)
|
|
9
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from pyproject_config_discovery import find_pyproject_configuring_tool
|
|
13
|
+
except ModuleNotFoundError:
|
|
14
|
+
if _validators_directory not in sys.path:
|
|
15
|
+
sys.path.insert(0, _validators_directory)
|
|
16
|
+
from pyproject_config_discovery import find_pyproject_configuring_tool
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from hooks_constants.mypy_integration_constants import PYTHON_SOURCE_SUFFIX
|
|
20
|
+
from hooks_constants.pyproject_config_discovery_constants import RUFF_TOOL_TABLE_NAME
|
|
21
|
+
except ModuleNotFoundError:
|
|
22
|
+
if _hooks_directory not in sys.path:
|
|
23
|
+
sys.path.insert(0, _hooks_directory)
|
|
24
|
+
from hooks_constants.mypy_integration_constants import PYTHON_SOURCE_SUFFIX
|
|
25
|
+
from hooks_constants.pyproject_config_discovery_constants import RUFF_TOOL_TABLE_NAME
|
|
26
|
+
|
|
7
27
|
|
|
8
28
|
@dataclass
|
|
9
29
|
class RuffResult:
|
|
@@ -26,26 +46,165 @@ def check_ruff_available() -> bool:
|
|
|
26
46
|
return False
|
|
27
47
|
|
|
28
48
|
|
|
29
|
-
def
|
|
30
|
-
"""
|
|
31
|
-
if not files:
|
|
32
|
-
return RuffResult(passed=True, output="No files to check", fixed_count=0)
|
|
49
|
+
def find_pyproject_with_ruff_config(starting_file: Path) -> Path | None:
|
|
50
|
+
"""Walk up from a starting file to a pyproject.toml that configures ruff.
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
The walk skips a pyproject.toml with no [tool.ruff] table, so a mypy-only
|
|
53
|
+
config is not handed to ``ruff --config``, which errors on the missing table.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
starting_file: The file (or directory) the walk begins from.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The first ``pyproject.toml`` Path declaring a ``[tool.ruff]`` table, or
|
|
60
|
+
``None`` when none exists up to the filesystem root.
|
|
61
|
+
"""
|
|
62
|
+
return find_pyproject_configuring_tool(starting_file, RUFF_TOOL_TABLE_NAME)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ruff_config_argument(config_source_path: Path | None) -> list[str]:
|
|
66
|
+
"""Return the ``--config`` argument resolved from the original path, or empty.
|
|
67
|
+
|
|
68
|
+
::
|
|
69
|
+
|
|
70
|
+
config_source_path resolves .../hooks/pyproject.toml
|
|
71
|
+
-> ["--config", ".../hooks/pyproject.toml"]
|
|
72
|
+
config_source_path given, no [tool.ruff] up-tree -> [] (native discovery)
|
|
73
|
+
config_source_path None -> [] (native discovery)
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
config_source_path: The original target path the staged copy stands in
|
|
77
|
+
for, or ``None`` for a native run.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The ``--config`` argument vector, empty when no ruff config resolves.
|
|
81
|
+
"""
|
|
82
|
+
if config_source_path is None:
|
|
83
|
+
return []
|
|
84
|
+
resolved_pyproject = find_pyproject_with_ruff_config(config_source_path)
|
|
85
|
+
if resolved_pyproject is None:
|
|
86
|
+
return []
|
|
87
|
+
return ["--config", str(resolved_pyproject)]
|
|
36
88
|
|
|
37
|
-
py_files = [str(f) for f in files if f.suffix == ".py"]
|
|
38
|
-
if not py_files:
|
|
39
|
-
return RuffResult(passed=True, output="No Python files", fixed_count=0)
|
|
40
89
|
|
|
90
|
+
def _config_relative_stdin_filename(target_path: Path, config_directory: Path) -> str:
|
|
91
|
+
"""Return *target_path* expressed relative to the ruff config directory.
|
|
92
|
+
|
|
93
|
+
::
|
|
94
|
+
|
|
95
|
+
target .../hooks/validators/run_all_validators.py, config dir .../hooks
|
|
96
|
+
-> "validators/run_all_validators.py" (path-scoped ignore matches)
|
|
97
|
+
target off-tree from the config dir -> the bare basename
|
|
98
|
+
|
|
99
|
+
Ruff matches ``[tool.ruff.lint.per-file-ignores]`` keys against the path a
|
|
100
|
+
file presents relative to ruff's working directory, so a path-scoped key
|
|
101
|
+
matches only when the staged copy is graded under that same relative name.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
target_path: The real target path the staged copy stands in for.
|
|
105
|
+
config_directory: The directory holding the resolved ruff pyproject.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The config-relative POSIX path when the target sits under the config
|
|
109
|
+
directory, else the target's basename.
|
|
110
|
+
"""
|
|
111
|
+
resolved_target = target_path.resolve()
|
|
112
|
+
try:
|
|
113
|
+
return resolved_target.relative_to(config_directory).as_posix()
|
|
114
|
+
except ValueError:
|
|
115
|
+
return resolved_target.name
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _read_staged_content(staged_file: Path) -> str | None:
|
|
119
|
+
"""Return the staged copy's text content, or None when it cannot be read."""
|
|
120
|
+
try:
|
|
121
|
+
return staged_file.read_text(encoding="utf-8")
|
|
122
|
+
except OSError:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _single_staged_python_file(all_files: list[Path]) -> Path | None:
|
|
127
|
+
"""Return the sole staged Python file, or None unless there is exactly one."""
|
|
128
|
+
all_python_files = [
|
|
129
|
+
each_file for each_file in all_files if each_file.suffix == PYTHON_SOURCE_SUFFIX
|
|
130
|
+
]
|
|
131
|
+
if len(all_python_files) != 1:
|
|
132
|
+
return None
|
|
133
|
+
return all_python_files[0]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _staged_ruff_command(resolved_pyproject: Path, stdin_filename: str) -> list[str]:
|
|
137
|
+
"""Return the ruff argv that grades stdin under a config-relative name."""
|
|
41
138
|
concise_output_arguments = ["--output-format", "concise"]
|
|
139
|
+
return [
|
|
140
|
+
"ruff",
|
|
141
|
+
"check",
|
|
142
|
+
"--config",
|
|
143
|
+
str(resolved_pyproject),
|
|
144
|
+
*concise_output_arguments,
|
|
145
|
+
"--stdin-filename",
|
|
146
|
+
stdin_filename,
|
|
147
|
+
"-",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _staged_ruff_result(
|
|
152
|
+
resolved_pyproject: Path, stdin_filename: str, staged_content: str
|
|
153
|
+
) -> RuffResult:
|
|
154
|
+
"""Pipe *staged_content* to ruff from the config directory and wrap the result."""
|
|
42
155
|
result = subprocess.run(
|
|
43
|
-
|
|
156
|
+
_staged_ruff_command(resolved_pyproject, stdin_filename),
|
|
44
157
|
check=False,
|
|
45
158
|
capture_output=True,
|
|
46
159
|
text=True,
|
|
160
|
+
input=staged_content,
|
|
161
|
+
cwd=str(resolved_pyproject.parent),
|
|
47
162
|
)
|
|
163
|
+
return RuffResult(
|
|
164
|
+
passed=result.returncode == 0,
|
|
165
|
+
output=result.stdout or result.stderr or "No issues found",
|
|
166
|
+
fixed_count=0,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _run_staged_ruff_check(
|
|
171
|
+
all_files: list[Path], config_source_path: Path
|
|
172
|
+
) -> RuffResult | None:
|
|
173
|
+
"""Grade one staged file under the config resolved from its original path.
|
|
174
|
+
|
|
175
|
+
The staged content is piped to ruff under ``--stdin-filename`` set to the
|
|
176
|
+
original target's config-relative name, so ruff applies path-scoped
|
|
177
|
+
per-file-ignores that never match the temporary copy's own location.
|
|
178
|
+
Returns None when the staged shape does not apply: no resolvable ruff
|
|
179
|
+
config, not exactly one Python file, or an unreadable staged copy.
|
|
180
|
+
"""
|
|
181
|
+
resolved_pyproject = find_pyproject_with_ruff_config(config_source_path)
|
|
182
|
+
if resolved_pyproject is None:
|
|
183
|
+
return None
|
|
184
|
+
staged_file = _single_staged_python_file(all_files)
|
|
185
|
+
if staged_file is None:
|
|
186
|
+
return None
|
|
187
|
+
staged_content = _read_staged_content(staged_file)
|
|
188
|
+
if staged_content is None:
|
|
189
|
+
return None
|
|
190
|
+
stdin_filename = _config_relative_stdin_filename(
|
|
191
|
+
config_source_path, resolved_pyproject.parent
|
|
192
|
+
)
|
|
193
|
+
return _staged_ruff_result(resolved_pyproject, stdin_filename, staged_content)
|
|
194
|
+
|
|
48
195
|
|
|
196
|
+
def _run_native_ruff_check(
|
|
197
|
+
all_python_files: list[str], config_source_path: Path | None
|
|
198
|
+
) -> RuffResult:
|
|
199
|
+
"""Run ruff over *all_python_files* directly, resolving ``--config`` from the path."""
|
|
200
|
+
config_argument = _ruff_config_argument(config_source_path)
|
|
201
|
+
concise_output_arguments = ["--output-format", "concise"]
|
|
202
|
+
result = subprocess.run(
|
|
203
|
+
["ruff", "check", *config_argument, *concise_output_arguments] + all_python_files,
|
|
204
|
+
check=False,
|
|
205
|
+
capture_output=True,
|
|
206
|
+
text=True,
|
|
207
|
+
)
|
|
49
208
|
return RuffResult(
|
|
50
209
|
passed=result.returncode == 0,
|
|
51
210
|
output=result.stdout or result.stderr or "No issues found",
|
|
@@ -53,32 +212,61 @@ def run_ruff_check(files: list[Path]) -> RuffResult:
|
|
|
53
212
|
)
|
|
54
213
|
|
|
55
214
|
|
|
215
|
+
def run_ruff_check(
|
|
216
|
+
all_files: list[Path], config_source_path: Path | None = None
|
|
217
|
+
) -> RuffResult:
|
|
218
|
+
"""Run ruff check on *all_files*, resolving config from *config_source_path*.
|
|
219
|
+
|
|
220
|
+
A given ``config_source_path`` grades the single staged copy under the project
|
|
221
|
+
config resolved from the original target, applying its path-scoped
|
|
222
|
+
per-file-ignores. Without it, the files are checked natively."""
|
|
223
|
+
if not all_files:
|
|
224
|
+
return RuffResult(passed=True, output="No files to check", fixed_count=0)
|
|
225
|
+
|
|
226
|
+
if not check_ruff_available():
|
|
227
|
+
return RuffResult(passed=True, output="Ruff not installed - skipping", fixed_count=0)
|
|
228
|
+
|
|
229
|
+
all_python_files = [
|
|
230
|
+
str(each_file) for each_file in all_files if each_file.suffix == PYTHON_SOURCE_SUFFIX
|
|
231
|
+
]
|
|
232
|
+
if not all_python_files:
|
|
233
|
+
return RuffResult(passed=True, output="No Python files", fixed_count=0)
|
|
234
|
+
if config_source_path is not None:
|
|
235
|
+
staged_result = _run_staged_ruff_check(all_files, config_source_path)
|
|
236
|
+
if staged_result is not None:
|
|
237
|
+
return staged_result
|
|
238
|
+
return _run_native_ruff_check(all_python_files, config_source_path)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _parse_fixed_count(fix_output: str) -> int:
|
|
242
|
+
"""Return the count ruff reports on its ``Fixed N`` line, or 0 when absent."""
|
|
243
|
+
for each_line in fix_output.split("\n"):
|
|
244
|
+
if "Fixed" not in each_line:
|
|
245
|
+
continue
|
|
246
|
+
try:
|
|
247
|
+
return int(each_line.split()[1])
|
|
248
|
+
except (IndexError, ValueError):
|
|
249
|
+
return 0
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
|
|
56
253
|
def run_ruff_fix(files: list[Path]) -> RuffResult:
|
|
57
254
|
"""Run ruff with --fix to auto-fix violations."""
|
|
58
255
|
if not check_ruff_available():
|
|
59
256
|
return RuffResult(passed=True, output="Ruff not installed", fixed_count=0)
|
|
60
|
-
|
|
61
|
-
|
|
257
|
+
py_files = [
|
|
258
|
+
str(each_file) for each_file in files if each_file.suffix == PYTHON_SOURCE_SUFFIX
|
|
259
|
+
]
|
|
62
260
|
if not py_files:
|
|
63
261
|
return RuffResult(passed=True, output="No Python files", fixed_count=0)
|
|
64
|
-
|
|
65
262
|
result = subprocess.run(
|
|
66
263
|
["ruff", "check", "--fix"] + py_files,
|
|
67
264
|
check=False,
|
|
68
265
|
capture_output=True,
|
|
69
266
|
text=True,
|
|
70
267
|
)
|
|
71
|
-
|
|
72
|
-
fixed_count = 0
|
|
73
|
-
for line in result.stdout.split("\n"):
|
|
74
|
-
if "Fixed" in line:
|
|
75
|
-
try:
|
|
76
|
-
fixed_count = int(line.split()[1])
|
|
77
|
-
except (IndexError, ValueError):
|
|
78
|
-
pass
|
|
79
|
-
|
|
80
268
|
return RuffResult(
|
|
81
269
|
passed=result.returncode == 0,
|
|
82
270
|
output=result.stdout or "No fixes applied",
|
|
83
|
-
fixed_count=
|
|
271
|
+
fixed_count=_parse_fixed_count(result.stdout),
|
|
84
272
|
)
|
|
@@ -413,7 +413,9 @@ def run_comment_checks(files: List[Path]) -> ValidatorResult:
|
|
|
413
413
|
)
|
|
414
414
|
|
|
415
415
|
|
|
416
|
-
def run_ruff_checks(
|
|
416
|
+
def run_ruff_checks(
|
|
417
|
+
files: List[Path], config_source_path: Optional[Path] = None
|
|
418
|
+
) -> ValidatorResult:
|
|
417
419
|
"""Run ruff for fast Python linting."""
|
|
418
420
|
if not check_ruff_available():
|
|
419
421
|
return ValidatorResult(
|
|
@@ -423,7 +425,7 @@ def run_ruff_checks(files: List[Path]) -> ValidatorResult:
|
|
|
423
425
|
output="Ruff not installed - skipping",
|
|
424
426
|
)
|
|
425
427
|
|
|
426
|
-
result = run_ruff_check(files)
|
|
428
|
+
result = run_ruff_check(files, config_source_path)
|
|
427
429
|
|
|
428
430
|
return ValidatorResult(
|
|
429
431
|
name="Ruff",
|
|
@@ -433,7 +435,9 @@ def run_ruff_checks(files: List[Path]) -> ValidatorResult:
|
|
|
433
435
|
)
|
|
434
436
|
|
|
435
437
|
|
|
436
|
-
def run_mypy_checks(
|
|
438
|
+
def run_mypy_checks(
|
|
439
|
+
files: List[Path], config_source_path: Optional[Path] = None
|
|
440
|
+
) -> ValidatorResult:
|
|
437
441
|
"""Run mypy for static type checking."""
|
|
438
442
|
if not check_mypy_available():
|
|
439
443
|
return ValidatorResult(
|
|
@@ -443,7 +447,7 @@ def run_mypy_checks(files: List[Path]) -> ValidatorResult:
|
|
|
443
447
|
output="Mypy not installed - skipping",
|
|
444
448
|
)
|
|
445
449
|
|
|
446
|
-
result = run_mypy_check(files)
|
|
450
|
+
result = run_mypy_check(files, config_source_path)
|
|
447
451
|
|
|
448
452
|
return ValidatorResult(
|
|
449
453
|
name="Mypy",
|
|
@@ -727,15 +731,16 @@ def reconstruct_proposed_content(
|
|
|
727
731
|
return apply_edits(existing_content, edits_for_tool(tool_name, dict(tool_input)))
|
|
728
732
|
|
|
729
733
|
|
|
730
|
-
def run_file_scoped_validators(
|
|
731
|
-
|
|
734
|
+
def run_file_scoped_validators(
|
|
735
|
+
all_files: List[Path], config_source_path: Optional[Path] = None
|
|
736
|
+
) -> List[ValidatorResult]:
|
|
737
|
+
"""Run every file-scoped validator against *all_files*.
|
|
732
738
|
|
|
733
|
-
Excludes the branch-scoped File Structure and Git validators
|
|
734
|
-
the whole project rather than a single proposed file.
|
|
739
|
+
Excludes the branch-scoped File Structure and Git validators.
|
|
735
740
|
|
|
736
741
|
Args:
|
|
737
|
-
all_files: The files under validation —
|
|
738
|
-
|
|
742
|
+
all_files: The files under validation — one reconstructed file in gate mode.
|
|
743
|
+
config_source_path: Original path ruff and mypy resolve their config from.
|
|
739
744
|
|
|
740
745
|
Returns:
|
|
741
746
|
One ValidatorResult per file-scoped validator, in run order.
|
|
@@ -744,8 +749,8 @@ def run_file_scoped_validators(all_files: List[Path]) -> List[ValidatorResult]:
|
|
|
744
749
|
run_python_style_checks(all_files),
|
|
745
750
|
run_test_safety_checks(all_files),
|
|
746
751
|
run_react_checks(all_files),
|
|
747
|
-
run_ruff_checks(all_files),
|
|
748
|
-
run_mypy_checks(all_files),
|
|
752
|
+
run_ruff_checks(all_files, config_source_path),
|
|
753
|
+
run_mypy_checks(all_files, config_source_path),
|
|
749
754
|
run_abbreviation_checks(all_files),
|
|
750
755
|
run_pr_reference_checks(all_files),
|
|
751
756
|
run_magic_value_checks(all_files),
|
|
@@ -765,7 +770,9 @@ def validate_proposed_file(
|
|
|
765
770
|
|
|
766
771
|
Writes the content to a temporary file that carries the target's basename so
|
|
767
772
|
suffix-based and test-name-based validator filtering matches the real path,
|
|
768
|
-
then runs the file-scoped validators against it.
|
|
773
|
+
then runs the file-scoped validators against it. Ruff and mypy resolve their
|
|
774
|
+
config by walking up from *file_path*, so the staged copy is graded under the
|
|
775
|
+
project config the real path sits in rather than the temp directory's.
|
|
769
776
|
|
|
770
777
|
Args:
|
|
771
778
|
file_path: The destination path the write or edit targets.
|
|
@@ -778,7 +785,7 @@ def validate_proposed_file(
|
|
|
778
785
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
779
786
|
temporary_file = Path(temporary_directory) / base_name
|
|
780
787
|
temporary_file.write_text(proposed_content, encoding="utf-8")
|
|
781
|
-
return run_file_scoped_validators([temporary_file])
|
|
788
|
+
return run_file_scoped_validators([temporary_file], Path(file_path))
|
|
782
789
|
|
|
783
790
|
|
|
784
791
|
def _validator_summaries(results: List[ValidatorResult]) -> str:
|