The matcher catalog for styleguide rules. A matcher is a predicate over abstract syntax tree (AST) nodes. It wraps a (node, parents) -> bool test, so M.calls("zip") asks “is this AST node a call to zip?” Matchers compose with boolean algebra (&, |, ~), refine with .where(...), and terminate with .over(tree), .violations(tree), or .exists(tree). Import as from captain_hook.style import matchers as M.
Node types
M.module |
The file root (ast.Module) |
M.cls |
class definitions |
M.func |
def / async def |
M.definition |
Classes or functions (M.cls \| M.func) |
M.imports |
import / from … import |
M.call |
Call expressions |
M.assignment |
x = … and x: T = … |
M.control_flow |
if / for / while / with / try / except (and async) |
M.type_checking |
if TYPE_CHECKING: blocks |
M.future_annotations |
Modules with from __future__ import annotations |
M.kind(*types, label=None) |
Any of the given ast node types |
Calls & references
M.calls(name) |
A call to the bare-name function name |
M.kwarg(name) |
A call passing keyword argument name |
M.ref(name) |
A bare-name reference (e.g. Any inside an annotation) |
Names
M.named(pattern) |
A class/func/assignment/param whose name matches the regex |
M.private |
Single-leading-underscore names (not dunder) |
M.dunder |
__dunder__ names |
M.constant |
SCREAMING_SNAKE names |
Annotations
M.annotated(inner=None) |
An annotated variable, parameter, or return (excludes *args/**kwargs); inner constrains the annotation |
M.forward_ref |
String forward references inside annotations |
Position
M.under(other) |
A node with any ancestor matching other |
M.child_of(other) |
A node whose immediate parent matches other |
M.following(boundary) |
A body statement after the first sibling matching boundary |
Combinators & terminals
a & b |
Both must match |
a \| b |
Either matches |
~a |
Must not match |
a.where(predicate) |
Refine with a node-local predicate |
m.over(tree) |
Yield every matching node |
m.violations(tree, label=None) |
Yield a Violation per match |
m.diff(pre, post, key=ast.unparse, label=None) |
Yield matches present in post but absent in pre (for diff rules) |
m.exists(tree) |
Whether any node matches |
m.matches(node) |
Whether a single node matches; node-local matchers only, structural matchers raise |
Examples
Block imports nested inside control flow, but allow if TYPE_CHECKING::
from captain_hook.style import StyleRule, matchers as M
class NoNestedImports(StyleRule):
"""Lazy imports go at the top of the function body, not inside if/for/try."""
match = M.imports & M.child_of(M.control_flow) & ~M.under(M.type_checking)
Flag only edits that newly widen a typed slot to Any (a diff rule):
from captain_hook.style import StyleDiffRule, matchers as M
class NoWeakeningToAny(StyleDiffRule):
"""Don't widen a typed slot to Any to silence the type checker."""
def check(self, pre, post):
yield from M.annotated(M.ref("Any")).diff(pre, post)