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:
Fabio Pliger
2022-11-28 12:39:31 -06:00
committed by GitHub
parent 446c131ccb
commit 3e408b7baa
35 changed files with 2378 additions and 1216 deletions

View File

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