quantcoin-pyodide
Version:
Quantcoin.co Python cells for Starboard Notebook
581 lines (573 loc) • 31.1 kB
JavaScript
var css = "@font-face{font-family:PyodideIcons;src:url(\"data:application/x-font-ttf;charset=utf-8;base64,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\") format('truetype');font-weight:400;font-style:normal;font-display:block}.fa{font-family:PyodideIcons;font-style:normal;font-weight:400;font-variant:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-search-plus:before{content:\"\\f00e\"}.fa-home:before{content:\"\\f015\"}.fa-download:before{content:\"\\f019\"}.fa-arrows:before{content:\"\\f047\"}.fa-arrow-left:before{content:\"\\f060\"}.fa-arrow-right:before{content:\"\\f061\"}.rendered_html{overflow:auto;max-height:30em;color:#000}.rendered_html em{font-style:italic}.rendered_html strong{font-weight:700}.rendered_html :link,.rendered_html :visited,.rendered_html u{text-decoration:underline}.rendered_html h1{font-size:185.7%;margin:1.08em 0 0;font-weight:700;line-height:1}.rendered_html h2{font-size:157.1%;margin:1.27em 0 0;font-weight:700;line-height:1}.rendered_html h3{font-size:128.6%;margin:1.55em 0 0;font-weight:700;line-height:1}.rendered_html h4{font-size:100%;margin:2em 0 0;font-weight:700;line-height:1}.rendered_html h5,.rendered_html h6{font-size:100%;margin:2em 0 0;font-weight:700;line-height:1;font-style:italic}.rendered_html h1:first-child{margin-top:.538em}.rendered_html h2:first-child{margin-top:.636em}.rendered_html h3:first-child{margin-top:.777em}.rendered_html h4:first-child,.rendered_html h5:first-child,.rendered_html h6:first-child{margin-top:1em}.rendered_html ol:not(.list-inline),.rendered_html ul:not(.list-inline){padding-left:2em}.rendered_html ul{list-style:disc}.rendered_html ul ul{list-style:square;margin-top:0}.rendered_html ul ul ul{list-style:circle}.rendered_html ol{list-style:decimal}.rendered_html ol ol{list-style:upper-alpha;margin-top:0}.rendered_html ol ol ol{list-style:lower-alpha}.rendered_html ol ol ol ol{list-style:lower-roman}.rendered_html ol ol ol ol ol{list-style:decimal}.rendered_html *+ol,.rendered_html *+ul{margin-top:1em}.rendered_html hr{color:#000;background-color:#000}.rendered_html pre{margin:1em 2em;padding:0;background-color:#fff}.rendered_html code{background-color:#eff0f1}.rendered_html p code{padding:1px 5px}.rendered_html pre code{background-color:#fff}.rendered_html code,.rendered_html pre{border:0;color:#000;font-size:100%}.rendered_html blockquote{margin:1em 2em}.rendered_html table{margin-left:auto;margin-right:auto;border:none;border-collapse:collapse;border-spacing:0;color:#000;font-size:12px;table-layout:fixed}.rendered_html thead{border-bottom:1px solid #000;vertical-align:bottom}.rendered_html td,.rendered_html th,.rendered_html tr{text-align:right;vertical-align:middle;padding:.5em;line-height:normal;white-space:normal;max-width:none;border:none}.rendered_html th{font-weight:700}.rendered_html tbody tr:nth-child(odd){background:#f5f5f5}.rendered_html tbody tr:hover{background:rgba(66,165,245,.2)}.rendered_html *+table{margin-top:1em}.rendered_html p{text-align:left}.rendered_html *+p{margin-top:1em}.rendered_html img{display:block;margin-left:auto;margin-right:auto}.rendered_html *+img{margin-top:1em}.rendered_html img,.rendered_html svg{max-width:100%;height:auto}.rendered_html img.unconfined,.rendered_html svg.unconfined{max-width:none}.rendered_html .alert{margin-bottom:initial}.rendered_html *+.alert{margin-top:1em}[dir=rtl] .rendered_html p{text-align:right}";
// Regexp for validating package name and URI
const PACKAGE_REGEX = '[a-z0-9_][a-z0-9_\-]*';
const PACKAGE_URI_REGEXP = new RegExp('^https?://.*?(' + PACKAGE_REGEX + ').js$', 'i');
const PACKAGE_NAME_REGEXP = new RegExp('^' + PACKAGE_REGEX + '$', 'i');
const PUBLIC_API = [
'globals',
'loadPackage',
'loadedPackages',
'pyimport',
'repr',
'runPython',
'runPythonAsync',
'checkABI',
'version',
'autocomplete',
];
function uriToPackageName(packageUri) {
// Generate a unique package name from URI
if (PACKAGE_NAME_REGEXP.test(packageUri)) {
return packageUri;
}
const matches = PACKAGE_URI_REGEXP.exec(packageUri);
if (matches !== null) {
// Get the regexp group corresponding to the package name
return matches[1];
}
else {
return null;
}
}
function getBaseUrl() {
var baseUrl = self.pyodideArtifactsUrl || self.languagePluginUrl || 'https://cdn.quantcoin.io/pyodide/v0.15.0/full/';
baseUrl = baseUrl.substr(0, baseUrl.lastIndexOf('/')) + '/';
return baseUrl;
}
function loadScript(url, onload, onerror) {
if (self.document) { // browser
const script = self.document.createElement('script');
script.src = url;
script.onload = (e) => { onload(); };
script.onerror = (e) => { onerror(); };
self.document.head.appendChild(script);
}
else if (self.importScripts) { // webworker
try {
self.importScripts(url);
onload();
}
catch {
onerror();
}
}
}
function fixRecursionLimit(pyodide) {
// The Javascript/Wasm call stack may be too small to handle the default
// Python call stack limit of 1000 frames. This is generally the case on
// Chrom(ium), but not on Firefox. Here, we determine the Javascript call
// stack depth available, and then divide by 50 (determined heuristically)
// to set the maximum Python call stack depth.
let depth = 0;
function recurse() {
depth += 1;
recurse();
}
try {
recurse();
}
catch (err) {
}
let recursionLimit = depth / 50;
if (recursionLimit > 1000) {
recursionLimit = 1000;
}
pyodide.runPython(`import sys; sys.setrecursionlimit(int(${recursionLimit}))`);
}
function makePublicAPI(module) {
const namespace = { _module: module };
for (const name of PUBLIC_API) {
namespace[name] = module[name];
}
return namespace;
}
async function preloadWasm(pyodideModule) {
const FS = self.pyodide._module.FS;
const recurseDir = async (rootPath) => {
let dirs;
try {
dirs = FS.readdir(rootPath);
}
catch {
return;
}
for (let entry of dirs) {
if (entry.startsWith('.')) {
continue;
}
const path = rootPath + entry;
if (entry.endsWith('.so')) {
if (pyodideModule.preloadedWasm[path] === undefined) {
pyodideModule.preloadedWasm[path] = await pyodideModule.loadWebAssemblyModule(FS.readFile(path), { loadAsync: true });
}
}
else if (FS.isDir(FS.lookupPath(path).node.mode)) {
await recurseDir(path + '/');
}
}
};
await recurseDir('/');
}
// @ts-ignore
let hasPrefetched = false;
function prefetchPyodideFiles() {
if (!hasPrefetched) {
const baseUrl = getBaseUrl();
for (const file of ["pyodide.asm.wasm", "pyodide.asm.js", "pyodide.asm.data", "pyodide.asm.data.js", "packages.json"]) {
const link = document.createElement(`link`);
link.rel = `prefetch`;
link.href = `${baseUrl}${file}`;
document.head.appendChild(link);
}
hasPrefetched = true;
}
}
function injectPyodideStyles() {
if (!document.querySelector("#pyodide-styles")) {
const styleSheet = document.createElement("style");
styleSheet.id = "pyodide-styles";
styleSheet.innerHTML = css;
document.head.appendChild(styleSheet);
}
}
/**
* The main bootstrap script for loading pyodide.
*/
const IS_FIREFOX = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
class PyodideLoader {
constructor() {
/**
* This promise is used to prevent two packages being loaded asynchronously at the same time.
*/
this.loadPackagePromise = Promise.resolve();
this.ready = new Promise((resolve) => this.readyPromiseResolve = () => resolve(this));
this.baseUrl = getBaseUrl();
}
async _loadPackage(names = [], messageCallback = (msg) => { console.log(msg); }, errorCallback = (errMsg) => { console.error(errMsg); }) {
const _messageCallback = (msg) => {
messageCallback(msg);
};
const _errorCallback = (errMsg) => {
errorCallback(errMsg);
};
// DFS to find all dependencies of the requested packages
const packages = self.pyodide._module.packages.dependencies;
const loadedPackages = self.pyodide.loadedPackages;
const queue = names.slice();
const toLoad = {};
while (queue.length) {
let packageUri = queue.pop();
const pkg = uriToPackageName(packageUri);
if (pkg == null) {
_errorCallback(`Invalid package name or URI '${packageUri}'`);
return;
}
else if (pkg == packageUri) {
packageUri = 'default channel';
}
if (pkg in loadedPackages) {
if (packageUri != loadedPackages[pkg]) {
_errorCallback(`URI mismatch, attempting to load package ` +
`${pkg} from ${packageUri} while it is already ` +
`loaded from ${loadedPackages[pkg]}!`);
return;
}
}
else if (pkg in toLoad) {
if (packageUri != toLoad[pkg]) {
_errorCallback(`URI mismatch, attempting to load package ` +
`${pkg} from ${packageUri} while it is already ` +
`being loaded from ${toLoad[pkg]}!`);
return;
}
}
else {
// console.log(`${pkg} to be loaded from ${package_uri}`); // debug level info.
toLoad[pkg] = packageUri;
if (packages.hasOwnProperty(pkg)) {
packages[pkg].forEach((subPackage) => {
if (!(subPackage in loadedPackages) && !(subPackage in toLoad)) {
queue.push(subPackage);
}
});
}
else {
_errorCallback(`Unknown package '${pkg}'`);
}
}
}
self.pyodide._module.locateFile = (path) => {
// handle packages loaded from custom URLs
const pkg = path.replace(/\.data$/, "");
if (pkg in toLoad) {
const packageUri = toLoad[pkg];
if (packageUri != 'default channel') {
return packageUri.replace(/\.js$/, ".data");
}
}
return this.baseUrl + path;
};
const promise = new Promise((resolve, reject) => {
if (Object.keys(toLoad).length === 0) {
resolve('No new packages to load');
return 'No new packages to load';
}
const packageList = Array.from(Object.keys(toLoad));
_messageCallback(`Loading ${packageList.join(', ')}`);
// monitorRunDependencies is called at the beginning and the end of each
// package being loaded. We know we are done when it has been called
// exactly "toLoad * 2" times.
var packageCounter = Object.keys(toLoad).length * 2;
// Add a handler for any exceptions that are thrown in the process of
// loading a package
const windowErrorHandler = (err) => {
delete self.pyodide._module.monitorRunDependencies;
self.removeEventListener('error', windowErrorHandler);
// Set up a new Promise chain, since this one failed
this.loadPackagePromise = new Promise((resolve) => resolve());
reject(err.message);
};
self.pyodide._module.monitorRunDependencies = () => {
packageCounter--;
if (packageCounter === 0) {
for (const pkg in toLoad) {
self.pyodide.loadedPackages[pkg] = toLoad[pkg];
}
delete self.pyodide._module.monitorRunDependencies;
self.removeEventListener('error', windowErrorHandler);
let resolveMsg = `Loaded `;
if (packageList.length > 0) {
resolveMsg += packageList.join(', ');
}
else {
resolveMsg += 'no packages';
}
if (!IS_FIREFOX) {
preloadWasm(this.pyodideModule).then(() => {
console.log(resolveMsg);
resolve(resolveMsg);
});
}
else {
console.log(resolveMsg);
resolve(resolveMsg);
}
}
};
self.addEventListener('error', windowErrorHandler);
for (const pkg in toLoad) {
let scriptSrc;
const packageUri = toLoad[pkg];
if (packageUri == 'default channel') {
scriptSrc = `${this.baseUrl}${pkg}.js`;
}
else {
scriptSrc = `${packageUri}`;
}
// _messageCallback(`Loading ${pkg} from ${scriptSrc}`)
loadScript(scriptSrc, () => { }, () => {
// If the packageUri fails to load, call monitorRunDependencies twice
// (so packageCounter will still hit 0 and finish loading), and remove
// the package from toLoad so we don't mark it as loaded, and remove
// the package from packageList so we don't say that it was loaded.
_errorCallback(`Couldn't load package from URL ${scriptSrc}`);
delete toLoad[pkg];
const packageListIndex = packageList.indexOf(pkg);
if (packageListIndex !== -1) {
packageList.splice(packageListIndex, 1);
}
for (let i = 0; i < 2; i++) {
self.pyodide._module.monitorRunDependencies();
}
});
}
// We have to invalidate Python's import caches, or it won't
// see the new files. This is done here so it happens in parallel
// with the fetching over the network.
self.pyodide.runPython('import importlib as _importlib\n' +
'_importlib.invalidate_caches()\n');
});
return promise;
}
loadPackage(names, messageCallback, errorCallback) {
/* We want to make sure that only one loadPackage invocation runs at any
* given time, so this creates a "chain" of promises. */
this.loadPackagePromise = this.loadPackagePromise.then(() => this._loadPackage(names, messageCallback, errorCallback));
return this.loadPackagePromise;
}
createModule() {
const module = {
noImageDecoding: true,
noAudioDecoding: true,
noWasmDecoding: true,
preloadedWasm: {},
};
module.checkABI = (AbiNumber) => {
if (AbiNumber !== parseInt('1')) {
const AbiMismatchException = `ABI numbers differ. Expected 1, got ${AbiNumber}`;
console.error(AbiMismatchException);
throw AbiMismatchException;
}
return true;
};
module.autocomplete = (path) => {
const pyodideModule = module.pyimport("pyodide");
return pyodideModule.get_completions(path);
};
module.locateFile = (path) => this.baseUrl + path;
return module;
}
async setup() {
const wasmUrl = `${this.baseUrl}pyodide.asm.wasm`;
this.pyodideModule = this.createModule();
// This global is used in one of the imported scripts.
// it gets deleted in Module.postRun()
self.Module = this.pyodideModule;
let wasmPromise;
const wasmFetch = fetch(wasmUrl);
if (WebAssembly.compileStreaming === undefined) {
wasmPromise = new Promise(async (resolve) => {
const bytes = await (await wasmFetch).arrayBuffer();
resolve(WebAssembly.compile(bytes));
});
}
else {
wasmPromise = WebAssembly.compileStreaming(wasmFetch);
}
this.pyodideModule.instantiateWasm = async (info, receiveInstance) => {
receiveInstance(await WebAssembly.instantiate(await wasmPromise, info));
return {};
};
const postRunPromise = new Promise((resolve, reject) => {
this.pyodideModule.postRun = async () => {
delete self.Module;
const json = await (await fetch(`${this.baseUrl}packages.json`)).json();
fixRecursionLimit(self.pyodide);
self.pyodide.globals = self.pyodide.runPython('import sys\nsys.modules["__main__"]');
self.pyodide = makePublicAPI(self.pyodide);
self.pyodide._module.packages = json;
resolve();
};
});
const dataLoadPromise = new Promise((resolve, reject) => {
this.pyodideModule.monitorRunDependencies =
(n) => {
if (n === 0) {
delete this.pyodideModule.monitorRunDependencies;
resolve();
}
};
});
const promises = Promise.all([postRunPromise, dataLoadPromise]);
const dataScriptSrc = `${this.baseUrl}pyodide.asm.data.js`;
loadScript(dataScriptSrc, () => {
const scriptSrc = `${this.baseUrl}pyodide.asm.js`;
loadScript(scriptSrc, () => {
// The emscripten module needs to be at this location for the core
// filesystem to install itself. Once that's complete, it will be replaced
// by the call to `makePublicAPI` with a more limited public API.
self.pyodide = self.pyodide(this.pyodideModule); // TODO type this better
self.pyodide.loadedPackages = {};
self.pyodide.loadPackage = (...v) => this.loadPackage(...v);
}, () => { });
}, () => { });
await promises;
this.readyPromiseResolve();
}
}
let loader;
function loadPyodide() {
if (!loader) {
loader = new PyodideLoader();
loader.setup();
}
return loader.ready;
}
/**
* Creates a promise with the resolve and reject function outside of it, useful for tasks that may complete at any time.
* Based on MIT licensed https://github.com/arikw/flat-promise, with typings added by gzuidhof.
* @param executor
*/
function flatPromise(executor) {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
if (executor) {
executor(resolve, reject);
}
return { promise, resolve, reject };
}
function registerPython() {
let CURRENT_HTML_OUTPUT_ELEMENT = undefined;
/**
* This is a promise chain used to make sure no cells overlap in execution.
*/
let currentExecutionPromise = Promise.resolve();
/**
* Dummy object to act like that used by Iodide.
* This is used for libraries that output to html (e.g. matplotlib), we imitate
* iodide's API here. Alternatively we could fork Pyodide and change the Python code, but
* let's avoid that for as long as possible.
*/
window.iodide = {
output: {
// Create a new element with tagName
// and add it to an element with id "root".
element: (tagName) => {
const elem = document.createElement(tagName);
if (!CURRENT_HTML_OUTPUT_ELEMENT) {
console.log("HTML output from pyodide but nowhere to put it, will append to body instead.");
document.querySelector("body").appendChild(elem);
}
else {
CURRENT_HTML_OUTPUT_ELEMENT.appendChild(elem);
}
return elem;
}
}
};
/** Naughty matplotlib WASM backend captures and disables contextmenu globally.. hack to prevent that */
window.addEventListener("contextmenu", function (event) {
if (event.target instanceof HTMLElement && event.target.id.startsWith("matplotlib_") && event.target.tagName === "CANVAS") {
return false;
}
event.stopPropagation();
}, true);
/* These globals are exposed by Starboard Notebook. We can re-use them so we don't have to bundle them again. */
const runtime = window.runtime;
const html = runtime.exports.libraries.LitHtml.html;
const lithtml = runtime.exports.libraries.LitHtml;
const StarboardTextEditor = runtime.exports.elements.StarboardTextEditor;
const ConsoleOutputElement = runtime.exports.elements.ConsoleOutputElement;
const cellControlsTemplate = runtime.exports.templates.cellControls;
const renderIfHtml = runtime.exports.core.renderIfHtmlOutput;
const icons = runtime.exports.templates.icons;
const PYTHON_CELL_TYPE_DEFINITION = {
name: "Python",
cellType: ["python", "python3", "ipython3", "pypy", "py"],
createHandler: (cell, runtime) => new PythonCellHandler(cell, runtime),
};
function isPyProxy(val) {
return typeof val === 'function' && window.pyodide._module.PyProxy.isPyProxy(val);
}
class PythonCellHandler {
constructor(cell, runtime) {
this.lastRunId = 0;
this.isCurrentlyRunning = false;
this.isCurrentlyLoadingPyodide = false;
this.cell = cell;
this.runtime = runtime;
}
getControls() {
const icon = this.isCurrentlyRunning ? icons.ClockIcon : icons.PlayCircleIcon;
const tooltip = this.isCurrentlyRunning ? "Run Cell" : "Cell is running";
const runButton = {
icon,
tooltip,
callback: () => this.runtime.controls.emit({ id: this.cell.id, type: "RUN_CELL" }),
};
let buttons = [runButton];
if (this.isCurrentlyLoadingPyodide) {
buttons = [{
icon: icons.GearsIcon,
tooltip: "Downloading and initializing Pyodide",
callback: () => { alert("Loading Python runtime. It's 5 to 15 MB in size, so it may take a while. It will be cached for next time."); }
}, ...buttons];
}
return cellControlsTemplate({ buttons });
}
attach(params) {
this.elements = params.elements;
const topElement = this.elements.topElement;
lithtml.render(this.getControls(), this.elements.topControlsElement);
this.editor = new StarboardTextEditor(this.cell, this.runtime, { language: "python" });
topElement.appendChild(this.editor);
injectPyodideStyles();
// When a Python cell is created - we can start downloading the Pyodide files as most likely we will need them soon.
prefetchPyodideFiles();
}
async waitForPyodide(pyoPromise) {
// We load the pyodide runtime and show an icon while that is happening..
this.isCurrentlyLoadingPyodide = true;
lithtml.render(this.getControls(), this.elements.topControlsElement);
await pyoPromise;
this.isCurrentlyLoadingPyodide = false;
lithtml.render(this.getControls(), this.elements.topControlsElement);
}
async run() {
const pyoPromise = loadPyodide();
const codeToRun = this.cell.textContent;
this.lastRunId++;
const currentRunId = this.lastRunId;
this.isCurrentlyRunning = true;
this.outputElement = new ConsoleOutputElement();
const htmlOutput = document.createElement("div");
lithtml.render(html `${this.outputElement}${htmlOutput}`, this.elements.bottomElement);
let val = undefined;
const { resolve, promise } = flatPromise();
await this.waitForPyodide(pyoPromise);
await currentExecutionPromise;
CURRENT_HTML_OUTPUT_ELEMENT = htmlOutput;
this.outputElement.hook(this.runtime.consoleCatcher);
currentExecutionPromise = promise;
try {
val = await window.pyodide.runPythonAsync(codeToRun, (msg) => console.log(msg), (err) => console.error("ERROR", err));
window.$_ = val;
const htmlWasRendered = renderIfHtml(val, htmlOutput);
if (!htmlWasRendered && val !== undefined) {
if (isPyProxy(val)) {
let hadHTMLOutput = false;
if (val._repr_html_ !== undefined) {
let result = val._repr_html_();
if (typeof result === 'string') {
let div = document.createElement('div');
div.className = 'rendered_html';
div.innerHTML = result;
htmlOutput.appendChild(div);
hadHTMLOutput = true;
}
}
if (!hadHTMLOutput) {
this.outputElement.addEntry({
method: "result",
data: [val]
});
}
}
else {
this.outputElement.addEntry({
method: "result",
data: [val]
});
}
}
}
catch (e) {
console.error(e);
this.outputElement.addEntry({
method: "error",
data: [e]
});
}
// Not entirely sure this has to be awaited, is any output delayed by a tick from pyodide?
await this.outputElement.unhookAfterOneTick(this.runtime.consoleCatcher);
resolve();
if (this.lastRunId === currentRunId) {
this.isCurrentlyRunning = false;
lithtml.render(this.getControls(), this.elements.topControlsElement);
}
return val;
}
focusEditor() {
this.editor.focus();
}
async dispose() {
this.editor.remove();
}
}
runtime.definitions.cellTypes.register(PYTHON_CELL_TYPE_DEFINITION.cellType, PYTHON_CELL_TYPE_DEFINITION);
}
export { registerPython };