Style Guide¶
Every team has Python conventions a linter can't express: no print() in committed code, no bare except:, no import *. captain-hook ships no rules of its own — you author them as StyleRule subclasses and hand them to styleguide(), which parses each edited file, runs every rule, and reports only the violations your edit actually introduced.
from __future__ import annotations
import ast
from captain_hook import Allow, Input, Warn
from captain_hook.style import StyleDiffRule, StyleRule, matchers as M, styleguide
class NoPrint(StyleRule):
"""
print() calls don't belong in committed code:
- {violations}
Use a logger (logger.info(...)) instead.
"""
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(),
}
match = M.calls("print")
label = "print() call"
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(),
}
match = M.kind(ast.ExceptHandler).where(lambda n: n.type is None)
label = "bare except"
class NoNewWildcardImport(StyleDiffRule):
"""
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(),
}
match = M.imports.where(lambda n: any(alias.name == "*" for alias in n.names))
styleguide(NoPrint, NoBareExcept, NoNewWildcardImport)
What to learn: A rule is a subclass whose docstring is the message — {violations} is substituted at fire time, and the docstring doubles as the rule's API-reference text. The class name is the identity (NoPrint → no-print). Each rule is data: set match to a composable matcher built from the matchers module (imported as M, with an optional label) and the base class does the walking — NoPrint is just M.calls("print"), and NoBareExcept refines a node type with a one-off predicate via .where(...). The runner renders label (line N) and, crucially, drops any violation whose line you didn't touch — so editing one function never lights up a pre-existing print() elsewhere in the file. NoNewWildcardImport subclasses StyleDiffRule instead: it flags nodes matching match in the new tree that weren't in the old one, so it reports only what the edit added. Reach for an explicit check() method only when a rule's logic can't be expressed as a matcher. A single styleguide(...) call registers one hook; pass block=True or scope it with only_if= to register a second, differently-scoped hook.