mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
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:
@@ -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
|
||||
|
||||
12
pyscriptjs/__mocks__/fileMock.js
Normal file
12
pyscriptjs/__mocks__/fileMock.js
Normal 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 = "";
|
||||
@@ -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",
|
||||
}
|
||||
};
|
||||
|
||||
55
pyscriptjs/package-lock.json
generated
55
pyscriptjs/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
@@ -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
175
pyscriptjs/src/runtime.ts
Normal 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===');
|
||||
}
|
||||
}
|
||||
@@ -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>>({});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
34
pyscriptjs/tests/unit/runtime.test.ts
Normal file
34
pyscriptjs/tests/unit/runtime.test.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user