Conversation
2c444ff to
4aeefea
Compare
When user code calls time.sleep(), emit the current turtle SVG so the drawing updates live rather than only appearing at the end. Deduplicate via _last_emitted_turtle_svg so identical frames are never sent twice, and apply the same dedup to _render_turtle and turtle.done()/mainloop(). Consolidate the three separate SVG-fetch-encode-emit blocks (turtle_hook render closure, debug frame_callback, _render_turtle) into the single _emit_turtle_snapshot helper. Fix the frontend regression where incremental SVGs were appended instead of replacing the previous frame by removing the debugger-only guard from the "show only latest SVG" filter in Output.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Turtle snapshots and regular image outputs were distinguished by
sniffing contentType.includes("svg"), which would misroute any
non-turtle SVG (e.g. matplotlib SVG backend) into the Turtle tab.
Introduce BackendEventType.Turtle and OutputType.turtle so turtle
snapshots travel on their own channel end-to-end. The Python side
emits via self.callback("turtle", ...) with a proper image/svg+xml
MIME type.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Turtle snapshots are vector SVG, so the generic 300px image cap is wasteful. Render them with their own class and scale to min(container-width, container-height) via container-query units so the square canvas fits cleanly on any layout. A subtle outline + surface background frames the 400x400 bounds. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The output area now matches the code editor's surface-container-highest fill, with padding so text doesn't hug the edges. The turtle tab keeps a transparent content area and moves the fill onto the canvas itself, so its 400x400 bounds read clearly against the tab-row background. The canvas pulls up 1px so its top border merges with the tab strip; tabs get a z-index bump to stay on top. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| FrameChange = "frame-change", | ||
| Stop = "stop", | ||
| Files = "files", | ||
| Turtle = "turtle", |
There was a problem hiding this comment.
note: added this type so we can act on turtle images vs. regular images. Previously in this branch, we did comparison based on the content type (img -> regular image, svg -> turtle images), but I found that too brittle.
| @stateProperty | ||
| activeEditorTab: string = CODE_TAB; | ||
| @stateProperty | ||
| activeOutputTab: OutputTab = OUTPUT_TAB; |
There was a problem hiding this comment.
note: I thought it would be nice to keep track of the active tab so that when a user is debugging (or rerunning code), we wouldn't switch tabs without their input. Especially useful if you step through programs.
|
WOW! https://en.wikipedia.org/wiki/Wow!_signal This will definitely be something we can reach out with
|
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| @@ -0,0 +1,59 @@ | |||
| # Astroid brain plugin for Python's turtle module. | |||
There was a problem hiding this comment.
note: note sure if this is the cleanest thing to teach pylint about the Turtle methods 🤔
Drop a dead one-line wrapper and hasattr guard, narrow two bare excepts to the real failure modes, collapse a double-defensive module lookup, and cache hasTurtleOutput as an eager flag instead of scanning the output array on every render. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Swallowing it left user code with a None turtle module and a confusing AttributeError instead of the real install failure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds Python Turtle graphics support to Papyros by intercepting import turtle in the Pyodide worker, generating SVG snapshots, and rendering them in the frontend via a dedicated “Turtle” output tab.
Changes:
- Python worker: add a Turtle import hook + emit incremental SVG snapshots (run, debug frames, sleep, end/interrupt).
- Frontend: add a Turtle output type/event, output tab state, and UI rendering for SVG snapshots (including a canvas border/placeholder).
- Tooling/tests: bundle
svg-turtle+turtle.py, add a Pylint astroid brain plugin forturtle, and add Vitest coverage for Turtle output.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/tests/state/Turtle.test.ts | New browser tests validating Turtle SVG output in run/debug and incremental snapshots on sleep |
| src/util/Util.ts | Extend parseData to accept image/* and svg+xml;base64 payloads |
| src/frontend/state/Translations.ts | Add UI strings for the Output/Turtle tabs (EN/NL) |
| src/frontend/state/InputOutput.ts | Add Turtle output type, output-tab state, and backend subscription for Turtle events |
| src/frontend/components/app/examples/PythonExamples.ts | Add a Turtle example snippet |
| src/frontend/components/Output.ts | Add tab UI + Turtle rendering (latest snapshot per step) and omit Turtle images in overflow downloads |
| src/communication/BackendEvent.ts | Add BackendEventType.Turtle |
| src/backend/workers/python/papyros/turtle_hook.py | New import hook that patches turtle to use svg-turtle and trigger snapshot rendering |
| src/backend/workers/python/papyros/pylint_turtle_brain.py | New astroid transform to stub turtle’s dynamically-generated module-level functions |
| src/backend/workers/python/papyros/papyros.py | Integrate Turtle hook + snapshot emission across run/debug/sleep/end/interrupt paths |
| src/backend/workers/python/papyros/linting.py | Load the new pylint_turtle_brain plugin |
| src/backend/workers/python/build_package.py | Bundle turtle.py and add svg-turtle to packaged deps |
| .tool-versions | Add Node/Python tool version pins |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const lastIdx = this.outputs.findLastIndex((o) => o.type === OutputType.turtle); | ||
| outputsToRender = lastIdx >= 0 ? [this.outputs[lastIdx]] : []; |
There was a problem hiding this comment.
renderedOutputs uses Array.prototype.findLastIndex, but the project’s browserslist includes Safari >= 12 / Chrome >= 69. findLastIndex isn’t available in those older targets and will throw at runtime. Replace this with a backwards-compatible loop (or another compatible approach) or ensure an explicit polyfill is shipped.
| const lastIdx = this.outputs.findLastIndex((o) => o.type === OutputType.turtle); | |
| outputsToRender = lastIdx >= 0 ? [this.outputs[lastIdx]] : []; | |
| const outputs = this.outputs; | |
| let lastIdx = -1; | |
| for (let i = outputs.length - 1; i >= 0; i--) { | |
| if (outputs[i].type === OutputType.turtle) { | |
| lastIdx = i; | |
| break; | |
| } | |
| } | |
| outputsToRender = lastIdx >= 0 ? [outputs[lastIdx]] : []; |
| }); | ||
| BackendManager.subscribe(BackendEventType.Turtle, (e) => { | ||
| const data = parseData(e.data, e.contentType); | ||
| this.logTurtle(data, e.contentType); |
There was a problem hiding this comment.
BackendEvent.contentType is optional (string | undefined), but the Turtle subscription passes e.contentType directly into logTurtle, which currently requires a string. This is a TypeScript type error under strictNullChecks and can be fixed by making logTurtle accept contentType?: string (and default internally) or by passing e.contentType ?? "image/svg+xml;base64".
| this.logTurtle(data, e.contentType); | |
| this.logTurtle(data, e.contentType ?? "image/svg+xml;base64"); |
| # Bundle CPython's turtle.py (removed from Pyodide's stdlib, required by svg-turtle) | ||
| import turtle as turtle_mod | ||
| shutil.copy(turtle_mod.__file__, os.path.join(package_name, "turtle.py")) |
There was a problem hiding this comment.
build_package.py does import turtle to locate and copy turtle.py. Importing turtle pulls in tkinter, which is often missing in minimal/headless Python installs (common in CI), causing the package build to fail. Prefer locating turtle.py via the stdlib path (e.g., sysconfig.get_path("stdlib")) and copying the file directly without importing the module.
This PR adds Turtle support to Papyros.
The integrations is based on svg-turtle (https://github.com/donkirkby/svg-turtle) which is what's used in the Turtle judge.
It's 100% seamless:
import turtleis caught in the python module, and emits svg, drawn by the frontend. All Papyros futures are supported: regular running, debugging (including stepping through code), andinput().Screen.Recording.2026-04-23.at.12.54.35.mov
When executing Turtle code, a new tab is added to the output pane and is switched to automatically. In case there's stdout, the user can switch back to output. Once a tab pane has been selected, it's persisted if the code is rerun, or when navigating through debugger steps.
To indicate the size of the canvas, a border is added to the Turtle output.
Since the tabs were floating in space, I added a background colour to the stdout output to connect them to the output.