From 441a2c6e9ea1d66536111065f1c6e01f4e0ad870 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Fri, 15 Apr 2022 18:42:16 -0500 Subject: [PATCH 01/13] introduce outputmanager to redirect output in repl --- pyscriptjs/src/components/pyrepl.ts | 16 ++++++++++------ pyscriptjs/src/interpreter.ts | 25 ++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index 361c01df..4da32260 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -202,28 +202,32 @@ export class PyRepl extends HTMLElement { async evaluate() { console.log('evaluate'); let pyodide = await pyodideReadyPromise; - // debugger + try { // @ts-ignore - let source = this.editor.state.doc.toString(); + const sourceStrings = [`output_manager.change("`+this.editorOut.id+`")`, + ...this.editor.state.doc.toString().split("\n")]; + const source = sourceStrings.join('\n') let output; + if (source.includes("asyncio")){ output = await pyodide.runPythonAsync(source); + await pyodide.runPythonAsync(`output_manager.revert()`) }else{ output = pyodide.runPython(source); + pyodide.runPython(`output_manager.revert()`) } - + 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}); + out.write.callKwargs(output, { append : true}); if (!this.hasAttribute('target')) { this.editorOut.hidden = false; } - // this.addToOutput(output); } if (this.hasAttribute('auto-generate')) { diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index 1fd3bf10..4bdc09c6 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() @@ -95,7 +95,30 @@ class Element: return Element(clone.id, clone) +class OutputManager: + def __init__(self, custom=None, output_to_console=True): + self._custom = custom + self.output_to_console = output_to_console + self.prev = None + + def change(self, custom): + self._prev = self._custom + self._custom = custom + console.log("----> changed to", self._custom) + + def revert(self): + console.log("----> reverted") + self._custom = self._prev + + def write(self, txt): + if self._custom: + pyscript.write(self._custom, txt, append=True) + if self.output_to_console: + console.log(self._custom, txt) + pyscript = PyScript() +output_manager = OutputManager() +sys.stdout = output_manager ` let loadInterpreter = async function(): Promise { From df5b4a42939211ae00a4138acec716da6a523608 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Sat, 16 Apr 2022 22:28:24 -0500 Subject: [PATCH 02/13] add module config to allow local imports --- pyscriptjs/tsconfig.json | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) 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 From 95d50269b7e1f9dc946c0ebe21fe39c972afed2f Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 10:13:59 -0500 Subject: [PATCH 03/13] add base component file with components base classe and adapt pyrepl to new base class --- pyscriptjs/src/components/base.ts | 145 ++++++++++++++++++++++++++++ pyscriptjs/src/components/pyrepl.ts | 97 ++++++------------- 2 files changed, 174 insertions(+), 68 deletions(-) create mode 100644 pyscriptjs/src/components/base.ts diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts new file mode 100644 index 00000000..fb884bb4 --- /dev/null +++ b/pyscriptjs/src/components/base.ts @@ -0,0 +1,145 @@ +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; + btnConfig: HTMLElement; + btnRun: HTMLElement; + outputElement: HTMLElement; //HTMLTextAreaElement; + 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(){ + + } + + getSource(): string{ + return ""; + } + + 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() { + console.log('evaluate'); + let pyodide = await pyodideReadyPromise; + + try { + // @ts-ignore + const source = this.getSource(); + await this._register_esm(pyodide) + + let output; + + if (source.includes("asyncio")){ + output = await pyodide.runPythonAsync(source); + await pyodide.runPythonAsync(`output_manager.revert()`) + }else{ + 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}); + + if (!this.hasAttribute('target')) { + this.outputElement.hidden = false; + } + } + + this.postEvaluate() + + // 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); + } + } + } diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index 4da32260..f3654442 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,15 @@ 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"]) @@ -172,7 +161,7 @@ export class PyRepl extends HTMLElement { } if (this.hasAttribute('target')) { - this.editorOut = document.getElementById(this.getAttribute('target')); + this.outputElement = document.getElementById(this.getAttribute('target')); // in this case, the default output-mode is append, if hasn't been specified if (!this.hasAttribute('output-mode')) { @@ -180,13 +169,13 @@ export class PyRepl extends HTMLElement { } }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"); + 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); + mainDiv.appendChild(this.outputElement); } this.appendChild(mainDiv); @@ -195,63 +184,35 @@ export class PyRepl extends HTMLElement { } 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; + 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('target')){ + newPyRepl.setAttribute('target', this.getAttribute('target')); + } + + newPyRepl.setAttribute('exec-id', nextExecId.toString()); + this.parentElement.appendChild(newPyRepl); + } + } - try { - // @ts-ignore - const sourceStrings = [`output_manager.change("`+this.editorOut.id+`")`, + getSource(): string { + const sourceStrings = [`output_manager.change("`+this.outputElement.id+`")`, ...this.editor.state.doc.toString().split("\n")]; - const source = sourceStrings.join('\n') - let output; + return sourceStrings.join('\n') + } - if (source.includes("asyncio")){ - output = await pyodide.runPythonAsync(source); - await pyodide.runPythonAsync(`output_manager.revert()`) - }else{ - output = pyodide.runPython(source); - pyodide.runPython(`output_manager.revert()`) - } - - if (output !== undefined){ - let Element = pyodide.globals.get('Element'); - let out = Element(this.editorOut.id); - - // @ts-ignore - out.write.callKwargs(output, { append : true}); - - if (!this.hasAttribute('target')) { - this.editorOut.hidden = false; - } - } - - 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); - } - } - render(){ console.log('rendered'); } } - \ No newline at end of file From 955fb6fd37d3d91b5b46d2f0b95a9518928bb1b3 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 12:06:05 -0500 Subject: [PATCH 04/13] fix missing initialization in outputmanager --- pyscriptjs/src/interpreter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index 4bdc09c6..d9816ab6 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -98,6 +98,7 @@ class Element: class OutputManager: def __init__(self, custom=None, output_to_console=True): self._custom = custom + self._prev = custom self.output_to_console = output_to_console self.prev = None From 4d890602d9b8fb812613855d8ced3a85e5084f87 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 12:06:30 -0500 Subject: [PATCH 05/13] adapt pyscript to new base class and clean Base --- pyscriptjs/src/components/base.ts | 69 +++++++++-------- pyscriptjs/src/components/pyrepl.ts | 2 +- pyscriptjs/src/components/pyscript.ts | 106 +++----------------------- 3 files changed, 46 insertions(+), 131 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index fb884bb4..696ad0f9 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -32,6 +32,7 @@ export class BaseEvalElement extends HTMLElement { shadow: ShadowRoot; wrapper: HTMLElement; code: string; + source: string; btnConfig: HTMLElement; btnRun: HTMLElement; outputElement: HTMLElement; //HTMLTextAreaElement; @@ -55,10 +56,17 @@ export class BaseEvalElement extends HTMLElement { } - getSource(): string{ + 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} = {} @@ -91,53 +99,44 @@ export class BaseEvalElement extends HTMLElement { pyodide.registerJsModule("esm", imports) } - async evaluate() { + async evaluate(): Promise { console.log('evaluate'); - let pyodide = await pyodideReadyPromise; - - try { + let pyodide = await pyodideReadyPromise; + let source: string; + let output; + try { // @ts-ignore - const source = this.getSource(); - await this._register_esm(pyodide) - - let output; + if (this.source){ + source = await this.getSourceFromFile(this.source); + }else{ + source = this.getSourceFromElement(); + } + + await this._register_esm(pyodide); if (source.includes("asyncio")){ - output = await pyodide.runPythonAsync(source); - await pyodide.runPythonAsync(`output_manager.revert()`) + output = await pyodide.runPythonAsync(source); + await pyodide.runPythonAsync(`output_manager.revert()`) }else{ - output = pyodide.runPython(source); - pyodide.runPython(`output_manager.revert()`) + output = pyodide.runPython(source); + pyodide.runPython(`output_manager.revert()`) } - + if (output !== undefined){ - if (Element === undefined){ + if (Element === undefined){ Element = pyodide.globals.get('Element'); - } - const out = Element(this.outputElement.id); - // @ts-ignore - out.write.callKwargs(output, { append : true}); + } + const out = Element(this.outputElement.id); + // @ts-ignore + out.write.callKwargs(output, { append : true}); - if (!this.hasAttribute('target')) { + if (!this.hasAttribute('target')) { this.outputElement.hidden = false; - } } + } - this.postEvaluate() + this.postEvaluate() - // 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); } diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index f3654442..b6a918b7 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -204,7 +204,7 @@ export class PyRepl extends BaseEvalElement { } } - getSource(): string { + getSourceFromElement(): string { const sourceStrings = [`output_manager.change("`+this.outputElement.id+`")`, ...this.editor.state.doc.toString().split("\n")]; return sourceStrings.join('\n') diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 30aef4ae..60cc86ef 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,6 +51,8 @@ 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; @@ -90,30 +93,13 @@ 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; +export class PyScript extends BaseEvalElement { // 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"]) this.shadow.appendChild(this.wrapper); } @@ -140,11 +126,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 @@ -190,18 +171,17 @@ 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')); + this.outputElement = document.getElementById(this.getAttribute('target')); }else{ // Editor Output Div - this.editorOut = document.createElement('div'); - this.editorOut.classList.add("output"); - this.editorOut.hidden = true; + this.outputElement = document.createElement('div'); + this.outputElement.classList.add("output"); + this.outputElement.hidden = true; // add the output div id there's not target - mainDiv.appendChild(this.editorOut); + mainDiv.appendChild(this.outputElement); } if (currentMode=="edit"){ @@ -217,35 +197,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 { const imports: {[key: string]: unknown} = {} @@ -278,43 +229,8 @@ export class PyScript extends HTMLElement { pyodide.registerJsModule("esm", imports) } - 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 { - // @ts-ignore - const source = htmlDecode(this.editor.state.doc.toString()); - 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 this.code; } } From 00b571d3dfd9fb29a3fc5fd27ad99505b6e4382d Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 15:54:59 -0500 Subject: [PATCH 06/13] change OutputManager to actually have separate contexts for out and err to scripts can manage both in one place --- pyscriptjs/src/components/base.ts | 2 ++ pyscriptjs/src/interpreter.ts | 54 +++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 696ad0f9..1db90e24 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -115,9 +115,11 @@ export class BaseEvalElement extends HTMLElement { await this._register_esm(pyodide); if (source.includes("asyncio")){ + await pyodide.runPythonAsync(`output_manager.change("`+this.outputElement.id+`")`); output = await pyodide.runPythonAsync(source); await pyodide.runPythonAsync(`output_manager.revert()`) }else{ + output = pyodide.runPython(`output_manager.change("`+this.outputElement.id+`")`); output = pyodide.runPython(source); pyodide.runPython(`output_manager.revert()`) } diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index d9816ab6..e90b945d 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -95,31 +95,57 @@ class Element: return Element(clone.id, clone) -class OutputManager: - def __init__(self, custom=None, output_to_console=True): - self._custom = custom - self._prev = custom +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.prev = None + self._append = append - def change(self, custom): - self._prev = self._custom - self._custom = custom - console.log("----> changed to", self._custom) + 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._custom = self._prev + self._out = self._prev def write(self, txt): - if self._custom: - pyscript.write(self._custom, txt, append=True) + 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._custom, txt) + 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() -sys.stdout = output_manager + ` let loadInterpreter = async function(): Promise { From 4ba90a925041e552ffbf3d13179c43d9e870a2b4 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 18:56:49 -0500 Subject: [PATCH 07/13] rename target attribute to output --- pyscriptjs/examples/repl2.html | 2 +- pyscriptjs/src/components/base.ts | 2 +- pyscriptjs/src/components/pyrepl.ts | 12 ++++++------ pyscriptjs/src/components/pyscript.ts | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pyscriptjs/examples/repl2.html b/pyscriptjs/examples/repl2.html index 53099a20..614f057e 100644 --- a/pyscriptjs/examples/repl2.html +++ b/pyscriptjs/examples/repl2.html @@ -21,7 +21,7 @@ - +
diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 1db90e24..129191e3 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -132,7 +132,7 @@ export class BaseEvalElement extends HTMLElement { // @ts-ignore out.write.callKwargs(output, { append : true}); - if (!this.hasAttribute('target')) { + if (!this.hasAttribute('output')) { this.outputElement.hidden = false; } } diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index b6a918b7..6ed79b1e 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -134,7 +134,7 @@ export class PyRepl extends BaseEvalElement { currentComponentDetails.set([ {key: "auto-generate", value: true}, - {key:"target", value: "default"}, + {key:"output", value: "default"}, {key: "source", value: "self"}, {key: "output-mode", value: "clear"} ]) @@ -160,8 +160,8 @@ export class PyRepl extends BaseEvalElement { this.setAttribute("root", this.id); } - if (this.hasAttribute('target')) { - this.outputElement = document.getElementById(this.getAttribute('target')); + if (this.hasAttribute('output')) { + 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')) { @@ -174,7 +174,7 @@ export class PyRepl extends BaseEvalElement { this.outputElement.hidden = true; this.outputElement.id = this.id + "-" + this.getAttribute("exec-id"); - // add the output div id there's not target + // add the output div id if there's not output pre-defined mainDiv.appendChild(this.outputElement); } @@ -195,8 +195,8 @@ export class PyRepl extends BaseEvalElement { 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')); + if (this.hasAttribute('output')){ + newPyRepl.setAttribute('output', this.getAttribute('output')); } newPyRepl.setAttribute('exec-id', nextExecId.toString()); diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 60cc86ef..f8e8951f 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -56,10 +56,10 @@ type PyodideInterface = { 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'; } @@ -78,7 +78,7 @@ class Script { output = pyodide.runPython(this.source); } - if (this.target){ + if (this.output){ // this.editorOut.innerHTML = s; } // if (output !== undefined){ @@ -161,7 +161,7 @@ export class PyScript extends BaseEvalElement { currentComponentDetails.set([ {key: "auto-generate", value: true}, - {key:"target", value: "default"}, + {key:"output", value: "default"}, {key: "source", value: "self"} ]) } @@ -172,15 +172,15 @@ export class PyScript extends BaseEvalElement { mainDiv.appendChild(eDiv); - if (this.hasAttribute('target')) { - this.outputElement = document.getElementById(this.getAttribute('target')); + if (this.hasAttribute('output')) { + this.outputElement = document.getElementById(this.getAttribute('output')); }else{ // Editor Output Div this.outputElement = document.createElement('div'); this.outputElement.classList.add("output"); this.outputElement.hidden = true; - // add the output div id there's not target + // add the output div id there's no output element mainDiv.appendChild(this.outputElement); } From 57d6ae967bf0b1320be62f4370edc62c1e2002c7 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 21:55:03 -0500 Subject: [PATCH 08/13] add x margin to pybox children --- pyscriptjs/src/components/pybox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From b38d2e5df12bf1f1d6a5b3ba88f176b9fb3e0e67 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 21:55:29 -0500 Subject: [PATCH 09/13] add support for std-out and std-err attributes in py-repl and py-script --- pyscriptjs/examples/repl2.html | 5 +++- pyscriptjs/src/components/base.ts | 21 ++++++++++------ pyscriptjs/src/components/pyrepl.ts | 36 ++++++++++++++++++++------- pyscriptjs/src/components/pyscript.ts | 33 ++++++++++++++++++------ 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/pyscriptjs/examples/repl2.html b/pyscriptjs/examples/repl2.html index 614f057e..39c595e8 100644 --- a/pyscriptjs/examples/repl2.html +++ b/pyscriptjs/examples/repl2.html @@ -20,9 +20,12 @@ +

Custom REPL

- +
+
+
diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 129191e3..0ec2a411 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -35,7 +35,8 @@ export class BaseEvalElement extends HTMLElement { source: string; btnConfig: HTMLElement; btnRun: HTMLElement; - outputElement: HTMLElement; //HTMLTextAreaElement; + outputElement: HTMLElement; + errorElement: HTMLElement; theme: string; constructor() { @@ -115,32 +116,38 @@ export class BaseEvalElement extends HTMLElement { await this._register_esm(pyodide); if (source.includes("asyncio")){ - await pyodide.runPythonAsync(`output_manager.change("`+this.outputElement.id+`")`); + 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+`")`); + 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'); + Element = pyodide.globals.get('Element'); } const out = Element(this.outputElement.id); // @ts-ignore out.write.callKwargs(output, { append : true}); - if (!this.hasAttribute('output')) { this.outputElement.hidden = false; + this.outputElement.style.display = 'block'; } - } this.postEvaluate() } catch (err) { - this.addToOutput(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/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index 6ed79b1e..dde75e36 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -100,7 +100,7 @@ export class PyRepl extends BaseEvalElement { }) 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 @@ -161,22 +161,34 @@ export class PyRepl extends BaseEvalElement { } if (this.hasAttribute('output')) { - this.outputElement = document.getElementById(this.getAttribute('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.outputElement = document.createElement('div'); - this.outputElement.classList.add("output"); - this.outputElement.hidden = true; - this.outputElement.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 if there's not output pre-defined - mainDiv.appendChild(this.outputElement); + // 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(); @@ -198,6 +210,12 @@ export class PyRepl extends BaseEvalElement { 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); diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index f8e8951f..7cc6e915 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -173,15 +173,32 @@ export class PyScript extends BaseEvalElement { mainDiv.appendChild(eDiv); if (this.hasAttribute('output')) { - this.outputElement = document.getElementById(this.getAttribute('output')); - }else{ - // Editor Output Div - this.outputElement = document.createElement('div'); - this.outputElement.classList.add("output"); - this.outputElement.hidden = true; + this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output')); - // add the output div id there's no output element - mainDiv.appendChild(this.outputElement); + // 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{ + 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 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"){ From 392b948db1d02e41c6c335daa45e6231b818b4b3 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 22:02:56 -0500 Subject: [PATCH 10/13] rename target with output in the examples --- pyscriptjs/examples/simple_script.html | 2 +- pyscriptjs/examples/simple_script2.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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() From 3c2ca6d68c4c57bbff995a01a151e63f70e8e9a4 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 23:10:41 -0500 Subject: [PATCH 11/13] fix error when trying to output to non existing element --- pyscriptjs/examples/bokeh.html | 2 +- pyscriptjs/examples/bokeh_interactive.html | 2 +- pyscriptjs/src/components/pyrepl.ts | 2 +- pyscriptjs/src/components/pyscript.ts | 2 +- pyscriptjs/src/interpreter.ts | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyscriptjs/examples/bokeh.html b/pyscriptjs/examples/bokeh.html index 12655808..f4de7d48 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..cc123192 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/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index dde75e36..e18392b1 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -188,7 +188,7 @@ export class PyRepl extends BaseEvalElement { this.errorElement = this.outputElement; } } - + this.appendChild(mainDiv); this.editor.focus(); diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 4fb9fefe..758038f0 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -247,7 +247,7 @@ export class PyScript extends BaseEvalElement { } getSourceFromElement(): string { - return this.code; + return htmlDecode(this.code); } } diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index e90b945d..76a5ff10 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -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): From 6541c9dc98b4791965f59821eea242b52d469ec0 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 18 Apr 2022 23:30:32 -0500 Subject: [PATCH 12/13] change id in exmaples --- pyscriptjs/examples/bokeh.html | 2 +- pyscriptjs/examples/bokeh_interactive.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyscriptjs/examples/bokeh.html b/pyscriptjs/examples/bokeh.html index f4de7d48..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 cc123192..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 From 9e476442388282516abf95d53aa59b4e8f4c0ef7 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Tue, 19 Apr 2022 10:58:20 -0500 Subject: [PATCH 13/13] clean up repl class attrs --- pyscriptjs/src/components/pyrepl.ts | 2 -- pyscriptjs/src/components/pyscript.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index e18392b1..5a9f9c18 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -47,8 +47,6 @@ function createCmdHandler(el){ export class PyRepl extends BaseEvalElement { editor: EditorView; editorNode: HTMLElement; - code: string; - theme: string; constructor() { super(); diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 758038f0..4b0da39d 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -94,7 +94,6 @@ class Script { } export class PyScript extends BaseEvalElement { - // editorState: EditorState; constructor() { super();