mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-22 19:53:00 -05:00
kill stores.runtimeLoaded and many other stores (#850)
As the title stays, the main goal of the branch is to kill the infamous runtimeLoaded global store and all the complications, problems and bugs caused by the fact that in many places we needed to ensure/wait that the global runtime was properly set before being able to execute code. The core idea is that runtime is never a global object and that it's passed around explicitly, which means that when a function receives it, it is guaranteed to be initialized&ready. This caused a bit of complications in pybutton.ts, pyinputbox.ts and pyrepl.ts, because they indirectly want to call runtime.run from connectedCallback, which is the only place where we cannot explicitly pass the runtime because it's automatically called by the browser. But also, it is also a sign of a bad design, because it were entirely possible that connectedCallback was called before the runtime was ready, which probably caused many bugs, see e.g. #673 and #747. The solution to is use dependency injection and create the class later on: so instead of having a global PyButton class which relies on a global runtime (whose state is uncertain) we have a make_PyButton function which takes a runtime and make a PyButton class which is tied to that specific runtime (whose state is certainly ready, because we call make_PyButton only when we know that the runtime is ready). Similar for PyInputBox and PyRepl. Other highlights: thanks to this, I could kill the also infamous runAfterRuntimeInitialized and a couple of smelly lines which used setTimeout to "wait" for the runtime. While I was at it, I also called a lot of other stores which were completely unused and where probably leftovers from a past universe.
This commit is contained in:
@@ -1,19 +1,11 @@
|
||||
import { runtimeLoaded } from '../stores';
|
||||
import { guidGenerator, addClasses, removeClasses } from '../utils';
|
||||
|
||||
import type { Runtime } from '../runtime';
|
||||
import { getLogger } from '../logger';
|
||||
|
||||
const logger = getLogger('pyscript/base');
|
||||
|
||||
// Global `Runtime` that implements the generic runtimes API
|
||||
let runtime: Runtime;
|
||||
let Element;
|
||||
|
||||
runtimeLoaded.subscribe(value => {
|
||||
runtime = value;
|
||||
});
|
||||
|
||||
export class BaseEvalElement extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
wrapper: HTMLElement;
|
||||
@@ -115,7 +107,7 @@ export class BaseEvalElement extends HTMLElement {
|
||||
runtime.registerJsModule('esm', imports);
|
||||
}
|
||||
|
||||
async evaluate(): Promise<void> {
|
||||
async evaluate(runtime: Runtime): Promise<void> {
|
||||
this.preEvaluate();
|
||||
|
||||
let source: string;
|
||||
@@ -184,187 +176,4 @@ export class BaseEvalElement extends HTMLElement {
|
||||
|
||||
}
|
||||
} // end evaluate
|
||||
|
||||
async eval(source: string): Promise<void> {
|
||||
try {
|
||||
const output = await runtime.run(source);
|
||||
if (output !== undefined) {
|
||||
logger.info(output);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
} // end eval
|
||||
|
||||
runAfterRuntimeInitialized(callback: () => Promise<void>) {
|
||||
runtimeLoaded.subscribe(value => {
|
||||
if ('run' in value) {
|
||||
setTimeout(() => {
|
||||
void callback();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createWidget(name: string, code: string, klass: string) {
|
||||
class CustomWidget extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
wrapper: HTMLElement;
|
||||
|
||||
name: string = name;
|
||||
klass: string = klass;
|
||||
code: string = code;
|
||||
proxy: any;
|
||||
proxyClass: any;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// TODO: we are calling with a 2secs delay to allow pyodide to load
|
||||
// ideally we can just wait for it to load and then run. To do
|
||||
// so we need to replace using the promise and actually using
|
||||
// the interpreter after it loads completely
|
||||
// setTimeout(() => {
|
||||
// void (async () => {
|
||||
// await this.eval(this.code);
|
||||
// this.proxy = this.proxyClass(this);
|
||||
// console.log('proxy', this.proxy);
|
||||
// this.proxy.connect();
|
||||
// this.registerWidget();
|
||||
// })();
|
||||
// }, 2000);
|
||||
runtimeLoaded.subscribe(value => {
|
||||
if ('run' in value) {
|
||||
runtime = value;
|
||||
setTimeout(() => {
|
||||
void (async () => {
|
||||
await this.eval(this.code);
|
||||
this.proxy = this.proxyClass(this);
|
||||
this.proxy.connect();
|
||||
this.registerWidget();
|
||||
})();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerWidget() {
|
||||
logger.info('new widget registered:', this.name);
|
||||
runtime.globals.set(this.id, this.proxy);
|
||||
}
|
||||
|
||||
async eval(source: string): Promise<void> {
|
||||
try {
|
||||
const output = await runtime.run(source);
|
||||
this.proxyClass = runtime.globals.get(this.klass);
|
||||
if (output !== undefined) {
|
||||
logger.info('CustomWidget.eval: ', output);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('CustomWidget.eval: ', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
const xPyWidget = customElements.define(name, CustomWidget);
|
||||
}
|
||||
|
||||
export class PyWidget extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
name: string;
|
||||
klass: string;
|
||||
outputElement: HTMLElement;
|
||||
errorElement: HTMLElement;
|
||||
wrapper: HTMLElement;
|
||||
theme: string;
|
||||
source: string;
|
||||
code: 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);
|
||||
|
||||
this.addAttributes('src','name','klass');
|
||||
}
|
||||
|
||||
addAttributes(...attrs:string[]){
|
||||
for (const each of attrs){
|
||||
const property = each === "src" ? "source" : each;
|
||||
if (this.hasAttribute(each)) {
|
||||
this[property]=this.getAttribute(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
if (this.id === undefined) {
|
||||
throw new ReferenceError(
|
||||
`No id specified for component. Components must have an explicit id. Please use id="" to specify your component id.`,
|
||||
);
|
||||
}
|
||||
|
||||
const mainDiv = document.createElement('div');
|
||||
mainDiv.id = this.id + '-main';
|
||||
this.appendChild(mainDiv);
|
||||
logger.debug('PyWidget: reading source', this.source);
|
||||
this.code = await this.getSourceFromFile(this.source);
|
||||
createWidget(this.name, this.code, this.klass);
|
||||
}
|
||||
|
||||
initOutErr(): void {
|
||||
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 {
|
||||
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');
|
||||
}
|
||||
|
||||
if (this.hasAttribute('std-err')) {
|
||||
this.errorElement = document.getElementById(this.getAttribute('std-err'));
|
||||
} else {
|
||||
this.errorElement = this.outputElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getSourceFromFile(s: string): Promise<string> {
|
||||
const response = await fetch(s);
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
async eval(source: string): Promise<void> {
|
||||
try {
|
||||
const output = await runtime.run(source);
|
||||
if (output !== undefined) {
|
||||
logger.info('PyWidget.eval: ', output);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('PyWidget.eval: ', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import { PyRepl } from './pyrepl';
|
||||
import type { Runtime } from '../runtime';
|
||||
import { make_PyRepl } from './pyrepl';
|
||||
import { PyBox } from './pybox';
|
||||
import { PyButton } from './pybutton';
|
||||
import { make_PyButton } from './pybutton';
|
||||
import { PyTitle } from './pytitle';
|
||||
import { PyInputBox } from './pyinputbox';
|
||||
import { PyWidget } from './base';
|
||||
import { make_PyInputBox } from './pyinputbox';
|
||||
import { make_PyWidget } from './pywidget';
|
||||
|
||||
/*
|
||||
These were taken from main.js because some of our components call
|
||||
runAfterRuntimeInitialized immediately when we are creating the custom
|
||||
element, this was causing tests to fail since runAfterRuntimeInitialized
|
||||
expects the runtime to have been loaded before being called.
|
||||
function createCustomElements(runtime: Runtime) {
|
||||
const PyInputBox = make_PyInputBox(runtime);
|
||||
const PyButton = make_PyButton(runtime);
|
||||
const PyWidget = make_PyWidget(runtime);
|
||||
const PyRepl = make_PyRepl(runtime);
|
||||
|
||||
This function is now called from within the `runtime.initialize`. Once
|
||||
the runtime finished initializing, then we will create the custom elements
|
||||
so they are rendered in the page and we will always have a runtime available.
|
||||
|
||||
Ideally, this would live under utils.js, but importing all the components in
|
||||
the utils.js file was causing jest to fail with weird errors such as:
|
||||
"ReferenceError: Cannot access 'BaseEvalElement' before initialization" coming
|
||||
from the PyScript class.
|
||||
|
||||
*/
|
||||
function createCustomElements() {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const xPyRepl = customElements.define('py-repl', PyRepl);
|
||||
const xPyBox = customElements.define('py-box', PyBox);
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
import { BaseEvalElement } from './base';
|
||||
import { addClasses, htmlDecode } from '../utils';
|
||||
import { getLogger } from '../logger'
|
||||
import type { Runtime } from '../runtime';
|
||||
|
||||
const logger = getLogger('py-button');
|
||||
|
||||
export function make_PyButton(runtime: Runtime) {
|
||||
class PyButton extends BaseEvalElement {
|
||||
widths: Array<string>;
|
||||
label: string;
|
||||
class: Array<string>;
|
||||
defaultClass: Array<string>;
|
||||
mount_name: string;
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
export class PyButton extends BaseEvalElement {
|
||||
widths: Array<string>;
|
||||
label: string;
|
||||
class: Array<string>;
|
||||
defaultClass: Array<string>;
|
||||
mount_name: string;
|
||||
constructor() {
|
||||
super();
|
||||
this.defaultClass = ['py-button'];
|
||||
|
||||
this.defaultClass = ['py-button'];
|
||||
|
||||
if (this.hasAttribute('label')) {
|
||||
this.label = this.getAttribute('label');
|
||||
}
|
||||
|
||||
// Styling does the same thing as class in normal HTML. Using the name "class" makes the style to malfunction
|
||||
if (this.hasAttribute('styling')) {
|
||||
const klass = this.getAttribute('styling').trim();
|
||||
if (klass === '') {
|
||||
this.class = this.defaultClass;
|
||||
} else {
|
||||
// trim each element to remove unnecessary spaces which makes the button style to malfunction
|
||||
this.class = klass
|
||||
.split(' ')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x !== '');
|
||||
if (this.hasAttribute('label')) {
|
||||
this.label = this.getAttribute('label');
|
||||
}
|
||||
} else {
|
||||
this.class = this.defaultClass;
|
||||
|
||||
// Styling does the same thing as class in normal HTML. Using the name "class" makes the style to malfunction
|
||||
if (this.hasAttribute('styling')) {
|
||||
const klass = this.getAttribute('styling').trim();
|
||||
if (klass === '') {
|
||||
this.class = this.defaultClass;
|
||||
} else {
|
||||
// trim each element to remove unnecessary spaces which makes the button style to malfunction
|
||||
this.class = klass
|
||||
.split(' ')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x !== '');
|
||||
}
|
||||
} else {
|
||||
this.class = this.defaultClass;
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.checkId();
|
||||
this.code = htmlDecode(this.innerHTML);
|
||||
this.mount_name = this.id.split('-').join('_');
|
||||
this.innerHTML = '';
|
||||
|
||||
const mainDiv = document.createElement('button');
|
||||
mainDiv.innerHTML = this.label;
|
||||
addClasses(mainDiv, this.class);
|
||||
|
||||
mainDiv.id = this.id;
|
||||
this.id = `${this.id}-container`;
|
||||
|
||||
this.appendChild(mainDiv);
|
||||
this.code = this.code.split('self').join(this.mount_name);
|
||||
let registrationCode = `from pyodide.ffi import create_proxy`;
|
||||
registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`;
|
||||
if (this.code.includes('def on_focus')) {
|
||||
this.code = this.code.replace('def on_focus', `def on_focus_${this.mount_name}`);
|
||||
registrationCode += `\n${this.mount_name}.element.addEventListener('focus', create_proxy(on_focus_${this.mount_name}))`;
|
||||
}
|
||||
|
||||
if (this.code.includes('def on_click')) {
|
||||
this.code = this.code.replace('def on_click', `def on_click_${this.mount_name}`);
|
||||
registrationCode += `\n${this.mount_name}.element.addEventListener('click', create_proxy(on_click_${this.mount_name}))`;
|
||||
}
|
||||
|
||||
// now that we appended and the element is attached, lets connect with the event handlers
|
||||
// defined for this widget
|
||||
await runtime.runButDontRaise(this.code);
|
||||
await runtime.runButDontRaise(registrationCode);
|
||||
logger.debug('py-button connected');
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.checkId();
|
||||
this.code = htmlDecode(this.innerHTML);
|
||||
this.mount_name = this.id.split('-').join('_');
|
||||
this.innerHTML = '';
|
||||
|
||||
const mainDiv = document.createElement('button');
|
||||
mainDiv.innerHTML = this.label;
|
||||
addClasses(mainDiv, this.class);
|
||||
|
||||
mainDiv.id = this.id;
|
||||
this.id = `${this.id}-container`;
|
||||
|
||||
this.appendChild(mainDiv);
|
||||
this.code = this.code.split('self').join(this.mount_name);
|
||||
let registrationCode = `from pyodide.ffi import create_proxy`;
|
||||
registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`;
|
||||
if (this.code.includes('def on_focus')) {
|
||||
this.code = this.code.replace('def on_focus', `def on_focus_${this.mount_name}`);
|
||||
registrationCode += `\n${this.mount_name}.element.addEventListener('focus', create_proxy(on_focus_${this.mount_name}))`;
|
||||
}
|
||||
|
||||
if (this.code.includes('def on_click')) {
|
||||
this.code = this.code.replace('def on_click', `def on_click_${this.mount_name}`);
|
||||
registrationCode += `\n${this.mount_name}.element.addEventListener('click', create_proxy(on_click_${this.mount_name}))`;
|
||||
}
|
||||
|
||||
// now that we appended and the element is attached, lets connect with the event handlers
|
||||
// defined for this widget
|
||||
this.runAfterRuntimeInitialized(async () => {
|
||||
await this.eval(this.code);
|
||||
await this.eval(registrationCode);
|
||||
logger.debug('registered handlers');
|
||||
});
|
||||
|
||||
logger.debug('py-button connected');
|
||||
}
|
||||
return PyButton;
|
||||
}
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
import { BaseEvalElement } from './base';
|
||||
import { addClasses, htmlDecode } from '../utils';
|
||||
import { getLogger } from '../logger'
|
||||
import type { Runtime } from '../runtime';
|
||||
|
||||
const logger = getLogger('py-inputbox');
|
||||
|
||||
export class PyInputBox extends BaseEvalElement {
|
||||
widths: Array<string>;
|
||||
label: string;
|
||||
mount_name: string;
|
||||
constructor() {
|
||||
super();
|
||||
export function make_PyInputBox(runtime: Runtime) {
|
||||
class PyInputBox extends BaseEvalElement {
|
||||
widths: Array<string>;
|
||||
label: string;
|
||||
mount_name: string;
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (this.hasAttribute('label')) {
|
||||
this.label = this.getAttribute('label');
|
||||
if (this.hasAttribute('label')) {
|
||||
this.label = this.getAttribute('label');
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.checkId();
|
||||
this.code = htmlDecode(this.innerHTML);
|
||||
this.mount_name = this.id.split('-').join('_');
|
||||
this.innerHTML = '';
|
||||
|
||||
const mainDiv = document.createElement('input');
|
||||
mainDiv.type = 'text';
|
||||
addClasses(mainDiv, ['py-input']);
|
||||
|
||||
mainDiv.id = this.id;
|
||||
this.id = `${this.id}-container`;
|
||||
this.appendChild(mainDiv);
|
||||
|
||||
// now that we appended and the element is attached, lets connect with the event handlers
|
||||
// defined for this widget
|
||||
this.appendChild(mainDiv);
|
||||
this.code = this.code.split('self').join(this.mount_name);
|
||||
let registrationCode = `from pyodide.ffi import create_proxy`;
|
||||
registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`;
|
||||
if (this.code.includes('def on_keypress')) {
|
||||
this.code = this.code.replace('def on_keypress', `def on_keypress_${this.mount_name}`);
|
||||
registrationCode += `\n${this.mount_name}.element.addEventListener('keypress', create_proxy(on_keypress_${this.mount_name}))`;
|
||||
}
|
||||
|
||||
await runtime.runButDontRaise(this.code);
|
||||
await runtime.runButDontRaise(registrationCode);
|
||||
logger.debug('py-inputbox connected');
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.checkId();
|
||||
this.code = htmlDecode(this.innerHTML);
|
||||
this.mount_name = this.id.split('-').join('_');
|
||||
this.innerHTML = '';
|
||||
|
||||
const mainDiv = document.createElement('input');
|
||||
mainDiv.type = 'text';
|
||||
addClasses(mainDiv, ['py-input']);
|
||||
|
||||
mainDiv.id = this.id;
|
||||
this.id = `${this.id}-container`;
|
||||
this.appendChild(mainDiv);
|
||||
|
||||
// now that we appended and the element is attached, lets connect with the event handlers
|
||||
// defined for this widget
|
||||
this.appendChild(mainDiv);
|
||||
this.code = this.code.split('self').join(this.mount_name);
|
||||
let registrationCode = `from pyodide.ffi import create_proxy`;
|
||||
registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`;
|
||||
if (this.code.includes('def on_keypress')) {
|
||||
this.code = this.code.replace('def on_keypress', `def on_keypress_${this.mount_name}`);
|
||||
registrationCode += `\n${this.mount_name}.element.addEventListener('keypress', create_proxy(on_keypress_${this.mount_name}))`;
|
||||
}
|
||||
|
||||
// TODO: For now we delay execution to allow pyodide to load but in the future this
|
||||
// should really wait for it to load..
|
||||
this.runAfterRuntimeInitialized(async () => {
|
||||
await this.eval(this.code);
|
||||
await this.eval(registrationCode);
|
||||
logger.debug('registered handlers');
|
||||
});
|
||||
}
|
||||
return PyInputBox;
|
||||
}
|
||||
|
||||
@@ -7,195 +7,200 @@ import { defaultKeymap } from '@codemirror/commands';
|
||||
import { oneDarkTheme } from '@codemirror/theme-one-dark';
|
||||
import { addClasses, htmlDecode } from '../utils';
|
||||
import { BaseEvalElement } from './base';
|
||||
import type { Runtime } from '../runtime';
|
||||
import { getLogger } from '../logger';
|
||||
|
||||
const logger = getLogger('py-repl');
|
||||
|
||||
export function make_PyRepl(runtime: Runtime) {
|
||||
|
||||
function createCmdHandler(el: PyRepl): StateCommand {
|
||||
// Creates a codemirror cmd handler that calls the el.evaluate when an event
|
||||
// triggers that specific cmd
|
||||
return () => {
|
||||
void el.evaluate();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
let initialTheme: string;
|
||||
function getEditorTheme(el: BaseEvalElement): string {
|
||||
return initialTheme || (initialTheme = el.getAttribute('theme'));
|
||||
}
|
||||
|
||||
export class PyRepl extends BaseEvalElement {
|
||||
editor: EditorView;
|
||||
editorNode: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 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);
|
||||
function createCmdHandler(el: PyRepl): StateCommand {
|
||||
// Creates a codemirror cmd handler that calls the el.evaluate when an event
|
||||
// triggers that specific cmd
|
||||
return () => {
|
||||
void el.evaluate(runtime);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.checkId();
|
||||
this.code = htmlDecode(this.innerHTML);
|
||||
this.innerHTML = '';
|
||||
const languageConf = new Compartment();
|
||||
let initialTheme: string;
|
||||
function getEditorTheme(el: BaseEvalElement): string {
|
||||
return initialTheme || (initialTheme = el.getAttribute('theme'));
|
||||
}
|
||||
|
||||
const extensions = [
|
||||
indentUnit.of(" "),
|
||||
basicSetup,
|
||||
languageConf.of(python()),
|
||||
keymap.of([
|
||||
...defaultKeymap,
|
||||
{ key: 'Ctrl-Enter', run: createCmdHandler(this) },
|
||||
{ key: 'Shift-Enter', run: createCmdHandler(this) },
|
||||
]),
|
||||
];
|
||||
class PyRepl extends BaseEvalElement {
|
||||
editor: EditorView;
|
||||
editorNode: HTMLElement;
|
||||
|
||||
if (getEditorTheme(this) === 'dark') {
|
||||
extensions.push(oneDarkTheme);
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
this.editor = new EditorView({
|
||||
doc: this.code.trim(),
|
||||
extensions,
|
||||
parent: this.editorNode,
|
||||
});
|
||||
connectedCallback() {
|
||||
this.checkId();
|
||||
this.code = htmlDecode(this.innerHTML);
|
||||
this.innerHTML = '';
|
||||
const languageConf = new Compartment();
|
||||
|
||||
const mainDiv = document.createElement('div');
|
||||
addClasses(mainDiv, ['py-repl-box']);
|
||||
const extensions = [
|
||||
indentUnit.of(" "),
|
||||
basicSetup,
|
||||
languageConf.of(python()),
|
||||
keymap.of([
|
||||
...defaultKeymap,
|
||||
{ key: 'Ctrl-Enter', run: createCmdHandler(this) },
|
||||
{ key: 'Shift-Enter', run: createCmdHandler(this) },
|
||||
]),
|
||||
];
|
||||
|
||||
// Styles that we use to hide the labels whilst also keeping it accessible for screen readers
|
||||
const labelStyle = 'overflow:hidden; display:block; width:1px; height:1px';
|
||||
if (getEditorTheme(this) === 'dark') {
|
||||
extensions.push(oneDarkTheme);
|
||||
}
|
||||
|
||||
// Code editor Label
|
||||
this.editorNode.id = 'code-editor';
|
||||
const editorLabel = document.createElement('label');
|
||||
editorLabel.innerHTML = 'Python Script Area';
|
||||
editorLabel.setAttribute('style', labelStyle);
|
||||
editorLabel.htmlFor = 'code-editor';
|
||||
this.editor = new EditorView({
|
||||
doc: this.code.trim(),
|
||||
extensions,
|
||||
parent: this.editorNode,
|
||||
});
|
||||
|
||||
mainDiv.append(editorLabel);
|
||||
const mainDiv = document.createElement('div');
|
||||
addClasses(mainDiv, ['py-repl-box']);
|
||||
|
||||
// add Editor to main PyScript div
|
||||
mainDiv.appendChild(this.editorNode);
|
||||
// Styles that we use to hide the labels whilst also keeping it accessible for screen readers
|
||||
const labelStyle = 'overflow:hidden; display:block; width:1px; height:1px';
|
||||
|
||||
// Play Button
|
||||
this.btnRun = document.createElement('button');
|
||||
this.btnRun.id = 'btnRun';
|
||||
this.btnRun.innerHTML =
|
||||
'<svg id="" style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>';
|
||||
addClasses(this.btnRun, ['absolute', 'repl-play-button']);
|
||||
// Code editor Label
|
||||
this.editorNode.id = 'code-editor';
|
||||
const editorLabel = document.createElement('label');
|
||||
editorLabel.innerHTML = 'Python Script Area';
|
||||
editorLabel.setAttribute('style', labelStyle);
|
||||
editorLabel.htmlFor = 'code-editor';
|
||||
|
||||
// Play Button Label
|
||||
const btnLabel = document.createElement('label');
|
||||
btnLabel.innerHTML = 'Python Script Run Button';
|
||||
btnLabel.setAttribute('style', labelStyle);
|
||||
btnLabel.htmlFor = 'btnRun';
|
||||
mainDiv.append(editorLabel);
|
||||
|
||||
this.editorNode.appendChild(btnLabel);
|
||||
this.editorNode.appendChild(this.btnRun);
|
||||
// add Editor to main PyScript div
|
||||
mainDiv.appendChild(this.editorNode);
|
||||
|
||||
this.btnRun.addEventListener('click', () => {
|
||||
void this.evaluate();
|
||||
});
|
||||
// Play Button
|
||||
this.btnRun = document.createElement('button');
|
||||
this.btnRun.id = 'btnRun';
|
||||
this.btnRun.innerHTML =
|
||||
'<svg id="" style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>';
|
||||
addClasses(this.btnRun, ['absolute', 'repl-play-button']);
|
||||
|
||||
if (!this.id) {
|
||||
logger.warn(
|
||||
"WARNING: <py-repl> defined without an id. <py-repl> should always have an id, otherwise multiple <py-repl> in the same page will not work!"
|
||||
);
|
||||
}
|
||||
// Play Button Label
|
||||
const btnLabel = document.createElement('label');
|
||||
btnLabel.innerHTML = 'Python Script Run Button';
|
||||
btnLabel.setAttribute('style', labelStyle);
|
||||
btnLabel.htmlFor = 'btnRun';
|
||||
|
||||
if (!this.hasAttribute('exec-id')) {
|
||||
this.setAttribute('exec-id', '1');
|
||||
}
|
||||
this.editorNode.appendChild(btnLabel);
|
||||
this.editorNode.appendChild(this.btnRun);
|
||||
|
||||
if (!this.hasAttribute('root')) {
|
||||
this.setAttribute('root', this.id);
|
||||
}
|
||||
this.btnRun.addEventListener('click', () => {
|
||||
void this.evaluate(runtime);
|
||||
});
|
||||
|
||||
if (this.hasAttribute('output')) {
|
||||
this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output'));
|
||||
} else {
|
||||
if (this.hasAttribute('std-out')) {
|
||||
this.outputElement = document.getElementById(this.getAttribute('std-out'));
|
||||
if (!this.id) {
|
||||
logger.warn(
|
||||
"WARNING: <py-repl> defined without an id. <py-repl> should always have an id, otherwise multiple <py-repl> in the same page will not work!"
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.hasAttribute('exec-id')) {
|
||||
this.setAttribute('exec-id', '1');
|
||||
}
|
||||
|
||||
if (!this.hasAttribute('root')) {
|
||||
this.setAttribute('root', this.id);
|
||||
}
|
||||
|
||||
if (this.hasAttribute('output')) {
|
||||
this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output'));
|
||||
} 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');
|
||||
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);
|
||||
}
|
||||
|
||||
this.errorElement = this.hasAttribute('std-err')
|
||||
? document.getElementById(this.getAttribute('std-err'))
|
||||
: this.outputElement;
|
||||
}
|
||||
|
||||
this.appendChild(mainDiv);
|
||||
this.editor.focus();
|
||||
logger.debug(`element ${this.id} successfully connected`);
|
||||
}
|
||||
|
||||
addToOutput(s: string): void {
|
||||
this.outputElement.innerHTML += '<div>' + s + '</div>';
|
||||
this.outputElement.hidden = false;
|
||||
}
|
||||
|
||||
preEvaluate(): void {
|
||||
this.setOutputMode("replace");
|
||||
if(!this.appendOutput) {
|
||||
this.outputElement.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
postEvaluate(): void {
|
||||
this.outputElement.hidden = false;
|
||||
this.outputElement.style.display = 'block';
|
||||
|
||||
if (this.hasAttribute('auto-generate')) {
|
||||
const allPyRepls = document.querySelectorAll(`py-repl[root='${this.getAttribute('root')}'][exec-id]`);
|
||||
const lastRepl = allPyRepls[allPyRepls.length - 1];
|
||||
const lastExecId = lastRepl.getAttribute('exec-id');
|
||||
const nextExecId = parseInt(lastExecId) + 1;
|
||||
|
||||
const newPyRepl = document.createElement('py-repl');
|
||||
newPyRepl.setAttribute('root', this.getAttribute('root'));
|
||||
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
|
||||
|
||||
if(this.hasAttribute('auto-generate')) {
|
||||
newPyRepl.setAttribute('auto-generate', '');
|
||||
this.removeAttribute('auto-generate');
|
||||
}
|
||||
|
||||
if(this.hasAttribute('output-mode')) {
|
||||
newPyRepl.setAttribute('output-mode', this.getAttribute('output-mode'));
|
||||
}
|
||||
|
||||
const addReplAttribute = (attribute: string) => {
|
||||
if (this.hasAttribute(attribute)) {
|
||||
newPyRepl.setAttribute(attribute, this.getAttribute(attribute));
|
||||
// add the output div id if there's not output pre-defined
|
||||
mainDiv.appendChild(this.outputElement);
|
||||
}
|
||||
};
|
||||
|
||||
addReplAttribute('output');
|
||||
addReplAttribute('std-out');
|
||||
addReplAttribute('std-err');
|
||||
this.errorElement = this.hasAttribute('std-err')
|
||||
? document.getElementById(this.getAttribute('std-err'))
|
||||
: this.outputElement;
|
||||
}
|
||||
|
||||
newPyRepl.setAttribute('exec-id', nextExecId.toString());
|
||||
this.parentElement.appendChild(newPyRepl);
|
||||
this.appendChild(mainDiv);
|
||||
this.editor.focus();
|
||||
logger.debug(`element ${this.id} successfully connected`);
|
||||
}
|
||||
|
||||
addToOutput(s: string): void {
|
||||
this.outputElement.innerHTML += '<div>' + s + '</div>';
|
||||
this.outputElement.hidden = false;
|
||||
}
|
||||
|
||||
preEvaluate(): void {
|
||||
this.setOutputMode("replace");
|
||||
if(!this.appendOutput) {
|
||||
this.outputElement.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
postEvaluate(): void {
|
||||
this.outputElement.hidden = false;
|
||||
this.outputElement.style.display = 'block';
|
||||
|
||||
if (this.hasAttribute('auto-generate')) {
|
||||
const allPyRepls = document.querySelectorAll(`py-repl[root='${this.getAttribute('root')}'][exec-id]`);
|
||||
const lastRepl = allPyRepls[allPyRepls.length - 1];
|
||||
const lastExecId = lastRepl.getAttribute('exec-id');
|
||||
const nextExecId = parseInt(lastExecId) + 1;
|
||||
|
||||
const newPyRepl = document.createElement('py-repl');
|
||||
newPyRepl.setAttribute('root', this.getAttribute('root'));
|
||||
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
|
||||
|
||||
if(this.hasAttribute('auto-generate')) {
|
||||
newPyRepl.setAttribute('auto-generate', '');
|
||||
this.removeAttribute('auto-generate');
|
||||
}
|
||||
|
||||
if(this.hasAttribute('output-mode')) {
|
||||
newPyRepl.setAttribute('output-mode', this.getAttribute('output-mode'));
|
||||
}
|
||||
|
||||
const addReplAttribute = (attribute: string) => {
|
||||
if (this.hasAttribute(attribute)) {
|
||||
newPyRepl.setAttribute(attribute, this.getAttribute(attribute));
|
||||
}
|
||||
};
|
||||
|
||||
addReplAttribute('output');
|
||||
addReplAttribute('std-out');
|
||||
addReplAttribute('std-err');
|
||||
|
||||
newPyRepl.setAttribute('exec-id', nextExecId.toString());
|
||||
this.parentElement.appendChild(newPyRepl);
|
||||
}
|
||||
}
|
||||
|
||||
getSourceFromElement(): string {
|
||||
return this.editor.state.doc.toString();
|
||||
}
|
||||
}
|
||||
|
||||
getSourceFromElement(): string {
|
||||
return this.editor.state.doc.toString();
|
||||
}
|
||||
return PyRepl
|
||||
}
|
||||
|
||||
127
pyscriptjs/src/components/pywidget.ts
Normal file
127
pyscriptjs/src/components/pywidget.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { Runtime } from '../runtime';
|
||||
import { getLogger } from '../logger';
|
||||
|
||||
const logger = getLogger('py-register-widget');
|
||||
|
||||
|
||||
function createWidget(runtime: Runtime, name: string, code: string, klass: string) {
|
||||
class CustomWidget extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
wrapper: HTMLElement;
|
||||
|
||||
name: string = name;
|
||||
klass: string = klass;
|
||||
code: string = code;
|
||||
proxy: any;
|
||||
proxyClass: any;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await runtime.runButDontRaise(this.code);
|
||||
this.proxyClass = runtime.globals.get(this.klass);
|
||||
this.proxy = this.proxyClass(this);
|
||||
this.proxy.connect();
|
||||
this.registerWidget();
|
||||
}
|
||||
|
||||
registerWidget() {
|
||||
logger.info('new widget registered:', this.name);
|
||||
runtime.globals.set(this.id, this.proxy);
|
||||
}
|
||||
}
|
||||
const xPyWidget = customElements.define(name, CustomWidget);
|
||||
}
|
||||
|
||||
export function make_PyWidget(runtime: Runtime) {
|
||||
class PyWidget extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
name: string;
|
||||
klass: string;
|
||||
outputElement: HTMLElement;
|
||||
errorElement: HTMLElement;
|
||||
wrapper: HTMLElement;
|
||||
theme: string;
|
||||
source: string;
|
||||
code: 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);
|
||||
|
||||
this.addAttributes('src','name','klass');
|
||||
}
|
||||
|
||||
addAttributes(...attrs:string[]){
|
||||
for (const each of attrs){
|
||||
const property = each === "src" ? "source" : each;
|
||||
if (this.hasAttribute(each)) {
|
||||
this[property]=this.getAttribute(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
if (this.id === undefined) {
|
||||
throw new ReferenceError(
|
||||
`No id specified for component. Components must have an explicit id. Please use id="" to specify your component id.`,
|
||||
);
|
||||
}
|
||||
|
||||
const mainDiv = document.createElement('div');
|
||||
mainDiv.id = this.id + '-main';
|
||||
this.appendChild(mainDiv);
|
||||
logger.debug('PyWidget: reading source', this.source);
|
||||
this.code = await this.getSourceFromFile(this.source);
|
||||
createWidget(runtime, this.name, this.code, this.klass);
|
||||
}
|
||||
|
||||
initOutErr(): void {
|
||||
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 {
|
||||
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');
|
||||
}
|
||||
|
||||
if (this.hasAttribute('std-err')) {
|
||||
this.errorElement = document.getElementById(this.getAttribute('std-err'));
|
||||
} else {
|
||||
this.errorElement = this.outputElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getSourceFromFile(s: string): Promise<string> {
|
||||
const response = await fetch(s);
|
||||
return await response.text();
|
||||
}
|
||||
}
|
||||
|
||||
return PyWidget;
|
||||
}
|
||||
@@ -7,10 +7,7 @@ import { PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
import { PyLoader } from './components/pyloader';
|
||||
import { PyodideRuntime } from './pyodide';
|
||||
import { getLogger } from './logger';
|
||||
import {
|
||||
runtimeLoaded,
|
||||
scriptsQueue,
|
||||
} from './stores';
|
||||
import { scriptsQueue } from './stores';
|
||||
import { handleFetchError, showError, globalExport } from './utils'
|
||||
import { createCustomElements } from './components/elements';
|
||||
|
||||
@@ -137,7 +134,6 @@ class PyScriptApp {
|
||||
|
||||
this.loader.log('Python startup...');
|
||||
await runtime.loadInterpreter();
|
||||
runtimeLoaded.set(runtime);
|
||||
this.loader.log('Python ready!');
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -152,7 +148,7 @@ class PyScriptApp {
|
||||
|
||||
this.loader.log('Initializing web components...');
|
||||
// lifecycle (8)
|
||||
createCustomElements();
|
||||
createCustomElements(runtime);
|
||||
|
||||
if (runtime.config.autoclose_loader) {
|
||||
this.loader.close();
|
||||
@@ -199,7 +195,7 @@ class PyScriptApp {
|
||||
// lifecycle (7)
|
||||
executeScripts(runtime: Runtime) {
|
||||
for (const script of scriptsQueue_) {
|
||||
void script.evaluate();
|
||||
void script.evaluate(runtime);
|
||||
}
|
||||
scriptsQueue.set([]);
|
||||
}
|
||||
|
||||
@@ -52,9 +52,23 @@ export abstract class Runtime extends Object {
|
||||
/**
|
||||
* delegates the code to be run to the underlying interpreter
|
||||
* (asynchronously) which can call its own API behind the scenes.
|
||||
* Python exceptions are turned into JS exceptions.
|
||||
* */
|
||||
abstract run(code: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* Same as run, but Python exceptions are not propagated: instead, they
|
||||
* are logged to the console.
|
||||
*
|
||||
* This is a bad API and should be killed/refactored/changed eventually,
|
||||
* but for now we have code which relies on it.
|
||||
* */
|
||||
async runButDontRaise(code: string): Promise<any> {
|
||||
return this.run(code).catch(err => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* delegates the setting of JS objects to
|
||||
* the underlying interpreter.
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { PyLoader } from './components/pyloader';
|
||||
import type { PyScript } from './components/pyscript';
|
||||
import type { Runtime } from './runtime';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import { getLogger } from './logger';
|
||||
|
||||
/*
|
||||
A store for Runtime which can encompass any
|
||||
runtime, but currently only has Pyodide as its offering.
|
||||
*/
|
||||
export const runtimeLoaded = writable<Runtime>();
|
||||
|
||||
export const navBarOpen = writable(false);
|
||||
export const componentsNavOpen = writable(false);
|
||||
export const componentDetailsNavOpen = writable(false);
|
||||
export const mainDiv = writable(null);
|
||||
export const currentComponentDetails = writable([]);
|
||||
export const scriptsQueue = writable<PyScript[]>([]);
|
||||
export const globalLoader = writable<PyLoader | undefined>();
|
||||
|
||||
export const addToScriptsQueue = (script: PyScript) => {
|
||||
scriptsQueue.update(scriptsQueue => [...scriptsQueue, script]);
|
||||
|
||||
Reference in New Issue
Block a user