Rename runtimes with interpreter (#1082)

This commit is contained in:
Fábio Rosado
2023-01-16 18:52:31 +00:00
committed by GitHub
parent 5a3c414c8f
commit bb5c59307a
33 changed files with 497 additions and 370 deletions

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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> {

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);
}
}
}

View File

@@ -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:
//

View File

@@ -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")

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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] = [];

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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])