claude-dev-env 1.94.0 → 2.0.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/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +41 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
- package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/CLAUDE.md +10 -1
- package/bin/ever-shipped-skills.mjs +73 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +140 -7
- package/bin/install.prune.test.mjs +457 -0
- package/docs/CODE_RULES.md +1 -1
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +83 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +97 -601
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +52 -4
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +202 -18
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +32 -22
- package/skills/orchestrator-refresh/SKILL.md +12 -8
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-fix-protocol/SKILL.md +65 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- package/skills/skill-builder/references/skill-modularity.md +1 -1
- package/skills/team-advisor/SKILL.md +16 -12
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
"""Tests for Python style checks."""
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
4
|
from pathlib import Path
|
|
8
5
|
|
|
9
6
|
import pytest
|
|
@@ -14,8 +11,6 @@ from .python_style_checks import (
|
|
|
14
11
|
check_imports_at_top,
|
|
15
12
|
check_no_empty_line_after_decorators,
|
|
16
13
|
check_view_function_naming,
|
|
17
|
-
fix_file,
|
|
18
|
-
fix_function_spacing,
|
|
19
14
|
validate_file,
|
|
20
15
|
)
|
|
21
16
|
|
|
@@ -138,14 +133,6 @@ def bar() -> None:
|
|
|
138
133
|
pass
|
|
139
134
|
'''
|
|
140
135
|
|
|
141
|
-
ONE_BLANK_BEFORE_DECORATED = '''def foo() -> None:
|
|
142
|
-
pass
|
|
143
|
-
|
|
144
|
-
@decorator
|
|
145
|
-
def bar() -> None:
|
|
146
|
-
pass
|
|
147
|
-
'''
|
|
148
|
-
|
|
149
136
|
COMMENT_BETWEEN_FUNCTIONS = '''def foo() -> None:
|
|
150
137
|
pass
|
|
151
138
|
|
|
@@ -178,8 +165,6 @@ def bar() -> None:
|
|
|
178
165
|
pass
|
|
179
166
|
'''
|
|
180
167
|
|
|
181
|
-
FORM_FEED_BETWEEN_FUNCTIONS = "def foo():\n pass\n\x0c\n\ndef bar():\n pass\n"
|
|
182
|
-
|
|
183
168
|
|
|
184
169
|
class TestImportsAtTop:
|
|
185
170
|
"""Test import positioning validation."""
|
|
@@ -401,152 +386,3 @@ class TestViolationClass:
|
|
|
401
386
|
"""Violation should format as file:line: message."""
|
|
402
387
|
violation = Violation("test.py", 42, "Test message")
|
|
403
388
|
assert str(violation) == "test.py:42: Test message"
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
class TestAutoFix:
|
|
407
|
-
"""Test auto-fix capabilities."""
|
|
408
|
-
|
|
409
|
-
def test_fix_empty_line_after_decorator(
|
|
410
|
-
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
411
|
-
) -> None:
|
|
412
|
-
"""Auto-fix should remove blank line between decorator and function."""
|
|
413
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
414
|
-
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
415
|
-
code = '''@decorator
|
|
416
|
-
|
|
417
|
-
def foo():
|
|
418
|
-
pass
|
|
419
|
-
'''
|
|
420
|
-
expected = '''@decorator
|
|
421
|
-
def foo():
|
|
422
|
-
pass
|
|
423
|
-
'''
|
|
424
|
-
temp_path = tmp_path / "decorator_module.py"
|
|
425
|
-
temp_path.write_text(code, encoding="utf-8")
|
|
426
|
-
fixed = fix_file(temp_path)
|
|
427
|
-
assert fixed is True
|
|
428
|
-
result_text = temp_path.read_text()
|
|
429
|
-
assert result_text.strip() == expected.strip()
|
|
430
|
-
|
|
431
|
-
def test_fix_collapses_three_blank_lines_to_two(
|
|
432
|
-
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
433
|
-
) -> None:
|
|
434
|
-
"""Auto-fix should collapse three or more blank lines down to two."""
|
|
435
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
436
|
-
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
437
|
-
code = '''def foo():
|
|
438
|
-
pass
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def bar():
|
|
443
|
-
pass
|
|
444
|
-
'''
|
|
445
|
-
expected = '''def foo():
|
|
446
|
-
pass
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
def bar():
|
|
450
|
-
pass
|
|
451
|
-
'''
|
|
452
|
-
temp_path = tmp_path / "spacing_module.py"
|
|
453
|
-
temp_path.write_text(code, encoding="utf-8")
|
|
454
|
-
fixed = fix_file(temp_path)
|
|
455
|
-
assert fixed is True
|
|
456
|
-
result_text = temp_path.read_text()
|
|
457
|
-
assert result_text.strip() == expected.strip()
|
|
458
|
-
|
|
459
|
-
def test_fix_inserts_missing_blank_lines(
|
|
460
|
-
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
461
|
-
) -> None:
|
|
462
|
-
"""Auto-fix should insert blank lines for under-spaced functions."""
|
|
463
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
464
|
-
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
465
|
-
temp_path = tmp_path / "underspaced_module.py"
|
|
466
|
-
temp_path.write_text(BAD_ONE_LINE_BETWEEN_FUNCTIONS, encoding="utf-8")
|
|
467
|
-
fixed = fix_file(temp_path)
|
|
468
|
-
assert fixed is True
|
|
469
|
-
assert temp_path.read_text() == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
|
|
470
|
-
|
|
471
|
-
def test_no_fix_needed_returns_false(
|
|
472
|
-
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
473
|
-
) -> None:
|
|
474
|
-
"""Auto-fix should return False when the file already uses two blank lines."""
|
|
475
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
476
|
-
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
477
|
-
code = '''def foo():
|
|
478
|
-
pass
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
def bar():
|
|
482
|
-
pass
|
|
483
|
-
'''
|
|
484
|
-
temp_path = tmp_path / "clean_module.py"
|
|
485
|
-
temp_path.write_text(code, encoding="utf-8")
|
|
486
|
-
fixed = fix_file(temp_path)
|
|
487
|
-
assert fixed is False
|
|
488
|
-
|
|
489
|
-
def test_form_feed_gap_stays_check_clean_after_fix(
|
|
490
|
-
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
491
|
-
) -> None:
|
|
492
|
-
"""A form feed in a blank gap converges without corrupting the file."""
|
|
493
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
494
|
-
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
495
|
-
assert (
|
|
496
|
-
check_blank_lines_between_functions(FORM_FEED_BETWEEN_FUNCTIONS, "test.py")
|
|
497
|
-
== []
|
|
498
|
-
)
|
|
499
|
-
temp_path = tmp_path / "form_feed_module.py"
|
|
500
|
-
temp_path.write_text(FORM_FEED_BETWEEN_FUNCTIONS, encoding="utf-8")
|
|
501
|
-
fix_file(temp_path)
|
|
502
|
-
fixed_text = temp_path.read_text()
|
|
503
|
-
assert check_blank_lines_between_functions(fixed_text, "test.py") == []
|
|
504
|
-
assert fix_file(temp_path) is False
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
class TestFixFunctionSpacing:
|
|
508
|
-
"""Test blank-line normalization between top-level functions."""
|
|
509
|
-
|
|
510
|
-
def test_inserts_two_blank_lines_when_adjacent(self) -> None:
|
|
511
|
-
"""Adjacent functions gain exactly two blank lines."""
|
|
512
|
-
fixed_source = fix_function_spacing(BAD_NO_LINE_BETWEEN_FUNCTIONS)
|
|
513
|
-
assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
|
|
514
|
-
|
|
515
|
-
def test_inserts_second_blank_line_when_one(self) -> None:
|
|
516
|
-
"""A single blank line grows to exactly two."""
|
|
517
|
-
fixed_source = fix_function_spacing(BAD_ONE_LINE_BETWEEN_FUNCTIONS)
|
|
518
|
-
assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
|
|
519
|
-
|
|
520
|
-
def test_collapses_three_blank_lines_to_two(self) -> None:
|
|
521
|
-
"""Three blank lines collapse to exactly two."""
|
|
522
|
-
fixed_source = fix_function_spacing(BAD_THREE_LINES_BETWEEN_FUNCTIONS)
|
|
523
|
-
assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
|
|
524
|
-
|
|
525
|
-
def test_normalizes_before_decorated_function(self) -> None:
|
|
526
|
-
"""Blank lines before a decorated function normalize to two."""
|
|
527
|
-
fixed_source = fix_function_spacing(ONE_BLANK_BEFORE_DECORATED)
|
|
528
|
-
assert fixed_source == DECORATED_NEXT_TWO_LINES
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
class TestDirectInvocation:
|
|
532
|
-
"""Test running the checker as a standalone script."""
|
|
533
|
-
|
|
534
|
-
def test_direct_invocation_resolves_hooks_constants(
|
|
535
|
-
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
536
|
-
) -> None:
|
|
537
|
-
"""Direct invocation bootstraps hooks_constants without PYTHONPATH."""
|
|
538
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
539
|
-
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
540
|
-
script_path = Path(__file__).resolve().parent / "python_style_checks.py"
|
|
541
|
-
target_path = tmp_path / "clean_sample.py"
|
|
542
|
-
target_path.write_text(GOOD_IMPORTS, encoding="utf-8")
|
|
543
|
-
scrubbed_environment = dict(os.environ)
|
|
544
|
-
scrubbed_environment.pop("PYTHONPATH", None)
|
|
545
|
-
completed_process = subprocess.run(
|
|
546
|
-
[sys.executable, "-S", str(script_path), str(target_path)],
|
|
547
|
-
capture_output=True,
|
|
548
|
-
text=True,
|
|
549
|
-
env=scrubbed_environment,
|
|
550
|
-
)
|
|
551
|
-
assert "ModuleNotFoundError" not in completed_process.stderr
|
|
552
|
-
assert completed_process.returncode == 0
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Tests for multi-line decorator gap detection and repair.
|
|
2
|
+
|
|
3
|
+
These cover the case where a decorator spans several source lines: the
|
|
4
|
+
validator measures the gap from the decorator's last line to the def, and
|
|
5
|
+
the fixer removes any blank line that falls in that gap.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
|
|
10
|
+
from .python_style_checks import (
|
|
11
|
+
check_no_empty_line_after_decorators,
|
|
12
|
+
fix_empty_lines_after_decorators,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
MULTILINE_DECORATOR_NO_BLANK = """@parametrize(
|
|
16
|
+
"value",
|
|
17
|
+
[1, 2, 3],
|
|
18
|
+
)
|
|
19
|
+
def check_values() -> None:
|
|
20
|
+
pass
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
MULTILINE_DECORATOR_WITH_BLANK = """@parametrize(
|
|
24
|
+
"value",
|
|
25
|
+
[1, 2, 3],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def check_values() -> None:
|
|
29
|
+
pass
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
SINGLE_DECORATOR_NO_BLANK = """@decorator
|
|
33
|
+
def foo() -> None:
|
|
34
|
+
pass
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
SINGLE_DECORATOR_WITH_BLANK = """@decorator
|
|
38
|
+
|
|
39
|
+
def foo() -> None:
|
|
40
|
+
pass
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
STACKED_DECORATORS_WITH_INNER_BLANK = """@first
|
|
44
|
+
|
|
45
|
+
@second
|
|
46
|
+
def foo() -> None:
|
|
47
|
+
pass
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
STACKED_DECORATORS_NO_BLANK = """@first
|
|
51
|
+
@second
|
|
52
|
+
def foo() -> None:
|
|
53
|
+
pass
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestMultilineDecoratorValidator:
|
|
58
|
+
"""Validate gap detection for decorators spanning several lines."""
|
|
59
|
+
|
|
60
|
+
def test_multiline_decorator_no_blank_valid(self) -> None:
|
|
61
|
+
"""A multi-line decorator directly above its def passes."""
|
|
62
|
+
violations = check_no_empty_line_after_decorators(MULTILINE_DECORATOR_NO_BLANK, "test.py")
|
|
63
|
+
assert violations == []
|
|
64
|
+
|
|
65
|
+
def test_multiline_decorator_with_blank_fails(self) -> None:
|
|
66
|
+
"""A blank line after a multi-line decorator is flagged."""
|
|
67
|
+
violations = check_no_empty_line_after_decorators(MULTILINE_DECORATOR_WITH_BLANK, "test.py")
|
|
68
|
+
assert len(violations) == 1
|
|
69
|
+
assert "decorator" in violations[0].message.lower()
|
|
70
|
+
|
|
71
|
+
def test_single_decorator_no_blank_valid(self) -> None:
|
|
72
|
+
"""A single-line decorator directly above its def passes."""
|
|
73
|
+
violations = check_no_empty_line_after_decorators(SINGLE_DECORATOR_NO_BLANK, "test.py")
|
|
74
|
+
assert violations == []
|
|
75
|
+
|
|
76
|
+
def test_single_decorator_with_blank_fails(self) -> None:
|
|
77
|
+
"""A blank line after a single-line decorator is flagged."""
|
|
78
|
+
violations = check_no_empty_line_after_decorators(SINGLE_DECORATOR_WITH_BLANK, "test.py")
|
|
79
|
+
assert len(violations) == 1
|
|
80
|
+
assert violations[0].line == 1
|
|
81
|
+
assert "decorator" in violations[0].message.lower()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestMultilineDecoratorFixer:
|
|
85
|
+
"""Validate blank-line removal around decorators spanning several lines."""
|
|
86
|
+
|
|
87
|
+
def test_fixer_removes_multiline_decorator_gap(self) -> None:
|
|
88
|
+
"""The fixer removes the blank between a multi-line decorator and its def."""
|
|
89
|
+
fixed_source = fix_empty_lines_after_decorators(MULTILINE_DECORATOR_WITH_BLANK)
|
|
90
|
+
ast.parse(fixed_source)
|
|
91
|
+
assert check_no_empty_line_after_decorators(fixed_source, "test.py") == []
|
|
92
|
+
assert fixed_source == MULTILINE_DECORATOR_NO_BLANK
|
|
93
|
+
|
|
94
|
+
def test_fixer_removes_single_decorator_gap(self) -> None:
|
|
95
|
+
"""The fixer removes the blank after a single-line decorator."""
|
|
96
|
+
fixed_source = fix_empty_lines_after_decorators(SINGLE_DECORATOR_WITH_BLANK)
|
|
97
|
+
assert fixed_source == SINGLE_DECORATOR_NO_BLANK
|
|
98
|
+
|
|
99
|
+
def test_fixer_removes_blank_between_stacked_decorators(self) -> None:
|
|
100
|
+
"""The fixer removes a blank line separating stacked decorators."""
|
|
101
|
+
fixed_source = fix_empty_lines_after_decorators(STACKED_DECORATORS_WITH_INNER_BLANK)
|
|
102
|
+
ast.parse(fixed_source)
|
|
103
|
+
assert fixed_source == STACKED_DECORATORS_NO_BLANK
|
|
104
|
+
|
|
105
|
+
def test_fixer_leaves_clean_multiline_decorator_untouched(self) -> None:
|
|
106
|
+
"""A multi-line decorator with no gap survives the fixer unchanged."""
|
|
107
|
+
fixed_source = fix_empty_lines_after_decorators(MULTILINE_DECORATOR_NO_BLANK)
|
|
108
|
+
assert fixed_source == MULTILINE_DECORATOR_NO_BLANK
|
|
109
|
+
|
|
110
|
+
def test_fixer_is_idempotent(self) -> None:
|
|
111
|
+
"""Applying the fixer twice matches applying it once."""
|
|
112
|
+
once = fix_empty_lines_after_decorators(MULTILINE_DECORATOR_WITH_BLANK)
|
|
113
|
+
twice = fix_empty_lines_after_decorators(once)
|
|
114
|
+
assert twice == once
|
|
115
|
+
|
|
116
|
+
def test_fixer_returns_source_unchanged_on_syntax_error(self) -> None:
|
|
117
|
+
"""Unparseable source is returned unchanged rather than corrupted."""
|
|
118
|
+
broken_source = "@decorator\n\ndef foo(\n"
|
|
119
|
+
assert fix_empty_lines_after_decorators(broken_source) == broken_source
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Tests for the blank-line auto-fixers and standalone invocation."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from .python_style_checks import (
|
|
12
|
+
check_blank_lines_between_functions,
|
|
13
|
+
fix_file,
|
|
14
|
+
fix_function_spacing,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
GOOD_IMPORTS = """import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
def foo() -> None:
|
|
21
|
+
pass
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
GOOD_TWO_LINES_BETWEEN_FUNCTIONS = """def foo() -> None:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def bar() -> None:
|
|
29
|
+
pass
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
BAD_NO_LINE_BETWEEN_FUNCTIONS = """def foo() -> None:
|
|
33
|
+
pass
|
|
34
|
+
def bar() -> None:
|
|
35
|
+
pass
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
BAD_ONE_LINE_BETWEEN_FUNCTIONS = """def foo() -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def bar() -> None:
|
|
42
|
+
pass
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
BAD_THREE_LINES_BETWEEN_FUNCTIONS = """def foo() -> None:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def bar() -> None:
|
|
51
|
+
pass
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
DECORATED_NEXT_TWO_LINES = """def foo() -> None:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@decorator
|
|
59
|
+
def bar() -> None:
|
|
60
|
+
pass
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
ONE_BLANK_BEFORE_DECORATED = """def foo() -> None:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@decorator
|
|
67
|
+
def bar() -> None:
|
|
68
|
+
pass
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
FORM_FEED_BETWEEN_FUNCTIONS = "def foo():\n pass\n\x0c\n\ndef bar():\n pass\n"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestAutoFix:
|
|
75
|
+
"""Test auto-fix capabilities."""
|
|
76
|
+
|
|
77
|
+
def test_fix_empty_line_after_decorator(
|
|
78
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Auto-fix should remove blank line between decorator and function."""
|
|
81
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
82
|
+
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
83
|
+
code = """@decorator
|
|
84
|
+
|
|
85
|
+
def foo():
|
|
86
|
+
pass
|
|
87
|
+
"""
|
|
88
|
+
expected = """@decorator
|
|
89
|
+
def foo():
|
|
90
|
+
pass
|
|
91
|
+
"""
|
|
92
|
+
temp_path = tmp_path / "decorator_module.py"
|
|
93
|
+
temp_path.write_text(code, encoding="utf-8")
|
|
94
|
+
fixed = fix_file(temp_path)
|
|
95
|
+
assert fixed is True
|
|
96
|
+
result_text = temp_path.read_text()
|
|
97
|
+
assert result_text.strip() == expected.strip()
|
|
98
|
+
|
|
99
|
+
def test_fix_collapses_three_blank_lines_to_two(
|
|
100
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Auto-fix should collapse three or more blank lines down to two."""
|
|
103
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
104
|
+
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
105
|
+
code = """def foo():
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def bar():
|
|
111
|
+
pass
|
|
112
|
+
"""
|
|
113
|
+
expected = """def foo():
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def bar():
|
|
118
|
+
pass
|
|
119
|
+
"""
|
|
120
|
+
temp_path = tmp_path / "spacing_module.py"
|
|
121
|
+
temp_path.write_text(code, encoding="utf-8")
|
|
122
|
+
fixed = fix_file(temp_path)
|
|
123
|
+
assert fixed is True
|
|
124
|
+
result_text = temp_path.read_text()
|
|
125
|
+
assert result_text.strip() == expected.strip()
|
|
126
|
+
|
|
127
|
+
def test_fix_inserts_missing_blank_lines(
|
|
128
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Auto-fix should insert blank lines for under-spaced functions."""
|
|
131
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
132
|
+
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
133
|
+
temp_path = tmp_path / "underspaced_module.py"
|
|
134
|
+
temp_path.write_text(BAD_ONE_LINE_BETWEEN_FUNCTIONS, encoding="utf-8")
|
|
135
|
+
fixed = fix_file(temp_path)
|
|
136
|
+
assert fixed is True
|
|
137
|
+
assert temp_path.read_text() == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
|
|
138
|
+
|
|
139
|
+
def test_no_fix_needed_returns_false(
|
|
140
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Auto-fix should return False when the file already uses two blank lines."""
|
|
143
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
144
|
+
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
145
|
+
code = """def foo():
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def bar():
|
|
150
|
+
pass
|
|
151
|
+
"""
|
|
152
|
+
temp_path = tmp_path / "clean_module.py"
|
|
153
|
+
temp_path.write_text(code, encoding="utf-8")
|
|
154
|
+
fixed = fix_file(temp_path)
|
|
155
|
+
assert fixed is False
|
|
156
|
+
|
|
157
|
+
def test_form_feed_gap_stays_check_clean_after_fix(
|
|
158
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
159
|
+
) -> None:
|
|
160
|
+
"""A form feed in a blank gap converges without corrupting the file."""
|
|
161
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
162
|
+
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
163
|
+
assert check_blank_lines_between_functions(FORM_FEED_BETWEEN_FUNCTIONS, "test.py") == []
|
|
164
|
+
temp_path = tmp_path / "form_feed_module.py"
|
|
165
|
+
temp_path.write_text(FORM_FEED_BETWEEN_FUNCTIONS, encoding="utf-8")
|
|
166
|
+
fix_file(temp_path)
|
|
167
|
+
fixed_text = temp_path.read_text()
|
|
168
|
+
assert check_blank_lines_between_functions(fixed_text, "test.py") == []
|
|
169
|
+
assert fix_file(temp_path) is False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestFixFunctionSpacing:
|
|
173
|
+
"""Test blank-line normalization between top-level functions."""
|
|
174
|
+
|
|
175
|
+
def test_inserts_two_blank_lines_when_adjacent(self) -> None:
|
|
176
|
+
"""Adjacent functions gain exactly two blank lines."""
|
|
177
|
+
fixed_source = fix_function_spacing(BAD_NO_LINE_BETWEEN_FUNCTIONS)
|
|
178
|
+
assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
|
|
179
|
+
|
|
180
|
+
def test_inserts_second_blank_line_when_one(self) -> None:
|
|
181
|
+
"""A single blank line grows to exactly two."""
|
|
182
|
+
fixed_source = fix_function_spacing(BAD_ONE_LINE_BETWEEN_FUNCTIONS)
|
|
183
|
+
assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
|
|
184
|
+
|
|
185
|
+
def test_collapses_three_blank_lines_to_two(self) -> None:
|
|
186
|
+
"""Three blank lines collapse to exactly two."""
|
|
187
|
+
fixed_source = fix_function_spacing(BAD_THREE_LINES_BETWEEN_FUNCTIONS)
|
|
188
|
+
assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
|
|
189
|
+
|
|
190
|
+
def test_normalizes_before_decorated_function(self) -> None:
|
|
191
|
+
"""Blank lines before a decorated function normalize to two."""
|
|
192
|
+
fixed_source = fix_function_spacing(ONE_BLANK_BEFORE_DECORATED)
|
|
193
|
+
assert fixed_source == DECORATED_NEXT_TWO_LINES
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TestDirectInvocation:
|
|
197
|
+
"""Test running the checker as a standalone script."""
|
|
198
|
+
|
|
199
|
+
def test_direct_invocation_resolves_hooks_constants(
|
|
200
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Direct invocation bootstraps hooks_constants without PYTHONPATH."""
|
|
203
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
204
|
+
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
205
|
+
script_path = Path(__file__).resolve().parent / "python_style_checks.py"
|
|
206
|
+
target_path = tmp_path / "clean_sample.py"
|
|
207
|
+
target_path.write_text(GOOD_IMPORTS, encoding="utf-8")
|
|
208
|
+
scrubbed_environment = dict(os.environ)
|
|
209
|
+
scrubbed_environment.pop("PYTHONPATH", None)
|
|
210
|
+
completed_process = subprocess.run(
|
|
211
|
+
[sys.executable, "-S", str(script_path), str(target_path)],
|
|
212
|
+
capture_output=True,
|
|
213
|
+
text=True,
|
|
214
|
+
env=scrubbed_environment,
|
|
215
|
+
check=False,
|
|
216
|
+
)
|
|
217
|
+
assert "ModuleNotFoundError" not in completed_process.stderr
|
|
218
|
+
assert completed_process.returncode == 0
|
|
219
|
+
|
|
220
|
+
def test_direct_invocation_with_hooks_constants_resolvable_off_path(
|
|
221
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
222
|
+
) -> None:
|
|
223
|
+
"""First import succeeds while the validators package stays off sys.path."""
|
|
224
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
225
|
+
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
226
|
+
synthetic_constants_root = tmp_path / "synthetic_root"
|
|
227
|
+
constants_package = synthetic_constants_root / "hooks_constants"
|
|
228
|
+
constants_package.mkdir(parents=True)
|
|
229
|
+
(constants_package / "__init__.py").write_text("", encoding="utf-8")
|
|
230
|
+
(constants_package / "python_style_checks_constants.py").write_text(
|
|
231
|
+
"EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS = 2\nMINIMUM_ARGUMENT_COUNT = 1\n",
|
|
232
|
+
encoding="utf-8",
|
|
233
|
+
)
|
|
234
|
+
script_path = Path(__file__).resolve().parent / "python_style_checks.py"
|
|
235
|
+
target_path = tmp_path / "clean_sample.py"
|
|
236
|
+
target_path.write_text(GOOD_IMPORTS, encoding="utf-8")
|
|
237
|
+
isolated_environment = dict(os.environ)
|
|
238
|
+
isolated_environment["PYTHONPATH"] = str(synthetic_constants_root)
|
|
239
|
+
completed_process = subprocess.run(
|
|
240
|
+
[sys.executable, str(script_path), str(target_path)],
|
|
241
|
+
capture_output=True,
|
|
242
|
+
text=True,
|
|
243
|
+
env=isolated_environment,
|
|
244
|
+
check=False,
|
|
245
|
+
)
|
|
246
|
+
assert "NameError" not in completed_process.stderr
|
|
247
|
+
assert completed_process.returncode == 0
|
|
248
|
+
|
|
249
|
+
def test_ast_module_import_available(self) -> None:
|
|
250
|
+
"""The ast module the checker depends on parses a trivial module."""
|
|
251
|
+
assert ast.parse("x = 1").body
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Tests for the shared source-line and function-discovery helpers."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .python_style_helpers import (
|
|
6
|
+
blank_line_for_source,
|
|
7
|
+
function_start_line,
|
|
8
|
+
gap_is_blank_only,
|
|
9
|
+
iter_function_definitions,
|
|
10
|
+
real_newline_lines,
|
|
11
|
+
top_level_functions,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
TWO_FUNCTIONS = """def foo() -> None:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bar() -> None:
|
|
19
|
+
pass
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
DECORATED_FUNCTION = """@decorator
|
|
23
|
+
def decorated() -> None:
|
|
24
|
+
pass
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
CRLF_SOURCE = "def foo() -> None:\r\n pass\r\n"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestIterFunctionDefinitions:
|
|
31
|
+
"""Discovery of every function definition in a tree."""
|
|
32
|
+
|
|
33
|
+
def test_yields_each_function(self) -> None:
|
|
34
|
+
"""Every top-level function is yielded."""
|
|
35
|
+
tree = ast.parse(TWO_FUNCTIONS)
|
|
36
|
+
names = [each.name for each in iter_function_definitions(tree)]
|
|
37
|
+
assert names == ["foo", "bar"]
|
|
38
|
+
|
|
39
|
+
def test_yields_nested_functions(self) -> None:
|
|
40
|
+
"""A nested function is discovered alongside its enclosing function."""
|
|
41
|
+
tree = ast.parse("def outer() -> None:\n def inner() -> None:\n pass\n")
|
|
42
|
+
names = {each.name for each in iter_function_definitions(tree)}
|
|
43
|
+
assert names == {"outer", "inner"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestTopLevelFunctions:
|
|
47
|
+
"""Ordered listing of a module's top-level functions."""
|
|
48
|
+
|
|
49
|
+
def test_returns_functions_in_line_order(self) -> None:
|
|
50
|
+
"""Top-level functions are returned ordered by line."""
|
|
51
|
+
ordered = top_level_functions(TWO_FUNCTIONS)
|
|
52
|
+
assert [each.name for each in ordered] == ["foo", "bar"]
|
|
53
|
+
|
|
54
|
+
def test_ignores_nested_functions(self) -> None:
|
|
55
|
+
"""A function nested inside another is not top-level."""
|
|
56
|
+
ordered = top_level_functions(
|
|
57
|
+
"def outer() -> None:\n def inner() -> None:\n pass\n"
|
|
58
|
+
)
|
|
59
|
+
assert [each.name for each in ordered] == ["outer"]
|
|
60
|
+
|
|
61
|
+
def test_syntax_error_returns_empty(self) -> None:
|
|
62
|
+
"""Unparseable source yields no functions."""
|
|
63
|
+
assert top_level_functions("def foo(") == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestFunctionStartLine:
|
|
67
|
+
"""First source line of a function, counting decorators."""
|
|
68
|
+
|
|
69
|
+
def test_undecorated_uses_def_line(self) -> None:
|
|
70
|
+
"""An undecorated function starts on its def line."""
|
|
71
|
+
function_node = top_level_functions(TWO_FUNCTIONS)[0]
|
|
72
|
+
assert function_start_line(function_node) == 1
|
|
73
|
+
|
|
74
|
+
def test_decorated_uses_first_decorator_line(self) -> None:
|
|
75
|
+
"""A decorated function starts on its first decorator line."""
|
|
76
|
+
function_node = top_level_functions(DECORATED_FUNCTION)[0]
|
|
77
|
+
assert function_start_line(function_node) == 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestGapIsBlankOnly:
|
|
81
|
+
"""Whether a run of lines between functions is entirely blank."""
|
|
82
|
+
|
|
83
|
+
def test_all_blank_lines_true(self) -> None:
|
|
84
|
+
"""A gap of blank lines reports blank-only."""
|
|
85
|
+
assert gap_is_blank_only(["\n", " \n", ""]) is True
|
|
86
|
+
|
|
87
|
+
def test_non_blank_line_false(self) -> None:
|
|
88
|
+
"""A gap holding any content is not blank-only."""
|
|
89
|
+
assert gap_is_blank_only(["\n", "# comment\n"]) is False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestRealNewlineLines:
|
|
93
|
+
"""Line splitting aligned with ast line numbers."""
|
|
94
|
+
|
|
95
|
+
def test_splits_on_lf(self) -> None:
|
|
96
|
+
"""LF-separated source splits into its lines with endings kept."""
|
|
97
|
+
assert real_newline_lines("a\nb\n") == ["a\n", "b\n"]
|
|
98
|
+
|
|
99
|
+
def test_splits_on_crlf(self) -> None:
|
|
100
|
+
"""CRLF pairs count as a single line ending."""
|
|
101
|
+
assert real_newline_lines("a\r\nb\r\n") == ["a\r\n", "b\r\n"]
|
|
102
|
+
|
|
103
|
+
def test_form_feed_stays_in_line(self) -> None:
|
|
104
|
+
"""A form feed does not start a new line, matching ast line numbers."""
|
|
105
|
+
assert real_newline_lines("a\x0cb\n") == ["a\x0cb\n"]
|
|
106
|
+
|
|
107
|
+
def test_trailing_partial_line_kept(self) -> None:
|
|
108
|
+
"""A final line with no ending is preserved."""
|
|
109
|
+
assert real_newline_lines("a\nb") == ["a\n", "b"]
|
|
110
|
+
|
|
111
|
+
def test_empty_source_returns_no_lines(self) -> None:
|
|
112
|
+
"""Empty source yields an empty list of lines."""
|
|
113
|
+
assert real_newline_lines("") == []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestBlankLineForSource:
|
|
117
|
+
"""Blank-line string matching the source newline convention."""
|
|
118
|
+
|
|
119
|
+
def test_lf_source_returns_lf(self) -> None:
|
|
120
|
+
"""LF source yields an LF blank line."""
|
|
121
|
+
assert blank_line_for_source("a\nb\n") == "\n"
|
|
122
|
+
|
|
123
|
+
def test_crlf_source_returns_crlf(self) -> None:
|
|
124
|
+
"""CRLF source yields a CRLF blank line."""
|
|
125
|
+
assert blank_line_for_source(CRLF_SOURCE) == "\r\n"
|