mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-21 03:05:38 -05:00
Allow fetching plugins from URL (#1065)
This commit is contained in:
@@ -8,7 +8,7 @@ import { PluginManager, define_custom_element } from './plugin';
|
||||
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
import { PyodideRuntime } from './pyodide';
|
||||
import { getLogger } from './logger';
|
||||
import { handleFetchError, showWarning, globalExport } from './utils';
|
||||
import { showWarning, globalExport } from './utils';
|
||||
import { calculatePaths } from './plugins/fetch';
|
||||
import { createCustomElements } from './components/elements';
|
||||
import { UserError, ErrorCode, _createAlertBanner } from './exceptions';
|
||||
@@ -20,6 +20,8 @@ import { StdioDirector as StdioDirector } from './plugins/stdiodirector';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import pyscript from './python/pyscript.py';
|
||||
import { robustFetch } from './fetch';
|
||||
import type { Plugin } from './plugin';
|
||||
|
||||
const logger = getLogger('pyscript/main');
|
||||
|
||||
@@ -238,7 +240,7 @@ export class PyScriptApp {
|
||||
//This may be unnecessary - only useful if plugins try to import files fetch'd in fetchPaths()
|
||||
runtime.invalidate_module_path_cache();
|
||||
// Finally load plugins
|
||||
await this.fetchPythonPlugins(runtime);
|
||||
await this.fetchUserPlugins(runtime);
|
||||
}
|
||||
|
||||
async fetchPaths(runtime: Runtime) {
|
||||
@@ -251,63 +253,108 @@ export class PyScriptApp {
|
||||
logger.info('Paths to fetch: ', fetchPaths);
|
||||
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') {
|
||||
handleFetchError(<Error>e, fetchPaths[i]);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Exceptions raised from here will create an alert banner
|
||||
await runtime.loadFromFile(paths[i], fetchPaths[i]);
|
||||
}
|
||||
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
|
||||
* Fetch user plugins and adds them to `this.plugins` so they can
|
||||
* 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 execute the plugins
|
||||
* @param runtime - runtime that will be used to execute the plugins that need it.
|
||||
*/
|
||||
async fetchPythonPlugins(runtime: Runtime) {
|
||||
async fetchUserPlugins(runtime: Runtime) {
|
||||
const plugins = this.config.plugins;
|
||||
logger.info('Python plugins to fetch: ', plugins);
|
||||
logger.info('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);
|
||||
|
||||
//refresh module cache before trying to import module files into runtime
|
||||
runtime.invalidate_module_path_cache();
|
||||
|
||||
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);
|
||||
if (singleFile.endsWith('.py')) {
|
||||
await this.fetchPythonPlugin(runtime, singleFile);
|
||||
} else if (singleFile.endsWith('.js')) {
|
||||
await this.fetchJSPlugin(singleFile);
|
||||
} else {
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_PLUGIN_FILE_EXTENSION,
|
||||
`Unable to load plugin from '${singleFile}'. ` +
|
||||
`Plugins need to contain a file extension and be ` +
|
||||
`either a python or javascript file.`,
|
||||
);
|
||||
}
|
||||
logger.info('All plugins fetched');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a javascript plugin from a filePath, it gets a blob from the
|
||||
* fetch and creates a file from it, then we create a URL from the file
|
||||
* so we can import it as a module.
|
||||
*
|
||||
* This allow us to instantiate the imported plugin with the default
|
||||
* export in the module (the plugin class) and add it to the plugins
|
||||
* list with `new importedPlugin()`.
|
||||
*
|
||||
* @param filePath - URL of the javascript file to fetch.
|
||||
*/
|
||||
async fetchJSPlugin(filePath: string) {
|
||||
const pluginBlob = await (await robustFetch(filePath)).blob();
|
||||
const blobFile = new File([pluginBlob], 'plugin.js', { type: 'text/javascript' });
|
||||
const fileUrl = URL.createObjectURL(blobFile);
|
||||
|
||||
const module = await import(fileUrl);
|
||||
// Note: We have to put module.default in a variable
|
||||
// because we have seen weird behaviour when doing
|
||||
// new module.default() directly.
|
||||
const importedPlugin = module.default;
|
||||
|
||||
// If the imported plugin doesn't have a default export
|
||||
// it will be undefined, so we throw a user error, so
|
||||
// an alter banner will be created.
|
||||
if (importedPlugin === undefined) {
|
||||
throw new UserError(
|
||||
ErrorCode.NO_DEFAULT_EXPORT,
|
||||
`Unable to load plugin from '${filePath}'. ` + `Plugins need to contain a default export.`,
|
||||
);
|
||||
} else {
|
||||
this.plugins.add(new importedPlugin());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 filePath - path to the python file to fetch
|
||||
*/
|
||||
async fetchPythonPlugin(runtime: Runtime, 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);
|
||||
|
||||
//refresh module cache before trying to import module files into runtime
|
||||
runtime.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);
|
||||
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.`);
|
||||
}
|
||||
logger.info('All plugins fetched');
|
||||
}
|
||||
|
||||
// lifecycle (7)
|
||||
|
||||
Reference in New Issue
Block a user