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 KeyboardInterrupt

What 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 type

The 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

See also