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.
Files changed (280) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +41 -33
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/CLAUDE.md +10 -1
  42. package/bin/ever-shipped-skills.mjs +73 -0
  43. package/bin/expand_home_directory_tokens.mjs +1 -1
  44. package/bin/install.mjs +140 -7
  45. package/bin/install.prune.test.mjs +457 -0
  46. package/docs/CODE_RULES.md +1 -1
  47. package/hooks/advisory/refactor_guard.py +3 -4
  48. package/hooks/blocking/CLAUDE.md +7 -1
  49. package/hooks/blocking/block_main_commit.py +2 -2
  50. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  61. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  62. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  63. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  64. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  65. package/hooks/blocking/code_rules_enforcer.py +4 -0
  66. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  67. package/hooks/blocking/code_rules_optional_params.py +2 -2
  68. package/hooks/blocking/code_rules_shared.py +83 -1
  69. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  70. package/hooks/blocking/code_rules_test_layout.py +9 -3
  71. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  72. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  73. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  74. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  75. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  76. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  77. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  78. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  79. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  80. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  85. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  86. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  87. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  88. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  89. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  90. package/hooks/blocking/pii_payload_scan.py +138 -42
  91. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  92. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  93. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  94. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  95. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  96. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  97. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  98. package/hooks/blocking/plain_language_blocker.py +138 -4
  99. package/hooks/blocking/pr_description_command_parser.py +8 -4
  100. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  101. package/hooks/blocking/sensitive_file_protector.py +114 -48
  102. package/hooks/blocking/tdd_enforcer.py +97 -601
  103. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  104. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  105. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  106. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  107. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  108. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  109. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  110. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  111. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  112. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  113. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  114. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  115. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  116. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  117. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  118. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  119. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  120. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  121. package/hooks/blocking/test_code_rules_shared.py +181 -0
  122. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  123. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  124. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  125. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  126. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  127. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  128. package/hooks/blocking/tests/conftest.py +10 -0
  129. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  130. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  131. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  132. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  133. package/hooks/git-hooks/post_commit.py +3 -4
  134. package/hooks/hooks_constants/CLAUDE.md +4 -2
  135. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  136. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  137. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  138. package/hooks/hooks_constants/local_identity.py +59 -8
  139. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  140. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  141. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  142. package/hooks/hooks_constants/test_local_identity.py +105 -3
  143. package/hooks/pyproject.toml +52 -4
  144. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  145. package/hooks/validation/mypy_validator.py +2 -2
  146. package/hooks/validators/CLAUDE.md +1 -1
  147. package/hooks/validators/README.md +2 -0
  148. package/hooks/validators/health_check.py +1 -0
  149. package/hooks/validators/mypy_integration.py +2 -0
  150. package/hooks/validators/python_style_checks.py +114 -136
  151. package/hooks/validators/python_style_helpers.py +95 -0
  152. package/hooks/validators/ruff_integration.py +3 -0
  153. package/hooks/validators/test_python_style_checks.py +0 -164
  154. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  155. package/hooks/validators/test_python_style_fixes.py +251 -0
  156. package/hooks/validators/test_python_style_helpers.py +125 -0
  157. package/hooks/workflow/auto_formatter.py +5 -4
  158. package/package.json +1 -1
  159. package/rules/CLAUDE.md +1 -0
  160. package/rules/anti-corollary-tests.md +69 -0
  161. package/rules/bdd.md +1 -3
  162. package/rules/code-reviews.md +1 -1
  163. package/rules/gh-paginate.md +1 -1
  164. package/rules/plain-language.md +2 -0
  165. package/scripts/CLAUDE.md +4 -0
  166. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  167. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  168. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  169. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  170. package/scripts/grok_headless_runner.py +294 -0
  171. package/scripts/grok_worker_preflight.py +410 -0
  172. package/scripts/invoke_code_review.py +463 -0
  173. package/scripts/resolve_worker_spawn.py +619 -0
  174. package/scripts/spawn_grok_batch.py +672 -0
  175. package/scripts/test_grok_headless_runner.py +626 -0
  176. package/scripts/test_grok_worker_preflight.py +1054 -0
  177. package/scripts/test_invoke_code_review.py +672 -0
  178. package/scripts/test_resolve_worker_spawn.py +1014 -0
  179. package/scripts/test_spawn_grok_batch.py +1017 -0
  180. package/skills/CLAUDE.md +6 -3
  181. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  182. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  183. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  184. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  185. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  186. package/skills/autoconverge/CLAUDE.md +3 -3
  187. package/skills/autoconverge/SKILL.md +9 -3
  188. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  189. package/skills/autoconverge/reference/convergence.md +33 -11
  190. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  191. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  192. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  193. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  194. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  195. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  196. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  197. package/skills/autoconverge/workflow/converge.mjs +202 -18
  198. package/skills/bugteam/CLAUDE.md +2 -2
  199. package/skills/bugteam/CONSTRAINTS.md +3 -2
  200. package/skills/bugteam/PROMPTS.md +7 -6
  201. package/skills/bugteam/SKILL.md +18 -13
  202. package/skills/bugteam/reference/README.md +2 -3
  203. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  204. package/skills/bugteam/reference/design-rationale.md +1 -1
  205. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  206. package/skills/bugteam/reference/team-setup.md +8 -2
  207. package/skills/closeout/SKILL.md +153 -0
  208. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  209. package/skills/closeout/reference/issue-body-templates.md +108 -0
  210. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  211. package/skills/codex-review/CLAUDE.md +46 -0
  212. package/skills/codex-review/SKILL.md +181 -0
  213. package/skills/codex-review/reference/CLAUDE.md +15 -0
  214. package/skills/codex-review/reference/cli-contract.md +253 -0
  215. package/skills/codex-review/reference/loop-integration.md +118 -0
  216. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  217. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  218. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  219. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  220. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  221. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  222. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  223. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  224. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  225. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  226. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  227. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  228. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  229. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  230. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  231. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  232. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  233. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  234. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  235. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  236. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  237. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  238. package/skills/codex-review/test_skill_scaffold.py +192 -0
  239. package/skills/grok-spawn/CLAUDE.md +28 -0
  240. package/skills/grok-spawn/SKILL.md +226 -0
  241. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  242. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  243. package/skills/grokify/SKILL.md +9 -1
  244. package/skills/grokify/capability-claims.test.mjs +28 -0
  245. package/skills/grokify/evals/README.md +72 -0
  246. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  247. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  248. package/skills/orchestrator/SKILL.md +32 -22
  249. package/skills/orchestrator-refresh/SKILL.md +12 -8
  250. package/skills/pr-converge/CLAUDE.md +1 -1
  251. package/skills/pr-converge/SKILL.md +34 -13
  252. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  253. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  254. package/skills/pr-converge/reference/ground-rules.md +1 -1
  255. package/skills/pr-converge/reference/per-tick.md +130 -42
  256. package/skills/pr-converge/reference/state-schema.md +10 -0
  257. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  258. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  259. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  260. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  261. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  262. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  263. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  264. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  265. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  266. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  267. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  268. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  269. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  270. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  271. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  272. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  273. package/skills/pr-fix-protocol/SKILL.md +65 -0
  274. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  275. package/skills/reviewer-gates/SKILL.md +7 -5
  276. package/skills/skill-builder/references/skill-modularity.md +1 -1
  277. package/skills/team-advisor/SKILL.md +16 -12
  278. package/system-prompts/software-engineer.xml +7 -6
  279. package/hooks/validators/test_verify_paths.py +0 -32
  280. 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"