mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Refactor py-config and the general initialization logic of the page (#806)
This PR is the first step to improve and rationalize the life-cycle of a pyscript app along the lines of what I described in #763 . It is not a complete solution, more PRs will follow. Highlights: - py-config is no longer a web component: the old code relied on PyConfig.connectedCallback to do some logic, but then if no <py-config> tag was present, we had to introduce a dummy one with the sole goal of activating the callback. Now the logic is much more linear. - the new pyconfig.ts only contains the code which is needed to parse the config; I also moved some relevant code from utils.ts because it didn't really belong to it - the old PyConfig class did much more than dealing with the config: in particular, it contained the code to initialize the env and the runtime. Now this logic has been moved directly into main.ts, inside the new PyScriptApp class. I plan to refactor the initialization code in further PRs - the current code relies too much on global state and global variables, they are everywhere. This PR is a first step to solve the problem by introducing a PyScriptApp class, which will hold all the mutable state of the page. Currently only config is stored there, but eventually I will migrate more state to it, until we will have only one global singleton, globalApp - thanks to what I described above, I could kill the appConfig svelte store: one less store to kill :).
This commit is contained in:
@@ -66,7 +66,7 @@ repos:
|
|||||||
- --py310-plus
|
- --py310-plus
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
rev: v8.24.0
|
rev: v8.23.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: eslint
|
- id: eslint
|
||||||
files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
|
files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
import { BaseEvalElement } from './base';
|
|
||||||
import { appConfig, addInitializer, runtimeLoaded } from '../stores';
|
|
||||||
import type { AppConfig, Runtime } from '../runtime';
|
|
||||||
import { version } from '../runtime';
|
|
||||||
import { PyodideRuntime } from '../pyodide';
|
|
||||||
import { getLogger } from '../logger';
|
|
||||||
import { readTextFromPath, handleFetchError, mergeConfig, validateConfig, defaultConfig } from '../utils'
|
|
||||||
|
|
||||||
// Subscriber used to connect to the first available runtime (can be pyodide or others)
|
|
||||||
let runtimeSpec: Runtime;
|
|
||||||
runtimeLoaded.subscribe(value => {
|
|
||||||
runtimeSpec = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
let appConfig_: AppConfig;
|
|
||||||
appConfig.subscribe(value => {
|
|
||||||
appConfig_ = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const logger = getLogger('py-config');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures general metadata about the PyScript application such
|
|
||||||
* as a list of runtimes, name, version, closing the loader
|
|
||||||
* automatically, etc.
|
|
||||||
*
|
|
||||||
* Also initializes the different runtimes passed. If no runtime is passed,
|
|
||||||
* the default runtime based on Pyodide is used.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class PyConfig extends BaseEvalElement {
|
|
||||||
widths: Array<string>;
|
|
||||||
label: string;
|
|
||||||
mount_name: string;
|
|
||||||
details: HTMLElement;
|
|
||||||
operation: HTMLElement;
|
|
||||||
values: AppConfig;
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
extractFromSrc(configType: string) {
|
|
||||||
if (this.hasAttribute('src'))
|
|
||||||
{
|
|
||||||
logger.info('config set from src attribute');
|
|
||||||
return validateConfig(readTextFromPath(this.getAttribute('src')), configType);
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
extractFromInline(configType: string) {
|
|
||||||
if (this.innerHTML!=='')
|
|
||||||
{
|
|
||||||
this.code = this.innerHTML;
|
|
||||||
this.innerHTML = '';
|
|
||||||
logger.info('config set from inline');
|
|
||||||
return validateConfig(this.code, configType);
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
injectMetadata() {
|
|
||||||
this.values.pyscript = {
|
|
||||||
"version": version,
|
|
||||||
"time": new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
const configType: string = this.hasAttribute("type") ? this.getAttribute("type") : "toml";
|
|
||||||
let srcConfig = this.extractFromSrc(configType);
|
|
||||||
const inlineConfig = this.extractFromInline(configType);
|
|
||||||
// first make config from src whole if it is partial
|
|
||||||
srcConfig = mergeConfig(srcConfig, defaultConfig);
|
|
||||||
// then merge inline config and config from src
|
|
||||||
this.values = mergeConfig(inlineConfig, srcConfig);
|
|
||||||
this.injectMetadata();
|
|
||||||
|
|
||||||
appConfig.set(this.values);
|
|
||||||
logger.info('config set:', this.values);
|
|
||||||
|
|
||||||
addInitializer(this.loadPackages);
|
|
||||||
addInitializer(this.loadPaths);
|
|
||||||
this.loadRuntimes();
|
|
||||||
}
|
|
||||||
|
|
||||||
log(msg: string) {
|
|
||||||
const newLog = document.createElement('p');
|
|
||||||
newLog.innerText = msg;
|
|
||||||
this.details.appendChild(newLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPackages = async () => {
|
|
||||||
const env = appConfig_.packages;
|
|
||||||
logger.info("Loading env: ", env);
|
|
||||||
await runtimeSpec.installPackage(env);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPaths = async () => {
|
|
||||||
const paths = appConfig_.paths;
|
|
||||||
logger.info("Paths to load: ", paths)
|
|
||||||
for (const singleFile of paths) {
|
|
||||||
logger.info(` loading path: ${singleFile}`);
|
|
||||||
try {
|
|
||||||
await runtimeSpec.loadFromFile(singleFile);
|
|
||||||
} catch (e) {
|
|
||||||
//Should we still export full error contents to console?
|
|
||||||
handleFetchError(<Error>e, singleFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info("All paths loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
loadRuntimes() {
|
|
||||||
logger.info('Initializing runtimes');
|
|
||||||
for (const runtime of this.values.runtimes) {
|
|
||||||
const runtimeObj: Runtime = new PyodideRuntime(runtime.src, runtime.name, runtime.lang);
|
|
||||||
const script = document.createElement('script'); // create a script DOM node
|
|
||||||
script.src = runtimeObj.src; // set its src to the provided URL
|
|
||||||
script.addEventListener('load', () => {
|
|
||||||
void runtimeObj.initialize();
|
|
||||||
});
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +1,104 @@
|
|||||||
import './styles/pyscript_base.css';
|
import './styles/pyscript_base.css';
|
||||||
|
|
||||||
|
import { loadConfigFromElement } from './pyconfig';
|
||||||
|
import type { AppConfig } from './pyconfig';
|
||||||
|
import type { Runtime } from './runtime';
|
||||||
import { PyScript } from './components/pyscript';
|
import { PyScript } from './components/pyscript';
|
||||||
import { PyEnv } from './components/pyenv';
|
import { PyEnv } from './components/pyenv';
|
||||||
import { PyLoader } from './components/pyloader';
|
import { PyLoader } from './components/pyloader';
|
||||||
import { PyConfig } from './components/pyconfig';
|
import { PyodideRuntime } from './pyodide';
|
||||||
import { getLogger } from './logger';
|
import { getLogger } from './logger';
|
||||||
import { globalLoader } from './stores';
|
import { globalLoader, runtimeLoaded, addInitializer } from './stores';
|
||||||
|
import { handleFetchError, globalExport } from './utils'
|
||||||
|
|
||||||
const logger = getLogger('pyscript/main');
|
const logger = getLogger('pyscript/main');
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
// XXX this should be killed eventually
|
||||||
const xPyScript = customElements.define('py-script', PyScript);
|
let runtimeSpec: Runtime;
|
||||||
const xPyLoader = customElements.define('py-loader', PyLoader);
|
runtimeLoaded.subscribe(value => {
|
||||||
const xPyConfig = customElements.define('py-config', PyConfig);
|
runtimeSpec = value;
|
||||||
const xPyEnv = customElements.define('py-env', PyEnv);
|
});
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
|
class PyScriptApp {
|
||||||
|
|
||||||
|
config: AppConfig;
|
||||||
|
|
||||||
|
main() {
|
||||||
|
this.loadConfig();
|
||||||
|
this.initialize();
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
const xPyScript = customElements.define('py-script', PyScript);
|
||||||
|
const xPyLoader = customElements.define('py-loader', PyLoader);
|
||||||
|
const xPyEnv = customElements.define('py-env', PyEnv);
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
|
// add loader to the page body
|
||||||
|
logger.info('add py-loader');
|
||||||
|
const loader = <PyLoader>document.createElement('py-loader');
|
||||||
|
document.body.append(loader);
|
||||||
|
globalLoader.set(loader);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfig() {
|
||||||
|
// find the <py-config> tag. If not found, we get null which means
|
||||||
|
// "use the default config"
|
||||||
|
// XXX: we should actively complain if there are multiple <py-config>
|
||||||
|
// and show a big error. PRs welcome :)
|
||||||
|
logger.info('searching for <py-config>');
|
||||||
|
const el = document.querySelector('py-config');
|
||||||
|
this.config = loadConfigFromElement(el);
|
||||||
|
logger.info('config loaded:\n' + JSON.stringify(this.config, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
addInitializer(this.loadPackages);
|
||||||
|
addInitializer(this.loadPaths);
|
||||||
|
this.loadRuntimes();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPackages = async () => {
|
||||||
|
logger.info("Packages to install: ", this.config.packages);
|
||||||
|
await runtimeSpec.installPackage(this.config.packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPaths = async () => {
|
||||||
|
const paths = this.config.paths;
|
||||||
|
logger.info("Paths to load: ", paths)
|
||||||
|
for (const singleFile of paths) {
|
||||||
|
logger.info(` loading path: ${singleFile}`);
|
||||||
|
try {
|
||||||
|
await runtimeSpec.loadFromFile(singleFile);
|
||||||
|
} catch (e) {
|
||||||
|
//Should we still export full error contents to console?
|
||||||
|
handleFetchError(<Error>e, singleFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("All paths loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRuntimes() {
|
||||||
|
logger.info('Initializing runtimes');
|
||||||
|
for (const runtime of this.config.runtimes) {
|
||||||
|
const runtimeObj: Runtime = new PyodideRuntime(this.config, runtime.src,
|
||||||
|
runtime.name, runtime.lang);
|
||||||
|
const script = document.createElement('script'); // create a script DOM node
|
||||||
|
script.src = runtimeObj.src; // set its src to the provided URL
|
||||||
|
script.addEventListener('load', () => {
|
||||||
|
void runtimeObj.initialize();
|
||||||
|
});
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// As first thing, loop for application configs
|
|
||||||
logger.info('checking for py-config');
|
|
||||||
const config: PyConfig = document.querySelector('py-config');
|
|
||||||
if (!config) {
|
|
||||||
const loader = document.createElement('py-config');
|
|
||||||
document.body.append(loader);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add loader to the page body
|
function pyscript_get_config() {
|
||||||
logger.info('add py-loader');
|
return globalApp.config;
|
||||||
const loader = <PyLoader>document.createElement('py-loader');
|
}
|
||||||
document.body.append(loader);
|
globalExport('pyscript_get_config', pyscript_get_config);
|
||||||
globalLoader.set(loader);
|
|
||||||
|
// main entry point of execution
|
||||||
|
const globalApp = new PyScriptApp();
|
||||||
|
globalApp.main();
|
||||||
|
|||||||
232
pyscriptjs/src/pyconfig.ts
Normal file
232
pyscriptjs/src/pyconfig.ts
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import toml from '../src/toml'
|
||||||
|
import { getLogger } from './logger';
|
||||||
|
import { version } from './runtime';
|
||||||
|
import { readTextFromPath, showError } from './utils'
|
||||||
|
|
||||||
|
const logger = getLogger('py-config');
|
||||||
|
|
||||||
|
export interface AppConfig extends Record<string, any> {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
version?: string;
|
||||||
|
schema_version?: number;
|
||||||
|
type?: string;
|
||||||
|
author_name?: string;
|
||||||
|
author_email?: string;
|
||||||
|
license?: string;
|
||||||
|
autoclose_loader?: boolean;
|
||||||
|
runtimes?: Array<RuntimeConfig>;
|
||||||
|
packages?: Array<string>;
|
||||||
|
paths?: Array<string>;
|
||||||
|
plugins?: Array<string>;
|
||||||
|
pyscript?: PyScriptMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RuntimeConfig = {
|
||||||
|
src?: string;
|
||||||
|
name?: string;
|
||||||
|
lang?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PyScriptMetadata = {
|
||||||
|
version?: string;
|
||||||
|
time?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allKeys = {
|
||||||
|
"string": ["name", "description", "version", "type", "author_name", "author_email", "license"],
|
||||||
|
"number": ["schema_version"],
|
||||||
|
"boolean": ["autoclose_loader"],
|
||||||
|
"array": ["runtimes", "packages", "paths", "plugins"]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultConfig: AppConfig = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"type": "app",
|
||||||
|
"autoclose_loader": true,
|
||||||
|
"runtimes": [{
|
||||||
|
"src": "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js",
|
||||||
|
"name": "pyodide-0.21.2",
|
||||||
|
"lang": "python"
|
||||||
|
}],
|
||||||
|
"packages":[],
|
||||||
|
"paths":[],
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function loadConfigFromElement(el: Element): AppConfig {
|
||||||
|
let srcConfig;
|
||||||
|
let inlineConfig;
|
||||||
|
if (el === null) {
|
||||||
|
srcConfig = {};
|
||||||
|
inlineConfig = {};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const configType: string = el.hasAttribute("type") ? el.getAttribute("type") : "toml";
|
||||||
|
srcConfig = extractFromSrc(el, configType);
|
||||||
|
inlineConfig = extractFromInline(el, configType);
|
||||||
|
}
|
||||||
|
srcConfig = mergeConfig(srcConfig, defaultConfig);
|
||||||
|
const result = mergeConfig(inlineConfig, srcConfig);
|
||||||
|
result.pyscript = {
|
||||||
|
"version": version,
|
||||||
|
"time": new Date().toISOString()
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFromSrc(el: Element, configType: string) {
|
||||||
|
if (el.hasAttribute('src'))
|
||||||
|
{
|
||||||
|
const src = el.getAttribute('src');
|
||||||
|
logger.info('loading ', src)
|
||||||
|
return validateConfig(readTextFromPath(src), configType);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function extractFromInline(el: Element, configType: string) {
|
||||||
|
if (el.innerHTML !== '')
|
||||||
|
{
|
||||||
|
logger.info('loading <py-config> content');
|
||||||
|
return validateConfig(el.innerHTML, configType);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillUserData(inputConfig: AppConfig, resultConfig: AppConfig): AppConfig
|
||||||
|
{
|
||||||
|
for (const key in inputConfig)
|
||||||
|
{
|
||||||
|
// fill in all extra keys ignored by the validator
|
||||||
|
if (!(key in defaultConfig))
|
||||||
|
{
|
||||||
|
resultConfig[key] = inputConfig[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeConfig(inlineConfig: AppConfig, externalConfig: AppConfig): AppConfig {
|
||||||
|
if (Object.keys(inlineConfig).length === 0 && Object.keys(externalConfig).length === 0)
|
||||||
|
{
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
else if (Object.keys(inlineConfig).length === 0)
|
||||||
|
{
|
||||||
|
return externalConfig;
|
||||||
|
}
|
||||||
|
else if(Object.keys(externalConfig).length === 0)
|
||||||
|
{
|
||||||
|
return inlineConfig;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let merged: AppConfig = {};
|
||||||
|
|
||||||
|
for (const keyType in allKeys)
|
||||||
|
{
|
||||||
|
const keys = allKeys[keyType];
|
||||||
|
keys.forEach(function(item: string){
|
||||||
|
if (keyType === "boolean")
|
||||||
|
{
|
||||||
|
merged[item] = (typeof inlineConfig[item] !== "undefined") ? inlineConfig[item] : externalConfig[item];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
merged[item] = inlineConfig[item] || externalConfig[item];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill extra keys from external first
|
||||||
|
// they will be overridden by inline if extra keys also clash
|
||||||
|
merged = fillUserData(externalConfig, merged);
|
||||||
|
merged = fillUserData(inlineConfig, merged);
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfig(configText: string, configType = "toml") {
|
||||||
|
let config: object;
|
||||||
|
if (configType === "toml") {
|
||||||
|
try {
|
||||||
|
// TOML parser is soft and can parse even JSON strings, this additional check prevents it.
|
||||||
|
if (configText.trim()[0] === "{")
|
||||||
|
{
|
||||||
|
const errMessage = `config supplied: ${configText} is an invalid TOML and cannot be parsed`;
|
||||||
|
showError(`<p>${errMessage}</p>`);
|
||||||
|
throw Error(errMessage);
|
||||||
|
}
|
||||||
|
config = toml.parse(configText);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
const errMessage: string = err.toString();
|
||||||
|
showError(`<p>config supplied: ${configText} is an invalid TOML and cannot be parsed: ${errMessage}</p>`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (configType === "json") {
|
||||||
|
try {
|
||||||
|
config = JSON.parse(configText);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
const errMessage: string = err.toString();
|
||||||
|
showError(`<p>config supplied: ${configText} is an invalid JSON and cannot be parsed: ${errMessage}</p>`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
showError(`<p>type of config supplied is: ${configType}, supported values are ["toml", "json"].</p>`);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConfig(configText: string, configType = "toml") {
|
||||||
|
const config = parseConfig(configText, configType);
|
||||||
|
|
||||||
|
const finalConfig: AppConfig = {}
|
||||||
|
|
||||||
|
for (const keyType in allKeys)
|
||||||
|
{
|
||||||
|
const keys = allKeys[keyType];
|
||||||
|
keys.forEach(function(item: string){
|
||||||
|
if (validateParamInConfig(item, keyType, config))
|
||||||
|
{
|
||||||
|
if (item === "runtimes")
|
||||||
|
{
|
||||||
|
finalConfig[item] = [];
|
||||||
|
const runtimes = config[item];
|
||||||
|
runtimes.forEach(function(eachRuntime: object){
|
||||||
|
const runtimeConfig: object = {};
|
||||||
|
for (const eachRuntimeParam in eachRuntime)
|
||||||
|
{
|
||||||
|
if (validateParamInConfig(eachRuntimeParam, "string", eachRuntime))
|
||||||
|
{
|
||||||
|
runtimeConfig[eachRuntimeParam] = eachRuntime[eachRuntimeParam];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalConfig[item].push(runtimeConfig);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finalConfig[item] = config[item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fillUserData(config, finalConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateParamInConfig(paramName: string, paramType: string, config: object): boolean {
|
||||||
|
if (paramName in config)
|
||||||
|
{
|
||||||
|
return paramType === "array" ? Array.isArray(config[paramName]) : typeof config[paramName] === paramType;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Runtime, RuntimeConfig } from './runtime';
|
import { Runtime } from './runtime';
|
||||||
import { getLastPath } from './utils';
|
import { getLastPath } from './utils';
|
||||||
import { getLogger } from './logger';
|
import { getLogger } from './logger';
|
||||||
import type { PyodideInterface } from 'pyodide';
|
import type { PyodideInterface } from 'pyodide';
|
||||||
@@ -16,12 +16,13 @@ export class PyodideRuntime extends Runtime {
|
|||||||
globals: any;
|
globals: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
config,
|
||||||
src = 'https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js',
|
src = 'https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js',
|
||||||
name = 'pyodide-default',
|
name = 'pyodide-default',
|
||||||
lang = 'python',
|
lang = 'python',
|
||||||
) {
|
) {
|
||||||
logger.info('Runtime config:', { name, lang, src });
|
logger.info('Runtime config:', { name, lang, src });
|
||||||
super();
|
super(config);
|
||||||
this.src = src;
|
this.src = src;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.lang = lang;
|
this.lang = lang;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AppConfig } from './pyconfig';
|
||||||
import type { PyodideInterface } from 'pyodide';
|
import type { PyodideInterface } from 'pyodide';
|
||||||
import type { PyLoader } from './components/pyloader';
|
import type { PyLoader } from './components/pyloader';
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
postInitializers,
|
postInitializers,
|
||||||
Initializer,
|
Initializer,
|
||||||
scriptsQueue,
|
scriptsQueue,
|
||||||
appConfig,
|
|
||||||
} from './stores';
|
} from './stores';
|
||||||
import { createCustomElements } from './components/elements';
|
import { createCustomElements } from './components/elements';
|
||||||
import type { PyScript } from './components/pyscript';
|
import type { PyScript } from './components/pyscript';
|
||||||
@@ -19,33 +19,6 @@ const logger = getLogger('pyscript/runtime');
|
|||||||
export const version = "<<VERSION>>";
|
export const version = "<<VERSION>>";
|
||||||
export type RuntimeInterpreter = PyodideInterface | null;
|
export type RuntimeInterpreter = PyodideInterface | null;
|
||||||
|
|
||||||
export interface AppConfig extends Record<string, any> {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
version?: string;
|
|
||||||
schema_version?: number;
|
|
||||||
type?: string;
|
|
||||||
author_name?: string;
|
|
||||||
author_email?: string;
|
|
||||||
license?: string;
|
|
||||||
autoclose_loader?: boolean;
|
|
||||||
runtimes?: Array<RuntimeConfig>;
|
|
||||||
packages?: Array<string>;
|
|
||||||
paths?: Array<string>;
|
|
||||||
plugins?: Array<string>;
|
|
||||||
pyscript?: PyScriptMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PyScriptMetadata = {
|
|
||||||
version?: string;
|
|
||||||
time?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RuntimeConfig = {
|
|
||||||
src?: string;
|
|
||||||
name?: string;
|
|
||||||
lang?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let loader: PyLoader | undefined;
|
let loader: PyLoader | undefined;
|
||||||
globalLoader.subscribe(value => {
|
globalLoader.subscribe(value => {
|
||||||
@@ -67,15 +40,6 @@ scriptsQueue.subscribe((value: PyScript[]) => {
|
|||||||
scriptsQueue_ = value;
|
scriptsQueue_ = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
let appConfig_: AppConfig = {
|
|
||||||
autoclose_loader: true
|
|
||||||
};
|
|
||||||
|
|
||||||
appConfig.subscribe((value: AppConfig) => {
|
|
||||||
if (value) {
|
|
||||||
appConfig_ = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Runtime class is a super class that all different runtimes must respect
|
Runtime class is a super class that all different runtimes must respect
|
||||||
@@ -95,6 +59,7 @@ For an example implementation, refer to the `PyodideRuntime` class
|
|||||||
in `pyodide.ts`
|
in `pyodide.ts`
|
||||||
*/
|
*/
|
||||||
export abstract class Runtime extends Object {
|
export abstract class Runtime extends Object {
|
||||||
|
config: AppConfig;
|
||||||
abstract src: string;
|
abstract src: string;
|
||||||
abstract name?: string;
|
abstract name?: string;
|
||||||
abstract lang?: string;
|
abstract lang?: string;
|
||||||
@@ -104,6 +69,11 @@ export abstract class Runtime extends Object {
|
|||||||
* */
|
* */
|
||||||
abstract globals: any;
|
abstract globals: any;
|
||||||
|
|
||||||
|
constructor(config: AppConfig) {
|
||||||
|
super();
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* loads the interpreter for the runtime and saves an instance of it
|
* loads the interpreter for the runtime and saves an instance of it
|
||||||
* in the `this.interpreter` property along with calling of other
|
* in the `this.interpreter` property along with calling of other
|
||||||
@@ -187,7 +157,7 @@ export abstract class Runtime extends Object {
|
|||||||
// Finally create the custom elements for pyscript such as pybutton
|
// Finally create the custom elements for pyscript such as pybutton
|
||||||
createCustomElements();
|
createCustomElements();
|
||||||
|
|
||||||
if (appConfig_ && appConfig_.autoclose_loader) {
|
if (this.config.autoclose_loader) {
|
||||||
loader?.close();
|
loader?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import type { PyLoader } from './components/pyloader';
|
import type { PyLoader } from './components/pyloader';
|
||||||
import type { PyScript } from './components/pyscript';
|
import type { PyScript } from './components/pyscript';
|
||||||
import type { Runtime, AppConfig } from './runtime';
|
import type { Runtime } from './runtime';
|
||||||
|
import type { AppConfig } from './pyconfig';
|
||||||
import { getLogger } from './logger';
|
import { getLogger } from './logger';
|
||||||
|
|
||||||
export type Initializer = () => Promise<void>;
|
export type Initializer = () => Promise<void>;
|
||||||
@@ -29,7 +30,6 @@ export const scriptsQueue = writable<PyScript[]>([]);
|
|||||||
export const initializers = writable<Initializer[]>([]);
|
export const initializers = writable<Initializer[]>([]);
|
||||||
export const postInitializers = writable<Initializer[]>([]);
|
export const postInitializers = writable<Initializer[]>([]);
|
||||||
export const globalLoader = writable<PyLoader | undefined>();
|
export const globalLoader = writable<PyLoader | undefined>();
|
||||||
export const appConfig = writable<AppConfig>();
|
|
||||||
|
|
||||||
export const addToScriptsQueue = (script: PyScript) => {
|
export const addToScriptsQueue = (script: PyScript) => {
|
||||||
scriptsQueue.update(scriptsQueue => [...scriptsQueue, script]);
|
scriptsQueue.update(scriptsQueue => [...scriptsQueue, script]);
|
||||||
|
|||||||
@@ -1,27 +1,3 @@
|
|||||||
import toml from '../src/toml'
|
|
||||||
import type { AppConfig } from "./runtime";
|
|
||||||
|
|
||||||
const allKeys = {
|
|
||||||
"string": ["name", "description", "version", "type", "author_name", "author_email", "license"],
|
|
||||||
"number": ["schema_version"],
|
|
||||||
"boolean": ["autoclose_loader"],
|
|
||||||
"array": ["runtimes", "packages", "paths", "plugins"]
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultConfig: AppConfig = {
|
|
||||||
"schema_version": 1,
|
|
||||||
"type": "app",
|
|
||||||
"autoclose_loader": true,
|
|
||||||
"runtimes": [{
|
|
||||||
"src": "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js",
|
|
||||||
"name": "pyodide-0.21.2",
|
|
||||||
"lang": "python"
|
|
||||||
}],
|
|
||||||
"packages":[],
|
|
||||||
"paths":[],
|
|
||||||
"plugins": []
|
|
||||||
}
|
|
||||||
|
|
||||||
function addClasses(element: HTMLElement, classes: Array<string>) {
|
function addClasses(element: HTMLElement, classes: Array<string>) {
|
||||||
for (const entry of classes) {
|
for (const entry of classes) {
|
||||||
element.classList.add(entry);
|
element.classList.add(entry);
|
||||||
@@ -121,139 +97,15 @@ function inJest(): boolean {
|
|||||||
return typeof process === 'object' && process.env.JEST_WORKER_ID !== undefined;
|
return typeof process === 'object' && process.env.JEST_WORKER_ID !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillUserData(inputConfig: AppConfig, resultConfig: AppConfig): AppConfig
|
function globalExport(name: string, obj: any) {
|
||||||
{
|
// attach the given object to the global object, so that it is globally
|
||||||
for (const key in inputConfig)
|
// visible everywhere. Should be used very sparingly!
|
||||||
{
|
|
||||||
// fill in all extra keys ignored by the validator
|
// `window` in the browser, `global` in node
|
||||||
if (!(key in defaultConfig))
|
const _global = (window || global) as any;
|
||||||
{
|
_global[name] = obj;
|
||||||
resultConfig[key] = inputConfig[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resultConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeConfig(inlineConfig: AppConfig, externalConfig: AppConfig): AppConfig {
|
|
||||||
if (Object.keys(inlineConfig).length === 0 && Object.keys(externalConfig).length === 0)
|
|
||||||
{
|
|
||||||
return defaultConfig;
|
|
||||||
}
|
|
||||||
else if (Object.keys(inlineConfig).length === 0)
|
|
||||||
{
|
|
||||||
return externalConfig;
|
|
||||||
}
|
|
||||||
else if(Object.keys(externalConfig).length === 0)
|
|
||||||
{
|
|
||||||
return inlineConfig;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
let merged: AppConfig = {};
|
|
||||||
|
|
||||||
for (const keyType in allKeys)
|
|
||||||
{
|
|
||||||
const keys = allKeys[keyType];
|
|
||||||
keys.forEach(function(item: string){
|
|
||||||
if (keyType === "boolean")
|
|
||||||
{
|
|
||||||
merged[item] = (typeof inlineConfig[item] !== "undefined") ? inlineConfig[item] : externalConfig[item];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
merged[item] = inlineConfig[item] || externalConfig[item];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill extra keys from external first
|
export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, globalExport, };
|
||||||
// they will be overridden by inline if extra keys also clash
|
|
||||||
merged = fillUserData(externalConfig, merged);
|
|
||||||
merged = fillUserData(inlineConfig, merged);
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseConfig(configText: string, configType = "toml") {
|
|
||||||
let config: object;
|
|
||||||
if (configType === "toml") {
|
|
||||||
try {
|
|
||||||
// TOML parser is soft and can parse even JSON strings, this additional check prevents it.
|
|
||||||
if (configText.trim()[0] === "{")
|
|
||||||
{
|
|
||||||
const errMessage = `config supplied: ${configText} is an invalid TOML and cannot be parsed`;
|
|
||||||
showError(`<p>${errMessage}</p>`);
|
|
||||||
throw Error(errMessage);
|
|
||||||
}
|
|
||||||
config = toml.parse(configText);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
const errMessage: string = err.toString();
|
|
||||||
showError(`<p>config supplied: ${configText} is an invalid TOML and cannot be parsed: ${errMessage}</p>`);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (configType === "json") {
|
|
||||||
try {
|
|
||||||
config = JSON.parse(configText);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
const errMessage: string = err.toString();
|
|
||||||
showError(`<p>config supplied: ${configText} is an invalid JSON and cannot be parsed: ${errMessage}</p>`);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
showError(`<p>type of config supplied is: ${configType}, supported values are ["toml", "json"].</p>`);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateConfig(configText: string, configType = "toml") {
|
|
||||||
const config = parseConfig(configText, configType);
|
|
||||||
|
|
||||||
const finalConfig: AppConfig = {}
|
|
||||||
|
|
||||||
for (const keyType in allKeys)
|
|
||||||
{
|
|
||||||
const keys = allKeys[keyType];
|
|
||||||
keys.forEach(function(item: string){
|
|
||||||
if (validateParamInConfig(item, keyType, config))
|
|
||||||
{
|
|
||||||
if (item === "runtimes")
|
|
||||||
{
|
|
||||||
finalConfig[item] = [];
|
|
||||||
const runtimes = config[item];
|
|
||||||
runtimes.forEach(function(eachRuntime: object){
|
|
||||||
const runtimeConfig: object = {};
|
|
||||||
for (const eachRuntimeParam in eachRuntime)
|
|
||||||
{
|
|
||||||
if (validateParamInConfig(eachRuntimeParam, "string", eachRuntime))
|
|
||||||
{
|
|
||||||
runtimeConfig[eachRuntimeParam] = eachRuntime[eachRuntimeParam];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finalConfig[item].push(runtimeConfig);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
finalConfig[item] = config[item];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return fillUserData(config, finalConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateParamInConfig(paramName: string, paramType: string, config: object): boolean {
|
|
||||||
if (paramName in config)
|
|
||||||
{
|
|
||||||
return paramType === "array" ? Array.isArray(config[paramName]) : typeof config[paramName] === paramType;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { defaultConfig, addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, mergeConfig, validateConfig };
|
|
||||||
|
|||||||
@@ -51,3 +51,50 @@ class TestBasic(PyScriptTest):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
assert self.console.log.lines == [self.PY_COMPLETE, "true false", "<div></div>"]
|
assert self.console.log.lines == [self.PY_COMPLETE, "true false", "<div></div>"]
|
||||||
|
|
||||||
|
def test_paths(self):
|
||||||
|
self.writefile("a.py", "x = 'hello from A'")
|
||||||
|
self.writefile("b.py", "x = 'hello from B'")
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<py-config>
|
||||||
|
paths = ["./a.py", "./b.py"]
|
||||||
|
</py-config>
|
||||||
|
|
||||||
|
<py-script>
|
||||||
|
import js
|
||||||
|
import a, b
|
||||||
|
js.console.log(a.x)
|
||||||
|
js.console.log(b.x)
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert self.console.log.lines == [
|
||||||
|
self.PY_COMPLETE,
|
||||||
|
"hello from A",
|
||||||
|
"hello from B",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_packages(self):
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<py-config>
|
||||||
|
# we use asciitree because it's one of the smallest packages
|
||||||
|
# which are built and distributed with pyodide
|
||||||
|
packages = ["asciitree"]
|
||||||
|
</py-config>
|
||||||
|
|
||||||
|
<py-script>
|
||||||
|
import js
|
||||||
|
import asciitree
|
||||||
|
js.console.log('hello', asciitree.__name__)
|
||||||
|
</py-script>
|
||||||
|
<py-repl></py-repl>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert self.console.log.lines == [
|
||||||
|
self.PY_COMPLETE,
|
||||||
|
"Loading asciitree", # printed by pyodide
|
||||||
|
"Loaded asciitree", # printed by pyodide
|
||||||
|
"hello asciitree", # printed by us
|
||||||
|
]
|
||||||
|
|||||||
@@ -30,7 +30,42 @@ def unzip(location, extract_to="."):
|
|||||||
file.extractall(path=extract_to)
|
file.extractall(path=extract_to)
|
||||||
|
|
||||||
|
|
||||||
class TestRuntimeConfig(PyScriptTest):
|
class TestConfig(PyScriptTest):
|
||||||
|
def test_py_config_inline(self):
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<py-config type="toml">
|
||||||
|
name = "foobar"
|
||||||
|
</py-config>
|
||||||
|
|
||||||
|
<py-script>
|
||||||
|
import js
|
||||||
|
config = js.pyscript_get_config()
|
||||||
|
js.console.log("config name:", config.name)
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert self.console.log.lines[-1] == "config name: foobar"
|
||||||
|
|
||||||
|
def test_py_config_external(self):
|
||||||
|
pyconfig_toml = """
|
||||||
|
name = "app with external config"
|
||||||
|
"""
|
||||||
|
self.writefile("pyconfig.toml", pyconfig_toml)
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<py-config src="pyconfig.toml" type="toml">
|
||||||
|
</py-config>
|
||||||
|
|
||||||
|
<py-script>
|
||||||
|
import js
|
||||||
|
config = js.pyscript_get_config()
|
||||||
|
js.console.log("config name:", config.name)
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert self.console.log.lines[-1] == "config name: app with external config"
|
||||||
|
|
||||||
# The default pyodide version is 0.21.2 as of writing
|
# The default pyodide version is 0.21.2 as of writing
|
||||||
# this test which is newer than the one we are loading below
|
# this test which is newer than the one we are loading below
|
||||||
# (after downloading locally) -- which is 0.20.0
|
# (after downloading locally) -- which is 0.20.0
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
import type { AppConfig, RuntimeConfig } from '../../src/runtime';
|
import type { AppConfig, RuntimeConfig } from '../../src/pyconfig';
|
||||||
import { PyConfig } from '../../src/components/pyconfig';
|
import { loadConfigFromElement, defaultConfig } from '../../src/pyconfig';
|
||||||
|
import { version } from '../../src/runtime';
|
||||||
|
|
||||||
// inspired by trump typos
|
// inspired by trump typos
|
||||||
const covfefeConfig = {
|
const covfefeConfig = {
|
||||||
name: 'covfefe',
|
name: 'covfefe',
|
||||||
@@ -11,13 +13,13 @@ const covfefeConfig = {
|
|||||||
lang: 'covfefe',
|
lang: 'covfefe',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
wonerful: 'discgrace',
|
wonderful: 'disgrace',
|
||||||
};
|
};
|
||||||
|
|
||||||
const covfefeConfigToml = `
|
const covfefeConfigToml = `
|
||||||
name = "covfefe"
|
name = "covfefe"
|
||||||
|
|
||||||
wonerful = "highjacked"
|
wonderful = "hijacked"
|
||||||
|
|
||||||
[[runtimes]]
|
[[runtimes]]
|
||||||
src = "/demo/covfefe.js"
|
src = "/demo/covfefe.js"
|
||||||
@@ -25,11 +27,29 @@ name = "covfefe"
|
|||||||
lang = "covfefe"
|
lang = "covfefe"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
customElements.define('py-config', PyConfig);
|
|
||||||
|
|
||||||
describe('PyConfig', () => {
|
// ideally, I would like to be able to just do "new HTMLElement" in the tests
|
||||||
let instance: PyConfig;
|
// below, but it is not permitted. The easiest work around is to create a fake
|
||||||
|
// custom element: not that we are not using any specific feature of custom
|
||||||
|
// elements: the sole purpose to FakeElement is to be able to instantiate them
|
||||||
|
// in the tests.
|
||||||
|
class FakeElement extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('fake-element', FakeElement);
|
||||||
|
|
||||||
|
function make_config_element(attrs) {
|
||||||
|
const el = new FakeElement();
|
||||||
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
|
el.setAttribute(key, value as string);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe('loadConfigFromElement', () => {
|
||||||
const xhrMockClass = () => ({
|
const xhrMockClass = () => ({
|
||||||
open: jest.fn(),
|
open: jest.fn(),
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
@@ -38,141 +58,102 @@ describe('PyConfig', () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass);
|
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass);
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
instance = new PyConfig();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get the Config to just instantiate', async () => {
|
|
||||||
expect(instance).toBeInstanceOf(PyConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load runtime from config and set as script src', () => {
|
|
||||||
instance.values = covfefeConfig;
|
|
||||||
instance.loadRuntimes();
|
|
||||||
expect(document.scripts[0].src).toBe('http://localhost/demo/covfefe.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load the default config', () => {
|
it('should load the default config', () => {
|
||||||
instance.connectedCallback();
|
const config = loadConfigFromElement(null);
|
||||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
expect(config).toBe(defaultConfig);
|
||||||
// @ts-ignore
|
expect(config.pyscript.version).toBe(version);
|
||||||
expect(instance.values.runtimes[0].lang).toBe('python');
|
});
|
||||||
|
|
||||||
|
it('an empty <py-config> should load the default config', () => {
|
||||||
|
const el = make_config_element({});
|
||||||
|
let config = loadConfigFromElement(el);
|
||||||
|
expect(config).toBe(defaultConfig);
|
||||||
|
expect(config.pyscript.version).toBe(version);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load the JSON config from inline', () => {
|
it('should load the JSON config from inline', () => {
|
||||||
instance.setAttribute('type', 'json');
|
const el = make_config_element({ type: 'json' });
|
||||||
instance.innerHTML = JSON.stringify(covfefeConfig);
|
el.innerHTML = JSON.stringify(covfefeConfig);
|
||||||
instance.connectedCallback();
|
const config = loadConfigFromElement(el);
|
||||||
// @ts-ignore
|
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||||
expect(instance.values.runtimes[0].lang).toBe('covfefe');
|
expect(config.pyscript?.time).not.toBeNull();
|
||||||
expect(instance.values.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);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load the JSON config from src attribute', () => {
|
it('should load the JSON config from src attribute', () => {
|
||||||
instance.setAttribute('type', 'json');
|
const el = make_config_element({ type: 'json', src: '/covfefe.json' });
|
||||||
instance.setAttribute('src', '/covfefe.json');
|
const config = loadConfigFromElement(el);
|
||||||
instance.connectedCallback();
|
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||||
// @ts-ignore
|
expect(config.pyscript?.time).not.toBeNull();
|
||||||
expect(instance.values.runtimes[0].lang).toBe('covfefe');
|
// wonderful is an extra key supplied by the user and is unaffected by merging process
|
||||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
expect(config.wonderful).toBe('disgrace');
|
||||||
// wonerful is an extra key supplied by the user and is unaffected by merging process
|
// schema_version wasn't present in `config from src` but is still set due to merging with default
|
||||||
expect(instance.values.wonerful).toBe('discgrace');
|
expect(config.schema_version).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should load the JSON config from both inline and src', () => {
|
it('should load the JSON config from both inline and src', () => {
|
||||||
instance.setAttribute('type', 'json');
|
const el = make_config_element({ type: 'json', src: '/covfefe.json' });
|
||||||
instance.innerHTML = JSON.stringify({ version: '0.2a', wonerful: 'highjacked' });
|
el.innerHTML = JSON.stringify({ version: '0.2a', wonderful: 'hijacked' });
|
||||||
instance.setAttribute('src', '/covfefe.json');
|
const config = loadConfigFromElement(el);
|
||||||
instance.connectedCallback();
|
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||||
// @ts-ignore
|
expect(config.pyscript?.time).not.toBeNull();
|
||||||
expect(instance.values.runtimes[0].lang).toBe('covfefe');
|
// config from src had an extra key "wonderful" with value "disgrace"
|
||||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
// inline config had the same extra key "wonderful" with value "hijacked"
|
||||||
// config from src had an extra key "wonerful" with value "discgrace"
|
|
||||||
// inline config had the same extra key "wonerful" with value "highjacked"
|
|
||||||
// the merge process works for extra keys that clash as well
|
// the merge process works for extra keys that clash as well
|
||||||
// so the final value is "highjacked" since inline takes precedence over src
|
// so the final value is "hijacked" since inline takes precedence over src
|
||||||
expect(instance.values.wonerful).toBe('highjacked');
|
expect(config.wonderful).toBe('hijacked');
|
||||||
// version wasn't present in `config from src` but is still set due to merging with default and inline
|
// version wasn't present in `config from src` but is still set due to merging with default and inline
|
||||||
expect(instance.values.version).toBe('0.2a');
|
expect(config.version).toBe('0.2a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to load an inline TOML config', () => {
|
it('should be able to load an inline TOML config', () => {
|
||||||
// type of config is TOML if not supplied
|
// TOML is the default type
|
||||||
instance.innerHTML = covfefeConfigToml;
|
const el = make_config_element({});
|
||||||
instance.connectedCallback();
|
el.innerHTML = covfefeConfigToml;
|
||||||
// @ts-ignore
|
const config = loadConfigFromElement(el);
|
||||||
expect(instance.values.runtimes[0].lang).toBe('covfefe');
|
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
expect(config.pyscript?.time).not.toBeNull();
|
||||||
expect(instance.values.wonerful).toBe('highjacked');
|
// schema_version wasn't present in `inline config` but is still set due to merging with default
|
||||||
|
expect(config.schema_version).toBe(1);
|
||||||
|
expect(config.wonderful).toBe('hijacked');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('should NOT be able to load an inline config in JSON format with type as TOML', () => {
|
it('should NOT be able to load an inline config in JSON format with type as TOML', () => {
|
||||||
instance.innerHTML = JSON.stringify(covfefeConfig);
|
const el = make_config_element({});
|
||||||
instance.connectedCallback();
|
el.innerHTML = JSON.stringify(covfefeConfig);
|
||||||
|
expect(()=>loadConfigFromElement(el)).toThrow(/config supplied: {.*} is an invalid TOML and cannot be parsed/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('should NOT be able to load an inline config in TOML format with type as JSON', () => {
|
it('should NOT be able to load an inline config in TOML format with type as JSON', () => {
|
||||||
instance.setAttribute('type', 'json');
|
const el = make_config_element({ type: 'json' });
|
||||||
instance.innerHTML = covfefeConfigToml;
|
el.innerHTML = covfefeConfigToml;
|
||||||
instance.connectedCallback();
|
expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as toml', () => {
|
it('should NOT be able to load an inline TOML config with a JSON config from src with type as toml', () => {
|
||||||
instance.innerHTML = covfefeConfigToml;
|
const el = make_config_element({ src: '/covfefe.json' });
|
||||||
instance.setAttribute('src', '/covfefe.json');
|
el.innerHTML = covfefeConfigToml;
|
||||||
instance.connectedCallback();
|
expect(()=>loadConfigFromElement(el)).toThrow(/config supplied: {.*} is an invalid TOML and cannot be parsed/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as json', () => {
|
it('should NOT be able to load an inline TOML config with a JSON config from src with type as json', () => {
|
||||||
instance.setAttribute('type', 'json');
|
const el = make_config_element({ type: 'json', src: '/covfefe.json' });
|
||||||
instance.innerHTML = covfefeConfigToml;
|
el.innerHTML = covfefeConfigToml;
|
||||||
instance.setAttribute('src', '/covfefe.json');
|
expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError);
|
||||||
instance.connectedCallback();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error out when passing an invalid JSON', () => {
|
it('should error out when passing an invalid JSON', () => {
|
||||||
instance.setAttribute('type', 'json');
|
const el = make_config_element({ type: 'json' });
|
||||||
instance.innerHTML = '[[';
|
el.innerHTML = '[[';
|
||||||
expect(()=>instance.connectedCallback()).toThrow(SyntaxError);
|
expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error out when passing an invalid TOML', () => {
|
it('should error out when passing an invalid TOML', () => {
|
||||||
instance.innerHTML = '[[';
|
const el = make_config_element({});
|
||||||
expect(()=>instance.connectedCallback()).toThrow(SyntaxError);
|
el.innerHTML = '[[';
|
||||||
|
expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('connectedCallback should call loadRuntimes', async () => {
|
|
||||||
const mockedMethod = jest.fn();
|
|
||||||
instance.loadRuntimes = mockedMethod;
|
|
||||||
|
|
||||||
instance.connectedCallback();
|
|
||||||
|
|
||||||
expect(mockedMethod).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('confirm connectedCallback happy path', async () => {
|
|
||||||
const mockedMethod = jest.fn();
|
|
||||||
instance.loadRuntimes = mockedMethod;
|
|
||||||
instance.innerHTML = 'test = 42';
|
|
||||||
|
|
||||||
instance.connectedCallback();
|
|
||||||
|
|
||||||
expect(instance.code).toBe('test = 42');
|
|
||||||
expect(instance.values['test']).toBe(42);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('log should add new message to the page', async () => {
|
|
||||||
// details are undefined, so let's create a div for it
|
|
||||||
instance.details = document.createElement('div');
|
|
||||||
instance.log('this is a log');
|
|
||||||
|
|
||||||
// @ts-ignore: typescript complains about accessing innerText
|
|
||||||
expect(instance.details.childNodes[0].innerText).toBe('this is a log');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('confirm that calling close would call this.remove', async () => {
|
|
||||||
instance.remove = jest.fn();
|
|
||||||
instance.close();
|
|
||||||
expect(instance.remove).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AppConfig } from '../../src/pyconfig';
|
||||||
import { Runtime } from '../../src/runtime';
|
import { Runtime } from '../../src/runtime';
|
||||||
import { PyodideRuntime } from '../../src/pyodide';
|
import { PyodideRuntime } from '../../src/pyodide';
|
||||||
|
|
||||||
@@ -8,7 +9,8 @@ global.TextDecoder = TextDecoder
|
|||||||
describe('PyodideRuntime', () => {
|
describe('PyodideRuntime', () => {
|
||||||
let runtime: PyodideRuntime;
|
let runtime: PyodideRuntime;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
runtime = new PyodideRuntime();
|
const config: AppConfig = {};
|
||||||
|
runtime = new PyodideRuntime(config);
|
||||||
/**
|
/**
|
||||||
* Since import { loadPyodide } from 'pyodide';
|
* Since import { loadPyodide } from 'pyodide';
|
||||||
* is not used inside `src/pyodide.ts`, the function
|
* is not used inside `src/pyodide.ts`, the function
|
||||||
|
|||||||
Reference in New Issue
Block a user