wrapture workshops
Back to WorkshopsGuided JupyterLab workshops that teach wrapture: wrapping call sites without changing the code being observed, for monkey patching, testing and tracing.
The wrapture package itself is on GitHub at github.com/GrahamDumpleton/wrapture, with its source code, documentation and issue tracker.
Launch the workshops
Each option opens the whole collection in a new window, where JupyterLab shows the workshops in the order to take them.
- Binder is a free public service needing no account. Sessions are temporary and can take a minute or two to start.
- Codespaces needs a GitHub account and uses your Codespaces allowance. A codespace is kept until you delete it.
1 Your first binding 10m
Create a wrapture binding on a method, apply and remove it, and change what a call does while the real code keeps running.
2 Testing by wrapping, not replacing 15m
Write the same unit tests with unittest.mock and with wrapture, and see what a test can say when the real code keeps running. Strict stubs, calls an object makes to itself, one thing changed, and what did not happen.
3 Recording what real code did 15m
Record real calls on a timeline and read the tape back. Find a connection leak that no return value reveals, name the method and the line that leaked it, and turn the record into a pytest test that fails until you fix the code.
4 Behaviour that changes over time 15m
Test retry logic, a circuit breaker and a polling loop with phases, where a binding's behaviour changes on a count, on a condition, when a sequence runs out, or on a signal from another binding.
5 Bindings that are not calls 15m
Watch and guard an attribute's reads and writes, hold a value in an environment variable or a module constant, pin the content of a settings dict for every holder, wrap a callable kept in a registry, and see how far a consumer read a generator.
6 A program that narrates itself 10m
Apply bindings in a program's entry point, with no timeline, and register a sink that prints each call as it happens. Keep card numbers out of the trace, then narrow it at the sink, at the binding, and for a whole subtree, and read the count of what was left out.
7 Tracing without touching the program 15m
Move the bindings and the sink out of the program into a wrapture.toml beside it and run the program unchanged, under the runner and then injected at interpreter startup. Ask a traced process what is installed and switch it off and on, keep the trace as JSON Lines, read it back, and draw it as a sequence diagram.
8 Analysing a trace in a notebook 15m
A JSON Lines trace is data. Run the shop for three hundred orders under a file sink, read the file into a DataFrame, rebuild the tree from the parent links, work out where the time and the errors went, chart the latency of an order by tenant, and draw a few orders as a sequence diagram and a diff.
9 One request as one tree 15m
Put the shop behind Flask and record each HTTP request as one tree, with the view and the shop's own methods nested beneath it, from a single instrument entry in the config. Send a quote, an order, a declined order and a request that fails with a 500, read the trees, then keep the health checks out.
10 Where the time goes 15m
The order endpoint of the Flask shop is slow. Read the time off one request's tree, tell slow itself from slow because of a child with self time, turn that into a test that asserts on it, get one report for thirty requests from an Aggregate window in the config, and tag each request with the tenant it was for.
11 The same events, sent to a backend 20m
Switch on OpenTelemetry export for the Flask shop with one table in the config and read what it sends, with no collector needed. The request span named by its route, the view span beneath it, the exception on both, and the histograms after an export interval. Then a client and a service, and one trace id across both.
12 wrapture and pytest, properly 20m
Scope bindings in a pytest suite without mixing styles, with the with-block, the decorators, yield fixtures that hand the binding to the test, and shared declarations applied per test. Then the plugin, whose sweep fails a leaking test by name and whose tape fixture attaches the call tree to a failure report, and a query budget for the whole suite from a counter.
13 Converting a mock test suite 20m
Take a test module written with unittest.mock and convert it to wrapture one idiom at a time, with the suite green after every step. A return value, a failure, a sequence of outcomes, several patches at once, a value in place of an environment variable, the real call with one change, the order of calls and a log message, and the one test to leave as mock.
14 When the test must supply the callable 15m
Test a pipeline whose transport and completion hook the test itself has to supply. A stub for the hook, opted back into strictness with the real signature, a collaborator double built from the named class that fabricates nothing, both recorded on the same tape, order asserted across a batch, and failure paths reconfigured in place.
15 Testing what a consumer does with a stream 15m
Test the consumers of a paginated catalogue against the real generator. One event per iteration with a live item count and whether it finished, an iterator proxy that sees every item and hears the end, a failure injected at exactly the page you choose, items transformed on the way through, the proxy on the consumer's argument, and the whole thing as a pytest test.
16 Async methods and generators 15m
Test a notifier whose client is async all the way down. A stub whose outcome arrives on await, a timeout that stops a broadcast early through the real loop, the coroutine that was created and never awaited caught in one line, concurrent sends attributed on one tape, an async generator with stubbed items, and the same assertions as a pytest suite under pytest-asyncio.
17 Changing what a library does 20m
Monkey patching as a discipline, on a vendored client you cannot edit. Inject a header with a transform, prove the patch reversible with suspend, resume and remove, reconfigure it while it is installed, wrap the whole call for a retry, clamp an attribute the library reads, apply from a post-import hook before the library is imported, record calls without recording the token, and finally the same patch from a config file with the code beside it.
18 Messages, phases and handled failures as events 15m
Three more producers of events on the same tape as the calls. Log messages captured and pinned to the call that emitted them, named blocks that give an integration test phases to assert within, annotations that attach what the code knows to the event in flight, and a noted exception for a failure the code handled itself, all inert when nothing is listening.
19 Where events go 20m
The other side of the tape. Write a sink of your own in a few lines, see what a process sink hears that a timeline cannot and what a bound method costs when nobody listens, compose fan-out, depth, filtering and sampling in one registration, keep numbers rather than events with Counter and Aggregate, declare a binding a terminal node with a category, and resolve the name, kind and tags of each event per operation.
20 Reading a trace after the fact 15m
JSON Lines as the durable form of a trace, from the command line. Stream a two-thread pipeline to disk from a config file, convert the file to Chrome trace JSON for Perfetto with one lane per thread, render its canonical form as a golden file that a test compares the live call tree against, and give the sink a path template and a rotation interval for a process that runs for days.
21 Reports on a schedule 15m
A service has been running for a day and nobody can say what it does. Put a Window around the shop with every= in code, then move the arrangement into the config file for the Flask shop, a summary every few seconds with one file per run, a report on demand from a signal, and an always-on JSON Lines stream that rotates rather than grows. Clocks, alignment and what a restart does to a schedule.
22 One trace across two processes 20m
A client and a server, standard library only, one trace id across both. The identity travels in the traceparent header on the way out, the WSGI boundary parses it on the way in, and the two JSON Lines files join on the id with no backend anywhere. Then work the caller does not wait for, a thread and a queue, linked back to the operation that started it rather than nested under it.
23 Requests in tests, and at the boundary 15m
Run the Flask instrumentation inside a pytest test and assert on the request event itself, its status as the result, its route and endpoint in data, a streamed body's chunks, and the exception behind a 500. Then the on_request namespace on a WSGI binding, a canned response the application never sees, a fault the server sees, and a status rewritten on the way out.
24 Instrumenting a package nobody has covered 20m
Write an Instrumentation class for a small library shipped with the workshop. The shape of the class, a hook that runs when the target module is imported, a registration choke point where handlers are observed as they register, an error handler that notes the exception, settings declared once and validated when the config loads, on_cleanup() so the patch comes down with the config, the class tested directly and through wrapture, and how it would be packaged with an entry point.