Skip to content

Commit be6e96d

Browse files
committed
Claude PR review
1 parent 5dff504 commit be6e96d

3 files changed

Lines changed: 90 additions & 92 deletions

File tree

src/backend/workers/python/papyros/papyros.py

Lines changed: 4 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -17,98 +17,14 @@
1717
from pyodide_worker_runner import install_imports
1818
from pyodide.ffi import JsException, create_proxy
1919
from .util import to_py
20+
from .turtle_hook import TurtleImportHook
2021
from pyodide.http import pyfetch
2122
from types import ModuleType
2223

2324
SYS_RECURSION_LIMIT = 500
2425
MODULE_NAME = "sandbox"
2526

2627

27-
class _TurtleImportHook:
28-
"""Import hook that lazily sets up SVG-based turtle graphics.
29-
30-
Installed in sys.meta_path. When user code does `import turtle`,
31-
this hook intercepts it, imports svg-turtle, creates a shared
32-
canvas/screen, and patches the turtle module to render SVG.
33-
34-
If user code never imports turtle, no setup occurs.
35-
"""
36-
37-
def __init__(self):
38-
self.papyros = None
39-
self.render = None
40-
self._loading = False
41-
self._turtle_module = None
42-
43-
def find_spec(self, name, path, target=None):
44-
if name == 'turtle' and not self._loading:
45-
import importlib.util
46-
return importlib.util.spec_from_loader(name, self)
47-
return None
48-
49-
def create_module(self, spec):
50-
# Return the patched turtle module directly
51-
return self._setup_turtle()
52-
53-
def exec_module(self, module):
54-
# Module was already set up in create_module
55-
pass
56-
57-
def _setup_turtle(self):
58-
self._loading = True
59-
try:
60-
from svg_turtle import SvgTurtle
61-
from svg_turtle.canvas import Canvas
62-
63-
if self._turtle_module is None:
64-
# First import: svg_turtle stubs tkinter, then imports turtle
65-
self._turtle_module = sys.modules.get('turtle')
66-
if self._turtle_module is None:
67-
import turtle
68-
self._turtle_module = sys.modules['turtle']
69-
70-
turtle_mod = self._turtle_module
71-
sys.modules['turtle'] = turtle_mod
72-
73-
# Fresh canvas and screen for this execution
74-
canvas = Canvas(400, 400)
75-
screen = SvgTurtle._Screen(canvas)
76-
screen.cv.config(bg="")
77-
78-
class PapyrosTurtle(SvgTurtle):
79-
def __init__(self):
80-
super().__init__(screen=screen)
81-
82-
SvgTurtle._screen = screen
83-
SvgTurtle._pen = PapyrosTurtle()
84-
85-
rendered = [False]
86-
papyros = self.papyros
87-
88-
def render():
89-
if rendered[0]:
90-
return
91-
rendered[0] = True
92-
svg_string = SvgTurtle._pen.to_svg()
93-
if svg_string:
94-
img = base64.b64encode(svg_string.encode("utf-8")).decode("utf-8")
95-
papyros.output("img", img, contentType="img/svg+xml;base64")
96-
97-
turtle_mod.Turtle = PapyrosTurtle
98-
turtle_mod.done = render
99-
turtle_mod.mainloop = render
100-
turtle_mod.exitonclick = render
101-
turtle_mod.bye = render
102-
103-
self.render = render
104-
return turtle_mod
105-
except Exception:
106-
self.render = None
107-
return None
108-
finally:
109-
self._loading = False
110-
111-
11228
class Papyros(python_runner.PyodideRunner):
11329
def __init__(
11430
self,
@@ -184,7 +100,7 @@ def _render_turtle(self):
184100

185101
def override_turtle(self):
186102
if not hasattr(self, '_turtle_hook'):
187-
self._turtle_hook = _TurtleImportHook()
103+
self._turtle_hook = TurtleImportHook()
188104

189105
hook = self._turtle_hook
190106
hook.papyros = self
@@ -345,9 +261,8 @@ async def run_async(self, source_code, mode="exec", top_level_await=True):
345261
def frame_callback(frame):
346262
self._flush_open_files()
347263
self._emit_created_files(emit_empty=True)
348-
# Progressive turtle rendering: only emit SVG when drawing actually changed.
349-
# Non-drawing commands (pencolor, left, etc.) don't alter the SVG output,
350-
# so the string comparison naturally skips them without needing a hard cap.
264+
# The hook fires lazily when user code runs `import turtle`, so the
265+
# pen only exists partway through the trace — check per frame.
351266
hook = getattr(self, '_turtle_hook', None)
352267
if hook and hook.render:
353268
try:
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import base64
2+
import sys
3+
4+
5+
class TurtleImportHook:
6+
"""Import hook that lazily sets up SVG-based turtle graphics.
7+
8+
Installed in sys.meta_path. When user code does `import turtle`,
9+
this hook intercepts it, imports svg-turtle, creates a shared
10+
canvas/screen, and patches the turtle module to render SVG.
11+
12+
If user code never imports turtle, no setup occurs.
13+
"""
14+
15+
def __init__(self):
16+
self.papyros = None
17+
self.render = None
18+
self._loading = False
19+
self._turtle_module = None
20+
21+
def find_spec(self, name, path, target=None):
22+
if name == 'turtle' and not self._loading:
23+
import importlib.util
24+
return importlib.util.spec_from_loader(name, self)
25+
return None
26+
27+
def create_module(self, spec):
28+
return self._setup_turtle()
29+
30+
def exec_module(self, module):
31+
pass
32+
33+
def _setup_turtle(self):
34+
self._loading = True
35+
try:
36+
from svg_turtle import SvgTurtle
37+
from svg_turtle.canvas import Canvas
38+
39+
if self._turtle_module is None:
40+
# First import: svg_turtle stubs tkinter, then imports turtle
41+
self._turtle_module = sys.modules.get('turtle')
42+
if self._turtle_module is None:
43+
import turtle
44+
self._turtle_module = sys.modules['turtle']
45+
46+
turtle_mod = self._turtle_module
47+
sys.modules['turtle'] = turtle_mod
48+
49+
# Fresh canvas and screen for this execution
50+
canvas = Canvas(400, 400)
51+
screen = SvgTurtle._Screen(canvas)
52+
screen.cv.config(bg="")
53+
54+
class PapyrosTurtle(SvgTurtle):
55+
def __init__(self):
56+
super().__init__(screen=screen)
57+
58+
SvgTurtle._screen = screen
59+
SvgTurtle._pen = PapyrosTurtle()
60+
61+
rendered = [False]
62+
papyros = self.papyros
63+
64+
def render():
65+
if rendered[0]:
66+
return
67+
rendered[0] = True
68+
svg_string = SvgTurtle._pen.to_svg()
69+
if svg_string:
70+
img = base64.b64encode(svg_string.encode("utf-8")).decode("utf-8")
71+
papyros.output("img", img, contentType="img/svg+xml;base64")
72+
73+
turtle_mod.Turtle = PapyrosTurtle
74+
turtle_mod.done = render
75+
turtle_mod.mainloop = render
76+
turtle_mod.exitonclick = render
77+
turtle_mod.bye = render
78+
79+
self.render = render
80+
return turtle_mod
81+
except Exception:
82+
self.render = None
83+
return None
84+
finally:
85+
self._loading = False

src/frontend/components/Output.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ export class Output extends PapyrosElement {
9999
get renderedOutputs(): TemplateResult[] {
100100
let outputsToRender = this.outputs;
101101
if (this.papyros.debugger.active) {
102-
// In debug mode, only show the last SVG image for progressive turtle rendering.
103-
// Multiple SVG snapshots are emitted during debug (one per turtle state change),
104-
// but only the latest one visible at the current frame should be displayed.
102+
// Only show the latest SVG snapshot so the debugger slider shows progressive rendering.
105103
const lastSvgIdx = outputsToRender.findLastIndex(
106104
(o) => o.type === OutputType.img && o.contentType?.includes("svg"),
107105
);

0 commit comments

Comments
 (0)