diff --git a/pyscriptjs/examples/bokeh.html b/pyscriptjs/examples/bokeh.html index 12655808..541040db 100644 --- a/pyscriptjs/examples/bokeh.html +++ b/pyscriptjs/examples/bokeh.html @@ -23,7 +23,7 @@

Bokeh Example

- + import json import pyodide diff --git a/pyscriptjs/examples/bokeh_interactive.html b/pyscriptjs/examples/bokeh_interactive.html index f47d8965..61cd9bb6 100644 --- a/pyscriptjs/examples/bokeh_interactive.html +++ b/pyscriptjs/examples/bokeh_interactive.html @@ -23,7 +23,7 @@

Bokeh Example

- + import asyncio import json import pyodide diff --git a/pyscriptjs/examples/repl2.html b/pyscriptjs/examples/repl2.html index 53099a20..39c595e8 100644 --- a/pyscriptjs/examples/repl2.html +++ b/pyscriptjs/examples/repl2.html @@ -20,9 +20,12 @@ +

Custom REPL

- +
+
+
diff --git a/pyscriptjs/examples/simple_script.html b/pyscriptjs/examples/simple_script.html index f1822741..33afdaec 100644 --- a/pyscriptjs/examples/simple_script.html +++ b/pyscriptjs/examples/simple_script.html @@ -14,7 +14,7 @@
- + from datetime import datetime now = datetime.now() now.strftime("%m/%d/%Y, %H:%M:%S") diff --git a/pyscriptjs/examples/simple_script2.html b/pyscriptjs/examples/simple_script2.html index 60a06fe2..c52e3b01 100644 --- a/pyscriptjs/examples/simple_script2.html +++ b/pyscriptjs/examples/simple_script2.html @@ -20,7 +20,7 @@
start time:
- + import utils utils.now() diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts new file mode 100644 index 00000000..0ec2a411 --- /dev/null +++ b/pyscriptjs/src/components/base.ts @@ -0,0 +1,153 @@ +import { pyodideLoaded, loadedEnvironments, componentDetailsNavOpen, mode } from '../stores'; + +// Premise used to connect to the first available pyodide interpreter +let pyodideReadyPromise; +let environments; +let currentMode; +let Element; + +pyodideLoaded.subscribe(value => { + pyodideReadyPromise = value; +}); +loadedEnvironments.subscribe(value => { + environments = value; +}); + +let propertiesNavOpen; +componentDetailsNavOpen.subscribe(value => { + propertiesNavOpen = value; +}); + +mode.subscribe(value => { + currentMode = value; +}); + +// TODO: use type declaractions +type PyodideInterface = { + registerJsModule(name: string, module: object): void +} + + +export class BaseEvalElement extends HTMLElement { + shadow: ShadowRoot; + wrapper: HTMLElement; + code: string; + source: string; + btnConfig: HTMLElement; + btnRun: HTMLElement; + outputElement: HTMLElement; + errorElement: HTMLElement; + theme: string; + + constructor() { + super(); + + // attach shadow so we can preserve the element original innerHtml content + this.shadow = this.attachShadow({ mode: 'open'}); + this.wrapper = document.createElement('slot'); + this.shadow.appendChild(this.wrapper); + } + + addToOutput(s: string) { + this.outputElement.innerHTML += "
"+s+"
"; + this.outputElement.hidden = false; + } + + postEvaluate(){ + + } + + getSourceFromElement(): string{ + return ""; + } + + async getSourceFromFile(s: string): Promise{ + let pyodide = await pyodideReadyPromise; + let response = await fetch(s); + this.code = await response.text(); + return this.code; + } + + protected async _register_esm(pyodide: PyodideInterface): Promise { + const imports: {[key: string]: unknown} = {} + + for (const node of document.querySelectorAll("script[type='importmap']")) { + const importmap = (() => { + try { + return JSON.parse(node.textContent) + } catch { + return null + } + })() + + if (importmap?.imports == null) + continue + + for (const [name, url] of Object.entries(importmap.imports)) { + if (typeof name != "string" || typeof url != "string") + continue + + try { + // XXX: pyodide doesn't like Module(), failing with + // "can't read 'name' of undefined" at import time + imports[name] = {...await import(url)} + } catch { + console.error(`failed to fetch '${url}' for '${name}'`) + } + } + } + + pyodide.registerJsModule("esm", imports) + } + + async evaluate(): Promise { + console.log('evaluate'); + let pyodide = await pyodideReadyPromise; + let source: string; + let output; + try { + // @ts-ignore + if (this.source){ + source = await this.getSourceFromFile(this.source); + }else{ + source = this.getSourceFromElement(); + } + + await this._register_esm(pyodide); + + if (source.includes("asyncio")){ + await pyodide.runPythonAsync(`output_manager.change("`+this.outputElement.id+`", "`+this.errorElement.id+`")`); + output = await pyodide.runPythonAsync(source); + await pyodide.runPythonAsync(`output_manager.revert()`) + }else{ + output = pyodide.runPython(`output_manager.change("`+this.outputElement.id+`", "`+this.errorElement.id+`")`); + output = pyodide.runPython(source); + pyodide.runPython(`output_manager.revert()`) + } + + if (output !== undefined){ + if (Element === undefined){ + Element = pyodide.globals.get('Element'); + } + const out = Element(this.outputElement.id); + // @ts-ignore + out.write.callKwargs(output, { append : true}); + + this.outputElement.hidden = false; + this.outputElement.style.display = 'block'; + } + + this.postEvaluate() + + } catch (err) { + if (Element === undefined){ + Element = pyodide.globals.get('Element'); + } + const out = Element(this.errorElement.id); + // @ts-ignore + out.write.callKwargs(err, { append : true}); + this.errorElement.hidden = false; + this.errorElement.style.display = 'block'; + } + } + } diff --git a/pyscriptjs/src/components/pybox.ts b/pyscriptjs/src/components/pybox.ts index 43c18533..c92845c1 100644 --- a/pyscriptjs/src/components/pybox.ts +++ b/pyscriptjs/src/components/pybox.ts @@ -58,7 +58,7 @@ export class PyBox extends HTMLElement { for (let i in this.widths) { // @ts-ignore - addClasses(mainDiv.childNodes[parseInt(i)], [this.widths[i]]); + addClasses(mainDiv.childNodes[parseInt(i)], [this.widths[i], 'mx-4']); } this.appendChild(mainDiv); diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index 361c01df..5a9f9c18 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -8,6 +8,7 @@ import { oneDarkTheme } from "@codemirror/theme-one-dark"; import { pyodideLoaded, loadedEnvironments, componentDetailsNavOpen, currentComponentDetails, mode } from '../stores'; import { addClasses } from '../utils'; +import { BaseEvalElement } from './base'; // Premise used to connect to the first available pyodide interpreter let pyodideReadyPromise; @@ -43,27 +44,13 @@ function createCmdHandler(el){ } -export class PyRepl extends HTMLElement { - shadow: ShadowRoot; - wrapper: HTMLElement; +export class PyRepl extends BaseEvalElement { editor: EditorView; editorNode: HTMLElement; - code: string; - cm: any; - btnConfig: HTMLElement; - btnRun: HTMLElement; - editorOut: HTMLElement; //HTMLTextAreaElement; - theme: string; - // editorState: EditorState; constructor() { super(); - // attach shadow so we can preserve the element original innerHtml content - this.shadow = this.attachShadow({ mode: 'open'}); - - this.wrapper = document.createElement('slot'); - // add an extra div where we can attach the codemirror editor this.editorNode = document.createElement('div'); addClasses(this.editorNode, ["editor-box"]) @@ -111,7 +98,7 @@ export class PyRepl extends HTMLElement { }) let mainDiv = document.createElement('div'); - addClasses(mainDiv, ["parentBox", "group", "flex", "flex-col", "mt-10", "border-2", "border-gray-200", "rounded-lg"]) + addClasses(mainDiv, ["parentBox", "group", "flex", "flex-col", "mt-2", "border-2", "border-gray-200", "rounded-lg"]) // add Editor to main PyScript div // Butons DIV @@ -145,7 +132,7 @@ export class PyRepl extends HTMLElement { currentComponentDetails.set([ {key: "auto-generate", value: true}, - {key:"target", value: "default"}, + {key:"output", value: "default"}, {key: "source", value: "self"}, {key: "output-mode", value: "clear"} ]) @@ -171,83 +158,77 @@ export class PyRepl extends HTMLElement { this.setAttribute("root", this.id); } - if (this.hasAttribute('target')) { - this.editorOut = document.getElementById(this.getAttribute('target')); + if (this.hasAttribute('output')) { + this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output')); // in this case, the default output-mode is append, if hasn't been specified if (!this.hasAttribute('output-mode')) { this.setAttribute('output-mode', 'append'); } }else{ - // Editor Output Div - this.editorOut = document.createElement('div'); - this.editorOut.classList.add("output"); - this.editorOut.hidden = true; - this.editorOut.id = this.id + "-" + this.getAttribute("exec-id"); + if (this.hasAttribute('std-out')){ + this.outputElement = document.getElementById(this.getAttribute('std-out')); + }else{ + // In this case neither output or std-out have been provided so we need + // to create a new output div to output to + this.outputElement = document.createElement('div'); + this.outputElement.classList.add("output"); + this.outputElement.hidden = true; + this.outputElement.id = this.id + "-" + this.getAttribute("exec-id"); - // add the output div id there's not target - mainDiv.appendChild(this.editorOut); + // add the output div id if there's not output pre-defined + mainDiv.appendChild(this.outputElement); + } + + if (this.hasAttribute('std-err')){ + this.errorElement = document.getElementById(this.getAttribute('std-err')); + }else{ + this.errorElement = this.outputElement; + } } + this.appendChild(mainDiv); this.editor.focus(); console.log('connected'); } addToOutput(s: string) { - this.editorOut.innerHTML += "
"+s+"
"; - this.editorOut.hidden = false; + this.outputElement.innerHTML += "
"+s+"
"; + this.outputElement.hidden = false; } - async evaluate() { - console.log('evaluate'); - let pyodide = await pyodideReadyPromise; - // debugger - try { - // @ts-ignore - let source = this.editor.state.doc.toString(); - let output; - if (source.includes("asyncio")){ - output = await pyodide.runPythonAsync(source); - }else{ - output = pyodide.runPython(source); - } - - if (output !== undefined){ - let Element = pyodide.globals.get('Element'); - let out = Element(this.editorOut.id); - // @ts-ignore - out.write(output); - out.write.callKwargs(output, { append : false}); - - if (!this.hasAttribute('target')) { - this.editorOut.hidden = false; - } - // this.addToOutput(output); - } - - if (this.hasAttribute('auto-generate')) { - let nextExecId = parseInt(this.getAttribute('exec-id')) + 1; - const newPyRepl = document.createElement("py-repl"); - newPyRepl.setAttribute('root', this.getAttribute('root')); - newPyRepl.id = this.getAttribute('root') + "-" + nextExecId.toString(); - newPyRepl.setAttribute('auto-generate', null); - if (this.hasAttribute('target')){ - newPyRepl.setAttribute('target', this.getAttribute('target')); - } - - newPyRepl.setAttribute('exec-id', nextExecId.toString()); - this.parentElement.appendChild(newPyRepl); - } - } catch (err) { - this.addToOutput(err); - } + postEvaluate(): void { + if (this.hasAttribute('auto-generate')) { + let nextExecId = parseInt(this.getAttribute('exec-id')) + 1; + const newPyRepl = document.createElement("py-repl"); + newPyRepl.setAttribute('root', this.getAttribute('root')); + newPyRepl.id = this.getAttribute('root') + "-" + nextExecId.toString(); + newPyRepl.setAttribute('auto-generate', null); + if (this.hasAttribute('output')){ + newPyRepl.setAttribute('output', this.getAttribute('output')); + } + if (this.hasAttribute('std-out')){ + newPyRepl.setAttribute('std-out', this.getAttribute('std-out')); + } + if (this.hasAttribute('std-err')){ + newPyRepl.setAttribute('std-err', this.getAttribute('std-err')); + } + + newPyRepl.setAttribute('exec-id', nextExecId.toString()); + this.parentElement.appendChild(newPyRepl); } - + } + + getSourceFromElement(): string { + const sourceStrings = [`output_manager.change("`+this.outputElement.id+`")`, + ...this.editor.state.doc.toString().split("\n")]; + return sourceStrings.join('\n') + } + render(){ console.log('rendered'); } } - \ No newline at end of file diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 8fc626c3..4b0da39d 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -8,6 +8,7 @@ import { oneDarkTheme } from "@codemirror/theme-one-dark"; import { pyodideLoaded, loadedEnvironments, componentDetailsNavOpen, currentComponentDetails, mode, addToScriptsQueue, addInitializer, addPostInitializer } from '../stores'; import { addClasses } from '../utils'; +import { BaseEvalElement } from './base'; // Premise used to connect to the first available pyodide interpreter let pyodideReadyPromise; @@ -50,13 +51,15 @@ type PyodideInterface = { registerJsModule(name: string, module: object): void } +// TODO: This should be used as base for generic scripts that need exectutoin +// from PyScript to initializers, etc... class Script { source: string; state: string; - target: string; + output: string; - constructor(source: string, target: string) { - this.target = target; + constructor(source: string, output: string) { + this.output = output; this.source = source; this.state = 'waiting'; } @@ -75,7 +78,7 @@ class Script { output = pyodide.runPython(this.source); } - if (this.target){ + if (this.output){ // this.editorOut.innerHTML = s; } // if (output !== undefined){ @@ -90,30 +93,12 @@ class Script { } } -export class PyScript extends HTMLElement { - shadow: ShadowRoot; - wrapper: HTMLElement; - editor: EditorView; - editorNode: HTMLElement; - code: string; - cm: any; - btnConfig: HTMLElement; - btnRun: HTMLElement; - editorOut: HTMLElement; //HTMLTextAreaElement; - source: string; - // editorState: EditorState; +export class PyScript extends BaseEvalElement { constructor() { super(); - // attach shadow so we can preserve the element original innerHtml content - this.shadow = this.attachShadow({ mode: 'open'}); - - this.wrapper = document.createElement('slot'); - // add an extra div where we can attach the codemirror editor - this.editorNode = document.createElement('div'); - addClasses(this.editorNode, ["editor-box"]) this.shadow.appendChild(this.wrapper); } @@ -140,11 +125,6 @@ export class PyScript extends HTMLElement { ] }) - this.editor = new EditorView({ - state: startState, - parent: this.editorNode - }) - let mainDiv = document.createElement('div'); addClasses(mainDiv, ["parentBox", "flex", "flex-col", "border-4", "border-dashed", "border-gray-200", "rounded-lg"]) // add Editor to main PyScript div @@ -180,7 +160,7 @@ export class PyScript extends HTMLElement { currentComponentDetails.set([ {key: "auto-generate", value: true}, - {key:"target", value: "default"}, + {key:"output", value: "default"}, {key: "source", value: "self"} ]) } @@ -190,18 +170,34 @@ export class PyScript extends HTMLElement { eDiv.appendChild(this.btnConfig); mainDiv.appendChild(eDiv); - mainDiv.appendChild(this.editorNode); - if (this.hasAttribute('target')) { - this.editorOut = document.getElementById(this.getAttribute('target')); + if (this.hasAttribute('output')) { + this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output')); + + // in this case, the default output-mode is append, if hasn't been specified + if (!this.hasAttribute('output-mode')) { + this.setAttribute('output-mode', 'append'); + } }else{ - // Editor Output Div - this.editorOut = document.createElement('div'); - this.editorOut.classList.add("output"); - this.editorOut.hidden = true; + if (this.hasAttribute('std-out')){ + this.outputElement = document.getElementById(this.getAttribute('std-out')); + }else{ + // In this case neither output or std-out have been provided so we need + // to create a new output div to output to + this.outputElement = document.createElement('div'); + this.outputElement.classList.add("output"); + this.outputElement.hidden = true; + this.outputElement.id = this.id + "-" + this.getAttribute("exec-id"); - // add the output div id there's not target - mainDiv.appendChild(this.editorOut); + // add the output div id if there's not output pre-defined + mainDiv.appendChild(this.outputElement); + } + + if (this.hasAttribute('std-err')){ + this.outputElement = document.getElementById(this.getAttribute('std-err')); + }else{ + this.errorElement = this.outputElement; + } } if (currentMode=="edit"){ @@ -217,35 +213,6 @@ export class PyScript extends HTMLElement { } } - addToOutput(s: string) { - this.editorOut.innerHTML = s; - this.editorOut.hidden = false; - } - - async loadFromFile(s: string){ - let pyodide = await pyodideReadyPromise; - let response = await fetch(s); - this.code = await response.text(); - - await pyodide.runPythonAsync(this.code); - await pyodide.runPythonAsync(` - from pyodide.http import pyfetch - from pyodide import eval_code - response = await pyfetch("`+s+`") - content = await response.bytes() - - with open("todo.py", "wb") as f: - print(content) - f.write(content) - print("done writing") - `) - // let pkg = pyodide.pyimport("todo"); - // pyodide.runPython(` - // import todo - // `) - // pkg.do_something(); - } - protected async _register_esm(pyodide: PyodideInterface): Promise { for (const node of document.querySelectorAll("script[type='importmap']")) { const importmap = (() => { @@ -278,64 +245,8 @@ export class PyScript extends HTMLElement { } } - async evaluate(): Promise { - console.log('evaluate'); - - if (this.source){ - this.loadFromFile(this.source) - }else{ - const pyodide = await pyodideReadyPromise; - await this._register_esm(pyodide) - // debugger - try { - function ltrim(code: string): string { - const lines = code.split("\n") - if (lines.length == 0) - return code - - const lengths = lines - .filter((line) => line.trim().length != 0) - .map((line) => { - const [prefix] = line.match(/^\s*/) - return prefix.length - }) - - const k = Math.min(...lengths) - - if (k != 0) - return lines.map((line) => line.substring(k)).join("\n") - else - return code - } - - const str = this.editor.state.doc.toString() - const source = htmlDecode(ltrim(str)) - - let output - if (source.includes("asyncio")) - output = await pyodide.runPythonAsync(source) - else - output = pyodide.runPython(source) - - if (output !== undefined) { - this.addToOutput(output) - } - - if (this.hasAttribute('auto-generate') && this.parentElement.lastChild === this) { - const newPyscript = document.createElement("py-script"); - newPyscript.setAttribute('auto-generate', null); - this.parentElement.appendChild(newPyscript); - } - } catch (err) { - this.addToOutput(err); - console.log(err); - } - } - } - - render(){ - console.log('rendered'); - + getSourceFromElement(): string { + return htmlDecode(this.code); } } diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index 1fd3bf10..76a5ff10 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -8,7 +8,7 @@ let pyodide; let additional_definitions = ` from js import document, setInterval, console import asyncio -import io, base64 +import io, base64, sys loop = asyncio.get_event_loop() @@ -22,6 +22,8 @@ class PyScript: if append: child = document.createElement('div'); element = document.querySelector(f'#{element_id}'); + if not element: + return exec_id = exec_id or element.childElementCount + 1 element_id = child.id = f"{element_id}-{exec_id}"; element.appendChild(child); @@ -34,11 +36,9 @@ class PyScript: img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8') document.getElementById(element_id).innerHTML = f'
' elif hasattr(value, "startswith") and value.startswith("data:image"): - console.log(f"DATA/IMAGE: {value}") document.getElementById(element_id).innerHTML = f'
' else: document.getElementById(element_id).innerHTML = value; - console.log(f"ELSE: {append} ==> {element_id} --> {value}") @staticmethod def run_until_complete(f): @@ -95,7 +95,57 @@ class Element: return Element(clone.id, clone) +class OutputCtxManager: + def __init__(self, out=None, output_to_console=True, append=True): + self._out = out + self._prev = out + self.output_to_console = output_to_console + self._append = append + + def change(self, out=None, err=None, output_to_console=True, append=True): + self._prevt = self._out + self._out = out + self.output_to_console = output_to_console + self._append = append + console.log("----> changed out to", self._out, self._append) + + def revert(self): + console.log("----> reverted") + self._out = self._prev + + def write(self, txt): + console.log('writing to', self._out, txt, self._append) + if self._out: + pyscript.write(self._out, txt, append=self._append) + if self.output_to_console: + console.log(self._out, txt) + +class OutputManager: + def __init__(self, out=None, err=None, output_to_console=True, append=True): + sys.stdout = self._out_manager = OutputCtxManager(out, output_to_console, append) + sys.strerr = self._err_manager = OutputCtxManager(err, output_to_console, append) + self.output_to_console = output_to_console + self._append = append + + def change(self, out=None, err=None, output_to_console=True, append=True): + self._out_manager.change(out, output_to_console, append) + sys.stdout = self._out_manager + self._err_manager.change(err, output_to_console, append) + sys.stderr = self._err_manager + self.output_to_console = output_to_console + self.append = append + + def revert(self): + self._out_manager.revert() + self._err_manager.revert() + sys.stdout = self._out_manager + sys.stdout = self._err_manager + console.log("----> reverted") + + pyscript = PyScript() +output_manager = OutputManager() + ` let loadInterpreter = async function(): Promise { diff --git a/pyscriptjs/tsconfig.json b/pyscriptjs/tsconfig.json index b082e968..ebebeeeb 100644 --- a/pyscriptjs/tsconfig.json +++ b/pyscriptjs/tsconfig.json @@ -2,5 +2,28 @@ "extends": "@tsconfig/svelte/tsconfig.json", "include": ["src/**/*"], - "exclude": ["node_modules/*", "__sapper__/*", "public/*"] + "exclude": ["node_modules/*", "__sapper__/*", "public/*"], + "compilerOptions": { + "moduleResolution": "node", + "target": "es2017", + "module": "esnext", + /** + Svelte Preprocess cannot figure out whether you have a value or a type, so tell TypeScript + to enforce using `import type` instead of `import` for Types. + */ + "importsNotUsedAsValues": "error", + "isolatedModules": true, + /** + To have warnings/errors of the Svelte compiler at the correct position, + enable source maps by default. + */ + "sourceMap": true, + /** Requests the runtime types from the svelte modules by default. Needed for TS files or else you get errors. */ + "types": ["svelte"], + + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } } \ No newline at end of file