Conditional handlers and predicates

Sometimes a handler should only fire for some of the nodes that emit an event — additions but not subtractions, a particular variable name, and so on. Every event decorator accepts a when= keyword for exactly this:

import ast

import pyccolo as pyc


class TraceAdditions(pyc.BaseTracer):
    @pyc.after_binop(when=lambda node: isinstance(node.op, ast.Add))
    def handle_add(self, ret, node, *_, **__):
        print(f"added -> {ret}")


with TraceAdditions:
    pyc.exec("x = 1 + 2\ny = 3 * 4")  # only the addition is reported

The when= callable receives the AST node for the instrumented construct and returns a truthy/falsy value. Because it is handed the node, it can inspect operator type, identifier names (node.id), literal values, and anything else in the tree.

The Predicate class

Under the hood, when= values are wrapped in pyccolo.Predicate. You can also pass a Predicate directly, which unlocks two extra knobs:

  • static vs. dynamic. Predicate(condition, static=True) is evaluated once at instrumentation (compile) time — if it is false, no instrumentation is emitted for that node at all, so there is zero runtime cost. A dynamic predicate (the default for raw-node-id predicates) is re-checked at runtime each time the node executes, which is necessary when the decision depends on runtime state.

  • raw node ids. Predicate(condition, use_raw_node_id=True) passes the node’s integer id rather than the node object, mirroring pyccolo.register_raw_handler().

The sentinels Predicate.TRUE and Predicate.FALSE are always-on and always-off predicates, handy as building blocks.

Composing predicates

pyccolo.predicate.CompositePredicate combines several predicates with a reducer. The classmethods CompositePredicate.any and CompositePredicate.all build the or / and combinations and short-circuit against the TRUE / FALSE sentinels:

from pyccolo.predicate import CompositePredicate, Predicate

# true if *either* base predicate is true
either = CompositePredicate.any([pred_a, pred_b])

# CompositePredicate.any([Predicate.TRUE, ...]) collapses to Predicate.TRUE
# CompositePredicate.all([]) is Predicate.TRUE, and any([]) is TRUE as well

A composite is considered static only when all of its constituents are static; if any constituent is dynamic, the composite is re-checked at runtime.

For worked examples, see test/test_predicate.py and the when= usages in test/test_handlers.py.