mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Rename runtimes with interpreter (#1082)
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { Runtime } from '../runtime';
|
||||
import type { Interpreter } from '../interpreter';
|
||||
import { make_PyRepl } from './pyrepl';
|
||||
import { make_PyWidget } from './pywidget';
|
||||
|
||||
function createCustomElements(runtime: Runtime) {
|
||||
const PyWidget = make_PyWidget(runtime);
|
||||
const PyRepl = make_PyRepl(runtime);
|
||||
function createCustomElements(interpreter: Interpreter) {
|
||||
const PyWidget = make_PyWidget(interpreter);
|
||||
const PyRepl = make_PyRepl(interpreter);
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const xPyRepl = customElements.define('py-repl', PyRepl);
|
||||
|
||||
@@ -7,14 +7,14 @@ import { defaultKeymap } from '@codemirror/commands';
|
||||
import { oneDarkTheme } from '@codemirror/theme-one-dark';
|
||||
|
||||
import { getAttribute, ensureUniqueId, htmlDecode } from '../utils';
|
||||
import type { Runtime } from '../runtime';
|
||||
import type { Interpreter } from '../interpreter';
|
||||
import { pyExec, pyDisplay } from '../pyexec';
|
||||
import { getLogger } from '../logger';
|
||||
|
||||
const logger = getLogger('py-repl');
|
||||
const RUNBUTTON = `<svg 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>`;
|
||||
|
||||
export function make_PyRepl(runtime: Runtime) {
|
||||
export function make_PyRepl(interpreter: Interpreter) {
|
||||
/* High level structure of py-repl DOM, and the corresponding JS names.
|
||||
|
||||
this <py-repl>
|
||||
@@ -166,11 +166,11 @@ export function make_PyRepl(runtime: Runtime) {
|
||||
outEl.innerHTML = '';
|
||||
|
||||
// execute the python code
|
||||
const pyResult = pyExec(runtime, pySrc, outEl);
|
||||
const pyResult = pyExec(interpreter, pySrc, outEl);
|
||||
|
||||
// display the value of the last evaluated expression (REPL-style)
|
||||
if (pyResult !== undefined) {
|
||||
pyDisplay(runtime, pyResult, { target: outEl.id });
|
||||
pyDisplay(interpreter, pyResult, { target: outEl.id });
|
||||
}
|
||||
|
||||
this.autogenerateMaybe();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { htmlDecode, ensureUniqueId, createDeprecationWarning } from '../utils';
|
||||
import type { Runtime } from '../runtime';
|
||||
import type { Interpreter } from '../interpreter';
|
||||
import { getLogger } from '../logger';
|
||||
import { pyExec } from '../pyexec';
|
||||
import { _createAlertBanner } from '../exceptions';
|
||||
@@ -9,11 +9,11 @@ import { Stdio } from '../stdio';
|
||||
|
||||
const logger = getLogger('py-script');
|
||||
|
||||
export function make_PyScript(runtime: Runtime, app: PyScriptApp) {
|
||||
export function make_PyScript(interpreter: Interpreter, app: PyScriptApp) {
|
||||
class PyScript extends HTMLElement {
|
||||
srcCode: string
|
||||
stdout_manager: Stdio | null
|
||||
stderr_manager: Stdio | null
|
||||
srcCode: string;
|
||||
stdout_manager: Stdio | null;
|
||||
stderr_manager: Stdio | null;
|
||||
|
||||
async connectedCallback() {
|
||||
ensureUniqueId(this);
|
||||
@@ -23,9 +23,10 @@ export function make_PyScript(runtime: Runtime, app: PyScriptApp) {
|
||||
this.srcCode = this.innerHTML;
|
||||
const pySrc = await this.getPySrc();
|
||||
this.innerHTML = '';
|
||||
app.plugins.beforePyScriptExec(runtime, pySrc, this);
|
||||
const result = pyExec(runtime, pySrc, this);
|
||||
app.plugins.afterPyScriptExec(runtime, pySrc, this, result);
|
||||
|
||||
app.plugins.beforePyScriptExec(interpreter, pySrc, this);
|
||||
const result = pyExec(interpreter, pySrc, this);
|
||||
app.plugins.afterPyScriptExec(interpreter, pySrc, this, result);
|
||||
}
|
||||
|
||||
async getPySrc(): Promise<string> {
|
||||
@@ -144,15 +145,15 @@ const pyAttributeToEvent: Map<string, string> = new Map<string, string>([
|
||||
]);
|
||||
|
||||
/** Initialize all elements with py-* handlers attributes */
|
||||
export function initHandlers(runtime: Runtime) {
|
||||
export function initHandlers(interpreter: Interpreter) {
|
||||
logger.debug('Initializing py-* event handlers...');
|
||||
for (const pyAttribute of pyAttributeToEvent.keys()) {
|
||||
createElementsWithEventListeners(runtime, pyAttribute);
|
||||
createElementsWithEventListeners(interpreter, pyAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes an element with the given py-on* attribute and its handler */
|
||||
function createElementsWithEventListeners(runtime: Runtime, pyAttribute: string) {
|
||||
function createElementsWithEventListeners(interpreter: Interpreter, pyAttribute: string) {
|
||||
const matches: NodeListOf<HTMLElement> = document.querySelectorAll(`[${pyAttribute}]`);
|
||||
for (const el of matches) {
|
||||
if (el.id.length === 0) {
|
||||
@@ -177,13 +178,13 @@ function createElementsWithEventListeners(runtime: Runtime, pyAttribute: string)
|
||||
// the source code may contain a syntax error, which will cause
|
||||
// the splashscreen to not be removed.
|
||||
try {
|
||||
runtime.run(source);
|
||||
interpreter.run(source);
|
||||
} catch (e) {
|
||||
logger.error((e as Error).message);
|
||||
}
|
||||
} else {
|
||||
el.addEventListener(event, () => {
|
||||
runtime.run(handlerCode);
|
||||
interpreter.run(handlerCode);
|
||||
});
|
||||
}
|
||||
// TODO: Should we actually map handlers in JS instead of Python?
|
||||
@@ -203,7 +204,7 @@ function createElementsWithEventListeners(runtime: Runtime, pyAttribute: string)
|
||||
}
|
||||
|
||||
/** Mount all elements with attribute py-mount into the Python namespace */
|
||||
export function mountElements(runtime: Runtime) {
|
||||
export function mountElements(interpreter: Interpreter) {
|
||||
const matches: NodeListOf<HTMLElement> = document.querySelectorAll('[py-mount]');
|
||||
logger.info(`py-mount: found ${matches.length} elements`);
|
||||
|
||||
@@ -212,5 +213,5 @@ export function mountElements(runtime: Runtime) {
|
||||
const mountName = el.getAttribute('py-mount') || el.id.split('-').join('_');
|
||||
source += `\n${mountName} = Element("${el.id}")`;
|
||||
}
|
||||
runtime.run(source);
|
||||
interpreter.run(source);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Runtime } from '../runtime';
|
||||
import type { Interpreter } from '../interpreter';
|
||||
import type { PyProxy } from 'pyodide';
|
||||
import { getLogger } from '../logger';
|
||||
import { robustFetch } from '../fetch';
|
||||
|
||||
const logger = getLogger('py-register-widget');
|
||||
|
||||
function createWidget(runtime: Runtime, name: string, code: string, klass: string) {
|
||||
function createWidget(interpreter: Interpreter, name: string, code: string, klass: string) {
|
||||
class CustomWidget extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
wrapper: HTMLElement;
|
||||
@@ -27,8 +27,8 @@ function createWidget(runtime: Runtime, name: string, code: string, klass: strin
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
runtime.runButDontRaise(this.code);
|
||||
this.proxyClass = runtime.globals.get(this.klass);
|
||||
interpreter.runButDontRaise(this.code);
|
||||
this.proxyClass = interpreter.globals.get(this.klass);
|
||||
this.proxy = this.proxyClass(this);
|
||||
this.proxy.connect();
|
||||
this.registerWidget();
|
||||
@@ -36,7 +36,7 @@ function createWidget(runtime: Runtime, name: string, code: string, klass: strin
|
||||
|
||||
registerWidget() {
|
||||
logger.info('new widget registered:', this.name);
|
||||
runtime.globals.set(this.id, this.proxy);
|
||||
interpreter.globals.set(this.id, this.proxy);
|
||||
}
|
||||
}
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
@@ -44,7 +44,7 @@ function createWidget(runtime: Runtime, name: string, code: string, klass: strin
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
}
|
||||
|
||||
export function make_PyWidget(runtime: Runtime) {
|
||||
export function make_PyWidget(interpreter: Interpreter) {
|
||||
class PyWidget extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
name: string;
|
||||
@@ -89,7 +89,7 @@ export function make_PyWidget(runtime: Runtime) {
|
||||
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);
|
||||
createWidget(interpreter, this.name, this.code, this.klass);
|
||||
}
|
||||
|
||||
async getSourceFromFile(s: string): Promise<string> {
|
||||
|
||||
@@ -2,35 +2,35 @@ import type { AppConfig } from './pyconfig';
|
||||
import type { PyodideInterface, PyProxy } from 'pyodide';
|
||||
import { getLogger } from './logger';
|
||||
|
||||
const logger = getLogger('pyscript/runtime');
|
||||
const logger = getLogger('pyscript/interpreter');
|
||||
|
||||
export type RuntimeInterpreter = PyodideInterface | null;
|
||||
export type InterpreterInterface = PyodideInterface | null;
|
||||
|
||||
/*
|
||||
Runtime class is a super class that all different runtimes must respect
|
||||
Interpreter class is a super class that all different interpreters 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.
|
||||
Currently, the only interpreter available is Pyodide as indicated by the
|
||||
`InterpreterInterface` type above. This serves as a Union of types of
|
||||
different interpreters which will be added in near future.
|
||||
|
||||
The class has abstract methods available which each runtime is supposed
|
||||
The class has abstract methods available which each interpreter 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
|
||||
For an example implementation, refer to the `PyodideInterpreter` class
|
||||
in `pyodide.ts`
|
||||
*/
|
||||
export abstract class Runtime extends Object {
|
||||
export abstract class Interpreter extends Object {
|
||||
config: AppConfig;
|
||||
abstract src: string;
|
||||
abstract name?: string;
|
||||
abstract lang?: string;
|
||||
abstract interpreter: RuntimeInterpreter;
|
||||
abstract interface: InterpreterInterface;
|
||||
/**
|
||||
* global symbols table for the underlying interpreter.
|
||||
* global symbols table for the underlying interface.
|
||||
* */
|
||||
abstract globals: PyProxy;
|
||||
|
||||
@@ -40,14 +40,14 @@ export abstract class Runtime extends Object {
|
||||
}
|
||||
|
||||
/**
|
||||
* loads the interpreter for the runtime and saves an instance of it
|
||||
* in the `this.interpreter` property along with calling of other
|
||||
* loads the interface for the interpreter and saves an instance of it
|
||||
* in the `this.interface` property along with calling of other
|
||||
* additional convenience functions.
|
||||
* */
|
||||
abstract loadInterpreter(): Promise<void>;
|
||||
|
||||
/**
|
||||
* delegates the code to be run to the underlying interpreter
|
||||
* delegates the code to be run to the underlying interface
|
||||
* (asynchronously) which can call its own API behind the scenes.
|
||||
* Python exceptions are turned into JS exceptions.
|
||||
* */
|
||||
@@ -72,20 +72,20 @@ export abstract class Runtime extends Object {
|
||||
|
||||
/**
|
||||
* delegates the setting of JS objects to
|
||||
* the underlying interpreter.
|
||||
* the underlying interface.
|
||||
* */
|
||||
abstract registerJsModule(name: string, module: object): void;
|
||||
|
||||
/**
|
||||
* delegates the loading of packages to
|
||||
* the underlying interpreter.
|
||||
* the underlying interface.
|
||||
* */
|
||||
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.
|
||||
* the interface) to the underlying interface.
|
||||
*
|
||||
* For Pyodide, we use `micropip`
|
||||
* */
|
||||
@@ -93,13 +93,13 @@ export abstract class Runtime extends Object {
|
||||
|
||||
/**
|
||||
* delegates the loading of files to the
|
||||
* underlying interpreter.
|
||||
* underlying interface.
|
||||
* */
|
||||
abstract loadFromFile(path: string, fetch_path: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* delegates clearing importlib's module path
|
||||
* caches to the underlying interpreter
|
||||
* caches to the underlying interface
|
||||
*/
|
||||
abstract invalidate_module_path_cache(): void;
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import './styles/pyscript_base.css';
|
||||
|
||||
import { loadConfigFromElement } from './pyconfig';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { Runtime } from './runtime';
|
||||
import type { Interpreter } from './interpreter';
|
||||
import { version } from './version';
|
||||
import { PluginManager, define_custom_element } from './plugin';
|
||||
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
import { PyodideRuntime } from './pyodide';
|
||||
import { PyodideInterpreter } from './pyodide';
|
||||
import { getLogger } from './logger';
|
||||
import { showWarning, globalExport } from './utils';
|
||||
import { calculatePaths } from './plugins/fetch';
|
||||
@@ -33,7 +33,7 @@ const logger = getLogger('pyscript/main');
|
||||
|
||||
3. (it used to be "show the splashscreen", but now it's a plugin)
|
||||
|
||||
4. loadRuntime(): start downloading the actual runtime (e.g. pyodide.js)
|
||||
4. loadInterpreter(): start downloading the actual interpreter (e.g. pyodide.js)
|
||||
|
||||
--- wait until (4) has finished ---
|
||||
|
||||
@@ -52,16 +52,16 @@ More concretely:
|
||||
|
||||
- Points 1-4 are implemented sequentially in PyScriptApp.main().
|
||||
|
||||
- PyScriptApp.loadRuntime adds a <script> tag to the document to initiate
|
||||
- PyScriptApp.loadInterpreter adds a <script> tag to the document to initiate
|
||||
the download, and then adds an event listener for the 'load' event, which
|
||||
in turns calls PyScriptApp.afterRuntimeLoad().
|
||||
in turns calls PyScriptApp.afterInterpreterLoad().
|
||||
|
||||
- PyScriptApp.afterRuntimeLoad() implements all the points >= 5.
|
||||
- PyScriptApp.afterInterpreterLoad() implements all the points >= 5.
|
||||
*/
|
||||
|
||||
export class PyScriptApp {
|
||||
config: AppConfig;
|
||||
runtime: Runtime;
|
||||
interpreter: Interpreter;
|
||||
PyScript: ReturnType<typeof make_PyScript>;
|
||||
plugins: PluginManager;
|
||||
_stdioMultiplexer: StdioMultiplexer;
|
||||
@@ -74,7 +74,7 @@ export class PyScriptApp {
|
||||
this._stdioMultiplexer = new StdioMultiplexer();
|
||||
this._stdioMultiplexer.addListener(DEFAULT_STDIO);
|
||||
|
||||
this.plugins.add(new StdioDirector(this._stdioMultiplexer))
|
||||
this.plugins.add(new StdioDirector(this._stdioMultiplexer));
|
||||
}
|
||||
|
||||
// Error handling logic: if during the execution we encounter an error
|
||||
@@ -106,7 +106,7 @@ export class PyScriptApp {
|
||||
this.loadConfig();
|
||||
this.plugins.configure(this.config);
|
||||
this.plugins.beforeLaunch(this.config);
|
||||
this.loadRuntime();
|
||||
this.loadInterpreter();
|
||||
}
|
||||
|
||||
// lifecycle (2)
|
||||
@@ -130,24 +130,25 @@ export class PyScriptApp {
|
||||
}
|
||||
|
||||
// lifecycle (4)
|
||||
loadRuntime() {
|
||||
logger.info('Initializing runtime');
|
||||
if (this.config.runtimes.length == 0) {
|
||||
throw new UserError(ErrorCode.BAD_CONFIG, 'Fatal error: config.runtimes is empty');
|
||||
loadInterpreter() {
|
||||
logger.info('Initializing interpreter');
|
||||
if (this.config.interpreters.length == 0) {
|
||||
throw new UserError(ErrorCode.BAD_CONFIG, 'Fatal error: config.interpreter is empty');
|
||||
}
|
||||
|
||||
if (this.config.runtimes.length > 1) {
|
||||
showWarning('Multiple runtimes are not supported yet.<br />Only the first will be used', 'html');
|
||||
if (this.config.interpreters.length > 1) {
|
||||
showWarning('Multiple interpreters are not supported yet.<br />Only the first will be used', 'html');
|
||||
}
|
||||
const runtime_cfg = this.config.runtimes[0];
|
||||
this.runtime = new PyodideRuntime(
|
||||
|
||||
const interpreter_cfg = this.config.interpreters[0];
|
||||
this.interpreter = new PyodideInterpreter(
|
||||
this.config,
|
||||
this._stdioMultiplexer,
|
||||
runtime_cfg.src,
|
||||
runtime_cfg.name,
|
||||
runtime_cfg.lang,
|
||||
interpreter_cfg.src,
|
||||
interpreter_cfg.name,
|
||||
interpreter_cfg.lang,
|
||||
);
|
||||
this.logStatus(`Downloading ${runtime_cfg.name}...`);
|
||||
this.logStatus(`Downloading ${interpreter_cfg.name}...`);
|
||||
|
||||
// download pyodide by using a <script> tag. Once it's ready, the
|
||||
// "load" event will be fired and the exeuction logic will continue.
|
||||
@@ -156,9 +157,9 @@ export class PyScriptApp {
|
||||
// by the try/catch inside main(): that's why we need to .catch() it
|
||||
// explicitly and call _handleUserErrorMaybe also there.
|
||||
const script = document.createElement('script'); // create a script DOM node
|
||||
script.src = this.runtime.src;
|
||||
script.src = this.interpreter.src;
|
||||
script.addEventListener('load', () => {
|
||||
this.afterRuntimeLoad(this.runtime).catch(error => {
|
||||
this.afterInterpreterLoad(this.interpreter).catch(error => {
|
||||
this._handleUserErrorMaybe(error);
|
||||
});
|
||||
});
|
||||
@@ -170,61 +171,61 @@ export class PyScriptApp {
|
||||
// point (4) to point (5).
|
||||
//
|
||||
// Invariant: this.config is set and available.
|
||||
async afterRuntimeLoad(runtime: Runtime): Promise<void> {
|
||||
async afterInterpreterLoad(interpreter: Interpreter): Promise<void> {
|
||||
console.assert(this.config !== undefined);
|
||||
|
||||
this.logStatus('Python startup...');
|
||||
await runtime.loadInterpreter();
|
||||
await this.interpreter.loadInterpreter();
|
||||
this.logStatus('Python ready!');
|
||||
|
||||
this.logStatus('Setting up virtual environment...');
|
||||
await this.setupVirtualEnv(runtime);
|
||||
mountElements(runtime);
|
||||
await this.setupVirtualEnv(interpreter);
|
||||
mountElements(interpreter);
|
||||
|
||||
// lifecycle (6.5)
|
||||
this.plugins.afterSetup(runtime);
|
||||
this.plugins.afterSetup(interpreter);
|
||||
|
||||
//Refresh module cache in case plugins have modified the filesystem
|
||||
runtime.invalidate_module_path_cache();
|
||||
interpreter.invalidate_module_path_cache();
|
||||
this.logStatus('Executing <py-script> tags...');
|
||||
this.executeScripts(runtime);
|
||||
this.executeScripts(interpreter);
|
||||
|
||||
this.logStatus('Initializing web components...');
|
||||
// lifecycle (8)
|
||||
createCustomElements(runtime);
|
||||
createCustomElements(interpreter);
|
||||
|
||||
initHandlers(runtime);
|
||||
initHandlers(interpreter);
|
||||
|
||||
// NOTE: runtime message is used by integration tests to know that
|
||||
// NOTE: interpreter message is used by integration tests to know that
|
||||
// pyscript initialization has complete. If you change it, you need to
|
||||
// change it also in tests/integration/support.py
|
||||
this.logStatus('Startup complete');
|
||||
this.plugins.afterStartup(runtime);
|
||||
this.plugins.afterStartup(interpreter);
|
||||
logger.info('PyScript page fully initialized');
|
||||
}
|
||||
|
||||
// lifecycle (6)
|
||||
async setupVirtualEnv(runtime: Runtime): Promise<void> {
|
||||
async setupVirtualEnv(interpreter: Interpreter): Promise<void> {
|
||||
// XXX: maybe the following calls could be parallelized, instead of
|
||||
// await()ing immediately. For now I'm using await to be 100%
|
||||
// compatible with the old behavior.
|
||||
logger.info('importing pyscript');
|
||||
|
||||
// Save and load pyscript.py from FS
|
||||
runtime.interpreter.FS.writeFile('pyscript.py', pyscript, { encoding: 'utf8' });
|
||||
interpreter.interface.FS.writeFile('pyscript.py', pyscript, { encoding: 'utf8' });
|
||||
//Refresh the module cache so Python consistently finds pyscript module
|
||||
runtime.invalidate_module_path_cache();
|
||||
interpreter.invalidate_module_path_cache();
|
||||
|
||||
// inject `define_custom_element` and showWarning it into the PyScript
|
||||
// module scope
|
||||
const pyscript_module = runtime.interpreter.pyimport('pyscript');
|
||||
const pyscript_module = interpreter.interface.pyimport('pyscript');
|
||||
pyscript_module.define_custom_element = define_custom_element;
|
||||
pyscript_module.showWarning = showWarning;
|
||||
pyscript_module._set_version_info(version);
|
||||
pyscript_module.destroy();
|
||||
|
||||
// import some carefully selected names into the global namespace
|
||||
await runtime.run(`
|
||||
await interpreter.run(`
|
||||
import js
|
||||
import pyscript
|
||||
from pyscript import Element, display, HTML
|
||||
@@ -233,20 +234,20 @@ export class PyScriptApp {
|
||||
|
||||
if (this.config.packages) {
|
||||
logger.info('Packages to install: ', this.config.packages);
|
||||
await runtime.installPackage(this.config.packages);
|
||||
await interpreter.installPackage(this.config.packages);
|
||||
}
|
||||
await this.fetchPaths(runtime);
|
||||
await this.fetchPaths(interpreter);
|
||||
|
||||
//This may be unnecessary - only useful if plugins try to import files fetch'd in fetchPaths()
|
||||
runtime.invalidate_module_path_cache();
|
||||
interpreter.invalidate_module_path_cache();
|
||||
// Finally load plugins
|
||||
await this.fetchUserPlugins(runtime);
|
||||
await this.fetchUserPlugins(interpreter);
|
||||
}
|
||||
|
||||
async fetchPaths(runtime: Runtime) {
|
||||
async fetchPaths(interpreter: Interpreter) {
|
||||
// XXX this can be VASTLY improved: for each path we need to fetch a
|
||||
// URL and write to the virtual filesystem: pyodide.loadFromFile does
|
||||
// it in Python, which means we need to have the runtime
|
||||
// it in Python, which means we need to have the interpreter
|
||||
// initialized. But we could easily do it in JS in parallel with the
|
||||
// download/startup of pyodide.
|
||||
const [paths, fetchPaths] = calculatePaths(this.config.fetch);
|
||||
@@ -255,7 +256,7 @@ export class PyScriptApp {
|
||||
logger.info(` fetching path: ${fetchPaths[i]}`);
|
||||
|
||||
// Exceptions raised from here will create an alert banner
|
||||
await runtime.loadFromFile(paths[i], fetchPaths[i]);
|
||||
await interpreter.loadFromFile(paths[i], fetchPaths[i]);
|
||||
}
|
||||
logger.info('All paths fetched');
|
||||
}
|
||||
@@ -265,15 +266,15 @@ export class PyScriptApp {
|
||||
* be loaded by the PluginManager. Currently, we are just looking
|
||||
* for .py and .js files and calling the appropriate methods.
|
||||
*
|
||||
* @param runtime - runtime that will be used to execute the plugins that need it.
|
||||
* @param interpreter - the interpreter that will be used to execute the plugins that need it.
|
||||
*/
|
||||
async fetchUserPlugins(runtime: Runtime) {
|
||||
async fetchUserPlugins(interpreter: Interpreter) {
|
||||
const plugins = this.config.plugins;
|
||||
logger.info('Plugins to fetch: ', plugins);
|
||||
for (const singleFile of plugins) {
|
||||
logger.info(` fetching plugins: ${singleFile}`);
|
||||
if (singleFile.endsWith('.py')) {
|
||||
await this.fetchPythonPlugin(runtime, singleFile);
|
||||
await this.fetchPythonPlugin(interpreter, singleFile);
|
||||
} else if (singleFile.endsWith('.js')) {
|
||||
await this.fetchJSPlugin(singleFile);
|
||||
} else {
|
||||
@@ -327,26 +328,26 @@ export class PyScriptApp {
|
||||
* Fetch python plugins from a filePath, saves it on the FS and import
|
||||
* it as a module, executing any plugin define the module scope.
|
||||
*
|
||||
* @param runtime - runtime that will execute the plugins
|
||||
* @param interpreter - the interpreter that will execute the plugins
|
||||
* @param filePath - path to the python file to fetch
|
||||
*/
|
||||
async fetchPythonPlugin(runtime: Runtime, filePath: string) {
|
||||
async fetchPythonPlugin(interpreter: Interpreter, filePath: string) {
|
||||
const pathArr = filePath.split('/');
|
||||
const filename = pathArr.pop();
|
||||
// TODO: Would be probably be better to store plugins somewhere like /plugins/python/ or similar
|
||||
const destPath = `./${filename}`;
|
||||
await runtime.loadFromFile(destPath, filePath);
|
||||
await interpreter.loadFromFile(destPath, filePath);
|
||||
|
||||
//refresh module cache before trying to import module files into runtime
|
||||
runtime.invalidate_module_path_cache();
|
||||
//refresh module cache before trying to import module files into interpreter
|
||||
interpreter.invalidate_module_path_cache();
|
||||
|
||||
const modulename = filePath.replace(/^.*[\\/]/, '').replace('.py', '');
|
||||
|
||||
console.log(`importing ${modulename}`);
|
||||
// TODO: This is very specific to Pyodide API and will not work for other interpreters,
|
||||
// when we add support for other interpreters we will need to move this to the
|
||||
// runtime (interpreter) API level and allow each one to implement it in its own way
|
||||
const module = runtime.interpreter.pyimport(modulename);
|
||||
// interpreter API level and allow each one to implement it in its own way
|
||||
const module = interpreter.interface.pyimport(modulename);
|
||||
if (typeof module.plugin !== 'undefined') {
|
||||
const py_plugin = module.plugin;
|
||||
py_plugin.init(this);
|
||||
@@ -358,9 +359,9 @@ modules must contain a "plugin" attribute. For more information check the plugin
|
||||
}
|
||||
|
||||
// lifecycle (7)
|
||||
executeScripts(runtime: Runtime) {
|
||||
// make_PyScript takes a runtime and a PyScriptApp as arguments
|
||||
this.PyScript = make_PyScript(runtime, this);
|
||||
executeScripts(interpreter: Interpreter) {
|
||||
// make_PyScript takes an interpreter and a PyScriptApp as arguments
|
||||
this.PyScript = make_PyScript(interpreter, this);
|
||||
customElements.define('py-script', this.PyScript);
|
||||
}
|
||||
|
||||
@@ -387,4 +388,7 @@ const globalApp = new PyScriptApp();
|
||||
globalApp.main();
|
||||
|
||||
export { version };
|
||||
export const runtime = globalApp.runtime;
|
||||
export const interpreter = globalApp.interpreter;
|
||||
// TODO: This is for backwards compatibility, it should be removed
|
||||
// when we finish the deprecation cycle of `runtime`
|
||||
export const runtime = globalApp.interpreter;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { Runtime } from './runtime';
|
||||
import type { Interpreter } from './interpreter';
|
||||
import type { UserError } from './exceptions';
|
||||
import { getLogger } from './logger';
|
||||
|
||||
@@ -37,32 +37,32 @@ export class Plugin {
|
||||
*
|
||||
* The <py-script> tags will be executed after this hook.
|
||||
*/
|
||||
afterSetup(runtime: Runtime) {}
|
||||
afterSetup(interpreter: Interpreter) {}
|
||||
|
||||
/** The source of a <py-script>> tag has been fetched, and we're about
|
||||
* to evaluate that source using the provided runtime.
|
||||
* to evaluate that source using the provided interpreter.
|
||||
*
|
||||
* @param runtime The Runtime object that will be used to evaluated the Python source code
|
||||
* @param interpreter The Interpreter object that will be used to evaluated the Python source code
|
||||
* @param src {string} The Python source code to be evaluated
|
||||
* @param PyScriptTag The <py-script> HTML tag that originated the evaluation
|
||||
*/
|
||||
beforePyScriptExec(runtime, src, PyScriptTag) {}
|
||||
beforePyScriptExec(interpreter: Interpreter, src: string, PyScriptTag: HTMLElement) {}
|
||||
|
||||
/** The Python in a <py-script> has just been evaluated, but control
|
||||
* has not been ceded back to the JavaScript event loop yet
|
||||
*
|
||||
* @param runtime The Runtime object that will be used to evaluated the Python source code
|
||||
* @param interpreter The Interpreter object that will be used to evaluated the Python source code
|
||||
* @param src {string} The Python source code to be evaluated
|
||||
* @param PyScriptTag The <py-script> HTML tag that originated the evaluation
|
||||
* @param result The returned result of evaluating the Python (if any)
|
||||
*/
|
||||
afterPyScriptExec(runtime, src, PyScriptTag, result) {}
|
||||
afterPyScriptExec(interpreter: Interpreter, src: string, PyScriptTag: HTMLElement, result) {}
|
||||
|
||||
/** Startup complete. The interpreter is initialized and ready, user
|
||||
* scripts have been executed: the main initialization logic ends here and
|
||||
* the page is ready to accept user interactions.
|
||||
*/
|
||||
afterStartup(runtime: Runtime) {}
|
||||
afterStartup(interpreter: Interpreter) {}
|
||||
|
||||
/** Called when an UserError is raised
|
||||
*/
|
||||
@@ -102,34 +102,34 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
afterSetup(runtime: Runtime) {
|
||||
afterSetup(interpreter: Interpreter) {
|
||||
for (const p of this._plugins) {
|
||||
try {
|
||||
p.afterSetup(runtime);
|
||||
p.afterSetup(interpreter);
|
||||
} catch (e) {
|
||||
logger.error(`Error while calling afterSetup hook of plugin ${p.constructor.name}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of this._pythonPlugins) p.afterSetup?.(runtime);
|
||||
for (const p of this._pythonPlugins) p.afterSetup?.(interpreter);
|
||||
}
|
||||
|
||||
afterStartup(runtime: Runtime) {
|
||||
for (const p of this._plugins) p.afterStartup(runtime);
|
||||
afterStartup(interpreter: Interpreter) {
|
||||
for (const p of this._plugins) p.afterStartup(interpreter);
|
||||
|
||||
for (const p of this._pythonPlugins) p.afterStartup?.(runtime);
|
||||
for (const p of this._pythonPlugins) p.afterStartup?.(interpreter);
|
||||
}
|
||||
|
||||
beforePyScriptExec(runtime, src, pyscriptTag) {
|
||||
for (const p of this._plugins) p.beforePyScriptExec(runtime, src, pyscriptTag);
|
||||
beforePyScriptExec(interpreter: Interpreter, src: string, pyscriptTag: HTMLElement) {
|
||||
for (const p of this._plugins) p.beforePyScriptExec(interpreter, src, pyscriptTag);
|
||||
|
||||
for (const p of this._pythonPlugins) p.beforePyScriptExec?.(runtime, src, pyscriptTag);
|
||||
for (const p of this._pythonPlugins) p.beforePyScriptExec?.(interpreter, src, pyscriptTag);
|
||||
}
|
||||
|
||||
afterPyScriptExec(runtime: Runtime, src, pyscriptTag, result) {
|
||||
for (const p of this._plugins) p.afterPyScriptExec(runtime, src, pyscriptTag, result);
|
||||
afterPyScriptExec(interpreter: Interpreter, src: string, pyscriptTag: HTMLElement, result) {
|
||||
for (const p of this._plugins) p.afterPyScriptExec(interpreter, src, pyscriptTag, result);
|
||||
|
||||
for (const p of this._pythonPlugins) p.afterPyScriptExec?.(runtime, src, pyscriptTag, result);
|
||||
for (const p of this._pythonPlugins) p.afterPyScriptExec?.(interpreter, src, pyscriptTag, result);
|
||||
}
|
||||
|
||||
onUserError(error: UserError) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Runtime } from '../runtime';
|
||||
import type { Interpreter } from '../interpreter';
|
||||
import { showWarning } from '../utils';
|
||||
import { Plugin } from '../plugin';
|
||||
import { getLogger } from '../logger';
|
||||
@@ -11,7 +11,7 @@ type ImportMapType = {
|
||||
};
|
||||
|
||||
export class ImportmapPlugin extends Plugin {
|
||||
async afterSetup(runtime: Runtime) {
|
||||
async afterSetup(interpreter: Interpreter) {
|
||||
// make importmap ES modules available from python using 'import'.
|
||||
//
|
||||
// XXX: this code can probably be improved because errors are silently
|
||||
@@ -46,7 +46,7 @@ export class ImportmapPlugin extends Plugin {
|
||||
}
|
||||
|
||||
logger.info('Registering JS module', name);
|
||||
runtime.registerJsModule(name, exports);
|
||||
interpreter.registerJsModule(name, exports);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { PyScriptApp } from '../main';
|
||||
import type { AppConfig } from '../pyconfig';
|
||||
import type { Runtime } from '../runtime';
|
||||
import type { Interpreter } from '../interpreter';
|
||||
import { Plugin } from '../plugin';
|
||||
import { UserError, ErrorCode } from "../exceptions"
|
||||
import { UserError, ErrorCode } from '../exceptions';
|
||||
import { getLogger } from '../logger';
|
||||
import { type Stdio } from '../stdio';
|
||||
|
||||
@@ -23,8 +23,8 @@ export class PyTerminalPlugin extends Plugin {
|
||||
const got = JSON.stringify(t);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
'Invalid value for config.terminal: the only accepted' +
|
||||
`values are true, false and "auto", got "${got}".`
|
||||
'Invalid value for config.terminal: the only accepted' +
|
||||
`values are true, false and "auto", got "${got}".`,
|
||||
);
|
||||
}
|
||||
if (t === undefined) {
|
||||
@@ -46,7 +46,7 @@ export class PyTerminalPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
afterSetup(runtime: Runtime) {
|
||||
afterSetup(interpreter: Interpreter) {
|
||||
// the Python interpreter has been initialized and we are ready to
|
||||
// execute user code:
|
||||
//
|
||||
|
||||
@@ -15,8 +15,8 @@ class MyPlugin(Plugin):
|
||||
def configure(self, config):
|
||||
console.log(f"configuration received: {config}")
|
||||
|
||||
def afterStartup(self, runtime):
|
||||
console.log(f"runtime received: {runtime}")
|
||||
def afterStartup(self, interpreter):
|
||||
console.log(f"interpreter received: {interpreter}")
|
||||
|
||||
|
||||
plugin = MyPlugin("py-markdown")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AppConfig } from '../pyconfig';
|
||||
import type { UserError } from '../exceptions';
|
||||
import type { Runtime } from '../runtime';
|
||||
import type { Interpreter } from '../interpreter';
|
||||
import { showWarning } from '../utils';
|
||||
import { Plugin } from '../plugin';
|
||||
import { getLogger } from '../logger';
|
||||
@@ -29,7 +29,7 @@ export class SplashscreenPlugin extends Plugin {
|
||||
|
||||
if ('autoclose_loader' in config) {
|
||||
this.autoclose = config.autoclose_loader;
|
||||
showWarning(AUTOCLOSE_LOADER_DEPRECATED, "html");
|
||||
showWarning(AUTOCLOSE_LOADER_DEPRECATED, 'html');
|
||||
}
|
||||
|
||||
if (config.splashscreen) {
|
||||
@@ -43,13 +43,13 @@ export class SplashscreenPlugin extends Plugin {
|
||||
customElements.define('py-splashscreen', PySplashscreen);
|
||||
this.elem = <PySplashscreen>document.createElement('py-splashscreen');
|
||||
document.body.append(this.elem);
|
||||
document.addEventListener("py-status-message", (e: CustomEvent) => {
|
||||
document.addEventListener('py-status-message', (e: CustomEvent) => {
|
||||
const msg = e.detail;
|
||||
this.elem.log(msg);
|
||||
});
|
||||
}
|
||||
|
||||
afterStartup(runtime: Runtime) {
|
||||
afterStartup(interpreter: Interpreter) {
|
||||
if (this.autoclose) {
|
||||
this.elem.close();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Plugin } from "../plugin";
|
||||
import { TargetedStdio, StdioMultiplexer } from "../stdio";
|
||||
import type { Interpreter } from "../interpreter";
|
||||
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ export class StdioDirector extends Plugin {
|
||||
* with that ID for the duration of the evaluation.
|
||||
*
|
||||
*/
|
||||
beforePyScriptExec(runtime: any, src: any, PyScriptTag): void {
|
||||
beforePyScriptExec(interpreter: Interpreter, src: string, PyScriptTag: any): void {
|
||||
if (PyScriptTag.hasAttribute("output")){
|
||||
const targeted_io = new TargetedStdio(PyScriptTag, "output", true, true)
|
||||
PyScriptTag.stdout_manager = targeted_io
|
||||
@@ -40,7 +41,7 @@ export class StdioDirector extends Plugin {
|
||||
/** After a <py-script> tag is evaluated, if that tag has a 'stdout_manager'
|
||||
* (presumably TargetedStdio, or some other future IO handler), it is removed.
|
||||
*/
|
||||
afterPyScriptExec(runtime: any, src: any, PyScriptTag: any, result: any): void {
|
||||
afterPyScriptExec(interpreter: Interpreter, src: string, PyScriptTag: any, result: any): void {
|
||||
if (PyScriptTag.stdout_manager != null){
|
||||
this._stdioMultiplexer.removeListener(PyScriptTag.stdout_manager)
|
||||
PyScriptTag.stdout_manager = null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import toml from '../src/toml';
|
||||
import { getLogger } from './logger';
|
||||
import { version } from './version';
|
||||
import { getAttribute, readTextFromPath, htmlDecode } from './utils';
|
||||
import { getAttribute, readTextFromPath, htmlDecode, createDeprecationWarning } from './utils';
|
||||
import { UserError, ErrorCode } from './exceptions';
|
||||
|
||||
const logger = getLogger('py-config');
|
||||
@@ -15,7 +15,9 @@ export interface AppConfig extends Record<string, any> {
|
||||
author_name?: string;
|
||||
author_email?: string;
|
||||
license?: string;
|
||||
runtimes?: RuntimeConfig[];
|
||||
interpreters?: InterpreterConfig[];
|
||||
// TODO: Remove `runtimes` once the deprecation cycle is over
|
||||
runtimes?: InterpreterConfig[];
|
||||
packages?: string[];
|
||||
fetch?: FetchConfig[];
|
||||
plugins?: string[];
|
||||
@@ -29,7 +31,7 @@ export type FetchConfig = {
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
export type RuntimeConfig = {
|
||||
export type InterpreterConfig = {
|
||||
src?: string;
|
||||
name?: string;
|
||||
lang?: string;
|
||||
@@ -43,19 +45,21 @@ export type PyScriptMetadata = {
|
||||
const allKeys = {
|
||||
string: ['name', 'description', 'version', 'type', 'author_name', 'author_email', 'license'],
|
||||
number: ['schema_version'],
|
||||
array: ['runtimes', 'packages', 'fetch', 'plugins'],
|
||||
array: ['runtimes', 'interpreters', 'packages', 'fetch', 'plugins'],
|
||||
};
|
||||
|
||||
export const defaultConfig: AppConfig = {
|
||||
schema_version: 1,
|
||||
type: 'app',
|
||||
runtimes: [
|
||||
interpreters: [
|
||||
{
|
||||
src: 'https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js',
|
||||
name: 'pyodide-0.21.3',
|
||||
lang: 'python',
|
||||
},
|
||||
],
|
||||
// This is for backward compatibility, we need to remove it in the future
|
||||
runtimes: [],
|
||||
packages: [],
|
||||
fetch: [],
|
||||
plugins: [],
|
||||
@@ -184,17 +188,40 @@ function validateConfig(configText: string, configType = 'toml') {
|
||||
const keys: string[] = allKeys[keyType];
|
||||
keys.forEach(function (item: string) {
|
||||
if (validateParamInConfig(item, keyType, config)) {
|
||||
if (item === 'runtimes') {
|
||||
if (item === 'interpreters') {
|
||||
finalConfig[item] = [];
|
||||
const runtimes = config[item] as RuntimeConfig[];
|
||||
runtimes.forEach(function (eachRuntime: RuntimeConfig) {
|
||||
const runtimeConfig: RuntimeConfig = {};
|
||||
for (const eachRuntimeParam in eachRuntime) {
|
||||
if (validateParamInConfig(eachRuntimeParam, 'string', eachRuntime)) {
|
||||
runtimeConfig[eachRuntimeParam] = eachRuntime[eachRuntimeParam];
|
||||
const interpreters = config[item] as InterpreterConfig[];
|
||||
interpreters.forEach(function (eachInterpreter: InterpreterConfig) {
|
||||
const interpreterConfig: InterpreterConfig = {};
|
||||
for (const eachInterpreterParam in eachInterpreter) {
|
||||
if (validateParamInConfig(eachInterpreterParam, 'string', eachInterpreter)) {
|
||||
interpreterConfig[eachInterpreterParam] = eachInterpreter[eachInterpreterParam];
|
||||
}
|
||||
}
|
||||
finalConfig[item].push(runtimeConfig);
|
||||
finalConfig[item].push(interpreterConfig);
|
||||
});
|
||||
} else if (item === 'runtimes') {
|
||||
// This code is a bit of a mess, but it's used for backwards
|
||||
// compatibility with the old runtimes config. It should be
|
||||
// removed when we remove support for the old config.
|
||||
// We also need the warning here since we are pushing
|
||||
// runtimes to `interpreter` and we can't show the warning
|
||||
// in main.js
|
||||
createDeprecationWarning(
|
||||
'The configuration option `config.runtimes` is deprecated. ' +
|
||||
'Please use `config.interpreters` instead.',
|
||||
'',
|
||||
);
|
||||
finalConfig['interpreters'] = [];
|
||||
const interpreters = config[item] as InterpreterConfig[];
|
||||
interpreters.forEach(function (eachInterpreter: InterpreterConfig) {
|
||||
const interpreterConfig: InterpreterConfig = {};
|
||||
for (const eachInterpreterParam in eachInterpreter) {
|
||||
if (validateParamInConfig(eachInterpreterParam, 'string', eachInterpreter)) {
|
||||
interpreterConfig[eachInterpreterParam] = eachInterpreter[eachInterpreterParam];
|
||||
}
|
||||
}
|
||||
finalConfig['interpreters'].push(interpreterConfig);
|
||||
});
|
||||
} else if (item === 'fetch') {
|
||||
finalConfig[item] = [];
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getLogger } from './logger';
|
||||
import { ensureUniqueId } from './utils';
|
||||
import { UserError, ErrorCode } from './exceptions';
|
||||
import type { Runtime } from './runtime';
|
||||
import type { Interpreter } from './interpreter';
|
||||
|
||||
const logger = getLogger('pyexec');
|
||||
|
||||
export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
|
||||
export function pyExec(interpreter: Interpreter, pysrc: string, outElem: HTMLElement) {
|
||||
//This is pyscript.py
|
||||
const pyscript_py = runtime.interpreter.pyimport('pyscript');
|
||||
const pyscript_py = interpreter.interface.pyimport('pyscript');
|
||||
|
||||
ensureUniqueId(outElem);
|
||||
pyscript_py.set_current_display_target(outElem.id);
|
||||
@@ -17,14 +17,14 @@ export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
|
||||
throw new UserError(
|
||||
ErrorCode.TOP_LEVEL_AWAIT,
|
||||
'The use of top-level "await", "async for", and ' +
|
||||
'"async with" is deprecated.' +
|
||||
'\nPlease write a coroutine containing ' +
|
||||
'your code and schedule it using asyncio.ensure_future() or similar.' +
|
||||
'\nSee https://docs.pyscript.net/latest/guides/asyncio.html for more information.',
|
||||
)
|
||||
}
|
||||
return runtime.run(pysrc);
|
||||
} catch (err) {
|
||||
'"async with" is deprecated.' +
|
||||
'\nPlease write a coroutine containing ' +
|
||||
'your code and schedule it using asyncio.ensure_future() or similar.' +
|
||||
'\nSee https://docs.pyscript.net/latest/guides/asyncio.html for more information.',
|
||||
);
|
||||
}
|
||||
return interpreter.run(pysrc);
|
||||
} catch (err) {
|
||||
// XXX: currently we display exceptions in the same position as
|
||||
// the output. But we probably need a better way to do that,
|
||||
// e.g. allowing plugins to intercept exceptions and display them
|
||||
@@ -41,11 +41,11 @@ export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
|
||||
* Javascript API to call the python display() function
|
||||
*
|
||||
* Expected usage:
|
||||
* pyDisplay(runtime, obj);
|
||||
* pyDisplay(runtime, obj, { target: targetID });
|
||||
* pyDisplay(interpreter, obj);
|
||||
* pyDisplay(interpreter, obj, { target: targetID });
|
||||
*/
|
||||
export function pyDisplay(runtime: Runtime, obj: any, kwargs: object) {
|
||||
const display = runtime.globals.get('display');
|
||||
export function pyDisplay(interpreter: Interpreter, obj: any, kwargs: object) {
|
||||
const display = interpreter.globals.get('display');
|
||||
if (kwargs === undefined) display(obj);
|
||||
else {
|
||||
display.callKwargs(obj, kwargs);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Runtime } from './runtime';
|
||||
import { Interpreter } from './interpreter';
|
||||
import { getLogger } from './logger';
|
||||
import { InstallError, ErrorCode } from './exceptions'
|
||||
import { InstallError, ErrorCode } from './exceptions';
|
||||
import type { loadPyodide as loadPyodideDeclaration, PyodideInterface, PyProxy } from 'pyodide';
|
||||
import { robustFetch } from './fetch';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
@@ -15,13 +15,15 @@ interface Micropip extends PyProxy {
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export class PyodideRuntime extends Runtime {
|
||||
export class PyodideInterpreter extends Interpreter {
|
||||
src: string;
|
||||
stdio: Stdio;
|
||||
name?: string;
|
||||
lang?: string;
|
||||
interpreter: PyodideInterface;
|
||||
interface: PyodideInterface;
|
||||
globals: PyProxy;
|
||||
// TODO: Remove this once `runtimes` is removed!
|
||||
interpreter: PyodideInterface;
|
||||
|
||||
constructor(
|
||||
config: AppConfig,
|
||||
@@ -30,7 +32,7 @@ export class PyodideRuntime extends Runtime {
|
||||
name = 'pyodide-default',
|
||||
lang = 'python',
|
||||
) {
|
||||
logger.info('Runtime config:', { name, lang, src });
|
||||
logger.info('Interpreter config:', { name, lang, src });
|
||||
super(config);
|
||||
this.stdio = stdio;
|
||||
this.src = src;
|
||||
@@ -56,7 +58,7 @@ export class PyodideRuntime extends Runtime {
|
||||
*/
|
||||
async loadInterpreter(): Promise<void> {
|
||||
logger.info('Loading pyodide');
|
||||
this.interpreter = await loadPyodide({
|
||||
this.interface = await loadPyodide({
|
||||
stdout: (msg: string) => {
|
||||
this.stdio.stdout_writeline(msg);
|
||||
},
|
||||
@@ -66,67 +68,66 @@ export class PyodideRuntime extends Runtime {
|
||||
fullStdLib: false,
|
||||
});
|
||||
|
||||
this.globals = this.interpreter.globals;
|
||||
// TODO: Remove this once `runtimes` is removed!
|
||||
this.interpreter = this.interface;
|
||||
|
||||
this.globals = this.interface.globals;
|
||||
|
||||
if (this.config.packages) {
|
||||
logger.info("Found packages in configuration to install. Loading micropip...")
|
||||
logger.info('Found packages in configuration to install. Loading micropip...');
|
||||
await this.loadPackage('micropip');
|
||||
}
|
||||
logger.info('pyodide loaded and initialized');
|
||||
}
|
||||
|
||||
run(code: string): unknown {
|
||||
return this.interpreter.runPython(code);
|
||||
return this.interface.runPython(code);
|
||||
}
|
||||
|
||||
registerJsModule(name: string, module: object): void {
|
||||
this.interpreter.registerJsModule(name, module);
|
||||
this.interface.registerJsModule(name, module);
|
||||
}
|
||||
|
||||
async loadPackage(names: string | string[]): Promise<void> {
|
||||
logger.info(`pyodide.loadPackage: ${names.toString()}`);
|
||||
await this.interpreter.loadPackage(names, logger.info.bind(logger), logger.info.bind(logger));
|
||||
await this.interface.loadPackage(names, logger.info.bind(logger), logger.info.bind(logger));
|
||||
}
|
||||
|
||||
async installPackage(package_name: string | string[]): Promise<void> {
|
||||
if (package_name.length > 0) {
|
||||
logger.info(`micropip install ${package_name.toString()}`);
|
||||
|
||||
const micropip = this.interpreter.pyimport('micropip') as Micropip;
|
||||
const micropip = this.interface.pyimport('micropip') as Micropip;
|
||||
try {
|
||||
await micropip.install(package_name);
|
||||
micropip.destroy();
|
||||
} catch(e) {
|
||||
let exceptionMessage = `Unable to install package(s) '` + package_name +`'.`
|
||||
} catch (e) {
|
||||
let exceptionMessage = `Unable to install package(s) '` + package_name + `'.`;
|
||||
|
||||
// If we can't fetch `package_name` micropip.install throws a huge
|
||||
// Python traceback in `e.message` this logic is to handle the
|
||||
// error and throw a more sensible error message instead of the
|
||||
// huge traceback.
|
||||
if (e.message.includes("Can't find a pure Python 3 wheel")) {
|
||||
exceptionMessage += (
|
||||
` Reason: Can't find a pure Python 3 Wheel for package(s) '` + package_name +
|
||||
exceptionMessage +=
|
||||
` Reason: Can't find a pure Python 3 Wheel for package(s) '` +
|
||||
package_name +
|
||||
`'. See: https://pyodide.org/en/stable/usage/faq.html#micropip-can-t-find-a-pure-python-wheel ` +
|
||||
`for more information.`
|
||||
)
|
||||
`for more information.`;
|
||||
} else if (e.message.includes("Can't fetch metadata")) {
|
||||
exceptionMessage += (
|
||||
" Unable to find package in PyPI. " +
|
||||
"Please make sure you have entered a correct package name."
|
||||
)
|
||||
exceptionMessage +=
|
||||
' Unable to find package in PyPI. ' +
|
||||
'Please make sure you have entered a correct package name.';
|
||||
} else {
|
||||
exceptionMessage += (
|
||||
exceptionMessage +=
|
||||
` Reason: ${e.message as string}. Please open an issue at ` +
|
||||
`https://github.com/pyscript/pyscript/issues/new if you require help or ` +
|
||||
`you think it's a bug.`)
|
||||
`you think it's a bug.`;
|
||||
}
|
||||
|
||||
logger.error(e);
|
||||
|
||||
throw new InstallError(
|
||||
ErrorCode.MICROPIP_INSTALL_ERROR,
|
||||
exceptionMessage
|
||||
)
|
||||
throw new InstallError(ErrorCode.MICROPIP_INSTALL_ERROR, exceptionMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,12 +165,11 @@ export class PyodideRuntime extends Runtime {
|
||||
const pathArr = path.split('/');
|
||||
const filename = pathArr.pop();
|
||||
for (let i = 0; i < pathArr.length; i++) {
|
||||
|
||||
// iteratively calculates parts of the path i.e. `a`, `a/b`, `a/b/c` for `a/b/c/foo.py`
|
||||
const eachPath = pathArr.slice(0, i + 1).join('/');
|
||||
|
||||
// analyses `eachPath` and returns if it exists along with if its parent directory exists or not
|
||||
const { exists, parentExists } = this.interpreter.FS.analyzePath(eachPath);
|
||||
const { exists, parentExists } = this.interface.FS.analyzePath(eachPath);
|
||||
|
||||
// due to the iterative manner in which we proceed, the parent directory should ALWAYS exist
|
||||
if (!parentExists) {
|
||||
@@ -178,7 +178,7 @@ export class PyodideRuntime extends Runtime {
|
||||
|
||||
// creates `eachPath` if it doesn't exist
|
||||
if (!exists) {
|
||||
this.interpreter.FS.mkdir(eachPath);
|
||||
this.interface.FS.mkdir(eachPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,13 +189,13 @@ export class PyodideRuntime extends Runtime {
|
||||
|
||||
pathArr.push(filename);
|
||||
// opens a file descriptor for the file at `path`
|
||||
const stream = this.interpreter.FS.open(pathArr.join('/'), 'w');
|
||||
this.interpreter.FS.write(stream, data, 0, data.length, 0);
|
||||
this.interpreter.FS.close(stream);
|
||||
const stream = this.interface.FS.open(pathArr.join('/'), 'w');
|
||||
this.interface.FS.write(stream, data, 0, data.length, 0);
|
||||
this.interface.FS.close(stream);
|
||||
}
|
||||
|
||||
invalidate_module_path_cache(): void {
|
||||
const importlib = this.interpreter.pyimport("importlib")
|
||||
importlib.invalidate_caches()
|
||||
const importlib = this.interface.pyimport('importlib');
|
||||
importlib.invalidate_caches();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,19 +72,19 @@ __version__ = None
|
||||
version_info = None
|
||||
|
||||
|
||||
def _set_version_info(version_from_runtime: str):
|
||||
def _set_version_info(version_from_interpreter: str):
|
||||
"""Sets the __version__ and version_info properties from provided JSON data
|
||||
Args:
|
||||
version_from_runtime (str): A "dotted" representation of the version:
|
||||
version_from_interpreter (str): A "dotted" representation of the version:
|
||||
YYYY.MM.m(m).releaselevel
|
||||
Year, Month, and Minor should be integers; releaselevel can be any string
|
||||
"""
|
||||
global __version__
|
||||
global version_info
|
||||
|
||||
__version__ = version_from_runtime
|
||||
__version__ = version_from_interpreter
|
||||
|
||||
version_parts = version_from_runtime.split(".")
|
||||
version_parts = version_from_interpreter.split(".")
|
||||
year = int(version_parts[0])
|
||||
month = int(version_parts[1])
|
||||
minor = int(version_parts[2])
|
||||
|
||||
@@ -242,7 +242,7 @@ class PyScriptTest:
|
||||
If check_js_errors is True (the default), it also checks that no JS
|
||||
errors were raised during the waiting.
|
||||
"""
|
||||
# this is printed by runtime.ts:Runtime.initialize
|
||||
# this is printed by interpreter.ts:Interpreter.initialize
|
||||
self.wait_for_console(
|
||||
"[pyscript/main] PyScript page fully initialized",
|
||||
timeout=timeout,
|
||||
|
||||
88
pyscriptjs/tests/integration/test_interpreter.py
Normal file
88
pyscriptjs/tests/integration/test_interpreter.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestInterpreterAccess(PyScriptTest):
|
||||
"""Test accessing Python objects from JS via pyscript.interpreter"""
|
||||
|
||||
def test_interpreter_python_access(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
x = 1
|
||||
def py_func():
|
||||
return 2
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.page.add_script_tag(
|
||||
content="""
|
||||
console.log(`x is ${pyscript.interpreter.globals.get('x')}`);
|
||||
console.log(`py_func() returns ${pyscript.interpreter.globals.get('py_func')()}`);
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[0] == self.PY_COMPLETE
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"x is 1",
|
||||
"py_func() returns 2",
|
||||
]
|
||||
|
||||
def test_interpreter_script_execution(self):
|
||||
"""Test running Python code from js via pyscript.interpreter"""
|
||||
self.pyscript_run("")
|
||||
|
||||
self.page.add_script_tag(
|
||||
content="""
|
||||
const interface = pyscript.interpreter.interface;
|
||||
interface.runPython('print("Interpreter Ran This")');
|
||||
"""
|
||||
)
|
||||
expected_message = "Interpreter Ran This"
|
||||
assert self.console.log.lines[0] == self.PY_COMPLETE
|
||||
assert self.console.log.lines[-1] == expected_message
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert py_terminal.text_content() == expected_message
|
||||
|
||||
def test_backward_compatibility_runtime_script_execution(self):
|
||||
"""Test running Python code from js via pyscript.runtime"""
|
||||
self.pyscript_run("")
|
||||
|
||||
self.page.add_script_tag(
|
||||
content="""
|
||||
const interface = pyscript.runtime.interpreter;
|
||||
interface.runPython('print("Interpreter Ran This")');
|
||||
"""
|
||||
)
|
||||
expected_message = "Interpreter Ran This"
|
||||
assert self.console.log.lines[0] == self.PY_COMPLETE
|
||||
assert self.console.log.lines[-1] == expected_message
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert py_terminal.text_content() == expected_message
|
||||
|
||||
def test_backward_compatibility_runtime_python_access(self):
|
||||
"""Test accessing Python objects from JS via pyscript.runtime"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
x = 1
|
||||
def py_func():
|
||||
return 2
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.page.add_script_tag(
|
||||
content="""
|
||||
console.log(`x is ${pyscript.interpreter.globals.get('x')}`);
|
||||
console.log(`py_func() returns ${pyscript.interpreter.globals.get('py_func')()}`);
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[0] == self.PY_COMPLETE
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"x is 1",
|
||||
"py_func() returns 2",
|
||||
]
|
||||
@@ -37,11 +37,11 @@ class TestLogger(Plugin):
|
||||
def afterStartup(self, config):
|
||||
console.log('afterStartup called')
|
||||
|
||||
def beforePyScriptExec(self, runtime, src, pyscript_tag):
|
||||
def beforePyScriptExec(self, interpreter, src, pyscript_tag):
|
||||
console.log(f'beforePyScriptExec called')
|
||||
console.log(f'before_src:{src}')
|
||||
|
||||
def afterPyScriptExec(self, runtime, src, pyscript_tag, result):
|
||||
def afterPyScriptExec(self, interpreter, src, pyscript_tag, result):
|
||||
console.log(f'afterPyScriptExec called')
|
||||
console.log(f'after_src:{src}')
|
||||
|
||||
@@ -60,12 +60,12 @@ from js import console
|
||||
|
||||
class ExecTestLogger(Plugin):
|
||||
|
||||
def beforePyScriptExec(self, runtime, src, pyscript_tag):
|
||||
def beforePyScriptExec(self, interpreter, src, pyscript_tag):
|
||||
console.log(f'beforePyScriptExec called')
|
||||
console.log(f'before_src:{src}')
|
||||
console.log(f'before_id:{pyscript_tag.id}')
|
||||
|
||||
def afterPyScriptExec(self, runtime, src, pyscript_tag, result):
|
||||
def afterPyScriptExec(self, interpreter, src, pyscript_tag, result):
|
||||
console.log(f'afterPyScriptExec called')
|
||||
console.log(f'after_src:{src}')
|
||||
console.log(f'after_id:{pyscript_tag.id}')
|
||||
|
||||
@@ -70,10 +70,41 @@ class TestConfig(PyScriptTest):
|
||||
# this test which is newer than the one we are loading below
|
||||
# (after downloading locally) -- which is 0.20.0
|
||||
|
||||
# The test checks if loading a different runtime is possible
|
||||
# The test checks if loading a different interpreter is possible
|
||||
# and that too from a locally downloaded file without needing
|
||||
# the use of explicit `indexURL` calculation.
|
||||
def test_runtime_config(self, tar_location):
|
||||
def test_interpreter_config(self, tar_location):
|
||||
unzip(
|
||||
location=tar_location,
|
||||
extract_to=self.tmpdir,
|
||||
)
|
||||
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config type="json">
|
||||
{
|
||||
"interpreters": [{
|
||||
"src": "/pyodide/pyodide.js",
|
||||
"name": "pyodide-0.20.0",
|
||||
"lang": "python"
|
||||
}]
|
||||
}
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
import sys, js
|
||||
pyodide_version = sys.modules["pyodide"].__version__
|
||||
js.console.log("version", pyodide_version)
|
||||
display(pyodide_version)
|
||||
</py-script>
|
||||
""",
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-1] == "version 0.20.0"
|
||||
version = self.page.locator("py-script").inner_text()
|
||||
assert version == "0.20.0"
|
||||
|
||||
def test_runtime_still_works_but_shows_deprecation_warning(self, tar_location):
|
||||
unzip(
|
||||
location=tar_location,
|
||||
extract_to=self.tmpdir,
|
||||
@@ -104,6 +135,13 @@ class TestConfig(PyScriptTest):
|
||||
version = self.page.locator("py-script").inner_text()
|
||||
assert version == "0.20.0"
|
||||
|
||||
deprecation_banner = self.page.wait_for_selector(".alert-banner")
|
||||
expected_message = (
|
||||
"The configuration option `config.runtimes` is deprecated. "
|
||||
"Please use `config.interpreters` instead."
|
||||
)
|
||||
assert deprecation_banner.inner_text() == expected_message
|
||||
|
||||
def test_invalid_json_config(self):
|
||||
# we need wait_for_pyscript=False because we bail out very soon,
|
||||
# before being able to write 'PyScript page fully initialized'
|
||||
@@ -168,23 +206,25 @@ class TestConfig(PyScriptTest):
|
||||
)
|
||||
assert banner.text_content() == expected
|
||||
|
||||
def test_no_runtimes(self):
|
||||
def test_no_interpreter(self):
|
||||
snippet = """
|
||||
<py-config type="json">
|
||||
{
|
||||
"runtimes": []
|
||||
"interpreters": []
|
||||
}
|
||||
</py-config>
|
||||
"""
|
||||
self.pyscript_run(snippet, wait_for_pyscript=False)
|
||||
div = self.page.wait_for_selector(".py-error")
|
||||
assert div.text_content() == "(PY1000): Fatal error: config.runtimes is empty"
|
||||
assert (
|
||||
div.text_content() == "(PY1000): Fatal error: config.interpreter is empty"
|
||||
)
|
||||
|
||||
def test_multiple_runtimes(self):
|
||||
def test_multiple_interpreter(self):
|
||||
snippet = """
|
||||
<py-config type="json">
|
||||
{
|
||||
"runtimes": [
|
||||
"interpreters": [
|
||||
{
|
||||
"src": "https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js",
|
||||
"name": "pyodide-0.21.3",
|
||||
@@ -206,7 +246,9 @@ class TestConfig(PyScriptTest):
|
||||
"""
|
||||
self.pyscript_run(snippet)
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
expected = "Multiple runtimes are not supported yet.Only the first will be used"
|
||||
expected = (
|
||||
"Multiple interpreters are not supported yet.Only the first will be used"
|
||||
)
|
||||
assert banner.text_content() == expected
|
||||
assert self.console.log.lines[-1] == "hello world"
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestRuntimeAccess(PyScriptTest):
|
||||
"""Test accessing Python objects from JS via pyscript.runtime"""
|
||||
|
||||
def test_runtime_python_access(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
x = 1
|
||||
def py_func():
|
||||
return 2
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.page.add_script_tag(
|
||||
content="""
|
||||
console.log(`x is ${pyscript.runtime.globals.get('x')}`);
|
||||
console.log(`py_func() returns ${pyscript.runtime.globals.get('py_func')()}`);
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[0] == self.PY_COMPLETE
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"x is 1",
|
||||
"py_func() returns 2",
|
||||
]
|
||||
|
||||
def test_runtime_script_execution(self):
|
||||
"""Test running Python code from js via pyscript.runtime"""
|
||||
self.pyscript_run("")
|
||||
|
||||
self.page.add_script_tag(
|
||||
content="""
|
||||
const interpreter = pyscript.runtime.interpreter;
|
||||
interpreter.runPython('console.log("Interpreter Ran This")');
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[0] == self.PY_COMPLETE
|
||||
assert self.console.log.lines[-1] == "Interpreter Ran This"
|
||||
@@ -8,10 +8,10 @@ class TestPyMarkdown:
|
||||
console_mock = Mock()
|
||||
monkeypatch.setattr(py_markdown, "console", console_mock)
|
||||
config = "just a config"
|
||||
runtime = "just a runtime"
|
||||
interpreter = "just an interpreter"
|
||||
|
||||
py_markdown.plugin.configure(config)
|
||||
console_mock.log.assert_called_with("configuration received: just a config")
|
||||
|
||||
py_markdown.plugin.afterStartup(runtime)
|
||||
console_mock.log.assert_called_with("runtime received: just a runtime")
|
||||
py_markdown.plugin.afterStartup(interpreter)
|
||||
console_mock.log.assert_called_with("interpreter received: just an interpreter")
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Runtime } from "../../src/runtime"
|
||||
import { Interpreter } from '../../src/interpreter';
|
||||
import type { PyodideInterface } from 'pyodide';
|
||||
|
||||
export class FakeRuntime extends Runtime {
|
||||
|
||||
export class FakeInterpreter extends Interpreter {
|
||||
src: string;
|
||||
name?: string;
|
||||
lang?: string;
|
||||
interpreter: PyodideInterface;
|
||||
interface: PyodideInterface;
|
||||
globals: any;
|
||||
|
||||
constructor() {
|
||||
@@ -18,26 +17,26 @@ export class FakeRuntime extends Runtime {
|
||||
}
|
||||
|
||||
async loadInterpreter() {
|
||||
throw new Error("not implemented");
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
registerJsModule(name: string, module: object) {
|
||||
throw new Error("not implemented");
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
async loadPackage(names: string | string[]) {
|
||||
throw new Error("not implemented");
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
async installPackage(package_name: string | string[]) {
|
||||
throw new Error("not implemented");
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
async loadFromFile(path: string, fetch_path: string) {
|
||||
throw new Error("not implemented");
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
invalidate_module_path_cache(): void {
|
||||
throw new Error("not implemented");
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { UserError } from '../../src/exceptions';
|
||||
// inspired by trump typos
|
||||
const covfefeConfig = {
|
||||
name: 'covfefe',
|
||||
runtimes: [
|
||||
interpreters: [
|
||||
{
|
||||
src: '/demo/covfefe.js',
|
||||
name: 'covfefe',
|
||||
@@ -21,7 +21,7 @@ name = "covfefe"
|
||||
|
||||
wonderful = "hijacked"
|
||||
|
||||
[[runtimes]]
|
||||
[[interpreters]]
|
||||
src = "/demo/covfefe.js"
|
||||
name = "covfefe"
|
||||
lang = "covfefe"
|
||||
@@ -73,7 +73,7 @@ describe('loadConfigFromElement', () => {
|
||||
const el = make_config_element({ type: 'json' });
|
||||
el.innerHTML = JSON.stringify(covfefeConfig);
|
||||
const config = loadConfigFromElement(el);
|
||||
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||
expect(config.interpreters[0].lang).toBe('covfefe');
|
||||
expect(config.pyscript?.time).not.toBeNull();
|
||||
// schema_version wasn't present in `inline config` but is still set due to merging with default
|
||||
expect(config.schema_version).toBe(1);
|
||||
@@ -82,7 +82,7 @@ describe('loadConfigFromElement', () => {
|
||||
it('should load the JSON config from src attribute', () => {
|
||||
const el = make_config_element({ type: 'json', src: '/covfefe.json' });
|
||||
const config = loadConfigFromElement(el);
|
||||
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||
expect(config.interpreters[0].lang).toBe('covfefe');
|
||||
expect(config.pyscript?.time).not.toBeNull();
|
||||
// wonderful is an extra key supplied by the user and is unaffected by merging process
|
||||
expect(config.wonderful).toBe('disgrace');
|
||||
@@ -94,7 +94,7 @@ describe('loadConfigFromElement', () => {
|
||||
const el = make_config_element({ type: 'json', src: '/covfefe.json' });
|
||||
el.innerHTML = JSON.stringify({ version: '0.2a', wonderful: 'hijacked' });
|
||||
const config = loadConfigFromElement(el);
|
||||
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||
expect(config.interpreters[0].lang).toBe('covfefe');
|
||||
expect(config.pyscript?.time).not.toBeNull();
|
||||
// config from src had an extra key "wonderful" with value "disgrace"
|
||||
// inline config had the same extra key "wonderful" with value "hijacked"
|
||||
@@ -110,7 +110,7 @@ describe('loadConfigFromElement', () => {
|
||||
const el = make_config_element({});
|
||||
el.innerHTML = covfefeConfigToml;
|
||||
const config = loadConfigFromElement(el);
|
||||
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||
expect(config.interpreters[0].lang).toBe('covfefe');
|
||||
expect(config.pyscript?.time).not.toBeNull();
|
||||
// schema_version wasn't present in `inline config` but is still set due to merging with default
|
||||
expect(config.schema_version).toBe(1);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { AppConfig } from '../../src/pyconfig';
|
||||
import { Runtime } from '../../src/runtime';
|
||||
import { PyodideRuntime } from '../../src/pyodide';
|
||||
import { Interpreter } from '../../src/interpreter';
|
||||
import { PyodideInterpreter } from '../../src/pyodide';
|
||||
import { CaptureStdio } from '../../src/stdio';
|
||||
|
||||
import { TextEncoder, TextDecoder } from 'util'
|
||||
global.TextEncoder = TextEncoder
|
||||
global.TextDecoder = TextDecoder
|
||||
import { TextEncoder, TextDecoder } from 'util';
|
||||
global.TextEncoder = TextEncoder;
|
||||
global.TextDecoder = TextDecoder;
|
||||
|
||||
describe('PyodideRuntime', () => {
|
||||
let runtime: PyodideRuntime;
|
||||
describe('PyodideInterpreter', () => {
|
||||
let interpreter: PyodideInterpreter;
|
||||
let stdio: CaptureStdio = new CaptureStdio();
|
||||
beforeAll(async () => {
|
||||
const config: AppConfig = {};
|
||||
runtime = new PyodideRuntime(config, stdio);
|
||||
interpreter = new PyodideInterpreter(config, stdio);
|
||||
|
||||
/**
|
||||
* Since import { loadPyodide } from 'pyodide';
|
||||
* is not used inside `src/pyodide.ts`, the function
|
||||
* `runtime.loadInterpreter();` below which calls
|
||||
* `interpreter.loadInterpreter();` below which calls
|
||||
* `loadPyodide()` results in an expected issue of:
|
||||
* ReferenceError: loadPyodide is not defined
|
||||
*
|
||||
@@ -37,33 +37,33 @@ describe('PyodideRuntime', () => {
|
||||
* See https://github.com/pyodide/pyodide/blob/7dfee03a82c19069f714a09da386547aeefef242/src/js/pyodide.ts#L161-L179
|
||||
*/
|
||||
const pyodideSpec = await import('pyodide');
|
||||
global.loadPyodide = async (options) => pyodideSpec.loadPyodide(Object.assign({indexURL: '../pyscriptjs/node_modules/pyodide/'}, options));
|
||||
await runtime.loadInterpreter();
|
||||
global.loadPyodide = async options =>
|
||||
pyodideSpec.loadPyodide(Object.assign({ indexURL: '../pyscriptjs/node_modules/pyodide/' }, options));
|
||||
await interpreter.loadInterpreter();
|
||||
});
|
||||
|
||||
it('should check if runtime is an instance of abstract Runtime', async () => {
|
||||
expect(runtime).toBeInstanceOf(Runtime);
|
||||
it('should check if interpreter is an instance of abstract Interpreter', async () => {
|
||||
expect(interpreter).toBeInstanceOf(Interpreter);
|
||||
});
|
||||
|
||||
it('should check if runtime is an instance of PyodideRuntime', async () => {
|
||||
expect(runtime).toBeInstanceOf(PyodideRuntime);
|
||||
it('should check if interpreter is an instance of PyodideInterpreter', async () => {
|
||||
expect(interpreter).toBeInstanceOf(PyodideInterpreter);
|
||||
});
|
||||
|
||||
it('should check if runtime can run python code asynchronously', async () => {
|
||||
expect(runtime.run("2+3")).toBe(5);
|
||||
it('should check if interpreter can run python code asynchronously', async () => {
|
||||
expect(interpreter.run('2+3')).toBe(5);
|
||||
});
|
||||
|
||||
it('should capture stdout', async () => {
|
||||
stdio.reset();
|
||||
runtime.run("print('hello')");
|
||||
expect(stdio.captured_stdout).toBe("hello\n");
|
||||
interpreter.run("print('hello')");
|
||||
expect(stdio.captured_stdout).toBe('hello\n');
|
||||
});
|
||||
|
||||
it('should check if runtime is able to load a package', async () => {
|
||||
await runtime.loadPackage("numpy");
|
||||
runtime.run("import numpy as np");
|
||||
runtime.run("x = np.ones((10,))");
|
||||
expect(runtime.globals.get('x').toJs()).toBeInstanceOf(Float64Array);
|
||||
it('should check if interpreter is able to load a package', async () => {
|
||||
await interpreter.loadPackage('numpy');
|
||||
interpreter.run('import numpy as np');
|
||||
interpreter.run('x = np.ones((10,))');
|
||||
expect(interpreter.globals.get('x').toJs()).toBeInstanceOf(Float64Array);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user