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

View File

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

View 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",
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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