From 4429f9095d013fef75cfdc68b1dde289adf5dd0e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 23 Jun 2022 09:55:20 -0400 Subject: [PATCH 1/6] python literals via pyodide --- bin/resolve-dependencies | 4 ++++ src/dependencies.js | 1 + src/library.js | 2 ++ src/py.js | 11 +++++++++++ test/index-test.js | 1 + 5 files changed, 19 insertions(+) create mode 100644 src/py.js diff --git a/bin/resolve-dependencies b/bin/resolve-dependencies index 49e03595..2218b7c5 100755 --- a/bin/resolve-dependencies +++ b/bin/resolve-dependencies @@ -86,6 +86,10 @@ const mains = ["unpkg", "jsdelivr", "browser", "main"]; const package = await resolve("leaflet"); console.log(`export const leaflet = dependency("${package.name}", "${package.version}", "${package.export.replace(/-src\.js$/, ".js")}");`); } + { + const package = await resolve("pyodide"); + console.log(`export const pyodide = {resolve: () => "https://cdn.jsdelivr.net/pyodide/v${package.version}/full/pyodide.js"};`); + } })(); async function resolve(specifier) { diff --git a/src/dependencies.js b/src/dependencies.js index 5a92fe9e..1aa67ff2 100644 --- a/src/dependencies.js +++ b/src/dependencies.js @@ -19,3 +19,4 @@ export const topojson = dependency("topojson-client", "3.1.0", "dist/topojson-cl export const exceljs = dependency("exceljs", "4.3.0", "dist/exceljs.min.js"); export const mermaid = dependency("mermaid", "9.1.1", "dist/mermaid.min.js"); export const leaflet = dependency("leaflet", "1.8.0", "dist/leaflet.js"); +export const pyodide = {resolve: () => "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"}; diff --git a/src/library.js b/src/library.js index 0d8578b8..1a6d8779 100644 --- a/src/library.js +++ b/src/library.js @@ -10,6 +10,7 @@ import mermaid from "./mermaid.js"; import Mutable from "./mutable.js"; import now from "./now.js"; import Promises from "./promises/index.js"; +import py from "./py.js"; import resolve from "./resolve.js"; import requirer, {requireDefault, setDefaultRequire} from "./require.js"; import SQLite, {SQLiteDatabaseClient} from "./sqlite.js"; @@ -44,6 +45,7 @@ export default Object.assign(Object.defineProperties(function Library(resolver) Inputs: () => require(inputs.resolve()).then(Inputs => ({...Inputs, file: Inputs.fileOf(AbstractFile)})), L: () => leaflet(require), mermaid: () => mermaid(require), + py: () => py(require), Plot: () => require(plot.resolve()), require: () => require, resolve: () => resolve, // deprecated; use async require.resolve instead diff --git a/src/py.js b/src/py.js new file mode 100644 index 00000000..7dce5cdc --- /dev/null +++ b/src/py.js @@ -0,0 +1,11 @@ +import {pyodide as Pyodide} from "./dependencies.js"; + +export default async function py(require) { + const pyodide = await (await require(Pyodide.resolve())).loadPyodide(); + return async function py() { + const code = String.raw.apply(String, arguments); + await pyodide.loadPackagesFromImports(code); + const value = await pyodide.runPython(code); + return pyodide.isPyProxy(value) ? value.toJs() : value; + }; +} diff --git a/test/index-test.js b/test/index-test.js index 9095531f..eb8b4a8c 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -34,6 +34,7 @@ test("new Library returns a library with the expected keys", async t => { "now", "olympians", "penguins", + "py", "require", "resolve", "svg", From 40d61935a074aef15efaa192e67d308d463e8b42 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 23 Jun 2022 10:05:59 -0400 Subject: [PATCH 2/6] allow value interpolation --- src/py.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/py.js b/src/py.js index 7dce5cdc..67795e4a 100644 --- a/src/py.js +++ b/src/py.js @@ -2,10 +2,17 @@ import {pyodide as Pyodide} from "./dependencies.js"; export default async function py(require) { const pyodide = await (await require(Pyodide.resolve())).loadPyodide(); - return async function py() { - const code = String.raw.apply(String, arguments); + return async function py(strings, ...values) { + let globals = {}; + const code = strings.reduce((code, string, i) => { + if (!(i in values)) return code + string; + const name = `_${i}`; + globals[name] = values[i]; + return code + string + name; + }, ""); + globals = pyodide.toPy(globals); await pyodide.loadPackagesFromImports(code); - const value = await pyodide.runPython(code); + const value = await pyodide.runPythonAsync(code, {globals}); return pyodide.isPyProxy(value) ? value.toJs() : value; }; } From 8350c07f080f8a90841ea9f341764807b1635148 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 23 Jun 2022 17:02:28 -0400 Subject: [PATCH 3/6] =?UTF-8?q?patch=20matplotlib=E2=80=99s=20show=20(#294?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * patch matplotlib’s show * font-awesome for matplotlib buttons * fix for redrawing figure * only remove if parent is body --- src/py.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/py.js b/src/py.js index 67795e4a..e96acabc 100644 --- a/src/py.js +++ b/src/py.js @@ -2,17 +2,62 @@ import {pyodide as Pyodide} from "./dependencies.js"; export default async function py(require) { const pyodide = await (await require(Pyodide.resolve())).loadPyodide(); + let patch; // a promise for patching matplotlib (if needed) return async function py(strings, ...values) { - let globals = {}; + const globals = {}; const code = strings.reduce((code, string, i) => { if (!(i in values)) return code + string; const name = `_${i}`; globals[name] = values[i]; return code + string + name; }, ""); - globals = pyodide.toPy(globals); - await pyodide.loadPackagesFromImports(code); - const value = await pyodide.runPythonAsync(code, {globals}); + const imports = findImports(pyodide, code); + if (imports.includes("matplotlib") && !patch) await (patch = patchMatplotlib(require, pyodide)); + if (imports.length) await pyodide.loadPackagesFromImports(code); + const value = await pyodide.runPythonAsync(code, {globals: pyodide.toPy(globals)}); return pyodide.isPyProxy(value) ? value.toJs() : value; }; } + +// https://github.com/pyodide/pyodide/blob/1624e4a62445876a2d810fdbfc9ddb69a8321a8e/src/js/api.ts#L119-L125 +function findImports(pyodide, code) { + const imports = pyodide.pyodide_py.find_imports(code); + try { + return imports.toJs(); + } finally { + imports.destroy(); + } +} + +// Overrides matplotlib’s show function to return a DIV such that when used as +// the last expression in an Observable cell, the inspector will display it. +async function patchMatplotlib(require, pyodide) { + require.resolve("font-awesome@4.7.0/css/font-awesome.min.css").then(href => { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = href; + document.head.appendChild(link); + }); + await pyodide.loadPackage("matplotlib"); + await pyodide.runPythonAsync(`from matplotlib import pyplot as plt +from js import document + +_show = plt.show + +def create_root_element(self): + div = document.createElement("div") + document.body.appendChild(div) + return div + +def show(self): + c = plt.gcf().canvas + c.create_root_element = create_root_element.__get__(c, c.__class__) + _show() + div = c.get_element("") + if (div.parentNode == document.body): + document.body.removeChild(div) + return div + +plt.show = show.__get__(plt, plt.__class__) +`); +} From f73f81e79bb49748b2e87da7e67f03bc64356620 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 23 Jun 2022 17:11:45 -0400 Subject: [PATCH 4/6] more efficient interpolation --- src/py.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/py.js b/src/py.js index e96acabc..68dc9e8b 100644 --- a/src/py.js +++ b/src/py.js @@ -3,14 +3,14 @@ import {pyodide as Pyodide} from "./dependencies.js"; export default async function py(require) { const pyodide = await (await require(Pyodide.resolve())).loadPyodide(); let patch; // a promise for patching matplotlib (if needed) - return async function py(strings, ...values) { + return async function py(strings) { const globals = {}; - const code = strings.reduce((code, string, i) => { - if (!(i in values)) return code + string; + let code = strings[0]; + for (let i = 1, n = arguments.length; i < n; ++i) { const name = `_${i}`; - globals[name] = values[i]; - return code + string + name; - }, ""); + globals[name] = arguments[i]; + code += name + strings[i]; + } const imports = findImports(pyodide, code); if (imports.includes("matplotlib") && !patch) await (patch = patchMatplotlib(require, pyodide)); if (imports.length) await pyodide.loadPackagesFromImports(code); From 03eb4ccb6bad0163f95fdd350dbbfe0ddfe99392 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 24 Jun 2022 22:42:53 -0400 Subject: [PATCH 5/6] simplify pyodide resolution --- bin/resolve-dependencies | 2 +- src/dependencies.js | 2 +- src/py.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/resolve-dependencies b/bin/resolve-dependencies index 2218b7c5..9d9b9eee 100755 --- a/bin/resolve-dependencies +++ b/bin/resolve-dependencies @@ -88,7 +88,7 @@ const mains = ["unpkg", "jsdelivr", "browser", "main"]; } { const package = await resolve("pyodide"); - console.log(`export const pyodide = {resolve: () => "https://cdn.jsdelivr.net/pyodide/v${package.version}/full/pyodide.js"};`); + console.log(`export const pyodide = "https://cdn.jsdelivr.net/pyodide/v${package.version}/full/pyodide.js";`); } })(); diff --git a/src/dependencies.js b/src/dependencies.js index 1aa67ff2..e7d1bc27 100644 --- a/src/dependencies.js +++ b/src/dependencies.js @@ -19,4 +19,4 @@ export const topojson = dependency("topojson-client", "3.1.0", "dist/topojson-cl export const exceljs = dependency("exceljs", "4.3.0", "dist/exceljs.min.js"); export const mermaid = dependency("mermaid", "9.1.1", "dist/mermaid.min.js"); export const leaflet = dependency("leaflet", "1.8.0", "dist/leaflet.js"); -export const pyodide = {resolve: () => "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"}; +export const pyodide = "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"; diff --git a/src/py.js b/src/py.js index 68dc9e8b..634e6e29 100644 --- a/src/py.js +++ b/src/py.js @@ -1,7 +1,7 @@ import {pyodide as Pyodide} from "./dependencies.js"; export default async function py(require) { - const pyodide = await (await require(Pyodide.resolve())).loadPyodide(); + const pyodide = await (await require(Pyodide)).loadPyodide(); let patch; // a promise for patching matplotlib (if needed) return async function py(strings) { const globals = {}; From 8c2eacb4891106c3bd67e1646c7fd59f2d5eeb79 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 24 Jun 2022 22:53:25 -0400 Subject: [PATCH 6/6] remove top element; autoclose --- src/py.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/py.js b/src/py.js index 634e6e29..0dfb3af9 100644 --- a/src/py.js +++ b/src/py.js @@ -50,12 +50,17 @@ def create_root_element(self): return div def show(self): - c = plt.gcf().canvas + f = plt.gcf() + c = f.canvas c.create_root_element = create_root_element.__get__(c, c.__class__) _show() + plt.close(f) + top = c.get_element("top") + if (top): + top.remove() div = c.get_element("") if (div.parentNode == document.body): - document.body.removeChild(div) + div.remove() return div plt.show = show.__get__(plt, plt.__class__)