1
0
mirror of https://github.com/pyscript/pyscript.git synced 2022-05-01 19:47:48 +03:00

Merge pull request #28 from anaconda/pys-19/allow_out_err_redirect

[PYS-19] allow out err redirect
This commit is contained in:
Fabio Pliger
2022-04-19 11:01:21 -05:00
committed by GitHub
11 changed files with 329 additions and 208 deletions

View File

@@ -23,7 +23,7 @@
<h1>Bokeh Example</h1>
<div id="myplot"></div>
<py-script>
<py-script id="main">
import json
import pyodide

View File

@@ -23,7 +23,7 @@
<h1>Bokeh Example</h1>
<div id="myplot"></div>
<py-script>
<py-script id="main">
import asyncio
import json
import pyodide

View File

@@ -20,9 +20,12 @@
</py-env>
<body>
<h1 class="font-semibold text-2xl ml-5">Custom REPL</h1>
<py-box widths="2/3;1/3">
<py-repl id="my-repl" auto-generate="true" target="output"> </py-repl>
<py-repl id="my-repl" auto-generate="true" std-out="output" std-err="err-div"> </py-repl>
<div id="output"></div>
</py-box>
<footer id="err-div" class="bg-red-700 text-white text-center border-t-4 border-gree-500 fixed inset-x-0 bottom-0 p-4 hidden">
</footer>
</body>
</html>

View File

@@ -14,7 +14,7 @@
<body>
<div id="outputDiv" class="font-mono" style="background-color:yellow"></div>
<py-script target="outputDiv">
<py-script output="outputDiv">
from datetime import datetime
now = datetime.now()
now.strftime("%m/%d/%Y, %H:%M:%S")

View File

@@ -20,7 +20,7 @@
<div class="font-mono">start time: <label id="outputDiv"></label></div>
<div id="outputDiv2" class="font-mono"></div>
<div id="outputDiv3" class="font-mono"></div>
<py-script target="outputDiv">
<py-script output="outputDiv">
import utils
utils.now()
</py-script>

View File

@@ -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 += "<div>"+s+"</div>";
this.outputElement.hidden = false;
}
postEvaluate(){
}
getSourceFromElement(): string{
return "";
}
async getSourceFromFile(s: string): Promise<string>{
let pyodide = await pyodideReadyPromise;
let response = await fetch(s);
this.code = await response.text();
return this.code;
}
protected async _register_esm(pyodide: PyodideInterface): Promise<void> {
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<void> {
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';
}
}
}

View File

@@ -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);

View File

@@ -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 += "<div>"+s+"</div>";
this.editorOut.hidden = false;
this.outputElement.innerHTML += "<div>"+s+"</div>";
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');
}
}

View File

@@ -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<void> {
for (const node of document.querySelectorAll("script[type='importmap']")) {
const importmap = (() => {
@@ -278,64 +245,8 @@ export class PyScript extends HTMLElement {
}
}
async evaluate(): Promise<void> {
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);
}
}

View File

@@ -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'<div><img id="plt" src="{img_str}"/></div>'
elif hasattr(value, "startswith") and value.startswith("data:image"):
console.log(f"DATA/IMAGE: {value}")
document.getElementById(element_id).innerHTML = f'<div><img id="plt" src="{value}"/></div>'
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<any> {

View File

@@ -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
}
}