Skip to content

Add Turtle support#998

Draft
TomNaessens wants to merge 12 commits intomainfrom
poc-turtle
Draft

Add Turtle support#998
TomNaessens wants to merge 12 commits intomainfrom
poc-turtle

Conversation

@TomNaessens
Copy link
Copy Markdown
Contributor

@TomNaessens TomNaessens commented Apr 13, 2026

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 turtle is caught in the python module, and emits svg, drawn by the frontend. All Papyros futures are supported: regular running, debugging (including stepping through code), and input().

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.

@TomNaessens TomNaessens force-pushed the poc-turtle branch 4 times, most recently from 2c444ff to 4aeefea Compare April 14, 2026 15:16
TomNaessens and others added 5 commits April 22, 2026 15:45
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>
TomNaessens and others added 3 commits April 23, 2026 11:48
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>
@TomNaessens TomNaessens changed the title POC Add Turtle support Apr 23, 2026
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FrameChange = "frame-change",
Stop = "stop",
Files = "files",
Turtle = "turtle",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pdawyndt
Copy link
Copy Markdown
Contributor

WOW! https://en.wikipedia.org/wiki/Wow!_signal

This will definitely be something we can reach out with

  • this replaces and the Turtle support from Trinket with additional possibility for debugging (and probably also testing in the future)
  • this is part of the GCSE curriculum, so included in many courses (Time2Code as an example)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@@ -0,0 +1,59 @@
# Astroid brain plugin for Python's turtle module.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: note sure if this is the cleanest thing to teach pylint about the Turtle methods 🤔

TomNaessens and others added 2 commits April 23, 2026 15:28
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for turtle, 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.

Comment on lines +143 to +144
const lastIdx = this.outputs.findLastIndex((o) => o.type === OutputType.turtle);
outputsToRender = lastIdx >= 0 ? [this.outputs[lastIdx]] : [];
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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]] : [];

Copilot uses AI. Check for mistakes.
});
BackendManager.subscribe(BackendEventType.Turtle, (e) => {
const data = parseData(e.data, e.contentType);
this.logTurtle(data, e.contentType);
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Suggested change
this.logTurtle(data, e.contentType);
this.logTurtle(data, e.contentType ?? "image/svg+xml;base64");

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +32
# 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"))
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants