diff --git a/bin/resolve-dependencies b/bin/resolve-dependencies index 49e03595..9d9b9eee 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 = "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..e7d1bc27 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 = "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..0dfb3af9 --- /dev/null +++ b/src/py.js @@ -0,0 +1,68 @@ +import {pyodide as Pyodide} from "./dependencies.js"; + +export default async function py(require) { + const pyodide = await (await require(Pyodide)).loadPyodide(); + let patch; // a promise for patching matplotlib (if needed) + return async function py(strings) { + const globals = {}; + let code = strings[0]; + for (let i = 1, n = arguments.length; i < n; ++i) { + const name = `_${i}`; + 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); + 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): + 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): + div.remove() + return div + +plt.show = show.__get__(plt, plt.__class__) +`); +} 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",