Style guide
Enforce the team conventions a linter can’t express, flagging only the violations your edit introduced.
Every team has rules a linter won’t catch: no print() in committed code, no import *, no bare except:. captain-hook ships no rules of its own. You write cross-language shape checks as ast-grep patterns, the Python-specific ones as StyleRule subclasses, and hand them all to styleguide(). It parses each edited file, runs every rule, and reports only what your edit added.
"""Enforce team conventions a linter can't express on every Python edit."""
from __future__ import annotations
import ast
from captain_hook import Allow, Input, Warn
from captain_hook.style import StyleRule, ast_grep_diff_rule, ast_grep_rule, styleguide
from captain_hook.style import matchers as M
NoPrint = ast_grep_rule(
"NoPrint",
pattern="print($$$)",
message="""
print() calls don't belong in committed code:
- {violations}
Use a logger (logger.info(...)) instead.
""",
label="print() call",
tests={
Input(file="app.py", content="def f():\n print('debug')\n"): Warn(),
Input(file="app.py", content="def f():\n logger.info('ok')\n"): Allow(),
},
)
NoNewWildcardImport = ast_grep_diff_rule(
"NoNewWildcardImport",
pattern="from $MOD import *",
message="""
Wildcard import added by this edit:
- {violations}
Import the names you use explicitly instead of `import *`.
""",
tests={
Input(file="m.py", old="import os\n", content="from os import *\n"): Warn(),
Input(file="m.py", old="from os import *\n", content="from os import *\nx = 1\n"): Allow(),
},
)
class NoBareExcept(StyleRule):
"""
Bare `except:` swallows every error, including KeyboardInterrupt:
- {violations}
Catch a specific exception type instead.
"""
tests = {
Input(file="app.py", content="try:\n f()\nexcept:\n pass\n"): Warn(),
Input(file="app.py", content="try:\n f()\nexcept ValueError:\n pass\n"): Allow(),
}
label = "bare except"
match = M.kind(ast.ExceptHandler).where(lambda n: isinstance(n, ast.ExceptHandler) and n.type is None)
styleguide(NoPrint, NoNewWildcardImport, NoBareExcept)This page composes three primitives in one styleguide() call: ast_grep_rule() for call shapes, ast_grep_diff_rule() for imports the edit adds, and a StyleRule subclass for Python-AST facts no pattern string expresses.
What it catches
print('debug') # print() calls don't belong in committed code; use a logger
from os import * # wildcard import added by this edit; import names explicitly
except: # bare except swallows every error, including KeyboardInterruptWhat it allows
logger.info('ok') # logging, not a print() call
from os import * # pre-existing wildcard import the edit didn't add
except ValueError: # catches a specific exception typeThe catch / allow split mirrors the hook inline tests, so it stays true as the hook evolves.
Run it yourself
uvx capt-hook --hooks docs/examples test