PyodideRuntime should be one of the runtimes (#698)

* PyodideRuntime should be one of the runtimes

* subsume interpreter into runtime API

* fix eslint

* add comments

* move initializers, postInitializers, scriptsQueue, etc. to initialize() of Runtime Super Class

* modify comment for initialize

* small renaming

* change id to default

* fix pyscript.py import

* try adding tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add inlineDynamicImports option

* Make jest happy about ESM modules

* Attempt to make jest happy about pyodide

* point to version in accordance with node module being used

* fix base.ts

* fix tests

* fix indexURL path determination

* edit pyodide.asm.js as a part of setup process

* load runtime beforeAll tests

* add test for loading a package

* use only runPythonAsync underneath for pyodide

* import PyodideInterface type directly from pyodide

* add some comments

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Philipp Rudiger <prudiger@anaconda.com>
This commit is contained in:
Madhur Tandon
2022-08-25 02:33:36 +05:30
committed by GitHub
parent 1054e8e644
commit 1db155570d
17 changed files with 438 additions and 270 deletions

View File

@@ -1,11 +1,13 @@
import { pyodideLoaded } from '../stores';
import { runtimeLoaded } from '../stores';
import { guidGenerator, addClasses, removeClasses } from '../utils';
import type { PyodideInterface } from '../pyodide';
// Premise used to connect to the first available pyodide interpreter
let runtime: PyodideInterface;
import type { Runtime } from '../runtime';
// Global `Runtime` that implements the generic runtimes API
let runtime: Runtime;
let Element;
pyodideLoaded.subscribe(value => {
runtimeLoaded.subscribe(value => {
runtime = value;
});
@@ -77,7 +79,7 @@ export class BaseEvalElement extends HTMLElement {
return this.code;
}
protected async _register_esm(pyodide: PyodideInterface): Promise<void> {
protected async _register_esm(runtime: Runtime): Promise<void> {
const imports: { [key: string]: unknown } = {};
const nodes = document.querySelectorAll("script[type='importmap']");
const importmaps: Array<any> = [];
@@ -107,7 +109,7 @@ export class BaseEvalElement extends HTMLElement {
}
}
pyodide.registerJsModule('esm', imports);
runtime.registerJsModule('esm', imports);
}
async evaluate(): Promise<void> {
@@ -119,20 +121,12 @@ export class BaseEvalElement extends HTMLElement {
try {
source = this.source ? await this.getSourceFromFile(this.source)
: this.getSourceFromElement();
const is_async = source.includes('asyncio')
this._register_esm(runtime);
if (is_async) {
<string>await runtime.runPythonAsync(
`output_manager.change(out="${this.outputElement.id}", err="${this.errorElement.id}", append=${this.appendOutput ? 'True' : 'False'})`,
);
output = <string>await runtime.runPythonAsync(source);
} else {
output = <string>runtime.runPython(
`output_manager.change(out="${this.outputElement.id}", err="${this.errorElement.id}", append=${this.appendOutput ? 'True' : 'False'})`,
);
output = <string>runtime.runPython(source);
}
<string>await runtime.run(
`output_manager.change(out="${this.outputElement.id}", err="${this.errorElement.id}", append=${this.appendOutput ? 'True' : 'False'})`,
);
output = <string>await runtime.run(source);
if (output !== undefined) {
if (Element === undefined) {
@@ -145,8 +139,7 @@ export class BaseEvalElement extends HTMLElement {
this.outputElement.style.display = 'block';
}
is_async ? await runtime.runPythonAsync(`output_manager.revert()`)
: await runtime.runPython(`output_manager.revert()`);
await runtime.run(`output_manager.revert()`);
// check if this REPL contains errors, delete them and remove error classes
const errorElements = document.querySelectorAll(`div[id^='${this.errorElement.id}'][error]`);
@@ -191,10 +184,8 @@ export class BaseEvalElement extends HTMLElement {
} // end evaluate
async eval(source: string): Promise<void> {
const pyodide = runtime;
try {
const output = await pyodide.runPythonAsync(source);
const output = await runtime.run(source);
if (output !== undefined) {
console.log(output);
}
@@ -204,8 +195,8 @@ export class BaseEvalElement extends HTMLElement {
} // end eval
runAfterRuntimeInitialized(callback: () => Promise<void>){
pyodideLoaded.subscribe(value => {
if ('runPythonAsync' in value) {
runtimeLoaded.subscribe(value => {
if ('run' in value) {
setTimeout(async () => {
await callback();
}, 100);
@@ -247,9 +238,9 @@ function createWidget(name: string, code: string, klass: string) {
// this.proxy.connect();
// this.registerWidget();
// }, 2000);
pyodideLoaded.subscribe(value => {
runtimeLoaded.subscribe(value => {
console.log('RUNTIME READY', value);
if ('runPythonAsync' in value) {
if ('run' in value) {
runtime = value;
setTimeout(async () => {
await this.eval(this.code);
@@ -263,16 +254,14 @@ function createWidget(name: string, code: string, klass: string) {
}
registerWidget() {
const pyodide = runtime;
console.log('new widget registered:', this.name);
pyodide.globals.set(this.id, this.proxy);
runtime.globals.set(this.id, this.proxy);
}
async eval(source: string): Promise<void> {
const pyodide = runtime;
try {
const output = await pyodide.runPythonAsync(source);
this.proxyClass = pyodide.globals.get(this.klass);
const output = await runtime.run(source);
this.proxyClass = runtime.globals.get(this.klass);
if (output !== undefined) {
console.log(output);
}
@@ -365,9 +354,8 @@ export class PyWidget extends HTMLElement {
}
async eval(source: string): Promise<void> {
const pyodide = runtime;
try {
const output = await pyodide.runPythonAsync(source);
const output = await runtime.run(source);
if (output !== undefined) {
console.log(output);
}

View File

@@ -1,127 +1,19 @@
import * as jsyaml from 'js-yaml';
import { BaseEvalElement } from './base';
import {
initializers,
loadedEnvironments,
postInitializers,
pyodideLoaded,
scriptsQueue,
globalLoader,
appConfig,
Initializer,
} from '../stores';
import { loadInterpreter } from '../interpreter';
import type { PyLoader } from './pyloader';
import type { PyScript } from './pyscript';
import type { PyodideInterface } from '../pyodide';
import { appConfig } from '../stores';
import type { Runtime, AppConfig } from '../runtime';
import { PyodideRuntime } from '../pyodide';
const DEFAULT_RUNTIME = {
src: 'https://cdn.jsdelivr.net/pyodide/v0.21.1/full/pyodide.js',
name: 'pyodide-default',
lang: 'python',
};
const DEFAULT_RUNTIME: Runtime = new PyodideRuntime();
export type Runtime = {
src: string;
name?: string;
lang?: string;
};
export type AppConfig = {
autoclose_loader: boolean;
name?: string;
version?: string;
runtimes?: Array<Runtime>;
};
let appConfig_: AppConfig = {
autoclose_loader: true,
};
appConfig.subscribe((value: AppConfig) => {
if (value) {
appConfig_ = value;
}
console.log('config set!');
});
let initializers_: Initializer[];
initializers.subscribe((value: Initializer[]) => {
initializers_ = value;
console.log('initializers set');
});
let postInitializers_: Initializer[];
postInitializers.subscribe((value: Initializer[]) => {
postInitializers_ = value;
console.log('post initializers set');
});
let scriptsQueue_: PyScript[];
scriptsQueue.subscribe((value: PyScript[]) => {
scriptsQueue_ = value;
console.log('post initializers set');
});
let loader: PyLoader | undefined;
globalLoader.subscribe(value => {
loader = value;
});
export class PyodideRuntime extends Object {
src: string;
constructor(url: string) {
super();
this.src = url;
}
async initialize() {
loader?.log('Loading runtime...');
const pyodide: PyodideInterface = await loadInterpreter(this.src);
const newEnv = {
id: 'a',
runtime: pyodide,
state: 'loading',
};
pyodideLoaded.set(pyodide);
// Inject the loader into the runtime namespace
// eslint-disable-next-line
pyodide.globals.set('pyscript_loader', loader);
loader?.log('Runtime created...');
loadedEnvironments.update(environments => ({
...environments,
[newEnv['id']]: newEnv,
}));
// now we call all initializers before we actually executed all page scripts
loader?.log('Initializing components...');
for (const initializer of initializers_) {
await initializer();
}
loader?.log('Initializing scripts...');
for (const script of scriptsQueue_) {
await script.evaluate();
}
scriptsQueue.set([]);
// now we call all post initializers AFTER we actually executed all page scripts
loader?.log('Running post initializers...');
if (appConfig_ && appConfig_.autoclose_loader) {
loader?.close();
console.log('------ loader closed ------');
}
for (const initializer of postInitializers_) {
await initializer();
}
console.log('===PyScript page fully initialized===');
}
}
/**
* Configures general metadata about the PyScript application such
* as a list of runtimes, name, version, closing the loader
* automatically, etc.
*
* Also initializes the different runtimes passed. If no runtime is passed,
* the default runtime based on Pyodide is used.
*/
export class PyConfig extends BaseEvalElement {
shadow: ShadowRoot;
@@ -175,10 +67,9 @@ export class PyConfig extends BaseEvalElement {
console.log('Initializing runtimes...');
for (const runtime of this.values.runtimes) {
const script = document.createElement('script'); // create a script DOM node
const runtimeSpec = new PyodideRuntime(runtime.src);
script.src = runtime.src; // set its src to the provided URL
script.src = runtime.src; // set its src to the provided URL
script.addEventListener('load', () => {
void runtimeSpec.initialize();
void runtime.initialize();
});
document.head.appendChild(script);
}

View File

@@ -1,14 +1,13 @@
import * as jsyaml from 'js-yaml';
import { pyodideLoaded, addInitializer } from '../stores';
import { loadPackage, loadFromFile } from '../interpreter';
import { runtimeLoaded, addInitializer } from '../stores';
import { handleFetchError } from '../utils';
import type { PyodideInterface } from '../pyodide';
import type { Runtime } from '../runtime';
// Premise used to connect to the first available pyodide interpreter
let runtime: PyodideInterface;
// Premise used to connect to the first available runtime (can be pyodide or others)
let runtime: Runtime;
pyodideLoaded.subscribe(value => {
runtimeLoaded.subscribe(value => {
runtime = value;
console.log('RUNTIME READY');
});
@@ -18,7 +17,7 @@ export class PyEnv extends HTMLElement {
wrapper: HTMLElement;
code: string;
environment: unknown;
runtime: PyodideInterface;
runtime: Runtime;
env: string[];
paths: string[];
@@ -56,7 +55,7 @@ export class PyEnv extends HTMLElement {
this.paths = paths;
async function loadEnv() {
await loadPackage(env, runtime);
await runtime.installPackage(env);
console.log('environment loaded');
}
@@ -64,7 +63,7 @@ export class PyEnv extends HTMLElement {
for (const singleFile of paths) {
console.log(`loading ${singleFile}`);
try {
await loadFromFile(singleFile, runtime);
await runtime.loadFromFile(singleFile);
} catch (e) {
//Should we still export full error contents to console?
handleFetchError(<Error>e, singleFile);

View File

@@ -3,19 +3,20 @@ import {
addPostInitializer,
addToScriptsQueue,
loadedEnvironments,
pyodideLoaded,
runtimeLoaded,
type Environment,
} from '../stores';
import { addClasses, htmlDecode } from '../utils';
import { BaseEvalElement } from './base';
import type { PyodideInterface } from '../pyodide';
import type { Runtime } from '../runtime';
// Premise used to connect to the first available pyodide interpreter
let pyodideReadyPromise: PyodideInterface;
// Premise used to connect to the first available runtime (can be pyodide or others)
let runtime: Runtime;
let environments: Record<Environment['id'], Environment> = {};
pyodideLoaded.subscribe(value => {
pyodideReadyPromise = value;
runtimeLoaded.subscribe(value => {
runtime = value;
});
loadedEnvironments.subscribe(value => {
environments = value;
@@ -78,7 +79,7 @@ export class PyScript extends BaseEvalElement {
}
}
protected async _register_esm(pyodide: PyodideInterface): Promise<void> {
protected async _register_esm(runtime: Runtime): Promise<void> {
for (const node of document.querySelectorAll("script[type='importmap']")) {
const importmap = (() => {
try {
@@ -103,7 +104,7 @@ export class PyScript extends BaseEvalElement {
continue;
}
pyodide.registerJsModule(name, exports);
runtime.registerJsModule(name, exports);
}
}
}
@@ -211,14 +212,13 @@ const pyAttributeToEvent: Map<string, string> = new Map<string, string>([
/** Initialize all elements with py-on* handlers attributes */
async function initHandlers() {
console.log('Collecting nodes...');
const pyodide = pyodideReadyPromise;
for (const pyAttribute of pyAttributeToEvent.keys()) {
await createElementsWithEventListeners(pyodide, pyAttribute);
await createElementsWithEventListeners(runtime, pyAttribute);
}
}
/** Initializes an element with the given py-on* attribute and its handler */
async function createElementsWithEventListeners(pyodide: PyodideInterface, pyAttribute: string): Promise<void> {
async function createElementsWithEventListeners(runtime: Runtime, pyAttribute: string): Promise<void> {
const matches: NodeListOf<HTMLElement> = document.querySelectorAll(`[${pyAttribute}]`);
for (const el of matches) {
if (el.id.length === 0) {
@@ -230,7 +230,7 @@ async function createElementsWithEventListeners(pyodide: PyodideInterface, pyAtt
from pyodide import create_proxy
Element("${el.id}").element.addEventListener("${event}", create_proxy(${handlerCode}))
`;
await pyodide.runPythonAsync(source);
await runtime.run(source);
// TODO: Should we actually map handlers in JS instead of Python?
// el.onclick = (evt: any) => {
@@ -252,7 +252,6 @@ async function createElementsWithEventListeners(pyodide: PyodideInterface, pyAtt
/** Mount all elements with attribute py-mount into the Python namespace */
async function mountElements() {
console.log('Collecting nodes to be mounted into python namespace...');
const pyodide = pyodideReadyPromise;
const matches: NodeListOf<HTMLElement> = document.querySelectorAll('[py-mount]');
let source = '';
@@ -260,7 +259,7 @@ async function mountElements() {
const mountName = el.getAttribute('py-mount') || el.id.split('-').join('_');
source += `\n${mountName} = Element("${el.id}")`;
}
await pyodide.runPythonAsync(source);
await runtime.run(source);
}
addInitializer(mountElements);
addPostInitializer(initHandlers);