Repository navigation
Conversation
Contributor
|
Preview root: https://posit-dev.github.io/commons/pr-411/ Python site preview: https://posit-dev.github.io/commons/pr-411/py/ Built from the latest commit on this branch. The R links in it point at the published R site, which no pull request rebuilds. |
jat255
marked this pull request as ready for review
October 10, 2026 01:28
jat255
added this pull request to stack #416
October 10, 2026 01:34
After each call the worker renders every open pyplot figure twice, once for the model and once at twice the pixels for display, then closes them all so none carries into the next call. Result and Error gain a `plots` field; an Error keeps what was drawn before the exception. A no-op matplotlib backend keeps `plt.show()` quiet. The model image keeps the figure's own size, scaled down so its long edge fits 1568 px. A call returns at most 20 plots, and the protocol drops plots before values when a reply would overrun the channel. A figure that fails to render, or a pyplot that model code broke, costs the plots and says so in stderr, never the session.
…raises Plots now have a byte budget (a quarter of the channel line). The worker stops rendering once a call's images exceed it, and the encoder drops over-budget plots before base64-encoding them, so oversized figures are never copied only to be thrown away. discard() catches any exception but KeyboardInterrupt, so model code replacing plt.close cannot end the session.
The early plot drop now clips the printed output before appending its note, so the clip that follows cannot cut the note off.
…ng plots Dropping over-budget plots no longer clips the printed output first. Clipping instead keeps a trailing dropped-plots note, so the note survives whichever path shrinks the reply.
…t PNGs Worker side: a figure whose draw raises an exception with a failing repr, or whose savefig writes something other than a PNG, now costs only that figure. Saves reset savefig.bbox, so a tight bounding box or padding from rcParams cannot grow an image past its size cap. Driver side: decoding checks each image's IHDR chunk, bounds both edges to PLOT_EDGE_LIMIT, and re-enforces PLOT_BYTES_LIMIT, since the worker runs model code and cannot be relied on to keep either bound.
Result and Error replace their stdout and stderr fields with `output`, a tuple of Text and Plot segments in the order they happened, so a plot sits between the text written before and after it, and a warning keeps its place between two prints. The worker captures both streams into one Transcript. plt.show() puts the open figures into it at that point, as Jupyter's inline backend does, and the worker flushes once more when the call ends. Notes about dropped or broken figures sit where those figures would have. Each stream keeps its own capture bound, and a call may start at most 9,000 text segments, which leaves room in the protocol's 10,000-segment limit for plots and notes. Clipping a reply now cuts each stream where it crosses the limit and keeps everything else in place.
A note naming a failed figure keeps at most 500 characters of the exception's repr, since notes bypass the capture's bound. Closing the figures after a call runs outside the interrupt window, where only model code can raise, so any exception there, KeyboardInterrupt included, now costs the open figures rather than the session.
A figure left open after a call would come back with the next one, so when model code has broken plt.close the figures are destroyed through Gcf instead, an interrupt from the broken close still propagating once they are gone. The 500-character clip on a note now covers the type-name fallback too.
…lies The figures still open when a call's code finishes are now drawn while output is still captured, so text and warnings from that drawing reach the reply instead of being lost. The worker clamps a reply to the segment and plot counts the driver accepts, and a malformed transcript entry is skipped instead of ending the worker, so model code that changes the transcript directly cannot make the driver restart the session. fig.show() now places a figure in the output the way plt.show() does, and the warning that the Agg backend cannot show figures is silenced, since model code that selects Agg still gets its figures at the end of the call. Comments and docstrings that misstated these limits are corrected.
jat255
force-pushed
the
jat255/fj8r-plot-capture
branch
from
October 10, 2026 01:51
e8f9d2f to
ebcdb79
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds plot capture to the Python execution worker. Figures a call draws come back as PNG images, in order with the text the call printed. This is the worker and protocol half of plotting; the
run_pythontool that shows plots to the model and the user comes separately.Review notes
ResultandErrorreplacestdout/stderrwithoutput, an ordered tuple of text and plot segments (_protocol.py, captured by_repl.py). This is internal API, and every caller is updated.plt.show()or when the call ends, which is how Jupyter behaves (_plots.py). R also flushes a plot when text follows it; matplotlib edits figures in place, so doing that here would capture half-built figures.run_pythonlands.Testing
The full Python suite passes on macOS under seatbelt. The execution tests pass on Linux under Landlock and seccomp, run in a container, and guardrails mode is covered too. matplotlib is a dev dependency only.
Some manual testing:
Agent-written detail
kata: fj8r (this work); 2q9b (
run_python, which will consumeoutput).Each figure is rendered twice (the model PNG and a 2× display PNG), then closed. Failures from model code (a broken artist, a replaced
savefigorplt.close, a broken pyplot, an exception whosereprfails) cost only the affected figure and add a stderr note where the figure would have been. They never end the session.