claude-dev-env 1.93.1 → 1.95.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 +35 -27
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -2
- package/_shared/advisor/scripts/model_tier_run_validator.py +23 -15
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +81 -17
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/audit-contract.md +1 -1
- package/_shared/pr-loop/gh-payloads.md +3 -3
- package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
- package/_shared/pr-loop/scripts/README.md +2 -2
- package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
- package/_shared/pr-loop/state-schema.md +3 -22
- package/bin/CLAUDE.md +11 -2
- package/bin/ever-shipped-skills.mjs +70 -0
- package/bin/install.mjs +138 -13
- package/bin/install.prune.test.mjs +457 -0
- package/bin/install.test.mjs +10 -4
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_shared.py +82 -0
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/pii_commit_command.py +398 -0
- package/hooks/blocking/pii_payload_scan.py +133 -0
- package/hooks/blocking/pii_prevention_blocker.py +151 -449
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +9 -2
- 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_pii_prevention_blocker.py +299 -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_scratchpad.py +105 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/hooks/pyproject.toml +75 -4
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -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/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 +10 -1
- package/scripts/Get-SessionAccount.ps1 +210 -0
- package/scripts/tests/CLAUDE.md +15 -1
- package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
- package/skills/CLAUDE.md +6 -27
- package/skills/anthropic-plan/SKILL.md +1 -13
- package/skills/autoconverge/CLAUDE.md +5 -2
- package/skills/autoconverge/SKILL.md +112 -387
- package/skills/autoconverge/reference/CLAUDE.md +4 -1
- package/skills/autoconverge/reference/closing-report.md +9 -2
- package/skills/autoconverge/reference/convergence.md +26 -22
- package/skills/autoconverge/reference/headless-safety.md +44 -0
- package/skills/autoconverge/reference/multi-pr.md +88 -0
- package/skills/autoconverge/reference/self-closing-loop.md +84 -0
- package/skills/autoconverge/reference/stop-conditions.md +21 -13
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
- package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +286 -96
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/CONSTRAINTS.md +3 -3
- package/skills/bugteam/PROMPTS.md +8 -19
- package/skills/bugteam/SKILL.md +13 -14
- package/skills/bugteam/reference/CLAUDE.md +1 -1
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
- package/skills/bugteam/reference/github-pr-reviews.md +56 -38
- package/skills/bugteam/reference/team-setup.md +11 -11
- package/skills/bugteam/scripts/CLAUDE.md +6 -6
- package/skills/bugteam/scripts/README.md +20 -8
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
- package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
- package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
- 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/copilot-finding-triage/SKILL.md +80 -28
- package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
- package/skills/copilot-finding-triage/templates/notification.md +8 -3
- package/skills/everything-search/SKILL.md +1 -2
- package/skills/grokify/SKILL.md +58 -0
- package/skills/grokify/templates/handoff-template.md +92 -0
- package/skills/orchestrator/SKILL.md +35 -25
- package/skills/orchestrator-refresh/SKILL.md +15 -11
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +132 -233
- package/skills/pr-converge/reference/convergence-gates.md +102 -66
- package/skills/pr-converge/reference/examples.md +37 -25
- package/skills/pr-converge/reference/fix-protocol.md +7 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
- package/skills/pr-converge/reference/per-tick.md +64 -41
- package/skills/pr-converge/reference/state-schema.md +27 -2
- package/skills/pr-converge/scripts/CLAUDE.md +8 -0
- package/skills/pr-converge/scripts/README.md +4 -3
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
- package/skills/pr-converge/scripts/check_convergence.py +189 -645
- package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
- package/skills/pr-converge/scripts/conftest.py +8 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
- package/skills/pr-fix-protocol/SKILL.md +20 -20
- package/skills/privacy-hygiene/SKILL.md +4 -3
- package/skills/reviewer-gates/SKILL.md +26 -8
- package/skills/session-log/SKILL.md +11 -12
- package/skills/session-tidy/SKILL.md +40 -30
- package/skills/skill-builder/CLAUDE.md +13 -10
- package/skills/skill-builder/SKILL.md +31 -31
- package/skills/skill-builder/references/CLAUDE.md +10 -6
- package/skills/skill-builder/references/delegation-map.md +13 -4
- package/skills/skill-builder/references/description-field.md +113 -0
- package/skills/skill-builder/references/self-audit-checklist.md +21 -4
- package/skills/skill-builder/references/skill-modularity.md +113 -0
- package/skills/skill-builder/references/skill-types.md +4 -0
- package/skills/skill-builder/templates/CLAUDE.md +4 -4
- package/skills/skill-builder/templates/gap-analysis.md +25 -1
- package/skills/skill-builder/workflows/CLAUDE.md +6 -4
- package/skills/skill-builder/workflows/improve-skill.md +17 -16
- package/skills/skill-builder/workflows/new-skill.md +34 -10
- package/skills/skill-builder/workflows/polish-skill.md +59 -43
- package/skills/team-advisor/SKILL.md +15 -11
- package/skills/usage-pause/SKILL.md +10 -14
- 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
- package/skills/bdd-protocol/CLAUDE.md +0 -26
- package/skills/bdd-protocol/SKILL.md +0 -30
- package/skills/bdd-protocol/references/CLAUDE.md +0 -10
- package/skills/bdd-protocol/references/anti-patterns.md +0 -26
- package/skills/bdd-protocol/references/example-mapping.md +0 -23
- package/skills/bg-agent/CLAUDE.md +0 -17
- package/skills/bg-agent/SKILL.md +0 -69
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
- package/skills/caveman/CLAUDE.md +0 -15
- package/skills/caveman/SKILL.md +0 -38
- package/skills/code/CLAUDE.md +0 -17
- package/skills/code/SKILL.md +0 -178
- package/skills/deep-research/CLAUDE.md +0 -17
- package/skills/deep-research/SKILL.md +0 -80
- package/skills/findbugs/CLAUDE.md +0 -20
- package/skills/findbugs/SKILL.md +0 -241
- package/skills/fixbugs/CLAUDE.md +0 -19
- package/skills/fixbugs/SKILL.md +0 -142
- package/skills/gh-paginate/CLAUDE.md +0 -18
- package/skills/gh-paginate/SKILL.md +0 -84
- package/skills/gotcha/CLAUDE.md +0 -33
- package/skills/gotcha/SKILL.md +0 -73
- package/skills/implement/CLAUDE.md +0 -27
- package/skills/implement/SKILL.md +0 -66
- package/skills/implement/scripts/CLAUDE.md +0 -22
- package/skills/implement/scripts/append_note.py +0 -133
- package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
- package/skills/implement/scripts/test_append_note.py +0 -191
- package/skills/log-audit/CLAUDE.md +0 -20
- package/skills/log-audit/SKILL.md +0 -66
- package/skills/log-audit/reference/CLAUDE.md +0 -9
- package/skills/log-audit/reference/charter.md +0 -52
- package/skills/log-audit/scripts/CLAUDE.md +0 -27
- package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
- package/skills/log-audit/scripts/collect_log_window.py +0 -199
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
- package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
- package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
- package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
- package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
- package/skills/logifix/CLAUDE.md +0 -36
- package/skills/logifix/SKILL.md +0 -69
- package/skills/logifix/scripts/CLAUDE.md +0 -16
- package/skills/logifix/scripts/logifix.ps1 +0 -205
- package/skills/monitor-open-prs/CLAUDE.md +0 -34
- package/skills/monitor-open-prs/SKILL.md +0 -88
- package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
- package/skills/monitor-open-prs/test_skill_contract.py +0 -27
- package/skills/post-audit-findings/SKILL.md +0 -80
- package/skills/pr-consistency-audit/CLAUDE.md +0 -34
- package/skills/pr-consistency-audit/SKILL.md +0 -116
- package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
- package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
- package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
- package/skills/pr-review-responder/CLAUDE.md +0 -35
- package/skills/pr-review-responder/EXAMPLES.md +0 -590
- package/skills/pr-review-responder/PRINCIPLES.md +0 -533
- package/skills/pr-review-responder/README.md +0 -168
- package/skills/pr-review-responder/SKILL.md +0 -203
- package/skills/pr-review-responder/TESTING.md +0 -350
- package/skills/pr-scope-resolve/SKILL.md +0 -45
- package/skills/pre-compact/CLAUDE.md +0 -24
- package/skills/pre-compact/SKILL.md +0 -134
- package/skills/qbug/CLAUDE.md +0 -40
- package/skills/qbug/SKILL.md +0 -387
- package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
- package/skills/refine/CLAUDE.md +0 -44
- package/skills/refine/SKILL.md +0 -262
- package/skills/refine/templates/CLAUDE.md +0 -17
- package/skills/refine/templates/implementation-notes-template.html +0 -56
- package/skills/refine/templates/plan-template.md +0 -60
- package/skills/research-mode/CLAUDE.md +0 -35
- package/skills/research-mode/SKILL.md +0 -53
- package/skills/structure-prompt/CLAUDE.md +0 -42
- package/skills/structure-prompt/SKILL.md +0 -44
- package/skills/structure-prompt/reference/CLAUDE.md +0 -28
- package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
- package/skills/structure-prompt/reference/block-classification.md +0 -27
- package/skills/structure-prompt/reference/canonical-case.md +0 -48
- package/skills/structure-prompt/reference/citation-depth.md +0 -70
- package/skills/structure-prompt/reference/cleanup.md +0 -33
- package/skills/structure-prompt/reference/constraints.md +0 -33
- package/skills/structure-prompt/reference/directives.md +0 -37
- package/skills/structure-prompt/reference/examples.md +0 -72
- package/skills/structure-prompt/reference/instantiation.md +0 -51
- package/skills/structure-prompt/reference/output-contract.md +0 -72
- package/skills/structure-prompt/reference/per-category.md +0 -23
- package/skills/structure-prompt/reference/persona.md +0 -38
- package/skills/structure-prompt/reference/research.md +0 -33
- package/skills/structure-prompt/reference/structure.md +0 -28
- package/skills/verified-build/CLAUDE.md +0 -33
- package/skills/verified-build/SKILL.md +0 -33
|
@@ -12,7 +12,8 @@ import logging
|
|
|
12
12
|
import sys
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
16
17
|
|
|
17
18
|
try:
|
|
18
19
|
from hooks_constants.python_style_checks_constants import (
|
|
@@ -20,7 +21,6 @@ try:
|
|
|
20
21
|
MINIMUM_ARGUMENT_COUNT,
|
|
21
22
|
)
|
|
22
23
|
except ModuleNotFoundError:
|
|
23
|
-
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
24
24
|
if _hooks_directory not in sys.path:
|
|
25
25
|
sys.path.insert(0, _hooks_directory)
|
|
26
26
|
from hooks_constants.python_style_checks_constants import (
|
|
@@ -28,14 +28,35 @@ except ModuleNotFoundError:
|
|
|
28
28
|
MINIMUM_ARGUMENT_COUNT,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
try:
|
|
32
|
+
from validators.python_style_helpers import (
|
|
33
|
+
FunctionNode,
|
|
34
|
+
blank_line_for_source,
|
|
35
|
+
function_start_line,
|
|
36
|
+
gap_is_blank_only,
|
|
37
|
+
iter_function_definitions,
|
|
38
|
+
real_newline_lines,
|
|
39
|
+
top_level_functions,
|
|
40
|
+
)
|
|
41
|
+
except ModuleNotFoundError:
|
|
42
|
+
if _hooks_directory not in sys.path:
|
|
43
|
+
sys.path.insert(0, _hooks_directory)
|
|
44
|
+
from validators.python_style_helpers import (
|
|
45
|
+
FunctionNode,
|
|
46
|
+
blank_line_for_source,
|
|
47
|
+
function_start_line,
|
|
48
|
+
gap_is_blank_only,
|
|
49
|
+
iter_function_definitions,
|
|
50
|
+
real_newline_lines,
|
|
51
|
+
top_level_functions,
|
|
52
|
+
)
|
|
53
|
+
|
|
31
54
|
logger = logging.getLogger(__name__)
|
|
32
55
|
|
|
33
56
|
VIEW_SUFFIX = "_view"
|
|
34
57
|
REQUEST_PARAM = "request"
|
|
35
58
|
VIEWS_FILENAME = "views.py"
|
|
36
59
|
|
|
37
|
-
FunctionNode = Union[ast.FunctionDef, ast.AsyncFunctionDef]
|
|
38
|
-
|
|
39
60
|
|
|
40
61
|
@dataclass
|
|
41
62
|
class Violation:
|
|
@@ -50,13 +71,13 @@ class Violation:
|
|
|
50
71
|
return f"{self.file}:{self.line}: {self.message}"
|
|
51
72
|
|
|
52
73
|
|
|
53
|
-
def check_imports_at_top(tree: ast.AST, filename: str) ->
|
|
74
|
+
def check_imports_at_top(tree: ast.AST, filename: str) -> list[Violation]:
|
|
54
75
|
"""Check that all imports sit at the top of the file.
|
|
55
76
|
|
|
56
77
|
Flags module-level imports that follow other statements and imports
|
|
57
78
|
nested inside function or method bodies.
|
|
58
79
|
"""
|
|
59
|
-
violations:
|
|
80
|
+
violations: list[Violation] = []
|
|
60
81
|
violations.extend(_check_module_level_import_order(tree, filename))
|
|
61
82
|
violations.extend(_check_no_inline_imports(tree, filename))
|
|
62
83
|
return violations
|
|
@@ -75,11 +96,11 @@ def _is_docstring_statement(statement: ast.stmt) -> bool:
|
|
|
75
96
|
return isinstance(literal, ast.Constant) and isinstance(literal.value, str)
|
|
76
97
|
|
|
77
98
|
|
|
78
|
-
def _check_module_level_import_order(tree: ast.AST, filename: str) ->
|
|
99
|
+
def _check_module_level_import_order(tree: ast.AST, filename: str) -> list[Violation]:
|
|
79
100
|
"""Flag module-level imports that appear after other statements."""
|
|
80
101
|
if not isinstance(tree, ast.Module):
|
|
81
102
|
return []
|
|
82
|
-
violations:
|
|
103
|
+
violations: list[Violation] = []
|
|
83
104
|
has_seen_non_import = False
|
|
84
105
|
for each_statement in tree.body:
|
|
85
106
|
is_import = _is_import_statement(each_statement)
|
|
@@ -96,17 +117,17 @@ def _check_module_level_import_order(tree: ast.AST, filename: str) -> List[Viola
|
|
|
96
117
|
return violations
|
|
97
118
|
|
|
98
119
|
|
|
99
|
-
def _check_no_inline_imports(tree: ast.AST, filename: str) ->
|
|
120
|
+
def _check_no_inline_imports(tree: ast.AST, filename: str) -> list[Violation]:
|
|
100
121
|
"""Flag import statements located inside function or method bodies."""
|
|
101
|
-
violations:
|
|
102
|
-
for each_function_node in
|
|
122
|
+
violations: list[Violation] = []
|
|
123
|
+
for each_function_node in iter_function_definitions(tree):
|
|
103
124
|
violations.extend(_inline_imports_in(each_function_node, filename))
|
|
104
125
|
return violations
|
|
105
126
|
|
|
106
127
|
|
|
107
|
-
def _inline_imports_in(function_node: FunctionNode, filename: str) ->
|
|
128
|
+
def _inline_imports_in(function_node: FunctionNode, filename: str) -> list[Violation]:
|
|
108
129
|
"""Return violations for imports nested inside a single function."""
|
|
109
|
-
violations:
|
|
130
|
+
violations: list[Violation] = []
|
|
110
131
|
for each_descendant in ast.walk(function_node):
|
|
111
132
|
if isinstance(each_descendant, (ast.Import, ast.ImportFrom)):
|
|
112
133
|
violations.append(
|
|
@@ -119,21 +140,14 @@ def _inline_imports_in(function_node: FunctionNode, filename: str) -> List[Viola
|
|
|
119
140
|
return violations
|
|
120
141
|
|
|
121
142
|
|
|
122
|
-
def
|
|
123
|
-
"""Yield every function and async-function definition in the tree."""
|
|
124
|
-
for each_node in ast.walk(tree):
|
|
125
|
-
if isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
126
|
-
yield each_node
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def check_no_empty_line_after_decorators(source: str, filename: str) -> List[Violation]:
|
|
143
|
+
def check_no_empty_line_after_decorators(source: str, filename: str) -> list[Violation]:
|
|
130
144
|
"""Check that no empty line separates a decorator from its function."""
|
|
131
|
-
violations:
|
|
145
|
+
violations: list[Violation] = []
|
|
132
146
|
try:
|
|
133
147
|
tree = ast.parse(source)
|
|
134
148
|
except SyntaxError:
|
|
135
149
|
return violations
|
|
136
|
-
for each_function_node in
|
|
150
|
+
for each_function_node in iter_function_definitions(tree):
|
|
137
151
|
violation = _decorator_gap_violation(each_function_node, filename)
|
|
138
152
|
if violation is not None:
|
|
139
153
|
violations.append(violation)
|
|
@@ -142,12 +156,13 @@ def check_no_empty_line_after_decorators(source: str, filename: str) -> List[Vio
|
|
|
142
156
|
|
|
143
157
|
def _decorator_gap_violation(
|
|
144
158
|
function_node: FunctionNode, filename: str
|
|
145
|
-
) ->
|
|
159
|
+
) -> Violation | None:
|
|
146
160
|
"""Return a violation when an empty line separates a decorator from its function."""
|
|
147
161
|
if not function_node.decorator_list:
|
|
148
162
|
return None
|
|
149
163
|
last_decorator_line = max(
|
|
150
|
-
each_decorator.
|
|
164
|
+
each_decorator.end_lineno or each_decorator.lineno
|
|
165
|
+
for each_decorator in function_node.decorator_list
|
|
151
166
|
)
|
|
152
167
|
if function_node.lineno - last_decorator_line <= 1:
|
|
153
168
|
return None
|
|
@@ -158,15 +173,13 @@ def _decorator_gap_violation(
|
|
|
158
173
|
)
|
|
159
174
|
|
|
160
175
|
|
|
161
|
-
def check_blank_lines_between_functions(
|
|
162
|
-
source: str, filename: str
|
|
163
|
-
) -> List[Violation]:
|
|
176
|
+
def check_blank_lines_between_functions(source: str, filename: str) -> list[Violation]:
|
|
164
177
|
"""Check that consecutive top-level functions carry the expected blank gap."""
|
|
165
|
-
violations:
|
|
166
|
-
source_lines =
|
|
167
|
-
|
|
178
|
+
violations: list[Violation] = []
|
|
179
|
+
source_lines = real_newline_lines(source)
|
|
180
|
+
ordered_functions = top_level_functions(source)
|
|
168
181
|
for each_current_function, each_next_function in zip(
|
|
169
|
-
|
|
182
|
+
ordered_functions, ordered_functions[1:], strict=False
|
|
170
183
|
):
|
|
171
184
|
violation = _spacing_violation(
|
|
172
185
|
each_current_function, each_next_function, source_lines, filename
|
|
@@ -176,72 +189,12 @@ def check_blank_lines_between_functions(
|
|
|
176
189
|
return violations
|
|
177
190
|
|
|
178
191
|
|
|
179
|
-
def _top_level_functions(source: str) -> List[FunctionNode]:
|
|
180
|
-
"""Return the module's top-level function definitions, ordered by line."""
|
|
181
|
-
try:
|
|
182
|
-
tree = ast.parse(source)
|
|
183
|
-
except SyntaxError:
|
|
184
|
-
return []
|
|
185
|
-
if not isinstance(tree, ast.Module):
|
|
186
|
-
return []
|
|
187
|
-
functions: List[FunctionNode] = [
|
|
188
|
-
node
|
|
189
|
-
for node in tree.body
|
|
190
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
191
|
-
]
|
|
192
|
-
functions.sort(key=lambda function_node: function_node.lineno)
|
|
193
|
-
return functions
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def _real_newline_lines(source: str) -> List[str]:
|
|
197
|
-
"""Split source on CR, LF, and CRLF only, keeping each line ending.
|
|
198
|
-
|
|
199
|
-
``str.splitlines`` breaks on form feed and other control characters that
|
|
200
|
-
Python's line numbering ignores, so its indices drift from ``ast`` line
|
|
201
|
-
numbers. This splitter keeps line indices aligned with ``ast`` line numbers.
|
|
202
|
-
"""
|
|
203
|
-
lines: List[str] = []
|
|
204
|
-
line_start = 0
|
|
205
|
-
scan_index = 0
|
|
206
|
-
total_length = len(source)
|
|
207
|
-
while scan_index < total_length:
|
|
208
|
-
character = source[scan_index]
|
|
209
|
-
if character == "\r":
|
|
210
|
-
scan_index += 1
|
|
211
|
-
if scan_index < total_length and source[scan_index] == "\n":
|
|
212
|
-
scan_index += 1
|
|
213
|
-
elif character == "\n":
|
|
214
|
-
scan_index += 1
|
|
215
|
-
else:
|
|
216
|
-
scan_index += 1
|
|
217
|
-
continue
|
|
218
|
-
lines.append(source[line_start:scan_index])
|
|
219
|
-
line_start = scan_index
|
|
220
|
-
if line_start < total_length:
|
|
221
|
-
lines.append(source[line_start:])
|
|
222
|
-
return lines
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _function_start_line(function_node: FunctionNode) -> int:
|
|
226
|
-
"""Return the first source line of a function, counting its decorators."""
|
|
227
|
-
if not function_node.decorator_list:
|
|
228
|
-
return function_node.lineno
|
|
229
|
-
return min(
|
|
230
|
-
each_decorator.lineno for each_decorator in function_node.decorator_list
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _gap_is_blank_only(gap_lines: List[str]) -> bool:
|
|
235
|
-
"""Return True when every line between two functions is blank."""
|
|
236
|
-
return all(each_line.strip() == "" for each_line in gap_lines)
|
|
237
|
-
|
|
238
|
-
|
|
239
192
|
def _spacing_violation(
|
|
240
193
|
current_function: FunctionNode,
|
|
241
194
|
next_function: FunctionNode,
|
|
242
|
-
|
|
195
|
+
all_source_lines: list[str],
|
|
243
196
|
filename: str,
|
|
244
|
-
) ->
|
|
197
|
+
) -> Violation | None:
|
|
245
198
|
"""Return a violation when two functions hold the wrong blank-line count.
|
|
246
199
|
|
|
247
200
|
A run of purely empty lines is measured; a run holding any other content
|
|
@@ -250,9 +203,9 @@ def _spacing_violation(
|
|
|
250
203
|
current_end = current_function.end_lineno
|
|
251
204
|
if current_end is None:
|
|
252
205
|
return None
|
|
253
|
-
next_start =
|
|
254
|
-
gap_lines =
|
|
255
|
-
if not
|
|
206
|
+
next_start = function_start_line(next_function)
|
|
207
|
+
gap_lines = all_source_lines[current_end : next_start - 1]
|
|
208
|
+
if not gap_is_blank_only(gap_lines):
|
|
256
209
|
return None
|
|
257
210
|
blank_line_count = len(gap_lines)
|
|
258
211
|
if blank_line_count == EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS:
|
|
@@ -265,12 +218,12 @@ def _spacing_violation(
|
|
|
265
218
|
)
|
|
266
219
|
|
|
267
220
|
|
|
268
|
-
def check_view_function_naming(tree: ast.AST, filename: str) ->
|
|
221
|
+
def check_view_function_naming(tree: ast.AST, filename: str) -> list[Violation]:
|
|
269
222
|
"""Check that request-handling functions in views.py end with _view."""
|
|
270
223
|
if not filename.endswith(VIEWS_FILENAME):
|
|
271
224
|
return []
|
|
272
|
-
violations:
|
|
273
|
-
for each_function_node in
|
|
225
|
+
violations: list[Violation] = []
|
|
226
|
+
for each_function_node in iter_function_definitions(tree):
|
|
274
227
|
if _is_misnamed_view(each_function_node):
|
|
275
228
|
violations.append(
|
|
276
229
|
Violation(
|
|
@@ -292,58 +245,84 @@ def _is_misnamed_view(function_node: FunctionNode) -> bool:
|
|
|
292
245
|
return not function_node.name.endswith(VIEW_SUFFIX)
|
|
293
246
|
|
|
294
247
|
|
|
248
|
+
def _decorator_span_line_numbers(all_decorators: list[ast.expr]) -> set[int]:
|
|
249
|
+
"""Return every source line a decorator expression occupies."""
|
|
250
|
+
occupied_lines: set[int] = set()
|
|
251
|
+
for each_decorator in all_decorators:
|
|
252
|
+
end_line = each_decorator.end_lineno or each_decorator.lineno
|
|
253
|
+
occupied_lines.update(range(each_decorator.lineno, end_line + 1))
|
|
254
|
+
return occupied_lines
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _blank_gap_lines_for_function(
|
|
258
|
+
function_node: FunctionNode, all_source_lines: list[str]
|
|
259
|
+
) -> set[int]:
|
|
260
|
+
"""Return blank line numbers between one function's decorators and its def."""
|
|
261
|
+
all_decorators = function_node.decorator_list
|
|
262
|
+
if not all_decorators:
|
|
263
|
+
return set()
|
|
264
|
+
occupied_lines = _decorator_span_line_numbers(all_decorators)
|
|
265
|
+
first_decorator_line = min(
|
|
266
|
+
each_decorator.lineno for each_decorator in all_decorators
|
|
267
|
+
)
|
|
268
|
+
return {
|
|
269
|
+
each_line_number
|
|
270
|
+
for each_line_number in range(first_decorator_line, function_node.lineno)
|
|
271
|
+
if each_line_number not in occupied_lines
|
|
272
|
+
and all_source_lines[each_line_number - 1].strip() == ""
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
295
276
|
def fix_empty_lines_after_decorators(source: str) -> str:
|
|
296
|
-
"""Remove empty lines between decorators and their function definitions.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
stripped = each_line.strip()
|
|
301
|
-
if should_skip_next_blank and stripped == "":
|
|
302
|
-
continue
|
|
303
|
-
should_skip_next_blank = stripped.startswith("@")
|
|
304
|
-
result_lines.append(each_line)
|
|
305
|
-
return "".join(result_lines)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def _blank_line_for_source(source: str) -> str:
|
|
309
|
-
"""Return the blank-line string matching the source newline convention.
|
|
310
|
-
|
|
311
|
-
Every disk reader in this package loads source through
|
|
312
|
-
Path.read_text(), whose universal-newline translation turns a bare
|
|
313
|
-
\\r or a \\r\\n pair into \\n before the string reaches this function.
|
|
314
|
-
A bare \\r never arrives from a file on disk. The \\r\\n branch below
|
|
315
|
-
serves an in-memory caller that builds a CRLF string directly.
|
|
277
|
+
"""Remove empty lines between decorators and their function definitions.
|
|
278
|
+
|
|
279
|
+
Blank lines separating stacked decorators, and blank lines between the
|
|
280
|
+
last line of a multi-line decorator and the def, are both removed.
|
|
316
281
|
"""
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
282
|
+
try:
|
|
283
|
+
tree = ast.parse(source)
|
|
284
|
+
except SyntaxError:
|
|
285
|
+
return source
|
|
286
|
+
source_lines = real_newline_lines(source)
|
|
287
|
+
blank_line_numbers: set[int] = set()
|
|
288
|
+
for each_function_node in iter_function_definitions(tree):
|
|
289
|
+
blank_line_numbers |= _blank_gap_lines_for_function(
|
|
290
|
+
each_function_node, source_lines
|
|
291
|
+
)
|
|
292
|
+
if not blank_line_numbers:
|
|
293
|
+
return source
|
|
294
|
+
return "".join(
|
|
295
|
+
each_line
|
|
296
|
+
for each_line_number, each_line in enumerate(source_lines, start=1)
|
|
297
|
+
if each_line_number not in blank_line_numbers
|
|
298
|
+
)
|
|
320
299
|
|
|
321
300
|
|
|
322
|
-
def _normalized_gap(all_gap_lines:
|
|
301
|
+
def _normalized_gap(all_gap_lines: list[str], blank_line: str) -> list[str]:
|
|
323
302
|
"""Return the between-function gap normalized to the expected blank count.
|
|
324
303
|
|
|
325
304
|
A gap that is entirely blank becomes exactly the expected number of blank
|
|
326
305
|
lines. A gap that holds any non-blank line stays untouched so no comment,
|
|
327
306
|
class, or statement in it is lost.
|
|
328
307
|
"""
|
|
329
|
-
if not
|
|
308
|
+
if not gap_is_blank_only(all_gap_lines):
|
|
330
309
|
return all_gap_lines
|
|
331
310
|
return [blank_line] * EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS
|
|
332
311
|
|
|
333
312
|
|
|
334
313
|
def fix_function_spacing(source: str) -> str:
|
|
335
314
|
"""Normalize blank lines between top-level functions to exactly two."""
|
|
336
|
-
functions =
|
|
315
|
+
functions = top_level_functions(source)
|
|
337
316
|
if len(functions) <= 1:
|
|
338
317
|
return source
|
|
339
|
-
source_lines =
|
|
340
|
-
blank_line =
|
|
318
|
+
source_lines = real_newline_lines(source)
|
|
319
|
+
blank_line = blank_line_for_source(source)
|
|
341
320
|
previous_end_line = functions[0].end_lineno
|
|
342
321
|
if previous_end_line is None:
|
|
343
322
|
return source
|
|
344
|
-
rebuilt:
|
|
323
|
+
rebuilt: list[str] = list(source_lines[:previous_end_line])
|
|
345
324
|
for each_next_function in functions[1:]:
|
|
346
|
-
next_start_line =
|
|
325
|
+
next_start_line = function_start_line(each_next_function)
|
|
347
326
|
gap_lines = source_lines[previous_end_line : next_start_line - 1]
|
|
348
327
|
rebuilt.extend(_normalized_gap(gap_lines, blank_line))
|
|
349
328
|
next_end_line = each_next_function.end_lineno
|
|
@@ -358,9 +337,8 @@ def fix_function_spacing(source: str) -> str:
|
|
|
358
337
|
def fix_file(file_path: Path) -> bool:
|
|
359
338
|
"""Apply the safe blank-line fixes and report whether the file changed.
|
|
360
339
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
\\n before either fix function below inspects the text.
|
|
340
|
+
Path.read_text() normalizes every line ending to \\n before either fix
|
|
341
|
+
below inspects the text.
|
|
364
342
|
"""
|
|
365
343
|
try:
|
|
366
344
|
original = file_path.read_text(encoding="utf-8")
|
|
@@ -374,9 +352,9 @@ def fix_file(file_path: Path) -> bool:
|
|
|
374
352
|
return True
|
|
375
353
|
|
|
376
354
|
|
|
377
|
-
def validate_file(file_path: Path) ->
|
|
355
|
+
def validate_file(file_path: Path) -> list[Violation]:
|
|
378
356
|
"""Validate a Python file against every style check."""
|
|
379
|
-
violations:
|
|
357
|
+
violations: list[Violation] = []
|
|
380
358
|
filename = str(file_path)
|
|
381
359
|
try:
|
|
382
360
|
source = file_path.read_text(encoding="utf-8")
|
|
@@ -403,7 +381,7 @@ def main() -> int:
|
|
|
403
381
|
if len(sys.argv) < minimum_argument_count:
|
|
404
382
|
logger.error("Usage: %s <file1.py> [file2.py ...]", Path(sys.argv[0]).name)
|
|
405
383
|
return 1
|
|
406
|
-
all_violations:
|
|
384
|
+
all_violations: list[Violation] = []
|
|
407
385
|
for each_file_arg in sys.argv[1:]:
|
|
408
386
|
file_path = Path(each_file_arg)
|
|
409
387
|
if not file_path.exists():
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Shared source-line and function-discovery helpers for the style checks.
|
|
2
|
+
|
|
3
|
+
These pure helpers underlie the style checks and the blank-line fixers:
|
|
4
|
+
splitting source into ast-aligned lines, locating function definitions, and
|
|
5
|
+
matching the source newline convention.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
|
|
11
|
+
FunctionNode = ast.FunctionDef | ast.AsyncFunctionDef
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def iter_function_definitions(tree: ast.AST) -> Iterator[FunctionNode]:
|
|
15
|
+
"""Yield every function and async-function definition in the tree."""
|
|
16
|
+
for each_node in ast.walk(tree):
|
|
17
|
+
if isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
18
|
+
yield each_node
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def top_level_functions(source: str) -> list[FunctionNode]:
|
|
22
|
+
"""Return the module's top-level function definitions, ordered by line."""
|
|
23
|
+
try:
|
|
24
|
+
tree = ast.parse(source)
|
|
25
|
+
except SyntaxError:
|
|
26
|
+
return []
|
|
27
|
+
if not isinstance(tree, ast.Module):
|
|
28
|
+
return []
|
|
29
|
+
functions: list[FunctionNode] = [
|
|
30
|
+
node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
31
|
+
]
|
|
32
|
+
functions.sort(key=lambda function_node: function_node.lineno)
|
|
33
|
+
return functions
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def function_start_line(function_node: FunctionNode) -> int:
|
|
37
|
+
"""Return the first source line of a function, counting its decorators."""
|
|
38
|
+
if not function_node.decorator_list:
|
|
39
|
+
return function_node.lineno
|
|
40
|
+
return min(each_decorator.lineno for each_decorator in function_node.decorator_list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def gap_is_blank_only(all_gap_lines: list[str]) -> bool:
|
|
44
|
+
"""Return True when every line between two functions is blank."""
|
|
45
|
+
return all(each_line.strip() == "" for each_line in all_gap_lines)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def blank_line_for_source(source: str) -> str:
|
|
49
|
+
"""Return the blank-line string matching the source newline convention.
|
|
50
|
+
|
|
51
|
+
Path.read_text() normalizes disk newlines to \\n before this runs, so the
|
|
52
|
+
CRLF branch serves an in-memory caller that builds a CRLF string directly.
|
|
53
|
+
"""
|
|
54
|
+
if "\r\n" in source:
|
|
55
|
+
return "\r\n"
|
|
56
|
+
return "\n"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _advance_past_newline(source: str, scan_index: int) -> int | None:
|
|
60
|
+
"""Return the index past a CR, LF, or CRLF at scan_index, or None.
|
|
61
|
+
|
|
62
|
+
None marks a character that is not a line ending.
|
|
63
|
+
"""
|
|
64
|
+
character = source[scan_index]
|
|
65
|
+
if character == "\r":
|
|
66
|
+
scan_index += 1
|
|
67
|
+
if scan_index < len(source) and source[scan_index] == "\n":
|
|
68
|
+
scan_index += 1
|
|
69
|
+
return scan_index
|
|
70
|
+
if character == "\n":
|
|
71
|
+
return scan_index + 1
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def real_newline_lines(source: str) -> list[str]:
|
|
76
|
+
"""Split source on CR, LF, and CRLF only, keeping each line ending.
|
|
77
|
+
|
|
78
|
+
Line indices stay aligned with ast line numbers because the split
|
|
79
|
+
ignores form feed and other control characters ast does not count.
|
|
80
|
+
"""
|
|
81
|
+
lines: list[str] = []
|
|
82
|
+
line_start = 0
|
|
83
|
+
scan_index = 0
|
|
84
|
+
total_length = len(source)
|
|
85
|
+
while scan_index < total_length:
|
|
86
|
+
line_end = _advance_past_newline(source, scan_index)
|
|
87
|
+
if line_end is None:
|
|
88
|
+
scan_index += 1
|
|
89
|
+
continue
|
|
90
|
+
scan_index = line_end
|
|
91
|
+
lines.append(source[line_start:scan_index])
|
|
92
|
+
line_start = scan_index
|
|
93
|
+
if line_start < total_length:
|
|
94
|
+
lines.append(source[line_start:])
|
|
95
|
+
return lines
|