capt-hook 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {capt_hook-0.2.0 → capt_hook-0.3.0}/PKG-INFO +1 -1
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/__init__.py +5 -11
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/cli.py +85 -66
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/primitives/__init__.py +0 -2
- {capt_hook-0.2.0/captain_hook/styleguide → capt_hook-0.3.0/captain_hook/style}/__init__.py +13 -55
- capt_hook-0.3.0/captain_hook/style/matchers.py +372 -0
- capt_hook-0.3.0/captain_hook/style/types.py +84 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/pyproject.toml +2 -1
- capt_hook-0.2.0/captain_hook/styleguide/query.py +0 -238
- capt_hook-0.2.0/captain_hook/styleguide/types.py +0 -70
- {capt_hook-0.2.0 → capt_hook-0.3.0}/LICENSE +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/README.md +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/__main__.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/app.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/command.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/conditions.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/context.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/events.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/file.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/llm/__init__.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/llm/backends.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/loader.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/log.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/primitives/audit.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-0.2.0/captain_hook → capt_hook-0.3.0/captain_hook/primitives}/workflow.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/prompt.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/py.typed +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/session.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/settings.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/state.py +0 -0
- {capt_hook-0.2.0/captain_hook/styleguide → capt_hook-0.3.0/captain_hook/style}/scope.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/tasks.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/tests/__init__.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/tests/helpers.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/tools.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/transcript/__init__.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/transcript/inputs.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/transcript/models.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/types.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-0.2.0 → capt_hook-0.3.0}/captain_hook/utils.py +0 -0
|
@@ -22,27 +22,25 @@ from captain_hook.primitives import (
|
|
|
22
22
|
GateVerdict,
|
|
23
23
|
NudgeVerdict,
|
|
24
24
|
PromptCheckVerdict,
|
|
25
|
-
audit,
|
|
26
25
|
block_command,
|
|
27
26
|
diff_lint,
|
|
28
27
|
gate,
|
|
29
|
-
lint,
|
|
30
28
|
llm_gate,
|
|
31
29
|
llm_nudge,
|
|
32
|
-
nudge,
|
|
33
30
|
prompt_check,
|
|
34
31
|
session_id_for,
|
|
35
|
-
styleguide,
|
|
36
32
|
warn_command,
|
|
37
33
|
)
|
|
34
|
+
from captain_hook.primitives.audit import audit
|
|
35
|
+
from captain_hook.primitives.lint import lint
|
|
38
36
|
from captain_hook.primitives.llm import llm_evaluate
|
|
37
|
+
from captain_hook.primitives.nudge import nudge
|
|
39
38
|
from captain_hook.prompt import Prompt, PromptMessage
|
|
40
39
|
from captain_hook.session import SessionSlot, SessionStore, session_state
|
|
41
40
|
from captain_hook.settings import AutoConf, HooksSettings, build_settings
|
|
42
41
|
from captain_hook.signals import cite_message, extract_signal_context, resolve_signals, score_signals, transcript_texts
|
|
43
42
|
from captain_hook.signals.nlp import Clause, NlpSignal, Phrase
|
|
44
43
|
from captain_hook.state import EchoTracker, HookState, PrimitiveState, workflow_state
|
|
45
|
-
from captain_hook.styleguide import StyleDiffRule, StyleRule, Violation
|
|
46
44
|
from captain_hook.tasks import Task, Tasks
|
|
47
45
|
from captain_hook.testing import Allow, Block, InlineTests, Input, TranscriptFixture, Warn
|
|
48
46
|
from captain_hook.tools import EditOp, TaskOp, WriteOp
|
|
@@ -101,8 +99,8 @@ from captain_hook.types import (
|
|
|
101
99
|
Waiting,
|
|
102
100
|
)
|
|
103
101
|
from captain_hook.utils import read_json
|
|
104
|
-
from captain_hook.workflow import Artifact, Step, Workflow, text_matches
|
|
105
|
-
from captain_hook.workflow import workflow as workflow
|
|
102
|
+
from captain_hook.primitives.workflow import Artifact, Step, Workflow, text_matches
|
|
103
|
+
from captain_hook.primitives.workflow import workflow as workflow
|
|
106
104
|
|
|
107
105
|
__all__ = [
|
|
108
106
|
# registration
|
|
@@ -165,10 +163,6 @@ __all__ = [
|
|
|
165
163
|
"prompt_check",
|
|
166
164
|
"PromptCheckVerdict",
|
|
167
165
|
"session_id_for",
|
|
168
|
-
"StyleDiffRule",
|
|
169
|
-
"StyleRule",
|
|
170
|
-
"styleguide",
|
|
171
|
-
"Violation",
|
|
172
166
|
"warn_command",
|
|
173
167
|
# signals
|
|
174
168
|
"cite_message",
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import argparse
|
|
4
3
|
import importlib.resources
|
|
5
4
|
import json
|
|
6
5
|
import os
|
|
7
6
|
import sys
|
|
8
7
|
from collections import defaultdict
|
|
8
|
+
from dataclasses import dataclass
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
import click
|
|
13
|
+
|
|
12
14
|
from captain_hook.app import _state, load_gitignore, reset
|
|
13
15
|
from captain_hook.context import HookContext
|
|
14
16
|
from captain_hook.dispatch import dispatch
|
|
@@ -19,6 +21,18 @@ from captain_hook.transcript import Transcript
|
|
|
19
21
|
from captain_hook.types import Event
|
|
20
22
|
|
|
21
23
|
DIST_NAME = "capt-hook"
|
|
24
|
+
EVENT_NAMES = ", ".join(n for e in Event if (n := e.name))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class CliState:
|
|
29
|
+
root: Path
|
|
30
|
+
hooks: str
|
|
31
|
+
|
|
32
|
+
def discover(self) -> None:
|
|
33
|
+
reset()
|
|
34
|
+
load_gitignore(self.root)
|
|
35
|
+
discover_hooks(self.hooks)
|
|
22
36
|
|
|
23
37
|
|
|
24
38
|
def example_hook_source() -> str:
|
|
@@ -212,41 +226,6 @@ def show_logs(session: str | None = None, tail: int | None = None) -> None:
|
|
|
212
226
|
print("\n".join(lines[-tail:] if tail else lines))
|
|
213
227
|
|
|
214
228
|
|
|
215
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
216
|
-
parser = argparse.ArgumentParser(
|
|
217
|
-
prog="capt-hook",
|
|
218
|
-
description="Captain Hook — declarative hook framework for Claude Code lifecycle events.",
|
|
219
|
-
)
|
|
220
|
-
parser.add_argument(
|
|
221
|
-
"--hooks",
|
|
222
|
-
default=None,
|
|
223
|
-
help="Path to hooks package directory (default: $CLAUDE_PROJECT_DIR/.claude/hooks)",
|
|
224
|
-
)
|
|
225
|
-
parser.add_argument("--root", default=None, help="Project root for gitignore and session resolution")
|
|
226
|
-
sub = parser.add_subparsers(dest="command", required=True)
|
|
227
|
-
|
|
228
|
-
run_parser = sub.add_parser("run", help="Dispatch a hook event (reads JSON from stdin, writes JSON to stdout)")
|
|
229
|
-
run_parser.add_argument("event", help=f"Event type: {', '.join(n for e in Event if (n := e.name))}")
|
|
230
|
-
run_parser.add_argument("--async", dest="async_", action="store_true", default=False, help="Run async hooks only")
|
|
231
|
-
|
|
232
|
-
settings_parser = sub.add_parser(
|
|
233
|
-
"generate-settings", help="Generate Claude Code settings JSON for .claude/settings.local.json"
|
|
234
|
-
)
|
|
235
|
-
settings_parser.add_argument("--hooks-dir", default=".claude/hooks", help="Hooks directory relative to project root")
|
|
236
|
-
settings_parser.add_argument("--no-merge", action="store_true", help="Output standalone JSON instead of merging")
|
|
237
|
-
settings_parser.add_argument("--from", dest="from_source", default=DIST_NAME, help=f"Package source for uvx --from (local path or PyPI spec, default: {DIST_NAME})")
|
|
238
|
-
|
|
239
|
-
test_parser = sub.add_parser("test", help="Run inline tests from all registered hooks")
|
|
240
|
-
test_parser.add_argument("--json", dest="json_output", action="store_true", help="Emit one JSON record per test (CI mode)")
|
|
241
|
-
sub.add_parser("init", help="Scaffold hooks directory, bin script, and settings")
|
|
242
|
-
|
|
243
|
-
logs_parser = sub.add_parser("logs", help="View a recent captain-hook session log")
|
|
244
|
-
logs_parser.add_argument("--session", default=None, help="Session id or transcript path (hashed) to view")
|
|
245
|
-
logs_parser.add_argument("--tail", type=int, default=None, help="Show only the last N lines")
|
|
246
|
-
|
|
247
|
-
return parser
|
|
248
|
-
|
|
249
|
-
|
|
250
229
|
def expected_kinds_from_state() -> dict[str, str]:
|
|
251
230
|
out: dict[str, str] = {}
|
|
252
231
|
for entry in _state.hooks:
|
|
@@ -305,37 +284,77 @@ def run_tests(json_output: bool = False) -> None:
|
|
|
305
284
|
sys.exit(1)
|
|
306
285
|
|
|
307
286
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
287
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
288
|
+
@click.option(
|
|
289
|
+
"--hooks",
|
|
290
|
+
default=None,
|
|
291
|
+
help="Path to hooks package directory (default: $CLAUDE_PROJECT_DIR/.claude/hooks)",
|
|
292
|
+
)
|
|
293
|
+
@click.option("--root", "root_path", default=None, help="Project root for gitignore and session resolution")
|
|
294
|
+
@click.pass_context
|
|
295
|
+
def cli(ctx: click.Context, hooks: str | None, root_path: str | None) -> None:
|
|
296
|
+
"""Captain Hook — declarative hook framework for Claude Code lifecycle events."""
|
|
297
|
+
root = Path(root_path) if root_path else Path(env) if (env := os.environ.get("CLAUDE_PROJECT_DIR")) else Path.cwd()
|
|
298
|
+
ctx.obj = CliState(root=root, hooks=hooks or str(root / ".claude" / "hooks"))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@cli.command(
|
|
302
|
+
short_help="Dispatch a hook event (reads JSON from stdin, writes JSON to stdout)",
|
|
303
|
+
help=(
|
|
304
|
+
"Dispatch a hook event (reads JSON from stdin, writes JSON to stdout).\n\n"
|
|
305
|
+
f"EVENT is one of: {EVENT_NAMES}."
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
@click.argument("event")
|
|
309
|
+
@click.option("--async", "async_", is_flag=True, default=False, help="Run async hooks only")
|
|
310
|
+
@click.pass_obj
|
|
311
|
+
def run(state: CliState, event: str, async_: bool) -> None:
|
|
312
|
+
state.discover()
|
|
313
|
+
run_event(event, async_=async_, root=state.root)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@cli.command(name="generate-settings")
|
|
317
|
+
@click.option("--hooks-dir", default=".claude/hooks", help="Hooks directory relative to project root")
|
|
318
|
+
@click.option("--no-merge", is_flag=True, default=False, help="Output standalone JSON instead of merging")
|
|
319
|
+
@click.option(
|
|
320
|
+
"--from",
|
|
321
|
+
"from_source",
|
|
322
|
+
default=DIST_NAME,
|
|
323
|
+
help=f"Package source for uvx --from (local path or PyPI spec, default: {DIST_NAME})",
|
|
324
|
+
)
|
|
325
|
+
@click.pass_obj
|
|
326
|
+
def generate_settings_cmd(state: CliState, hooks_dir: str, no_merge: bool, from_source: str) -> None:
|
|
327
|
+
"""Generate Claude Code settings JSON for .claude/settings.local.json."""
|
|
328
|
+
state.discover()
|
|
329
|
+
if no_merge:
|
|
330
|
+
click.echo(generate_settings_json(hooks_dir, from_source=from_source))
|
|
331
|
+
else:
|
|
332
|
+
settings_path = state.root / ".claude" / "settings.local.json"
|
|
333
|
+
click.echo(json.dumps(merge_settings(hooks_dir, settings_path, from_source=from_source), indent=2))
|
|
311
334
|
|
|
312
|
-
project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
|
|
313
|
-
root = Path(args.root) if args.root else Path(project_dir) if project_dir else Path.cwd()
|
|
314
|
-
hooks = args.hooks or str(root / ".claude" / "hooks")
|
|
315
335
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
336
|
+
@cli.command()
|
|
337
|
+
@click.option("--json", "json_output", is_flag=True, default=False, help="Emit one JSON record per test (CI mode)")
|
|
338
|
+
@click.pass_obj
|
|
339
|
+
def test(state: CliState, json_output: bool) -> None:
|
|
340
|
+
"""Run inline tests from all registered hooks."""
|
|
341
|
+
state.discover()
|
|
342
|
+
run_tests(json_output=json_output)
|
|
319
343
|
|
|
320
|
-
if args.command == "logs":
|
|
321
|
-
show_logs(session=args.session, tail=args.tail)
|
|
322
|
-
return
|
|
323
344
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
case _:
|
|
341
|
-
parser.error(f"Unknown command: {args.command}")
|
|
345
|
+
@cli.command()
|
|
346
|
+
@click.pass_obj
|
|
347
|
+
def init(state: CliState) -> None:
|
|
348
|
+
"""Scaffold hooks directory, bin script, and settings."""
|
|
349
|
+
init_project(state.root)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@cli.command()
|
|
353
|
+
@click.option("--session", default=None, help="Session id or transcript path (hashed) to view")
|
|
354
|
+
@click.option("--tail", type=int, default=None, help="Show only the last N lines")
|
|
355
|
+
def logs(session: str | None, tail: int | None) -> None:
|
|
356
|
+
"""View a recent captain-hook session log."""
|
|
357
|
+
show_logs(session=session, tail=tail)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
main = cli
|
|
@@ -29,7 +29,6 @@ from captain_hook.primitives.llm import (
|
|
|
29
29
|
)
|
|
30
30
|
from captain_hook.primitives.nudge import gate as gate
|
|
31
31
|
from captain_hook.primitives.nudge import nudge as nudge
|
|
32
|
-
from captain_hook.styleguide import styleguide as styleguide
|
|
33
32
|
|
|
34
33
|
__all__ = [
|
|
35
34
|
"GateVerdict",
|
|
@@ -46,6 +45,5 @@ __all__ = [
|
|
|
46
45
|
"nudge",
|
|
47
46
|
"prompt_check",
|
|
48
47
|
"session_id_for",
|
|
49
|
-
"styleguide",
|
|
50
48
|
"warn_command",
|
|
51
49
|
]
|
|
@@ -8,32 +8,10 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
|
|
9
9
|
from captain_hook.app import on
|
|
10
10
|
from captain_hook.state import hook_name
|
|
11
|
-
from captain_hook.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
ControlFlow,
|
|
16
|
-
Definition,
|
|
17
|
-
Function,
|
|
18
|
-
Import,
|
|
19
|
-
Kind,
|
|
20
|
-
Module,
|
|
21
|
-
Query,
|
|
22
|
-
Slot,
|
|
23
|
-
TypeChecking,
|
|
24
|
-
annotated_slots,
|
|
25
|
-
annotations,
|
|
26
|
-
calls,
|
|
27
|
-
has_future_annotations,
|
|
28
|
-
has_keyword,
|
|
29
|
-
is_name,
|
|
30
|
-
name_of,
|
|
31
|
-
named,
|
|
32
|
-
parent_map,
|
|
33
|
-
string_literals,
|
|
34
|
-
)
|
|
35
|
-
from captain_hook.styleguide.scope import changed_lines, read_source, reconstruct_pre
|
|
36
|
-
from captain_hook.styleguide.types import StyleDiffRule, StyleRule, Violation
|
|
11
|
+
from captain_hook.style import matchers
|
|
12
|
+
from captain_hook.style.matchers import Matcher
|
|
13
|
+
from captain_hook.style.scope import changed_lines, read_source, reconstruct_pre
|
|
14
|
+
from captain_hook.style.types import StyleDiffRule, StyleRule, Violation
|
|
37
15
|
from captain_hook.types import Action, Event, FilePath, HookResult, TCondition, TestFile, Tool
|
|
38
16
|
|
|
39
17
|
if TYPE_CHECKING:
|
|
@@ -45,31 +23,11 @@ GUARD_ONLY_IF: tuple[TCondition, ...] = (Tool("Edit|Write"), FilePath("*.py", pr
|
|
|
45
23
|
GUARD_SKIP_IF: tuple[TCondition, ...] = (TestFile(),)
|
|
46
24
|
|
|
47
25
|
__all__ = [
|
|
48
|
-
"
|
|
49
|
-
"Call",
|
|
50
|
-
"Class",
|
|
51
|
-
"ControlFlow",
|
|
52
|
-
"Definition",
|
|
53
|
-
"Function",
|
|
54
|
-
"Import",
|
|
55
|
-
"Kind",
|
|
56
|
-
"Module",
|
|
57
|
-
"Query",
|
|
58
|
-
"Slot",
|
|
26
|
+
"Matcher",
|
|
59
27
|
"StyleDiffRule",
|
|
60
28
|
"StyleRule",
|
|
61
|
-
"TypeChecking",
|
|
62
29
|
"Violation",
|
|
63
|
-
"
|
|
64
|
-
"annotations",
|
|
65
|
-
"calls",
|
|
66
|
-
"has_future_annotations",
|
|
67
|
-
"has_keyword",
|
|
68
|
-
"is_name",
|
|
69
|
-
"name_of",
|
|
70
|
-
"named",
|
|
71
|
-
"parent_map",
|
|
72
|
-
"string_literals",
|
|
30
|
+
"matchers",
|
|
73
31
|
"styleguide",
|
|
74
32
|
]
|
|
75
33
|
|
|
@@ -84,8 +42,8 @@ def styleguide(
|
|
|
84
42
|
) -> None:
|
|
85
43
|
"""Register one change-scoped hook applying the given style rules to Python edits and writes.
|
|
86
44
|
|
|
87
|
-
Each rule is a [`StyleRule`][captain_hook.
|
|
88
|
-
[`StyleDiffRule`][captain_hook.
|
|
45
|
+
Each rule is a [`StyleRule`][captain_hook.style.StyleRule] (or
|
|
46
|
+
[`StyleDiffRule`][captain_hook.style.StyleDiffRule]) subclass whose docstring is its
|
|
89
47
|
message. The single registered hook parses the edited file once, runs every rule against the
|
|
90
48
|
post-edit tree, scopes each violation to the changed lines, and emits one aggregated warning
|
|
91
49
|
(or block, when ``block`` is set). Call again with different ``only_if`` / ``skip_if`` /
|
|
@@ -123,11 +81,14 @@ def styleguide(
|
|
|
123
81
|
)(handler)
|
|
124
82
|
|
|
125
83
|
|
|
126
|
-
def validate(rule:
|
|
84
|
+
def validate(rule: object) -> type[StyleRule]:
|
|
127
85
|
if not (isinstance(rule, type) and issubclass(rule, StyleRule)):
|
|
128
86
|
raise TypeError(f"styleguide() expects StyleRule subclasses, got {rule!r}")
|
|
129
87
|
if rule.__doc__ is None:
|
|
130
88
|
raise ValueError(f"{rule.__name__} must define a docstring — it is the rule's message")
|
|
89
|
+
base = StyleDiffRule if issubclass(rule, StyleDiffRule) else StyleRule
|
|
90
|
+
if rule.match is None and rule.check is base.check:
|
|
91
|
+
raise TypeError(f"{rule.__name__} must define `match` or override `check`")
|
|
131
92
|
return rule
|
|
132
93
|
|
|
133
94
|
|
|
@@ -145,7 +106,7 @@ def run_rules(rules: list[StyleRule], evt: BaseHookEvent, *, block: bool, max_sh
|
|
|
145
106
|
sections := [
|
|
146
107
|
section
|
|
147
108
|
for rule in rules
|
|
148
|
-
if (section := run_one(rule, tree, pre_tree,
|
|
109
|
+
if (section := run_one(rule, tree, pre_tree, changed, max_shown)) is not None
|
|
149
110
|
]
|
|
150
111
|
):
|
|
151
112
|
return None
|
|
@@ -156,12 +117,9 @@ def run_one(
|
|
|
156
117
|
rule: StyleRule,
|
|
157
118
|
tree: ast.Module,
|
|
158
119
|
pre_tree: ast.Module | None,
|
|
159
|
-
source: str,
|
|
160
120
|
changed: set[int],
|
|
161
121
|
max_shown: int,
|
|
162
122
|
) -> str | None:
|
|
163
|
-
if rule.trigger and rule.trigger not in source:
|
|
164
|
-
return None
|
|
165
123
|
match rule:
|
|
166
124
|
case StyleDiffRule() if pre_tree is not None:
|
|
167
125
|
violations = rule.check(pre_tree, tree)
|