claude-dev-env 2.18.0 → 2.20.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/.agents/skills/AGENTS.md +4 -4
- package/.agents/skills/_shared/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +119 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +5 -0
- package/.agents/skills/e-code-review/SKILL.md +12 -1
- package/.agents/skills/e-code-review/reference/fix.md +5 -1
- package/.agents/skills/e-code-review/reference/loop.md +4 -0
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
- package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
- package/.agents/skills/pr-cleanup/SKILL.md +105 -75
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +15 -0
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +20 -0
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/SKILL.md +18 -12
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/offense-examples.md +1 -1
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/task-seeds.md +3 -2
- package/.agents/skills/pr-refinement/SKILL.md +31 -0
- package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/SKILL.md +24 -7
- package/.agents/skills/{small-cl → pr-small-cl}/SKILL.md +1 -1
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +33 -5
- package/.agents/skills/skill-builder/SKILL.md +1 -0
- package/.agents/skills-archived/bugteam/SKILL.md +1 -1
- package/.agents/skills-archived/bugteam/test_skill_additions.py +1 -1
- package/.agents/skills-archived/comments/SKILL.md +1 -1
- package/.agents/skills-archived/descriptions/SKILL.md +1 -1
- package/.agents/skills-archived/reviews/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/pr-loop/AGENTS.md +2 -1
- package/_shared/pr-loop/preflight-proposal.md +56 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +47 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
- package/bin/ever-shipped-skills.mjs +14 -0
- package/bin/install-constants.mjs +5 -0
- package/bin/install.agents-home.test.mjs +23 -0
- package/bin/install.codex-rules.test.mjs +61 -6
- package/bin/install.mjs +57 -9
- package/bin/install.prune.test.mjs +65 -3
- package/bin/install.test.mjs +202 -2
- package/docs/CODE_RULES.md +6 -0
- package/hooks/advisory/conftest.py +10 -0
- package/hooks/advisory/migration_safety_advisor.py +14 -14
- package/hooks/advisory/refactor_guard.py +250 -144
- package/hooks/advisory/refactor_guard_test_support.py +46 -0
- package/hooks/advisory/test_migration_safety_advisor.py +120 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
- package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
- package/hooks/atomic_file_writer.py +81 -0
- package/hooks/blocking/AGENTS.md +2 -1
- package/hooks/blocking/block_main_commit.py +66 -33
- package/hooks/blocking/code_rules_banned_identifiers.py +55 -4
- package/hooks/blocking/code_rules_blast_radius.py +194 -0
- package/hooks/blocking/code_rules_enforcer.py +12 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +92 -0
- package/hooks/blocking/destructive_command_blocker.py +8 -7
- package/hooks/blocking/pre_tool_use_dispatcher.py +11 -7
- package/hooks/blocking/precommit_code_rules_gate.py +14 -139
- package/hooks/blocking/sensitive_file_protector.py +8 -6
- package/hooks/blocking/session_edit_stage_gate.py +31 -1
- package/hooks/blocking/test_block_main_commit.py +145 -0
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +171 -1
- package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +37 -12
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +54 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +15 -53
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +13 -62
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +309 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +14 -71
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +14 -16
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +31 -46
- package/hooks/blocking/test_code_rules_enforcer_stage.py +192 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +13 -59
- package/hooks/blocking/test_destructive_command_blocker.py +154 -138
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +85 -25
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +58 -38
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +113 -1
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +70 -3
- package/hooks/blocking/test_hook_subprocess_support.py +296 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +1 -1
- package/hooks/blocking/test_pii_scanner.py +81 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +32 -3
- package/hooks/blocking/test_precommit_code_rules_gate.py +11 -16
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +234 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +25 -18
- package/hooks/blocking/test_sensitive_file_protector.py +145 -5
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +64 -0
- package/hooks/blocking/test_session_edit_stage_gate_support.py +204 -0
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +116 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +31 -20
- package/hooks/blocking/test_test_preflight_check.py +80 -0
- package/hooks/git-hooks/AGENTS.md +1 -1
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
- package/hooks/git-hooks/post_commit.py +160 -51
- package/hooks/git-hooks/pre_commit.py +9 -5
- package/hooks/git-hooks/test_post_commit.py +203 -0
- package/hooks/git-hooks/test_pre_commit.py +2 -2
- package/hooks/git-hooks/test_pre_push.py +57 -0
- package/hooks/hooks_constants/AGENTS.md +3 -1
- package/hooks/hooks_constants/atomic_file_writer_constants.py +4 -0
- package/hooks/hooks_constants/banned_identifiers_constants.py +1 -0
- package/hooks/hooks_constants/blast_radius_constants.py +14 -0
- package/hooks/hooks_constants/destructive_command_environment_constants.py +18 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +1 -11
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +9 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +1 -1
- package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +2 -4
- package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
- package/hooks/json_file_reader.py +22 -0
- package/hooks/observability/test_instructions_loaded_logger.py +54 -0
- package/hooks/pending_sidecars.py +33 -0
- package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
- package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
- package/hooks/test_atomic_file_writer.py +73 -0
- package/hooks/test_json_file_reader.py +24 -0
- package/hooks/test_pending_sidecars.py +25 -0
- package/hooks/validation/mypy_validator.py +213 -80
- package/hooks/validation/test_mypy_validator.py +288 -13
- package/hooks/workflow/auto_formatter.py +225 -93
- package/hooks/workflow/investigation_tracker_reset.py +2 -0
- package/hooks/workflow/test_auto_formatter.py +261 -12
- package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
- package/package.json +1 -1
- package/rules/failure-blast-radius.md +126 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/fetch-commands.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/report-template.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/rule-checklist.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/examples.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/offense-taxonomy.md +0 -0
|
@@ -38,6 +38,11 @@ LOOSE_TOOL_MYPY_PYPROJECT = "[tool.mypy]\nignore_missing_imports = true\n"
|
|
|
38
38
|
STRICT_TOOL_MYPY_PYPROJECT = (
|
|
39
39
|
"[tool.mypy]\nignore_missing_imports = true\ndisallow_untyped_defs = true\n"
|
|
40
40
|
)
|
|
41
|
+
SAME_SIZE_LOOSE_TOOL_MYPY_PYPROJECT = "[tool.mypy]\ndisallow_any_generics = false\n"
|
|
42
|
+
SAME_SIZE_STRICT_TOOL_MYPY_PYPROJECT = "[tool.mypy]\ndisallow_any_generics = true \n"
|
|
43
|
+
UNCHECKED_GENERIC_MODULE_SOURCE = "values: list = []\n"
|
|
44
|
+
SAME_SIZE_NON_MYPY_PYPROJECT = "[tool.ruff]\nline-length = 100\n#123456789\n"
|
|
45
|
+
SAME_SIZE_STRICT_MYPY_PYPROJECT = "[tool.mypy]\ndisallow_untyped_defs = true\n"
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
def _load_validator() -> ModuleType:
|
|
@@ -83,6 +88,95 @@ def test_discover_mypy_config_returns_none_without_tool_mypy(tmp_path: Path) ->
|
|
|
83
88
|
assert validator.discover_mypy_config(standalone_module) is None
|
|
84
89
|
|
|
85
90
|
|
|
91
|
+
def test_discover_mypy_config_refreshes_after_pyproject_changes(tmp_path: Path) -> None:
|
|
92
|
+
validator = _load_validator()
|
|
93
|
+
nested_module = tmp_path / "package" / "module.py"
|
|
94
|
+
nested_module.parent.mkdir(parents=True)
|
|
95
|
+
nested_module.write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
96
|
+
|
|
97
|
+
assert validator.discover_mypy_config(nested_module) is None
|
|
98
|
+
|
|
99
|
+
config_path = tmp_path / "pyproject.toml"
|
|
100
|
+
config_path.write_text(TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
101
|
+
discovered_config = validator.discover_mypy_config(nested_module)
|
|
102
|
+
|
|
103
|
+
assert discovered_config is not None
|
|
104
|
+
assert discovered_config.resolve() == config_path.resolve()
|
|
105
|
+
|
|
106
|
+
config_path.unlink()
|
|
107
|
+
|
|
108
|
+
assert validator.discover_mypy_config(nested_module) is None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_persisted_config_cache_refreshes_after_pyproject_changes(tmp_path: Path) -> None:
|
|
112
|
+
nested_module = tmp_path / "package" / "module.py"
|
|
113
|
+
nested_module.parent.mkdir(parents=True)
|
|
114
|
+
nested_module.write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
first_validator = _load_validator()
|
|
117
|
+
assert first_validator.discover_mypy_config(nested_module) is None
|
|
118
|
+
|
|
119
|
+
config_path = tmp_path / "pyproject.toml"
|
|
120
|
+
config_path.write_text(TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
121
|
+
|
|
122
|
+
second_validator = _load_validator()
|
|
123
|
+
discovered_config = second_validator.discover_mypy_config(nested_module)
|
|
124
|
+
|
|
125
|
+
assert discovered_config is not None
|
|
126
|
+
assert discovered_config.resolve() == config_path.resolve()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_persisted_cache_reuses_metadata_and_refreshes_once_after_config_change(
|
|
130
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
131
|
+
) -> None:
|
|
132
|
+
project_root = tmp_path / "project"
|
|
133
|
+
project_root.mkdir()
|
|
134
|
+
config_path = project_root / "pyproject.toml"
|
|
135
|
+
config_path.write_text(LOOSE_TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
136
|
+
target_module = project_root / "module.py"
|
|
137
|
+
target_module.write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
138
|
+
|
|
139
|
+
first_validator = _load_validator()
|
|
140
|
+
assert first_validator.discover_mypy_config(target_module) == config_path
|
|
141
|
+
|
|
142
|
+
second_validator = _load_validator()
|
|
143
|
+
unchanged_walk_call_count = 0
|
|
144
|
+
real_ancestor_directories = second_validator.ancestor_directories
|
|
145
|
+
|
|
146
|
+
def _count_unchanged_ancestor_walks(starting_file: Path) -> list[Path]:
|
|
147
|
+
nonlocal unchanged_walk_call_count
|
|
148
|
+
unchanged_walk_call_count += 1
|
|
149
|
+
return real_ancestor_directories(starting_file)
|
|
150
|
+
|
|
151
|
+
monkeypatch.setattr(
|
|
152
|
+
second_validator,
|
|
153
|
+
"ancestor_directories",
|
|
154
|
+
_count_unchanged_ancestor_walks,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
assert second_validator.discover_mypy_config(target_module) == config_path
|
|
158
|
+
assert unchanged_walk_call_count == 0
|
|
159
|
+
|
|
160
|
+
config_path.write_text(STRICT_TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
161
|
+
third_validator = _load_validator()
|
|
162
|
+
changed_walk_call_count = 0
|
|
163
|
+
real_changed_walk = third_validator.find_pyproject_with_mypy_config
|
|
164
|
+
|
|
165
|
+
def _count_changed_config_walks(starting_file: Path) -> Path | None:
|
|
166
|
+
nonlocal changed_walk_call_count
|
|
167
|
+
changed_walk_call_count += 1
|
|
168
|
+
return real_changed_walk(starting_file)
|
|
169
|
+
|
|
170
|
+
monkeypatch.setattr(
|
|
171
|
+
third_validator,
|
|
172
|
+
"find_pyproject_with_mypy_config",
|
|
173
|
+
_count_changed_config_walks,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
assert third_validator.discover_mypy_config(target_module) == config_path
|
|
177
|
+
assert changed_walk_call_count == 1
|
|
178
|
+
|
|
179
|
+
|
|
86
180
|
def test_build_mypy_command_includes_config_file_when_present(tmp_path: Path) -> None:
|
|
87
181
|
validator = _load_validator()
|
|
88
182
|
config_file = tmp_path / "pyproject.toml"
|
|
@@ -157,6 +251,43 @@ def test_config_walk_runs_once_per_root_across_two_edits(
|
|
|
157
251
|
assert walk_count_after_second_edit == walk_count_after_first_edit
|
|
158
252
|
|
|
159
253
|
|
|
254
|
+
def test_warm_config_cache_uses_metadata_and_config_digest_without_ancestor_walk(
|
|
255
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
256
|
+
) -> None:
|
|
257
|
+
validator = _load_validator()
|
|
258
|
+
project_root = tmp_path / "project"
|
|
259
|
+
project_root.mkdir()
|
|
260
|
+
config_path = project_root / "pyproject.toml"
|
|
261
|
+
config_path.write_text(TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
262
|
+
target_module = project_root / "module.py"
|
|
263
|
+
target_module.write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
264
|
+
|
|
265
|
+
assert validator.discover_mypy_config(target_module) == config_path
|
|
266
|
+
|
|
267
|
+
ancestor_walk_call_count = 0
|
|
268
|
+
config_read_call_count = 0
|
|
269
|
+
real_ancestor_directories = validator.ancestor_directories
|
|
270
|
+
real_read_bytes = Path.read_bytes
|
|
271
|
+
|
|
272
|
+
def _counting_ancestor_directories(starting_file: Path) -> list[Path]:
|
|
273
|
+
nonlocal ancestor_walk_call_count
|
|
274
|
+
ancestor_walk_call_count += 1
|
|
275
|
+
return real_ancestor_directories(starting_file)
|
|
276
|
+
|
|
277
|
+
def _counting_read_bytes(file_path: Path) -> bytes:
|
|
278
|
+
nonlocal config_read_call_count
|
|
279
|
+
if file_path == config_path:
|
|
280
|
+
config_read_call_count += 1
|
|
281
|
+
return real_read_bytes(file_path)
|
|
282
|
+
|
|
283
|
+
monkeypatch.setattr(validator, "ancestor_directories", _counting_ancestor_directories)
|
|
284
|
+
monkeypatch.setattr(Path, "read_bytes", _counting_read_bytes)
|
|
285
|
+
|
|
286
|
+
assert validator.discover_mypy_config(target_module) == config_path
|
|
287
|
+
assert ancestor_walk_call_count == 0
|
|
288
|
+
assert config_read_call_count == 1
|
|
289
|
+
|
|
290
|
+
|
|
160
291
|
def test_sibling_subtrees_each_resolve_their_own_nested_config(
|
|
161
292
|
tmp_path: Path,
|
|
162
293
|
) -> None:
|
|
@@ -194,15 +325,11 @@ def test_warm_cache_still_blocks_file_edited_to_introduce_type_error(
|
|
|
194
325
|
edited_module = project_root / "edited.py"
|
|
195
326
|
edited_module.write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
196
327
|
|
|
197
|
-
clean_exit_code, _clean_output = validator.run_mypy(
|
|
198
|
-
str(edited_module), str(project_root)
|
|
199
|
-
)
|
|
328
|
+
clean_exit_code, _clean_output = validator.run_mypy(str(edited_module), str(project_root))
|
|
200
329
|
assert clean_exit_code == 0
|
|
201
330
|
|
|
202
331
|
edited_module.write_text(TYPE_ERROR_MODULE_SOURCE, encoding="utf-8")
|
|
203
|
-
error_exit_code, error_output = validator.run_mypy(
|
|
204
|
-
str(edited_module), str(project_root)
|
|
205
|
-
)
|
|
332
|
+
error_exit_code, error_output = validator.run_mypy(str(edited_module), str(project_root))
|
|
206
333
|
|
|
207
334
|
assert error_exit_code != 0
|
|
208
335
|
assert ": error:" in error_output
|
|
@@ -230,9 +357,7 @@ def test_warm_cache_skips_mypy_run_when_content_unchanged(
|
|
|
230
357
|
|
|
231
358
|
monkeypatch.setattr(validator.subprocess, "run", _counting_subprocess_run)
|
|
232
359
|
|
|
233
|
-
second_exit_code, _second_output = validator.run_mypy(
|
|
234
|
-
str(unchanged_module), str(project_root)
|
|
235
|
-
)
|
|
360
|
+
second_exit_code, _second_output = validator.run_mypy(str(unchanged_module), str(project_root))
|
|
236
361
|
|
|
237
362
|
assert second_exit_code == 0
|
|
238
363
|
assert subprocess_run_call_count == 0
|
|
@@ -250,13 +375,10 @@ def test_content_hash_skip_invalidated_when_mypy_config_tightens(
|
|
|
250
375
|
checked_module = project_root / "checked.py"
|
|
251
376
|
checked_module.write_text(UNTYPED_DEF_MODULE_SOURCE, encoding="utf-8")
|
|
252
377
|
|
|
253
|
-
loose_exit_code, _loose_output = validator.run_mypy(
|
|
254
|
-
str(checked_module), str(project_root)
|
|
255
|
-
)
|
|
378
|
+
loose_exit_code, _loose_output = validator.run_mypy(str(checked_module), str(project_root))
|
|
256
379
|
assert loose_exit_code == 0
|
|
257
380
|
|
|
258
381
|
config_path.write_text(STRICT_TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
259
|
-
validator.reset_session_config_cache()
|
|
260
382
|
|
|
261
383
|
subprocess_run_call_count = 0
|
|
262
384
|
real_subprocess_run = validator.subprocess.run
|
|
@@ -279,6 +401,159 @@ def test_content_hash_skip_invalidated_when_mypy_config_tightens(
|
|
|
279
401
|
assert ": error:" in tightened_output
|
|
280
402
|
|
|
281
403
|
|
|
404
|
+
def test_identical_config_relocation_invalidates_content_hash(
|
|
405
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
406
|
+
) -> None:
|
|
407
|
+
validator = _load_validator()
|
|
408
|
+
project_root = tmp_path / "project"
|
|
409
|
+
package_directory = project_root / "package"
|
|
410
|
+
package_directory.mkdir(parents=True)
|
|
411
|
+
nested_config_path = package_directory / "pyproject.toml"
|
|
412
|
+
root_config_path = project_root / "pyproject.toml"
|
|
413
|
+
nested_config_path.write_text(TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
414
|
+
checked_module = package_directory / "checked.py"
|
|
415
|
+
checked_module.write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
416
|
+
|
|
417
|
+
initial_config = validator.discover_mypy_config(checked_module)
|
|
418
|
+
assert initial_config == nested_config_path
|
|
419
|
+
initial_exit_code, _initial_output = validator.run_mypy(str(checked_module), str(project_root))
|
|
420
|
+
assert initial_exit_code == 0
|
|
421
|
+
|
|
422
|
+
nested_config_path.replace(root_config_path)
|
|
423
|
+
|
|
424
|
+
subprocess_run_call_count = 0
|
|
425
|
+
real_subprocess_run = validator.subprocess.run
|
|
426
|
+
|
|
427
|
+
def _counting_subprocess_run(*positional: object, **keyword: object) -> object:
|
|
428
|
+
nonlocal subprocess_run_call_count
|
|
429
|
+
subprocess_run_call_count += 1
|
|
430
|
+
return real_subprocess_run(*positional, **keyword)
|
|
431
|
+
|
|
432
|
+
monkeypatch.setattr(validator.subprocess, "run", _counting_subprocess_run)
|
|
433
|
+
|
|
434
|
+
relocated_config = validator.discover_mypy_config(checked_module)
|
|
435
|
+
relocated_exit_code, relocated_output = validator.run_mypy(
|
|
436
|
+
str(checked_module), str(project_root)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
assert relocated_config == root_config_path
|
|
440
|
+
assert subprocess_run_call_count == 1
|
|
441
|
+
assert relocated_exit_code == 0, relocated_output
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def test_same_stat_config_rewrite_runs_mypy_and_reports_strict_error(
|
|
445
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
446
|
+
) -> None:
|
|
447
|
+
assert len(SAME_SIZE_LOOSE_TOOL_MYPY_PYPROJECT) == len(SAME_SIZE_STRICT_TOOL_MYPY_PYPROJECT)
|
|
448
|
+
validator = _load_validator()
|
|
449
|
+
project_root = tmp_path / "project"
|
|
450
|
+
project_root.mkdir()
|
|
451
|
+
config_path = project_root / "pyproject.toml"
|
|
452
|
+
config_path.write_text(SAME_SIZE_LOOSE_TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
453
|
+
checked_module = project_root / "checked.py"
|
|
454
|
+
checked_module.write_text(UNCHECKED_GENERIC_MODULE_SOURCE, encoding="utf-8")
|
|
455
|
+
|
|
456
|
+
initial_exit_code, _initial_output = validator.run_mypy(str(checked_module), str(project_root))
|
|
457
|
+
assert initial_exit_code == 0
|
|
458
|
+
original_stat = config_path.stat()
|
|
459
|
+
config_path.write_text(SAME_SIZE_STRICT_TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
460
|
+
os.utime(
|
|
461
|
+
config_path,
|
|
462
|
+
ns=(original_stat.st_atime_ns, original_stat.st_mtime_ns),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
real_path_stat = Path.stat
|
|
466
|
+
|
|
467
|
+
def _restore_original_config_stat(
|
|
468
|
+
file_path: Path, *, follow_symlinks: bool = True
|
|
469
|
+
) -> os.stat_result:
|
|
470
|
+
if file_path == config_path:
|
|
471
|
+
return original_stat
|
|
472
|
+
return real_path_stat(file_path, follow_symlinks=follow_symlinks)
|
|
473
|
+
|
|
474
|
+
monkeypatch.setattr(Path, "stat", _restore_original_config_stat)
|
|
475
|
+
subprocess_run_call_count = 0
|
|
476
|
+
real_subprocess_run = validator.subprocess.run
|
|
477
|
+
|
|
478
|
+
def _counting_subprocess_run(*positional: object, **keyword: object) -> object:
|
|
479
|
+
nonlocal subprocess_run_call_count
|
|
480
|
+
subprocess_run_call_count += 1
|
|
481
|
+
return real_subprocess_run(*positional, **keyword)
|
|
482
|
+
|
|
483
|
+
monkeypatch.setattr(validator.subprocess, "run", _counting_subprocess_run)
|
|
484
|
+
|
|
485
|
+
strict_exit_code, strict_output = validator.run_mypy(str(checked_module), str(project_root))
|
|
486
|
+
|
|
487
|
+
assert subprocess_run_call_count == 1
|
|
488
|
+
assert strict_exit_code != 0
|
|
489
|
+
assert ": error:" in strict_output
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_same_stat_non_mypy_rewrite_discovers_strict_mypy_config(
|
|
493
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
494
|
+
) -> None:
|
|
495
|
+
assert len(SAME_SIZE_NON_MYPY_PYPROJECT) == len(SAME_SIZE_STRICT_MYPY_PYPROJECT)
|
|
496
|
+
validator = _load_validator()
|
|
497
|
+
project_root = tmp_path / "project"
|
|
498
|
+
project_root.mkdir()
|
|
499
|
+
config_path = project_root / "pyproject.toml"
|
|
500
|
+
config_path.write_text(SAME_SIZE_NON_MYPY_PYPROJECT, encoding="utf-8")
|
|
501
|
+
checked_module = project_root / "checked.py"
|
|
502
|
+
checked_module.write_text(UNTYPED_DEF_MODULE_SOURCE, encoding="utf-8")
|
|
503
|
+
|
|
504
|
+
initial_exit_code, _initial_output = validator.run_mypy(str(checked_module), str(project_root))
|
|
505
|
+
assert initial_exit_code == 0
|
|
506
|
+
original_stat = config_path.stat()
|
|
507
|
+
config_path.write_text(SAME_SIZE_STRICT_MYPY_PYPROJECT, encoding="utf-8")
|
|
508
|
+
os.utime(config_path, ns=(original_stat.st_atime_ns, original_stat.st_mtime_ns))
|
|
509
|
+
|
|
510
|
+
real_path_stat = Path.stat
|
|
511
|
+
|
|
512
|
+
def _restore_original_config_stat(
|
|
513
|
+
file_path: Path, *, follow_symlinks: bool = True
|
|
514
|
+
) -> os.stat_result:
|
|
515
|
+
if file_path == config_path:
|
|
516
|
+
return original_stat
|
|
517
|
+
return real_path_stat(file_path, follow_symlinks=follow_symlinks)
|
|
518
|
+
|
|
519
|
+
monkeypatch.setattr(Path, "stat", _restore_original_config_stat)
|
|
520
|
+
subprocess_run_call_count = 0
|
|
521
|
+
real_subprocess_run = validator.subprocess.run
|
|
522
|
+
|
|
523
|
+
def _counting_subprocess_run(*positional: object, **keyword: object) -> object:
|
|
524
|
+
nonlocal subprocess_run_call_count
|
|
525
|
+
subprocess_run_call_count += 1
|
|
526
|
+
return real_subprocess_run(*positional, **keyword)
|
|
527
|
+
|
|
528
|
+
monkeypatch.setattr(validator.subprocess, "run", _counting_subprocess_run)
|
|
529
|
+
|
|
530
|
+
strict_exit_code, strict_output = validator.run_mypy(str(checked_module), str(project_root))
|
|
531
|
+
|
|
532
|
+
assert subprocess_run_call_count == 1
|
|
533
|
+
assert strict_exit_code != 0
|
|
534
|
+
assert ": error:" in strict_output
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def test_config_signature_falls_back_when_config_resolution_hits_symlink_loop(
|
|
538
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
539
|
+
) -> None:
|
|
540
|
+
validator = _load_validator()
|
|
541
|
+
config_path = tmp_path / "pyproject.toml"
|
|
542
|
+
config_path.write_text(TOOL_MYPY_PYPROJECT, encoding="utf-8")
|
|
543
|
+
real_resolve = Path.resolve
|
|
544
|
+
|
|
545
|
+
def _raise_resolution_loop(file_path: Path, *, strict: bool = False) -> Path:
|
|
546
|
+
if file_path == config_path:
|
|
547
|
+
raise RuntimeError("symlink loop")
|
|
548
|
+
return real_resolve(file_path, strict=strict)
|
|
549
|
+
|
|
550
|
+
monkeypatch.setattr(Path, "resolve", _raise_resolution_loop)
|
|
551
|
+
|
|
552
|
+
signature = validator._config_signature(config_path)
|
|
553
|
+
|
|
554
|
+
assert signature.startswith(str(config_path).encode(validator.CACHE_FILE_ENCODING))
|
|
555
|
+
|
|
556
|
+
|
|
282
557
|
def test_project_relative_path_within_root_returns_relative() -> None:
|
|
283
558
|
validator = _load_validator()
|
|
284
559
|
project_root = os.path.join("base", "project")
|