Skip to content

Commit 9be0e5d

Browse files
loiswells97create-issue-branch[bot]sra405Scott
authored
Seaborn support (#1106)
Co-authored-by: create-issue-branch[bot] <53036503+create-issue-branch[bot]@users.noreply.github.com> Co-authored-by: Scott Adams <74183390+sra405@users.noreply.github.com> Co-authored-by: Scott <scott.adams@raspberrypi.org>
1 parent c7e50af commit 9be0e5d

File tree

4 files changed

+96
-5
lines changed

4 files changed

+96
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1818
- `Pyodide` `matplotlib` support (#1087)
1919
- Tests for running simple programs in `pyodide` and `skulpt` (#1100)
2020
- Fall back to `skulpt` if the host is not `crossOriginIsolated` (#1107)
21+
- `Pyodide` `seaborn` support (#1106)
2122

2223
### Changed
2324

@@ -26,9 +27,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2627

2728
### Fixed
2829

29-
- Build to include public files (#1112)
3030
- Dynamic runner switching with more than one `python` file (#1097)
3131
- Pyodide running the correct file (`main.py`) when there are multiple `python` files (#1097)
32+
- Build to include public files (#1112)
3233
- Persisting choice of tabbed/split view when running `python` code (#1114)
3334

3435
## [0.27.1] - 2024-10-01

cypress/e2e/spec-wc-pyodide.cy.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,29 @@ describe("Running the code with pyodide", () => {
100100
.should("be.visible");
101101
});
102102

103+
it("runs a simple seaborn program", () => {
104+
runCode("import seaborn as sns\ndata = [50, 30, 100]\nsns.displot(data)");
105+
cy.wait(12000);
106+
cy.get("editor-wc")
107+
.shadow()
108+
.find(".pyodiderunner")
109+
.find("img")
110+
.should("be.visible");
111+
});
112+
113+
it("runs a simple urllib program", () => {
114+
cy.intercept("GET", "https://www.my-amazing-website.com", {
115+
statusCode: 200,
116+
});
117+
runCode(
118+
"import urllib.request\nresponse = urllib.request.urlopen('https://www.my-amazing-website.com')\nprint(response.getcode())",
119+
);
120+
cy.get("editor-wc")
121+
.shadow()
122+
.find(".pythonrunner-console-output-line")
123+
.should("contain", "200");
124+
});
125+
103126
it("runs a simple program with a module from PyPI", () => {
104127
runCode(
105128
"from strsimpy.levenshtein import Levenshtein\nlevenshtein = Levenshtein()\nprint(levenshtein.distance('hello', 'world'))",

src/PyodideWorker.js

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ const PyodideWorker = () => {
6060

6161
const runPython = async (python) => {
6262
stopped = false;
63+
await pyodide.loadPackage("pyodide_http");
64+
6365
await pyodide.runPythonAsync(`
66+
import pyodide_http
67+
pyodide_http.patch_all()
68+
6469
old_input = input
6570
6671
def patched_input(prompt=False):
@@ -249,19 +254,69 @@ const PyodideWorker = () => {
249254
pyodidePackage = pyodide.pyimport("matplotlib");
250255
} catch (_) {}
251256
if (pyodidePackage) {
257+
pyodide.runPython(`
258+
import matplotlib.pyplot as plt
259+
import io
260+
import basthon
261+
262+
def show_chart():
263+
bytes_io = io.BytesIO()
264+
plt.savefig(bytes_io, format='jpg')
265+
bytes_io.seek(0)
266+
basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
267+
plt.show = show_chart
268+
`);
252269
return;
253270
}
254271
},
272+
after: () => {},
273+
},
274+
seaborn: {
275+
before: async () => {
276+
pyodide.registerJsModule("basthon", fakeBasthonPackage);
277+
// Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
278+
// the document object is not available. We will instead capture the image and send it back to the main thread.
279+
pyodide.runPython(`
280+
import js
281+
282+
class DummyDocument:
283+
def __init__(self, *args, **kwargs) -> None:
284+
return
285+
def __getattr__(self, __name: str):
286+
return DummyDocument
287+
js.document = DummyDocument()
288+
`);
289+
290+
// Ensure micropip is loaded which can fetch packages from PyPi.
291+
// See: https://pyodide.org/en/stable/usage/loading-packages.html
292+
if (!pyodide.micropip) {
293+
await pyodide.loadPackage("micropip");
294+
pyodide.micropip = pyodide.pyimport("micropip");
295+
}
296+
297+
// If the import is for a PyPi package then load it.
298+
// Otherwise, don't error now so that we get an error later from Python.
299+
await pyodide.micropip.install("seaborn").catch(() => {});
300+
},
255301
after: () => {
256302
pyodide.runPython(`
257303
import matplotlib.pyplot as plt
258304
import io
259305
import basthon
260306
261-
bytes_io = io.BytesIO()
262-
plt.savefig(bytes_io, format='jpg')
263-
bytes_io.seek(0)
264-
basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
307+
def is_plot_empty():
308+
fig = plt.gcf()
309+
for ax in fig.get_axes():
310+
# Check if the axes contain any lines, patches, collections, etc.
311+
if ax.lines or ax.patches or ax.collections or ax.images or ax.texts:
312+
return False
313+
return True
314+
315+
if not is_plot_empty():
316+
bytes_io = io.BytesIO()
317+
plt.savefig(bytes_io, format='jpg')
318+
bytes_io.seek(0)
319+
basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
265320
`);
266321
},
267322
},

src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideWorker.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ describe("PyodideWorker", () => {
100100
);
101101
});
102102

103+
test("it patches urllib and requests modules", async () => {
104+
await worker.onmessage({
105+
data: {
106+
method: "runPython",
107+
python: "print('hello')",
108+
},
109+
});
110+
expect(pyodide.runPythonAsync).toHaveBeenCalledWith(
111+
expect.stringMatching(/pyodide_http.patch_all()/),
112+
);
113+
});
114+
103115
test("it tries to load package from file system", async () => {
104116
pyodide._api.pyodide_code.find_imports = () => new MockPythonArray("numpy");
105117
await worker.onmessage({

0 commit comments

Comments
 (0)