mirror of
https://github.com/pyscript/pyscript.git
synced 2026-02-17 10:01:09 -05:00
Python Plugins (#961)
* add test and example files * update config to include python plugins in build * add markdown plugin * remove full pyscript execution from pyodide * move loading of pyscript.py from pyodide loagInterpreter to main setupVirtualEnv and add function to create python CE plugins * add plugin class to pyscript.py * add missing import * fix plugin path * add fetchPythonPlugins to PyScriptApp * remove old comments * fix test * add support for python plugins beyond custom elements and add app to python namespace in main * inject reference to PyScript app onto python plugins * add example hook onto markdown plugin * change plugin events logs * remove unused PyPlugin * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix type import * add docstring to fetchPythonPlugins * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * rename addPythonPlugin method * address PR comment * call python plugins on hooks after the interpreted is ready * add test for event hooks and split the test in 2 separate plugins to isolte type of plugins tests * change python plugins initialization and registration, to inject the app from app itself instead of on the plugins themselves * handle case when plugin cannot load due to missing plugin attribute * add test for fail scenario when a plugin module does not have a plugin attribute * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add deprecation warning for pyscript objects loaded in global namespace * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove all from global scope * remove create_custom_element from global scope * rename create_custom_element to define_custom_element * rename attributes in define_custom_element and add docstrings * better handle connect event output * add warning to py_markdown plugin * remove debugging logs * improve tests * remove debugging log * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove unused import * add executable shebang * add pyodide mock module * fmt and lint * Update to pyodide.ffi.create_proxy per pyodide v21 api change * Mock pyodide as package instead of mdoule * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add __init__ to pyodide package * Update pyscriptjs/src/plugin.ts fix logger name Co-authored-by: Antonio Cuni <anto.cuni@gmail.com> * fix pyodide import but handling the diff in their API change * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * oops, conflict resolution blooper * Fix failing integration tests Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jeff Glass <glass.jeffrey@gmail.com> Co-authored-by: Antonio Cuni <anto.cuni@gmail.com> Co-authored-by: FabioRosado <fabiorosado@outlook.com>
This commit is contained in:
@@ -20,11 +20,10 @@ export class PyBox extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const deprecationMessage = (
|
||||
const deprecationMessage =
|
||||
'The element <py-box> is deprecated, you should create a ' +
|
||||
'div with "py-box" class name instead. For example: <div class="py-box">'
|
||||
)
|
||||
createDeprecationWarning(deprecationMessage, "py-box")
|
||||
'div with "py-box" class name instead. For example: <div class="py-box">';
|
||||
createDeprecationWarning(deprecationMessage, 'py-box');
|
||||
const mainDiv = document.createElement('div');
|
||||
addClasses(mainDiv, ['py-box']);
|
||||
|
||||
|
||||
@@ -42,11 +42,10 @@ export function make_PyButton(runtime: Runtime) {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const deprecationMessage = (
|
||||
const deprecationMessage =
|
||||
'The element <py-button> is deprecated, create a function with your ' +
|
||||
'inline code and use <button py-click="function()" class="py-button"> instead.'
|
||||
)
|
||||
createDeprecationWarning(deprecationMessage, "py-button")
|
||||
'inline code and use <button py-click="function()" class="py-button"> instead.';
|
||||
createDeprecationWarning(deprecationMessage, 'py-button');
|
||||
|
||||
ensureUniqueId(this);
|
||||
this.code = htmlDecode(this.innerHTML) || '';
|
||||
|
||||
@@ -21,11 +21,9 @@ export function make_PyInputBox(runtime: Runtime) {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const deprecationMessage = (
|
||||
'The element <py-input> is deprecated, ' +
|
||||
'use <input class="py-input"> instead.'
|
||||
)
|
||||
createDeprecationWarning(deprecationMessage, "py-input")
|
||||
const deprecationMessage =
|
||||
'The element <py-input> is deprecated, ' + 'use <input class="py-input"> instead.';
|
||||
createDeprecationWarning(deprecationMessage, 'py-input');
|
||||
ensureUniqueId(this);
|
||||
this.code = htmlDecode(this.innerHTML);
|
||||
this.mount_name = this.id.split('-').join('_');
|
||||
|
||||
@@ -9,10 +9,8 @@ export class PyTitle extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const deprecationMessage = (
|
||||
'The element <py-title> is deprecated, please use an <h1> tag instead.'
|
||||
)
|
||||
createDeprecationWarning(deprecationMessage, "py-title")
|
||||
const deprecationMessage = 'The element <py-title> is deprecated, please use an <h1> tag instead.';
|
||||
createDeprecationWarning(deprecationMessage, 'py-title');
|
||||
this.label = htmlDecode(this.innerHTML);
|
||||
this.mount_name = this.id.split('-').join('_');
|
||||
this.innerHTML = '';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const CLOSEBUTTON = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill="currentColor" width="12px"><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>`;
|
||||
|
||||
type MessageType = "text" | "html";
|
||||
type MessageType = 'text' | 'html';
|
||||
|
||||
/*
|
||||
These error codes are used to identify the type of error that occurred.
|
||||
@@ -49,41 +49,41 @@ export class FetchError extends Error {
|
||||
}
|
||||
|
||||
export function _createAlertBanner(
|
||||
message: string,
|
||||
level: "error" | "warning" = "error",
|
||||
messageType: MessageType = "text",
|
||||
logMessage = true) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
switch (`log-${level}-${logMessage}`) {
|
||||
case "log-error-true":
|
||||
console.error(message);
|
||||
break;
|
||||
case "log-warning-true":
|
||||
console.warn(message)
|
||||
break;
|
||||
}
|
||||
message: string,
|
||||
level: 'error' | 'warning' = 'error',
|
||||
messageType: MessageType = 'text',
|
||||
logMessage = true,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
switch (`log-${level}-${logMessage}`) {
|
||||
case 'log-error-true':
|
||||
console.error(message);
|
||||
break;
|
||||
case 'log-warning-true':
|
||||
console.warn(message);
|
||||
break;
|
||||
}
|
||||
|
||||
const banner = document.createElement("div")
|
||||
banner.className = `alert-banner py-${level}`
|
||||
const banner = document.createElement('div');
|
||||
banner.className = `alert-banner py-${level}`;
|
||||
|
||||
if (messageType === "html") {
|
||||
banner.innerHTML = message;
|
||||
}
|
||||
else {
|
||||
banner.textContent = message;
|
||||
}
|
||||
if (messageType === 'html') {
|
||||
banner.innerHTML = message;
|
||||
} else {
|
||||
banner.textContent = message;
|
||||
}
|
||||
|
||||
if (level === "warning") {
|
||||
const closeButton = document.createElement("button");
|
||||
if (level === 'warning') {
|
||||
const closeButton = document.createElement('button');
|
||||
|
||||
closeButton.id = "alert-close-button"
|
||||
closeButton.addEventListener("click", () => {
|
||||
banner.remove();
|
||||
})
|
||||
closeButton.innerHTML = CLOSEBUTTON;
|
||||
closeButton.id = 'alert-close-button';
|
||||
closeButton.addEventListener('click', () => {
|
||||
banner.remove();
|
||||
});
|
||||
closeButton.innerHTML = CLOSEBUTTON;
|
||||
|
||||
banner.appendChild(closeButton);
|
||||
}
|
||||
banner.appendChild(closeButton);
|
||||
}
|
||||
|
||||
document.body.prepend(banner);
|
||||
document.body.prepend(banner);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { loadConfigFromElement } from './pyconfig';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { Runtime } from './runtime';
|
||||
import { version } from './runtime';
|
||||
import { PluginManager } from './plugin';
|
||||
import { PluginManager, define_custom_element } from './plugin';
|
||||
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
import { PyodideRuntime } from './pyodide';
|
||||
import { getLogger } from './logger';
|
||||
@@ -16,6 +16,9 @@ import { type Stdio, StdioMultiplexer, DEFAULT_STDIO } from './stdio';
|
||||
import { PyTerminalPlugin } from './plugins/pyterminal';
|
||||
import { SplashscreenPlugin } from './plugins/splashscreen';
|
||||
import { ImportmapPlugin } from './plugins/importmap';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import pyscript from './python/pyscript.py';
|
||||
|
||||
const logger = getLogger('pyscript/main');
|
||||
|
||||
@@ -53,8 +56,6 @@ More concretely:
|
||||
- PyScriptApp.afterRuntimeLoad() implements all the points >= 5.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export class PyScriptApp {
|
||||
config: AppConfig;
|
||||
runtime: Runtime;
|
||||
@@ -65,11 +66,7 @@ export class PyScriptApp {
|
||||
constructor() {
|
||||
// initialize the builtin plugins
|
||||
this.plugins = new PluginManager();
|
||||
this.plugins.add(
|
||||
new SplashscreenPlugin(),
|
||||
new PyTerminalPlugin(this),
|
||||
new ImportmapPlugin(),
|
||||
);
|
||||
this.plugins.add(new SplashscreenPlugin(), new PyTerminalPlugin(this), new ImportmapPlugin());
|
||||
|
||||
this._stdioMultiplexer = new StdioMultiplexer();
|
||||
this._stdioMultiplexer.addListener(DEFAULT_STDIO);
|
||||
@@ -83,18 +80,16 @@ export class PyScriptApp {
|
||||
main() {
|
||||
try {
|
||||
this._realMain();
|
||||
}
|
||||
catch(error) {
|
||||
} catch (error) {
|
||||
this._handleUserErrorMaybe(error);
|
||||
}
|
||||
}
|
||||
|
||||
_handleUserErrorMaybe(error) {
|
||||
if (error instanceof UserError) {
|
||||
_createAlertBanner(error.message, "error", error.messageType);
|
||||
_createAlertBanner(error.message, 'error', error.messageType);
|
||||
this.plugins.onUserError(error);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +117,7 @@ export class PyScriptApp {
|
||||
if (elements.length >= 2) {
|
||||
showWarning(
|
||||
'Multiple <py-config> tags detected. Only the first is ' +
|
||||
'going to be parsed, all the others will be ignored',
|
||||
'going to be parsed, all the others will be ignored',
|
||||
);
|
||||
}
|
||||
this.config = loadConfigFromElement(el);
|
||||
@@ -137,14 +132,16 @@ export class PyScriptApp {
|
||||
}
|
||||
|
||||
if (this.config.runtimes.length > 1) {
|
||||
showWarning('Multiple runtimes are not supported yet.<br />Only the first will be used', "html");
|
||||
showWarning('Multiple runtimes are not supported yet.<br />Only the first will be used', 'html');
|
||||
}
|
||||
const runtime_cfg = this.config.runtimes[0];
|
||||
this.runtime = new PyodideRuntime(this.config,
|
||||
this._stdioMultiplexer,
|
||||
runtime_cfg.src,
|
||||
runtime_cfg.name,
|
||||
runtime_cfg.lang);
|
||||
this.runtime = new PyodideRuntime(
|
||||
this.config,
|
||||
this._stdioMultiplexer,
|
||||
runtime_cfg.src,
|
||||
runtime_cfg.name,
|
||||
runtime_cfg.lang,
|
||||
);
|
||||
this.logStatus(`Downloading ${runtime_cfg.name}...`);
|
||||
|
||||
// download pyodide by using a <script> tag. Once it's ready, the
|
||||
@@ -156,7 +153,7 @@ export class PyScriptApp {
|
||||
const script = document.createElement('script'); // create a script DOM node
|
||||
script.src = this.runtime.src;
|
||||
script.addEventListener('load', () => {
|
||||
this.afterRuntimeLoad(this.runtime).catch((error) => {
|
||||
this.afterRuntimeLoad(this.runtime).catch(error => {
|
||||
this._handleUserErrorMaybe(error);
|
||||
});
|
||||
});
|
||||
@@ -194,7 +191,7 @@ export class PyScriptApp {
|
||||
// NOTE: runtime 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.logStatus('Startup complete');
|
||||
this.plugins.afterStartup(runtime);
|
||||
logger.info('PyScript page fully initialized');
|
||||
}
|
||||
@@ -204,9 +201,34 @@ export class PyScriptApp {
|
||||
// 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' });
|
||||
|
||||
// inject `define_custom_element` it into the PyScript module scope
|
||||
const pyscript_module = runtime.interpreter.pyimport('pyscript');
|
||||
pyscript_module.define_custom_element = define_custom_element;
|
||||
pyscript_module.PyScript.set_version_info(version);
|
||||
pyscript_module.destroy();
|
||||
|
||||
// TODO: Currently adding the imports for backwards compatibility, we should
|
||||
// remove it
|
||||
await runtime.run(`
|
||||
from pyscript import *
|
||||
`);
|
||||
logger.warn(`DEPRECATION WARNING: 'micropip', 'Element', 'console', 'document' and several other \
|
||||
objects form the pyscript module (with the exception of 'display') will be \
|
||||
be removed from the Python global namespace in the following release. \
|
||||
To avoid errors in future releases use import from pyscript instead. For instance: \
|
||||
from pyscript import micropip, Element, console, document`);
|
||||
|
||||
logger.info('Packages to install: ', this.config.packages);
|
||||
await runtime.installPackage(this.config.packages);
|
||||
await this.fetchPaths(runtime);
|
||||
|
||||
// Finally load plugins
|
||||
await this.fetchPythonPlugins(runtime);
|
||||
}
|
||||
|
||||
async fetchPaths(runtime: Runtime) {
|
||||
@@ -217,23 +239,63 @@ export class PyScriptApp {
|
||||
// download/startup of pyodide.
|
||||
const [paths, fetchPaths] = calculatePaths(this.config.fetch);
|
||||
logger.info('Paths to fetch: ', fetchPaths);
|
||||
for (let i=0; i<paths.length; i++) {
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
logger.info(` fetching path: ${fetchPaths[i]}`);
|
||||
try {
|
||||
await runtime.loadFromFile(paths[i], fetchPaths[i]);
|
||||
} catch (e) {
|
||||
// The 'TypeError' here happens when running pytest
|
||||
// I'm not particularly happy with this solution.
|
||||
if (e.name === "FetchError" || e.name === "TypeError") {
|
||||
if (e.name === 'FetchError' || e.name === 'TypeError') {
|
||||
handleFetchError(<Error>e, fetchPaths[i]);
|
||||
} else {
|
||||
throw e
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info('All paths fetched');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all the python plugins specified in this.config, saves them on the FS and import
|
||||
* them as modules, executing any plugin define the module scope
|
||||
*
|
||||
* @param runtime - runtime that will execute the plugins
|
||||
*/
|
||||
async fetchPythonPlugins(runtime: Runtime) {
|
||||
const plugins = this.config.plugins;
|
||||
logger.info('Python plugins to fetch: ', plugins);
|
||||
for (const singleFile of plugins) {
|
||||
logger.info(` fetching plugins: ${singleFile}`);
|
||||
try {
|
||||
const pathArr = singleFile.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, singleFile);
|
||||
const modulename = singleFile.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);
|
||||
if (typeof module.plugin !== 'undefined') {
|
||||
const py_plugin = module.plugin;
|
||||
py_plugin.init(this);
|
||||
this.plugins.addPythonPlugin(py_plugin);
|
||||
} else {
|
||||
logger.error(`Cannot find plugin on Python module ${modulename}! Python plugins \
|
||||
modules must contain a "plugin" attribute. For more information check the plugins documentation.`);
|
||||
}
|
||||
} catch (e) {
|
||||
//Should we still export full error contents to console?
|
||||
handleFetchError(<Error>e, singleFile);
|
||||
}
|
||||
}
|
||||
logger.info('All plugins fetched');
|
||||
}
|
||||
|
||||
// lifecycle (7)
|
||||
executeScripts(runtime: Runtime) {
|
||||
this.PyScript = make_PyScript(runtime);
|
||||
@@ -244,7 +306,7 @@ export class PyScriptApp {
|
||||
|
||||
logStatus(msg: string) {
|
||||
logger.info(msg);
|
||||
const ev = new CustomEvent("py-status-message", { detail: msg });
|
||||
const ev = new CustomEvent('py-status-message', { detail: msg });
|
||||
document.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
@@ -262,5 +324,5 @@ globalExport('pyscript_get_config', pyscript_get_config);
|
||||
const globalApp = new PyScriptApp();
|
||||
globalApp.main();
|
||||
|
||||
export { version }
|
||||
export { version };
|
||||
export const runtime = globalApp.runtime;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { Runtime } from './runtime';
|
||||
import type { UserError } from './exceptions';
|
||||
import { getLogger } from './logger';
|
||||
|
||||
const logger = getLogger('plugin');
|
||||
|
||||
export class Plugin {
|
||||
|
||||
/** Validate the configuration of the plugin and handle default values.
|
||||
*
|
||||
* Individual plugins are expected to check that the config keys/sections
|
||||
@@ -16,8 +18,7 @@ export class Plugin {
|
||||
* This hook should **NOT** contain expensive operations, else it delays
|
||||
* the download of the python interpreter which is initiated later.
|
||||
*/
|
||||
configure(config: AppConfig) {
|
||||
}
|
||||
configure(config: AppConfig) {}
|
||||
|
||||
/** The preliminary initialization phase is complete and we are about to
|
||||
* download and launch the Python interpreter.
|
||||
@@ -29,66 +30,107 @@ export class Plugin {
|
||||
* This hook should **NOT** contain expensive operations, else it delays
|
||||
* the download of the python interpreter which is initiated later.
|
||||
*/
|
||||
beforeLaunch(config: AppConfig) {
|
||||
}
|
||||
beforeLaunch(config: AppConfig) {}
|
||||
|
||||
/** The Python interpreter has been launched, the virtualenv has been
|
||||
* installed and we are ready to execute user code.
|
||||
*
|
||||
* The <py-script> tags will be executed after this hook.
|
||||
*/
|
||||
afterSetup(runtime: Runtime) {
|
||||
}
|
||||
|
||||
* installed and we are ready to execute user code.
|
||||
*
|
||||
* The <py-script> tags will be executed after this hook.
|
||||
*/
|
||||
afterSetup(runtime: Runtime) {}
|
||||
|
||||
/** 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(runtime: Runtime) {}
|
||||
|
||||
/** Called when an UserError is raised
|
||||
*/
|
||||
onUserError(error: UserError) {
|
||||
}
|
||||
onUserError(error: UserError) {}
|
||||
}
|
||||
|
||||
|
||||
export class PluginManager {
|
||||
_plugins: Plugin[];
|
||||
_pythonPlugins: any[];
|
||||
|
||||
constructor() {
|
||||
this._plugins = [];
|
||||
this._pythonPlugins = [];
|
||||
}
|
||||
|
||||
add(...plugins: Plugin[]) {
|
||||
for (const p of plugins)
|
||||
this._plugins.push(p);
|
||||
for (const p of plugins) this._plugins.push(p);
|
||||
}
|
||||
|
||||
addPythonPlugin(plugin: any) {
|
||||
this._pythonPlugins.push(plugin);
|
||||
}
|
||||
|
||||
configure(config: AppConfig) {
|
||||
for (const p of this._plugins)
|
||||
p.configure(config);
|
||||
for (const p of this._plugins) p.configure(config);
|
||||
|
||||
for (const p of this._pythonPlugins) p.configure?.(config);
|
||||
}
|
||||
|
||||
beforeLaunch(config: AppConfig) {
|
||||
for (const p of this._plugins)
|
||||
p.beforeLaunch(config);
|
||||
for (const p of this._plugins) p.beforeLaunch(config);
|
||||
}
|
||||
|
||||
afterSetup(runtime: Runtime) {
|
||||
for (const p of this._plugins)
|
||||
p.afterSetup(runtime);
|
||||
for (const p of this._plugins) p.afterSetup(runtime);
|
||||
|
||||
for (const p of this._pythonPlugins) p.afterSetup?.(runtime);
|
||||
}
|
||||
|
||||
afterStartup(runtime: Runtime) {
|
||||
for (const p of this._plugins)
|
||||
p.afterStartup(runtime);
|
||||
for (const p of this._plugins) p.afterStartup(runtime);
|
||||
|
||||
for (const p of this._pythonPlugins) p.afterStartup?.(runtime);
|
||||
}
|
||||
|
||||
onUserError(error: UserError) {
|
||||
for (const p of this._plugins)
|
||||
p.onUserError(error);
|
||||
for (const p of this._plugins) p.onUserError(error);
|
||||
|
||||
for (const p of this._pythonPlugins) p.onUserError?.(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a new CustomElement (via customElement.defines) with `tag`,
|
||||
* where the new CustomElement is a proxy that delegates the logic to
|
||||
* pyPluginClass.
|
||||
*
|
||||
* @param tag - tag that will be used to define the new CustomElement (i.e: "py-script")
|
||||
* @param pyPluginClass - class that will be used to create instance to be
|
||||
* used as CustomElement logic handler. Any DOM event
|
||||
* received by the newly created CustomElement will be
|
||||
* delegated to that instance.
|
||||
*/
|
||||
export function define_custom_element(tag: string, pyPluginClass: any): any {
|
||||
logger.info(`creating plugin: ${tag}`);
|
||||
class ProxyCustomElement extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
wrapper: HTMLElement;
|
||||
pyPluginInstance: any;
|
||||
originalInnerHTML: string;
|
||||
|
||||
constructor() {
|
||||
logger.debug(`creating ${tag} plugin instance`);
|
||||
super();
|
||||
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
this.wrapper = document.createElement('slot');
|
||||
this.shadow.appendChild(this.wrapper);
|
||||
this.originalInnerHTML = this.innerHTML;
|
||||
this.pyPluginInstance = pyPluginClass(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const innerHTML = this.pyPluginInstance.connect();
|
||||
if (typeof innerHTML === 'string') this.innerHTML = innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(tag, ProxyCustomElement);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export function calculatePaths(fetch_cfg: FetchConfig[]) {
|
||||
const fetchPaths: string[] = [];
|
||||
const paths: string[] = [];
|
||||
fetch_cfg.forEach(function (each_fetch_cfg: FetchConfig) {
|
||||
const from = each_fetch_cfg.from || "";
|
||||
const to_folder = each_fetch_cfg.to_folder || ".";
|
||||
const from = each_fetch_cfg.from || '';
|
||||
const to_folder = each_fetch_cfg.to_folder || '.';
|
||||
const to_file = each_fetch_cfg.to_file;
|
||||
const files = each_fetch_cfg.files;
|
||||
if (files !== undefined)
|
||||
@@ -19,16 +19,13 @@ export function calculatePaths(fetch_cfg: FetchConfig[]) {
|
||||
`Cannot use 'to_file' and 'files' parameters together!`
|
||||
);
|
||||
}
|
||||
for (const each_f of files)
|
||||
{
|
||||
for (const each_f of files) {
|
||||
const each_fetch_path = joinPaths([from, each_f]);
|
||||
fetchPaths.push(each_fetch_path);
|
||||
const each_path = joinPaths([to_folder, each_f]);
|
||||
paths.push(each_path);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
fetchPaths.push(from);
|
||||
const filename = to_file || from.split('/').pop();
|
||||
if (filename === '') {
|
||||
|
||||
@@ -11,7 +11,6 @@ type ImportMapType = {
|
||||
};
|
||||
|
||||
export class ImportmapPlugin extends Plugin {
|
||||
|
||||
async afterSetup(runtime: Runtime) {
|
||||
// make importmap ES modules available from python using 'import'.
|
||||
//
|
||||
@@ -26,9 +25,8 @@ export class ImportmapPlugin extends Plugin {
|
||||
const importmap: ImportMapType = (() => {
|
||||
try {
|
||||
return JSON.parse(node.textContent) as ImportMapType;
|
||||
}
|
||||
catch(error) {
|
||||
showWarning("Failed to parse import map: " + error.message);
|
||||
} catch (error) {
|
||||
showWarning('Failed to parse import map: ' + error.message);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -47,10 +45,9 @@ export class ImportmapPlugin extends Plugin {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info("Registering JS module", name);
|
||||
logger.info('Registering JS module', name);
|
||||
runtime.registerJsModule(name, exports);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,10 +19,7 @@ export class PyTerminalPlugin extends Plugin {
|
||||
configure(config: AppConfig) {
|
||||
// validate the terminal config and handle default values
|
||||
const t = config.terminal;
|
||||
if (t !== undefined &&
|
||||
t !== true &&
|
||||
t !== false &&
|
||||
t !== "auto") {
|
||||
if (t !== undefined && t !== true && t !== false && t !== 'auto') {
|
||||
const got = JSON.stringify(t);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
@@ -31,7 +28,7 @@ export class PyTerminalPlugin extends Plugin {
|
||||
);
|
||||
}
|
||||
if (t === undefined) {
|
||||
config.terminal = "auto"; // default value
|
||||
config.terminal = 'auto'; // default value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +36,11 @@ export class PyTerminalPlugin extends Plugin {
|
||||
// if config.terminal is "yes" or "auto", let's add a <py-terminal> to
|
||||
// the document, unless it's already present.
|
||||
const t = config.terminal;
|
||||
if (t === true || t === "auto") {
|
||||
if (t === true || t === 'auto') {
|
||||
if (document.querySelector('py-terminal') === null) {
|
||||
logger.info("No <py-terminal> found, adding one");
|
||||
logger.info('No <py-terminal> found, adding one');
|
||||
const termElem = document.createElement('py-terminal');
|
||||
if (t === "auto")
|
||||
termElem.setAttribute("auto", "");
|
||||
if (t === 'auto') termElem.setAttribute('auto', '');
|
||||
document.body.appendChild(termElem);
|
||||
}
|
||||
}
|
||||
@@ -74,9 +70,7 @@ export class PyTerminalPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function make_PyTerminal(app: PyScriptApp) {
|
||||
|
||||
/** The <py-terminal> custom element, which automatically register a stdio
|
||||
* listener to capture and display stdout/stderr
|
||||
*/
|
||||
@@ -95,8 +89,7 @@ function make_PyTerminal(app: PyScriptApp) {
|
||||
if (this.isAuto()) {
|
||||
this.classList.add('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = true;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.autoShowOnNextLine = false;
|
||||
}
|
||||
|
||||
@@ -105,12 +98,12 @@ function make_PyTerminal(app: PyScriptApp) {
|
||||
}
|
||||
|
||||
isAuto() {
|
||||
return this.hasAttribute("auto");
|
||||
return this.hasAttribute('auto');
|
||||
}
|
||||
|
||||
// implementation of the Stdio interface
|
||||
stdout_writeline(msg: string) {
|
||||
this.outElem.innerText += msg + "\n";
|
||||
this.outElem.innerText += msg + '\n';
|
||||
if (this.autoShowOnNextLine) {
|
||||
this.classList.remove('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = false;
|
||||
|
||||
31
pyscriptjs/src/plugins/python/py_markdown.py
Normal file
31
pyscriptjs/src/plugins/python/py_markdown.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from textwrap import dedent
|
||||
|
||||
from markdown import markdown
|
||||
from pyscript import Plugin, console
|
||||
|
||||
console.warning(
|
||||
"WARNING: This plugin is still in a very experimental phase and will likely change"
|
||||
" and potentially break in the future releases. Use it with caution."
|
||||
)
|
||||
|
||||
|
||||
class MyPlugin(Plugin):
|
||||
def configure(self, config):
|
||||
console.log(f"configuration received: {config}")
|
||||
|
||||
def afterStartup(self, runtime):
|
||||
console.log(f"runtime received: {runtime}")
|
||||
|
||||
|
||||
plugin = MyPlugin("py-markdown")
|
||||
|
||||
|
||||
@plugin.register_custom_element("py-md")
|
||||
class PyMarkdown:
|
||||
def __init__(self, element):
|
||||
self.element = element
|
||||
|
||||
def connect(self):
|
||||
self.element.innerHTML = markdown(
|
||||
dedent(self.element.source), extensions=["fenced_code"]
|
||||
)
|
||||
@@ -27,7 +27,7 @@ export class SplashscreenPlugin extends Plugin {
|
||||
// deprecation warning)
|
||||
this.autoclose = true;
|
||||
|
||||
if ("autoclose_loader" in config) {
|
||||
if ('autoclose_loader' in config) {
|
||||
this.autoclose = config.autoclose_loader;
|
||||
showWarning(AUTOCLOSE_LOADER_DEPRECATED);
|
||||
}
|
||||
|
||||
@@ -198,8 +198,7 @@ function validateConfig(configText: string, configType = 'toml') {
|
||||
}
|
||||
finalConfig[item].push(runtimeConfig);
|
||||
});
|
||||
}
|
||||
else if (item === 'fetch') {
|
||||
} else if (item === 'fetch') {
|
||||
finalConfig[item] = [];
|
||||
const fetchList = config[item] as FetchConfig[];
|
||||
fetchList.forEach(function (eachFetch: FetchConfig) {
|
||||
@@ -212,8 +211,7 @@ function validateConfig(configText: string, configType = 'toml') {
|
||||
}
|
||||
finalConfig[item].push(eachFetchConfig);
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
finalConfig[item] = config[item];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
|
||||
ensureUniqueId(outElem);
|
||||
set_current_display_target(outElem.id);
|
||||
//This is the python function defined in pyscript.py
|
||||
const usesTopLevelAwait = runtime.globals.get('uses_top_level_await')
|
||||
const usesTopLevelAwait = runtime.globals.get('uses_top_level_await');
|
||||
try {
|
||||
try {
|
||||
if (usesTopLevelAwait(pysrc)){
|
||||
if (usesTopLevelAwait(pysrc)) {
|
||||
throw new UserError(
|
||||
ErrorCode.TOP_LEVEL_AWAIT,
|
||||
'The use of top-level "await", "async for", and ' +
|
||||
|
||||
@@ -59,8 +59,12 @@ export class PyodideRuntime extends Runtime {
|
||||
async loadInterpreter(): Promise<void> {
|
||||
logger.info('Loading pyodide');
|
||||
this.interpreter = await loadPyodide({
|
||||
stdout: (msg: string) => { this.stdio.stdout_writeline(msg); },
|
||||
stderr: (msg: string) => { this.stdio.stderr_writeline(msg); },
|
||||
stdout: (msg: string) => {
|
||||
this.stdio.stdout_writeline(msg);
|
||||
},
|
||||
stderr: (msg: string) => {
|
||||
this.stdio.stderr_writeline(msg);
|
||||
},
|
||||
fullStdLib: false,
|
||||
});
|
||||
|
||||
@@ -68,11 +72,6 @@ export class PyodideRuntime extends Runtime {
|
||||
|
||||
// XXX: ideally, we should load micropip only if we actually need it
|
||||
await this.loadPackage('micropip');
|
||||
|
||||
logger.info('importing pyscript.py');
|
||||
this.run(pyscript as string);
|
||||
this.run(`PyScript.set_version_info('${version}')`)
|
||||
|
||||
logger.info('pyodide loaded and initialized');
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ from textwrap import dedent
|
||||
import micropip # noqa: F401
|
||||
from js import console, document
|
||||
|
||||
try:
|
||||
from pyodide import create_proxy
|
||||
except ImportError:
|
||||
from pyodide.ffi import create_proxy
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
MIME_METHODS = {
|
||||
@@ -463,4 +468,27 @@ def uses_top_level_await(source: str) -> bool:
|
||||
return TopLevelAsyncFinder().is_source_top_level_await(source)
|
||||
|
||||
|
||||
class Plugin:
|
||||
def __init__(self, name=None):
|
||||
if not name:
|
||||
name = self.__class__.__name__
|
||||
|
||||
self.name = name
|
||||
|
||||
def init(self, app):
|
||||
self.app = app
|
||||
self.app.plugins.addPythonPlugin(create_proxy(self))
|
||||
|
||||
def register_custom_element(self, tag):
|
||||
# TODO: Ideally would be better to use the logger.
|
||||
console.info(f"Defining new custom element {tag}")
|
||||
|
||||
def wrapper(class_):
|
||||
# TODO: this is very pyodide specific but will have to do
|
||||
# until we have JS interface that works across interpreters
|
||||
define_custom_element(tag, create_proxy(class_)) # noqa: F821
|
||||
|
||||
return create_proxy(wrapper)
|
||||
|
||||
|
||||
pyscript = PyScript()
|
||||
|
||||
@@ -8,8 +8,8 @@ export interface Stdio {
|
||||
*/
|
||||
export const DEFAULT_STDIO: Stdio = {
|
||||
stdout_writeline: console.log,
|
||||
stderr_writeline: console.log
|
||||
}
|
||||
stderr_writeline: console.log,
|
||||
};
|
||||
|
||||
/** Stdio provider which captures and store the messages.
|
||||
* Useful for tests.
|
||||
@@ -23,16 +23,16 @@ export class CaptureStdio implements Stdio {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.captured_stdout = "";
|
||||
this.captured_stderr = "";
|
||||
this.captured_stdout = '';
|
||||
this.captured_stderr = '';
|
||||
}
|
||||
|
||||
stdout_writeline(msg: string) {
|
||||
this.captured_stdout += msg + "\n";
|
||||
this.captured_stdout += msg + '\n';
|
||||
}
|
||||
|
||||
stderr_writeline(msg: string) {
|
||||
this.captured_stderr += msg + "\n";
|
||||
this.captured_stderr += msg + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,10 @@ export class StdioMultiplexer implements Stdio {
|
||||
}
|
||||
|
||||
stdout_writeline(msg: string) {
|
||||
for(const obj of this._listeners)
|
||||
obj.stdout_writeline(msg);
|
||||
for (const obj of this._listeners) obj.stdout_writeline(msg);
|
||||
}
|
||||
|
||||
stderr_writeline(msg: string) {
|
||||
for(const obj of this._listeners)
|
||||
obj.stderr_writeline(msg);
|
||||
for (const obj of this._listeners) obj.stderr_writeline(msg);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,8 @@ export function ensureUniqueId(el: HTMLElement) {
|
||||
if (el.id === '') el.id = `py-internal-${_uniqueIdCounter++}`;
|
||||
}
|
||||
|
||||
export function showWarning(msg: string, messageType: "text" | "html" = "text"): void {
|
||||
_createAlertBanner(msg, "warning", messageType);
|
||||
export function showWarning(msg: string, messageType: 'text' | 'html' = 'text'): void {
|
||||
_createAlertBanner(msg, 'warning', messageType);
|
||||
}
|
||||
|
||||
export function handleFetchError(e: Error, singleFile: string) {
|
||||
@@ -64,7 +64,7 @@ export function handleFetchError(e: Error, singleFile: string) {
|
||||
} else {
|
||||
errorContent = `PyScript encountered an error while loading from file: ${e.message}`;
|
||||
}
|
||||
throw new UserError(ErrorCode.FETCH_ERROR, errorContent, "html");
|
||||
throw new UserError(ErrorCode.FETCH_ERROR, errorContent, 'html');
|
||||
}
|
||||
|
||||
export function readTextFromPath(path: string) {
|
||||
@@ -98,10 +98,14 @@ export function getAttribute(el: Element, attr: string): string | null {
|
||||
}
|
||||
|
||||
export function joinPaths(parts: string[], separator = '/') {
|
||||
const res = parts.map(function(part) { return part.trim().replace(/(^[/]*|[/]*$)/g, ''); }).filter(p => p!== "").join(separator || '/');
|
||||
if (parts[0].startsWith('/'))
|
||||
{
|
||||
return '/'+res;
|
||||
const res = parts
|
||||
.map(function (part) {
|
||||
return part.trim().replace(/(^[/]*|[/]*$)/g, '');
|
||||
})
|
||||
.filter(p => p !== '')
|
||||
.join(separator || '/');
|
||||
if (parts[0].startsWith('/')) {
|
||||
return '/' + res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -115,6 +119,6 @@ export function createDeprecationWarning(msg: string, elementName: string): void
|
||||
}
|
||||
}
|
||||
if (bannerCount == 0) {
|
||||
_createAlertBanner(msg, "warning");
|
||||
_createAlertBanner(msg, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user