IPython and Jupyter
In a notebook you don’t want to wrap every cell in pyccolo.exec(). Pyccolo
ships an IPython extension that instruments each cell automatically: load it, then
register one or more tracers, and every subsequent cell is traced as if it had
been run through pyc.exec — including notebook niceties like Out[N] and
composition with other cell-level tools such as ipyflow.
Load the extension and register a tracer
In [1]: %load_ext pyccolo
In [2]: import pyccolo as pyc
...: class HelloTracer(pyc.BaseTracer):
...: @pyc.before_stmt
...: def handle_stmt(self, *_, **__):
...: print("Hello, world!")
In [3]: %pyccolo register HelloTracer
In [4]: for _ in range(3): pass
Hello, world!
Hello, world!
Hello, world!
Hello, world!
%unload_ext pyccolo removes the instrumentation and restores the shell to
exactly how it was.
The four ways to name a tracer
%pyccolo register and its programmatic twin
pyccolo.register_ipython_tracer() accept the tracer in four spellings, so
you can register from a cell, a variable, or a library:
pyc.register_ipython_tracer(HelloTracer) # a class
pyc.register_ipython_tracer(HelloTracer.instance()) # an instance
pyc.register_ipython_tracer("HelloTracer") # a name in the user namespace
pyc.register_ipython_tracer("mypkg.tracers.HelloTracer") # a dotted path
The extension must already be loaded or register_ipython_tracer raises. The
companion subcommands are %pyccolo deregister <tracer> (or deregister
all) and %pyccolo list, plus pyccolo.deregister_ipython_tracer() and
pyccolo.registered_ipython_tracers().
Registration takes effect on the next cell: by the time your register call
runs, the current cell has already been rewritten.
Registration order is canonical
The first tracer registered comes first in the tracer stack: its syntax augmenter rewrites the source outermost, and its handlers see each event first.
pyc.register_ipython_tracer(A)
pyc.register_ipython_tracer(B)
# A's augmenter runs before B's; A's handlers run before B's
Composing with ipyflow
ipyflow drives Pyccolo’s cell-tracing driver directly instead of layering a second
set of transformers on the shell, so %load_ext pyccolo and %load_ext
ipyflow compose in either order and a cell is instrumented exactly once.
%flow register_tracer and pyc.register_ipython_tracer write to the same
registry, and ipyflow’s own tracers sort after yours. %unload_ext ipyflow
hands the driver back, leaving your tracers running under Pyccolo alone. The
handoff primitives — pyccolo.take_over_ipython_driver() and
pyccolo.release_ipython_driver() — are what make this seamless.
How it works
Pyccolo installs one input transformer (which applies each tracer’s syntax
augmenters and builds the cell’s AstRewriter) and one AST
transformer (which runs that rewriter). It learns the cell’s filename by wrapping
shell.compile.cache — the caching compiler that mints it, called between the
two transforms — and adds it to each tracer’s tracing-enabled files so the
file filter admits the cell. Tracing is
resident: tracers are entered once and merely enabled/disabled around each cell,
rather than pushed and popped every time.
Two things worth knowing when you write a tracer for the notebook
Define tracers in a module, not a cell, when you can. A tracer defined in a
cell has its handler’s code object attributed to that cell’s filename, so
co-resident tracers that use sys.settrace may follow execution into your
handler.
You don’t lose Out[N]. Instrumenting a statement wraps it in an if, so
a cell body no longer ends in a bare expression statement — which would normally
make IPython’s last_expr interactivity suppress the output. Pyccolo detects
this and weaves in an after_module_stmt handler that preserves the trailing
expression’s value, so Out[N] behaves as usual. Nothing is required of you.
JupyterLite
The extension imports only from IPython.core and never touches
shell.kernel, so it runs unmodified under JupyterLite / Pyodide, where
ipykernel is absent. The in-browser demo is exactly
this path — Pyccolo tracing cells, entirely client-side.