Registering handlers
There are three equivalent ways to attach a handler to an event. All accept the same options.
The event decorator
Every event is re-exported as a decorator, so the shortest form is to decorate a method with the event itself:
class HelloTracer(pyc.BaseTracer):
@pyc.before_stmt
def handle(self, *_, **__):
print("Hello, world!")
This is exactly sugar for register_handler(pyc.before_stmt) and accepts the
same keyword arguments:
@pyc.before_attribute_load(when=lambda node: node.attr == "secret", reentrant=True)
def handle(self, obj, node, *_, **__):
...
register_handler
- pyccolo.register_handler(event: TraceEvent | Type[AST] | Tuple[TraceEvent | Type[AST], ...], when: Callable[[...], bool] | Predicate | None = None, reentrant: bool = False, static: bool = False, use_raw_node_id: bool = False, guard: Callable[[AST], str] | None = None, exempt_from_guards: bool = False)[source]
The event argument may be a single TraceEvent,
an ast.AST subclass (resolved through AST_TO_EVENT_MAPPING — e.g.
ast.Assign means after_assign_rhs), or a tuple of either to register one
handler for several events at once:
@pyc.register_handler((pyc.after_for_loop_iter, pyc.after_while_loop_iter))
def after_loop_iter(self, *_, guard, **__):
self.activate_guard(guard)
Keyword options:
Option |
Effect |
|---|---|
|
A predicate gating the handler — a plain callable of the node, or a Predicate. Documented in detail below. |
|
Allow the handler to fire while an event is already
being handled (see |
|
Evaluate |
|
Pass the integer node id instead of the resolved
|
|
Associate a guard with this handler so it can be switched off at runtime; see Guards and performance. |
|
Keep firing even when the enclosing guard is active. |
Gating a handler with when
when decides, per node, whether this handler should fire at all. Where
pay-as-you-go instrumentation chooses whether an event is
emitted at all (based on whether any active tracer listens for it), when
narrows a specific handler down to the specific nodes it cares about — for
example “only .load accesses whose attribute is secret”, or “only
or-shaped boolean ops”.
What you can pass
when accepts either of:
A plain callable — receives the node and returns a
bool. By default the node is the resolvedast.AST; withuse_raw_node_id=Trueit is the integer node id instead (use this when you only need identity or intend to look the node up yourself). ReturningTruelets the handler fire;Falsesuppresses it.@pyc.before_attribute_load(when=lambda node: node.attr == "secret") def redact(self, obj, node, *_, **__): ...
A Predicate — the same idea, but an object that also carries when the decision is made (see below) and can be composed. A plain callable is wrapped into a
Predicatefor you.
Static vs. dynamic evaluation
This is the important distinction, and it is what static controls:
Static (
static=True, or aPredicate(..., static=True)) — the predicate depends only on the shape of the tree, so it is evaluated once, at instrument time, and the answer is baked into the rewrite. A static predicate therefore costs nothing at runtime: nodes it rejects are simply never wired to your handler. Reach for this whenever the condition is a pure function of the AST (anisinstancecheck onnode.op, an attribute name, a node’s position in the tree viais_outer_stmt(), …).import ast @pyc.before_boolop(when=lambda node: isinstance(node.op, ast.Or), static=True) def only_or(self, ret, node, *_, **__): ...
Dynamic (the default) — the predicate is re-evaluated on every event, via
Predicate.dynamic_call. Use this only when the decision depends on runtime state the tree cannot tell you about; it is strictly more expensive than a static predicate.
If you pass a bare callable without static=True, it is treated as dynamic. If
you pass a Predicate, its own .static flag wins.
Composing predicates
Combine conditions with CompositePredicate.any /
all(), and short-circuit against the
Predicate.TRUE /
FALSE constants. A composite stays static only if
every constituent is static, so mixing in one dynamic predicate makes the whole
check dynamic.
See also
Observe and override values for a task-oriented walkthrough, and
Predicates for the full Predicate API.
register_raw_handler
- pyccolo.register_raw_handler(event: TraceEvent | Type[AST] | Tuple[TraceEvent | Type[AST], ...], **kwargs)[source]
Identical to register_handler() with use_raw_node_id=True: the handler
receives the raw integer node id rather than a resolved ast.AST node.
This is the fastest path when you only need node identity (e.g. to look
values up in a dict keyed by id(node)) and want to avoid resolving the node:
@pyc.register_raw_handler(pyc.before_stmt)
def handle_stmt(self, _ret, stmt_id, frame, *_, **__):
if stmt_id not in self.seen_stmts:
self.seen_stmts.add(stmt_id)