# 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](https://ast-grep.github.io) patterns, the Python-specific ones as [StyleRule](../../reference/primitives.md#captain_hook.style.StyleRule) subclasses, and hand them all to [styleguide()](../../reference/primitives.md#captain_hook.style.styleguide). It parses each edited file, runs every rule, and reports only what your edit added.

``` python
"""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()](../../reference/primitives.md#captain_hook.style.styleguide) call: [ast_grep_rule()](../../reference/primitives.md#captain_hook.style.ast_grep_rule) for call shapes, [ast_grep_diff_rule()](../../reference/primitives.md#captain_hook.style.ast_grep_diff_rule) for imports the edit adds, and a [StyleRule](../../reference/primitives.md#captain_hook.style.StyleRule) subclass for Python-AST facts no pattern string expresses.


# What it catches

``` python
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

``` python
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

``` bash
uvx capt-hook --hooks docs/examples test
```


# See also

- [Style guide](../../docs/guide/primitives.md#style-rules)
- [Conditions guide](../../docs/guide/primitives.md#filter-with-conditions)
- [Matchers reference](../../docs/reference/matchers.md)
