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

@@ -40,6 +40,10 @@ setup:
$(CONDA_EXE) env $(shell [ -d $(env) ] && echo update || echo create) -p $(env) --file environment.yml
$(conda_run) playwright install
$(CONDA_EXE) install -c anaconda pytest -y
# context for the removal of assert statement below: https://github.com/pyodide/pyodide/issues/2764
# the assert statement fails even if `arrayBuffer.constructor.name == "ArrayBuffer"` is `true`
# thus, the statement is removed from the auto-generated `pyodide.asm.js` file
sed $(SED_I_ARG) 's/assert(arrayBuffer instanceof ArrayBuffer,"bad input to processPackageData");//g' node_modules/pyodide/pyodide.asm.js
clean:
find . -name \*.py[cod] -delete

View File

@@ -0,0 +1,12 @@
/**
* this file mocks the `src/python/pyscript.py` file
* since importing of `.py` files isn't usually supported
* inside JS/TS files.
*
* It sets the value of whatever is imported from
* `src/python/pyscript.py` to be an empty string i.e. ""
*
* This is needed since the imported object is further
* passed to a function which only accepts a string.
*/
module.exports = "";

View File

@@ -2,13 +2,18 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jest-environment-jsdom',
extensionsToTreatAsEsm: ['.ts'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json'
tsconfig: 'tsconfig.json',
useESM: true
}
},
verbose: true,
testEnvironmentOptions: {
url: "http://localhost"
},
moduleNameMapper: {
"^[./a-zA-Z0-9$_-]+\\.py$": "<rootDir>/__mocks__/fileMock.js",
}
};

View File

@@ -26,9 +26,11 @@
"@tsconfig/svelte": "^1.0.0",
"@types/jest": "^28.1.6",
"@types/js-yaml": "^4.0.5",
"@types/node": "^18.7.11",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"autoprefixer": "^10.4.7",
"cross-env": "^7.0.3",
"eslint": "^8.14.0",
"eslint-plugin-svelte3": "^3.4.1",
"jest": "^28.1.3",
@@ -36,7 +38,7 @@
"postcss": "^8.4.13",
"prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0",
"pyodide": "^0.20.1-alpha.2",
"pyodide": "^0.21.0",
"rollup": "^2.71.1",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
@@ -2076,9 +2078,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "17.0.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==",
"version": "18.7.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.11.tgz",
"integrity": "sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw==",
"dev": true
},
"node_modules/@types/parse5": {
@@ -3032,6 +3034,24 @@
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -6976,9 +6996,9 @@
}
},
"node_modules/pyodide": {
"version": "0.20.1-alpha.2",
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.20.1-alpha.2.tgz",
"integrity": "sha512-LfPGsLZoXtPxWiQaFa9SncxxDAdwtbHEvtbEIInvlOKo/Y0nzuxgrsbg7H/aZ1RmeuFQPwcWS0JvmpKORe98og==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.21.1.tgz",
"integrity": "sha512-QsWqyRRVc9PVRgX0d4GoGa2Y+8pedCP9SB5nPvYN9cepdgpyC3U/kLfOb8jcvFKjWNi2334o+QJmj6wxRr3NPg==",
"dev": true,
"dependencies": {
"base-64": "^1.0.0",
@@ -10015,9 +10035,9 @@
"dev": true
},
"@types/node": {
"version": "17.0.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==",
"version": "18.7.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.11.tgz",
"integrity": "sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw==",
"dev": true
},
"@types/parse5": {
@@ -10700,6 +10720,15 @@
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
},
"cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.1"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -13619,9 +13648,9 @@
"dev": true
},
"pyodide": {
"version": "0.20.1-alpha.2",
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.20.1-alpha.2.tgz",
"integrity": "sha512-LfPGsLZoXtPxWiQaFa9SncxxDAdwtbHEvtbEIInvlOKo/Y0nzuxgrsbg7H/aZ1RmeuFQPwcWS0JvmpKORe98og==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.21.1.tgz",
"integrity": "sha512-QsWqyRRVc9PVRgX0d4GoGa2Y+8pedCP9SB5nPvYN9cepdgpyC3U/kLfOb8jcvFKjWNi2334o+QJmj6wxRr3NPg==",
"dev": true,
"requires": {
"base-64": "^1.0.0",

View File

@@ -12,8 +12,8 @@
"lint": "eslint './src/**/*.{js,svelte,html,ts}'",
"lint:fix": "eslint --fix './src/**/*.{js,svelte,html,ts}'",
"xprelint": "npm run format",
"test": "jest --coverage",
"test:watch": "jest --watch"
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
"test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
@@ -22,9 +22,11 @@
"@tsconfig/svelte": "^1.0.0",
"@types/jest": "^28.1.6",
"@types/js-yaml": "^4.0.5",
"@types/node": "^18.7.11",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"autoprefixer": "^10.4.7",
"cross-env": "^7.0.3",
"eslint": "^8.14.0",
"eslint-plugin-svelte3": "^3.4.1",
"jest": "^28.1.3",
@@ -32,7 +34,7 @@
"postcss": "^8.4.13",
"prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0",
"pyodide": "^0.20.1-alpha.2",
"pyodide": "^0.21.0",
"rollup": "^2.71.1",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",

View File

@@ -17,6 +17,7 @@ export default {
{
sourcemap: true,
format: "iife",
inlineDynamicImports: true,
name: "app",
file: "build/pyscript.js",
},
@@ -24,6 +25,7 @@ export default {
file: "build/pyscript.min.js",
format: "iife",
sourcemap: true,
inlineDynamicImports: true,
plugins: [terser()],
},
],

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(
<string>await runtime.run(
`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);
}
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.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);

View File

@@ -1,63 +0,0 @@
import { getLastPath } from './utils';
import type { PyodideInterface } from './pyodide';
// eslint-disable-next-line
// @ts-ignore
import pyscript from './python/pyscript.py';
let pyodide: PyodideInterface;
const loadInterpreter = async function (indexUrl: string): Promise<PyodideInterface> {
console.log('creating pyodide runtime');
// eslint-disable-next-line
// @ts-ignore
pyodide = await loadPyodide({
// indexURL: indexUrl,
stdout: console.log,
stderr: console.log,
fullStdLib: false,
});
// now that we loaded, add additional convenience functions
console.log('loading micropip');
await pyodide.loadPackage('micropip');
console.log('loading pyscript...');
const output = await pyodide.runPythonAsync(pyscript);
if (output !== undefined) {
console.log(output);
}
console.log('done setting up environment');
return pyodide;
};
const loadPackage = async function (package_name: string[] | string, runtime: PyodideInterface): Promise<void> {
if (package_name.length > 0){
const micropip = runtime.globals.get('micropip');
await micropip.install(package_name);
micropip.destroy();
}
};
const loadFromFile = async function (s: string, runtime: PyodideInterface): Promise<void> {
const filename = getLastPath(s);
await runtime.runPythonAsync(
`
from pyodide.http import pyfetch
from js import console
try:
response = await pyfetch("${s}")
except Exception as err:
console.warn("PyScript: Access to local files (using 'paths:' in py-env) is not available when directly opening a HTML file; you must use a webserver to serve the additional files. See https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062 on starting a simple webserver with Python.")
raise(err)
content = await response.bytes()
with open("${filename}", "wb") as f:
f.write(content)
`,
);
};
export { loadInterpreter, loadPackage, loadFromFile };

View File

@@ -1,4 +1,84 @@
import type { loadPyodide } from 'pyodide';
import { Runtime } from './runtime';
import { getLastPath, inJest } from './utils';
import type { PyodideInterface } from 'pyodide';
import { loadPyodide } from 'pyodide';
// eslint-disable-next-line
// @ts-ignore
import pyscript from './python/pyscript.py';
// The current release doesn't export `PyodideInterface` type
export type PyodideInterface = Awaited<ReturnType<typeof loadPyodide>>;
export class PyodideRuntime extends Runtime {
src = 'https://cdn.jsdelivr.net/pyodide/v0.21.1/full/pyodide.js';
name = 'pyodide-default';
lang = 'python';
interpreter: PyodideInterface;
globals: any;
async loadInterpreter(): Promise<void> {
console.log('creating pyodide runtime');
// eslint-disable-next-line
// @ts-ignore
let extraOpts: any = {}
if (inJest()) {
extraOpts = {indexURL: [process.cwd(), 'node_modules', 'pyodide'].join('/') }
}
this.interpreter = await loadPyodide({
stdout: console.log,
stderr: console.log,
fullStdLib: false,
...extraOpts
});
this.globals = this.interpreter.globals;
// now that we loaded, add additional convenience functions
console.log('loading micropip');
await this.loadPackage('micropip');
console.log('loading pyscript...');
const output = await this.run(pyscript);
if (output !== undefined) {
console.log(output);
}
console.log('done setting up environment');
}
async run(code: string): Promise<any> {
return await this.interpreter.runPythonAsync(code);
}
registerJsModule(name: string, module: object): void {
this.interpreter.registerJsModule(name, module);
}
async loadPackage(names: string | string[]): Promise<void> {
await this.interpreter.loadPackage(names);
}
async installPackage(package_name: string | string[]): Promise<void> {
if (package_name.length > 0){
const micropip = this.globals.get('micropip');
await micropip.install(package_name);
micropip.destroy();
}
}
async loadFromFile(path: string): Promise<void> {
const filename = getLastPath(path);
await this.run(
`
from pyodide.http import pyfetch
from js import console
try:
response = await pyfetch("${path}")
except Exception as err:
console.warn("PyScript: Access to local files (using 'paths:' in py-env) is not available when directly opening a HTML file; you must use a webserver to serve the additional files. See https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062 on starting a simple webserver with Python.")
raise(err)
content = await response.bytes()
with open("${filename}", "wb") as f:
f.write(content)
`,
);
}
}

175
pyscriptjs/src/runtime.ts Normal file
View File

@@ -0,0 +1,175 @@
import type { PyodideInterface } from 'pyodide';
import type { PyLoader } from './components/pyloader';
import {
runtimeLoaded,
loadedEnvironments,
globalLoader,
initializers,
postInitializers,
Initializer,
scriptsQueue,
appConfig
} from './stores'
import type { PyScript } from './components/pyscript';
export type RuntimeInterpreter = PyodideInterface | null;
export type AppConfig = {
autoclose_loader: boolean;
name?: string;
version?: string;
runtimes?: Array<Runtime>;
};
let loader: PyLoader | undefined;
globalLoader.subscribe(value => {
loader = value;
});
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('scripts queue set');
});
let appConfig_: AppConfig = {
autoclose_loader: true,
};
appConfig.subscribe((value: AppConfig) => {
if (value) {
appConfig_ = value;
}
console.log('config set!');
});
/*
Runtime class is a super class that all different runtimes must respect
and adhere to.
Currently, the only runtime available is Pyodide as indicated by the
`RuntimeInterpreter` type above. This serves as a Union of types of
different runtimes/interpreters which will be added in near future.
The class has abstract methods available which each runtime is supposed
to implement.
Methods available handle loading of the interpreter, initialization,
running code, loading and installation of packages, loading from files etc.
For an example implementation, refer to the `PyodideRuntime` class
in `pyodide.ts`
*/
export abstract class Runtime extends Object {
abstract src: string;
abstract name?: string;
abstract lang?: string;
abstract interpreter: RuntimeInterpreter;
/**
* global symbols table for the underlying interpreter.
* */
abstract globals: any;
/**
* loads the interpreter for the runtime and saves an instance of it
* in the `this.interpreter` property along with calling of other
* additional convenience functions.
* */
abstract loadInterpreter(): Promise<void>;
/**
* delegates the code to be run to the underlying interpreter
* (asynchronously) which can call its own API behind the scenes.
* */
abstract run(code: string): Promise<any>;
/**
* delegates the setting of JS objects to
* the underlying interpreter.
* */
abstract registerJsModule(name: string, module: object): void;
/**
* delegates the loading of packages to
* the underlying interpreter.
* */
abstract loadPackage(names: string | string[]): Promise<void>;
/**
* delegates the installation of packages
* (using a package manager, which can be specific to
* the runtime) to the underlying interpreter.
*
* For Pyodide, we use `micropip`
* */
abstract installPackage(package_name: string | string[]): Promise<void>;
/**
* delegates the loading of files to the
* underlying interpreter.
* */
abstract loadFromFile(path: string): Promise<void>;
/**
* initializes the page which involves loading of runtime,
* as well as evaluating all the code inside <py-script> tags
* along with initializers and postInitializers
* */
async initialize(): Promise<void> {
loader?.log('Loading runtime...');
await this.loadInterpreter();
const newEnv = {
id: 'default',
runtime: this,
state: 'loading',
};
runtimeLoaded.set(this);
// Inject the loader into the runtime namespace
// eslint-disable-next-line
this.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===');
}
}

View File

@@ -1,17 +1,21 @@
import { writable } from 'svelte/store';
import type { PyLoader } from './components/pyloader';
import type { PyScript } from './components/pyscript';
import type { PyodideInterface } from './pyodide';
import type { Runtime } from './runtime';
export type Initializer = () => Promise<void>;
export type Environment = {
id: string;
runtime: PyodideInterface;
runtime: Runtime;
state: string;
};
export const pyodideLoaded = writable<PyodideInterface>();
/*
A store for Runtime which can encompass any
runtime, but currently only has Pyodide as its offering.
*/
export const runtimeLoaded = writable<Runtime>();
export const loadedEnvironments = writable<Record<Environment['id'], Environment>>({});

View File

@@ -84,4 +84,11 @@ function handleFetchError(e: Error, singleFile: string) {
showError(errorContent);
}
export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError };
/**
* determines if the process is running inside the testing suite i.e. jest
*/
function inJest(): boolean {
return process.env.JEST_WORKER_ID !== undefined;
}
export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, inJest };

View File

@@ -0,0 +1,34 @@
import { Runtime } from '../../src/runtime';
import { PyodideRuntime } from '../../src/pyodide';
import { TextEncoder, TextDecoder } from 'util'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
describe('PyodideRuntime', () => {
let runtime: PyodideRuntime;
beforeAll(async () => {
runtime = new PyodideRuntime();
await runtime.initialize();
});
it('should check if runtime is an instance of abstract Runtime', async () => {
expect(runtime).toBeInstanceOf(Runtime);
});
it('should check if runtime is an instance of PyodideRuntime', async () => {
expect(runtime).toBeInstanceOf(PyodideRuntime);
});
it('should check if runtime can run python code asynchronously', async () => {
expect(await runtime.run("2+3")).toBe(5);
});
it('should check if runtime is able to load a package', async () => {
await runtime.loadPackage("numpy");
await runtime.run("import numpy as np");
await runtime.run("x = np.ones((10,))");
expect(runtime.globals.get('x').toJs()).toBeInstanceOf(Float64Array);
});
});

View File

@@ -7,7 +7,7 @@
"exclude": ["node_modules/*", "__sapper__/*", "public/*"],
"compilerOptions": {
"moduleResolution": "node",
"target": "es2017",
"target": "esnext",
"module": "esnext",
/**
Svelte Preprocess cannot figure out whether you have a value or a type, so tell TypeScript
@@ -21,7 +21,7 @@
*/
"sourceMap": true,
/** Requests the runtime types from the svelte modules by default. Needed for TS files or else you get errors. */
"types": ["svelte", "jest"],
"types": ["svelte", "jest", "node"],
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,