UNPKG

quantcoin-pyodide

Version:

Quantcoin.co Python cells for Starboard Notebook

581 lines (573 loc) 31.1 kB
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 };