From 77be53a67037b5a02ceae06703e00ddd513b4a2a Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 13:47:05 +0200 Subject: [PATCH 01/32] start to remove module-level statements, and put all the logic in its own function --- pyscriptjs/src/main.ts | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 7f491abf211..ea129c553ab 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -9,23 +9,30 @@ import { globalLoader } from './stores'; const logger = getLogger('pyscript/main'); -/* eslint-disable @typescript-eslint/no-unused-vars */ -const xPyScript = customElements.define('py-script', PyScript); -const xPyLoader = customElements.define('py-loader', PyLoader); -const xPyConfig = customElements.define('py-config', PyConfig); -const xPyEnv = customElements.define('py-env', PyEnv); -/* eslint-disable @typescript-eslint/no-unused-vars */ - -// 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'); +function pyscript_main() { + /* eslint-disable @typescript-eslint/no-unused-vars */ + const xPyScript = customElements.define('py-script', PyScript); + const xPyLoader = customElements.define('py-loader', PyLoader); + const xPyConfig = customElements.define('py-config', PyConfig); + const xPyEnv = customElements.define('py-env', PyEnv); + /* eslint-disable @typescript-eslint/no-unused-vars */ + + // 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 + logger.info('add py-loader'); + const loader = document.createElement('py-loader'); document.body.append(loader); + globalLoader.set(loader); + + } -// add loader to the page body -logger.info('add py-loader'); -const loader = document.createElement('py-loader'); -document.body.append(loader); -globalLoader.set(loader); +// main entry point of execution +pyscript_main(); From a1124dafef5b600143b69a5304a11231499e98ae Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 15:40:23 +0200 Subject: [PATCH 02/32] add a JS API to get the pyscript config and two integration tests to check that the tag actually works --- pyscriptjs/src/components/pyconfig.ts | 8 +++- pyscriptjs/src/utils.ts | 12 +++++- .../integration/test_py_runtime_config.py | 37 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index 6ee271d488e..8179593ba85 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -4,7 +4,7 @@ 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' +import { readTextFromPath, handleFetchError, mergeConfig, validateConfig, defaultConfig, globalExport } from '../utils' // Subscriber used to connect to the first available runtime (can be pyodide or others) let runtimeSpec: Runtime; @@ -19,6 +19,12 @@ appConfig.subscribe(value => { const logger = getLogger('py-config'); +function pyscript_get_config() { + return appConfig_; +} +globalExport('pyscript_get_config', pyscript_get_config); + + /** * Configures general metadata about the PyScript application such * as a list of runtimes, name, version, closing the loader diff --git a/pyscriptjs/src/utils.ts b/pyscriptjs/src/utils.ts index ecb7ca3adaa..555a049631f 100644 --- a/pyscriptjs/src/utils.ts +++ b/pyscriptjs/src/utils.ts @@ -141,6 +141,16 @@ function fillUserData(inputConfig: AppConfig, resultConfig: AppConfig): AppConfi return resultConfig; } +function globalExport(name: string, obj: any) { + // attach the given object to the global object, so that it is globally + // visible everywhere. Should be used very sparingly! + + // `window` in the browser, `global` in node + const _global = (window || global) as any; + _global[name] = obj; +} + + function mergeConfig(inlineConfig: AppConfig, externalConfig: AppConfig): AppConfig { if (Object.keys(inlineConfig).length === 0 && Object.keys(externalConfig).length === 0) { @@ -263,4 +273,4 @@ function validateParamInConfig(paramName: string, paramType: string, config: obj return false; } -export { defaultConfig, addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, mergeConfig, validateConfig }; +export { defaultConfig, addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, globalExport, mergeConfig, validateConfig }; diff --git a/pyscriptjs/tests/integration/test_py_runtime_config.py b/pyscriptjs/tests/integration/test_py_runtime_config.py index 07b6ea9d83e..d46bd2bc9f0 100644 --- a/pyscriptjs/tests/integration/test_py_runtime_config.py +++ b/pyscriptjs/tests/integration/test_py_runtime_config.py @@ -30,7 +30,42 @@ def unzip(location, extract_to="."): file.extractall(path=extract_to) -class TestRuntimeConfig(PyScriptTest): +class TestConfig(PyScriptTest): + def test_py_config_inline(self): + self.pyscript_run( + """ + + name = "foobar" + + + + import js + config = js.pyscript_get_config() + js.console.log("config name:", config.name) + + """ + ) + 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( + """ + + + + + import js + config = js.pyscript_get_config() + js.console.log("config name:", config.name) + + """ + ) + assert self.console.log.lines[-1] == "config name: app with external config" + # The default pyodide version is 0.21.2 as of writing # this test which is newer than the one we are loading below # (after downloading locally) -- which is 0.20.0 From 894a1c71c55993baa8efd074c9f745cfe1cbecfe Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 15:43:21 +0200 Subject: [PATCH 03/32] rename this file --- .../integration/{test_py_runtime_config.py => test_py_config.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pyscriptjs/tests/integration/{test_py_runtime_config.py => test_py_config.py} (100%) diff --git a/pyscriptjs/tests/integration/test_py_runtime_config.py b/pyscriptjs/tests/integration/test_py_config.py similarity index 100% rename from pyscriptjs/tests/integration/test_py_runtime_config.py rename to pyscriptjs/tests/integration/test_py_config.py From 4bc402920df5a953391fcfc966fd785ab1ee956f Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 15:48:53 +0200 Subject: [PATCH 04/32] introduce the class PyScriptApp, so have a global singleton where to store per-app data (e.g. the config) --- pyscriptjs/src/main.ts | 47 ++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index ea129c553ab..9c687525b09 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -9,30 +9,33 @@ import { globalLoader } from './stores'; const logger = getLogger('pyscript/main'); -function pyscript_main() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const xPyScript = customElements.define('py-script', PyScript); - const xPyLoader = customElements.define('py-loader', PyLoader); - const xPyConfig = customElements.define('py-config', PyConfig); - const xPyEnv = customElements.define('py-env', PyEnv); - /* eslint-disable @typescript-eslint/no-unused-vars */ - - // 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'); +class PyScriptApp { + + main() { + /* eslint-disable @typescript-eslint/no-unused-vars */ + const xPyScript = customElements.define('py-script', PyScript); + const xPyLoader = customElements.define('py-loader', PyLoader); + const xPyConfig = customElements.define('py-config', PyConfig); + const xPyEnv = customElements.define('py-env', PyEnv); + /* eslint-disable @typescript-eslint/no-unused-vars */ + + // 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 + logger.info('add py-loader'); + const loader = document.createElement('py-loader'); document.body.append(loader); + globalLoader.set(loader); } - - // add loader to the page body - logger.info('add py-loader'); - const loader = document.createElement('py-loader'); - document.body.append(loader); - globalLoader.set(loader); - - } + // main entry point of execution -pyscript_main(); +const globalApp = new PyScriptApp(); +globalApp.main(); From 5798923e3f7eead3f1695720900ade4dd6a7e1e5 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 17:45:32 +0200 Subject: [PATCH 05/32] small refactoring of pyconfig.ts, in preparation to make py-config NOT a web component --- pyscriptjs/src/components/pyconfig.ts | 73 +++++++++++++------------- pyscriptjs/tests/unit/pyconfig.test.ts | 3 +- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index 8179593ba85..56fc11e6a31 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -25,6 +25,42 @@ function pyscript_get_config() { globalExport('pyscript_get_config', pyscript_get_config); +function loadConfigFromElement(el: HTMLElement): AppConfig { + const configType: string = el.hasAttribute("type") ? el.getAttribute("type") : "toml"; + let srcConfig = extractFromSrc(el, configType); + const inlineConfig = extractFromInline(el, configType); + // first make config from src whole if it is partial + srcConfig = mergeConfig(srcConfig, defaultConfig); + // then merge inline config and config from src + const result = mergeConfig(inlineConfig, srcConfig); + result.pyscript = { + "version": version, + "time": new Date().toISOString() + }; + return result; +} + +function extractFromSrc(el: HTMLElement, configType: string) { + if (el.hasAttribute('src')) + { + logger.info('config set from src attribute'); + return validateConfig(readTextFromPath(el.getAttribute('src')), configType); + } + return {}; +} + + +function extractFromInline(el: HTMLElement, configType: string) { + if (el.innerHTML!=='') + { + logger.info('config set from inline'); + return validateConfig(el.innerHTML, configType); + } + return {}; +} + + + /** * Configures general metadata about the PyScript application such * as a list of runtimes, name, version, closing the loader @@ -45,43 +81,8 @@ export class PyConfig extends BaseEvalElement { 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(); - + this.values = loadConfigFromElement(this); appConfig.set(this.values); logger.info('config set:', this.values); diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index 73fa4b5a561..d40e868b06a 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -1,5 +1,5 @@ import { jest } from '@jest/globals'; -import type { AppConfig, RuntimeConfig } from '../../src/runtime'; +import type { AppConfig, RuntimeConfig } from '../../src/config'; import { PyConfig } from '../../src/components/pyconfig'; // inspired by trump typos const covfefeConfig = { @@ -154,7 +154,6 @@ describe('PyConfig', () => { instance.connectedCallback(); - expect(instance.code).toBe('test'); expect(instance.values['0']).toBe('test'); }); From 1faeb3bc0a9c7aacd6f19f8ee2195d9b0f3008c2 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 17:57:51 +0200 Subject: [PATCH 06/32] improve logging --- pyscriptjs/src/components/pyconfig.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index 56fc11e6a31..98d5fb4f285 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -29,7 +29,6 @@ function loadConfigFromElement(el: HTMLElement): AppConfig { const configType: string = el.hasAttribute("type") ? el.getAttribute("type") : "toml"; let srcConfig = extractFromSrc(el, configType); const inlineConfig = extractFromInline(el, configType); - // first make config from src whole if it is partial srcConfig = mergeConfig(srcConfig, defaultConfig); // then merge inline config and config from src const result = mergeConfig(inlineConfig, srcConfig); @@ -43,8 +42,9 @@ function loadConfigFromElement(el: HTMLElement): AppConfig { function extractFromSrc(el: HTMLElement, configType: string) { if (el.hasAttribute('src')) { - logger.info('config set from src attribute'); - return validateConfig(readTextFromPath(el.getAttribute('src')), configType); + const src = el.getAttribute('src'); + logger.info('loading ', src) + return validateConfig(readTextFromPath(src), configType); } return {}; } @@ -53,7 +53,7 @@ function extractFromSrc(el: HTMLElement, configType: string) { function extractFromInline(el: HTMLElement, configType: string) { if (el.innerHTML!=='') { - logger.info('config set from inline'); + logger.info('loading content'); return validateConfig(el.innerHTML, configType); } return {}; From 64798607c6c89f80f6917119499b7227cc4e0142 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 18:02:31 +0200 Subject: [PATCH 07/32] start moving all the config logic to config.ts --- pyscriptjs/src/components/pyconfig.ts | 40 +--------------- pyscriptjs/src/config.ts | 67 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 38 deletions(-) create mode 100644 pyscriptjs/src/config.ts diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index 98d5fb4f285..7a7cdf75fe3 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -1,10 +1,10 @@ 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, globalExport } from '../utils' +import { loadConfigFromElement } from '../config'; +import { readTextFromPath, handleFetchError, globalExport } from '../utils' // Subscriber used to connect to the first available runtime (can be pyodide or others) let runtimeSpec: Runtime; @@ -25,42 +25,6 @@ function pyscript_get_config() { globalExport('pyscript_get_config', pyscript_get_config); -function loadConfigFromElement(el: HTMLElement): AppConfig { - const configType: string = el.hasAttribute("type") ? el.getAttribute("type") : "toml"; - let srcConfig = extractFromSrc(el, configType); - const inlineConfig = extractFromInline(el, configType); - srcConfig = mergeConfig(srcConfig, defaultConfig); - // then merge inline config and config from src - const result = mergeConfig(inlineConfig, srcConfig); - result.pyscript = { - "version": version, - "time": new Date().toISOString() - }; - return result; -} - -function extractFromSrc(el: HTMLElement, configType: string) { - if (el.hasAttribute('src')) - { - const src = el.getAttribute('src'); - logger.info('loading ', src) - return validateConfig(readTextFromPath(src), configType); - } - return {}; -} - - -function extractFromInline(el: HTMLElement, configType: string) { - if (el.innerHTML!=='') - { - logger.info('loading content'); - return validateConfig(el.innerHTML, configType); - } - return {}; -} - - - /** * Configures general metadata about the PyScript application such * as a list of runtimes, name, version, closing the loader diff --git a/pyscriptjs/src/config.ts b/pyscriptjs/src/config.ts new file mode 100644 index 00000000000..33eeede79f0 --- /dev/null +++ b/pyscriptjs/src/config.ts @@ -0,0 +1,67 @@ +import { getLogger } from './logger'; +import { version } from './runtime'; +import { readTextFromPath, mergeConfig, validateConfig, defaultConfig } from './utils' + +const logger = getLogger('py-config'); + +export interface AppConfig extends Record { + name?: string; + description?: string; + version?: string; + schema_version?: number; + type?: string; + author_name?: string; + author_email?: string; + license?: string; + autoclose_loader?: boolean; + runtimes?: Array; + packages?: Array; + paths?: Array; + plugins?: Array; + pyscript?: PyScriptMetadata; +} + +export type RuntimeConfig = { + src?: string; + name?: string; + lang?: string; +}; + +export type PyScriptMetadata = { + version?: string; + time?: string; +} + +export function loadConfigFromElement(el: HTMLElement): AppConfig { + const configType: string = el.hasAttribute("type") ? el.getAttribute("type") : "toml"; + let srcConfig = extractFromSrc(el, configType); + const inlineConfig = extractFromInline(el, configType); + srcConfig = mergeConfig(srcConfig, defaultConfig); + // then merge inline config and config from src + const result = mergeConfig(inlineConfig, srcConfig); + result.pyscript = { + "version": version, + "time": new Date().toISOString() + }; + return result; +} + +function extractFromSrc(el: HTMLElement, configType: string) { + if (el.hasAttribute('src')) + { + const src = el.getAttribute('src'); + logger.info('loading ', src) + return validateConfig(readTextFromPath(src), configType); + } + return {}; +} + + +function extractFromInline(el: HTMLElement, configType: string) { + if (el.innerHTML!=='') + { + logger.info('loading content'); + return validateConfig(el.innerHTML, configType); + } + return {}; +} From 540b12918e5e113cbc97c8bbbc69b098e688d3f4 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 18:11:09 +0200 Subject: [PATCH 08/32] move all the config logic from utils.ts into config.ts --- pyscriptjs/src/config.ts | 166 +++++++++++++++++++++++++++++++++++++- pyscriptjs/src/utils.ts | 167 +-------------------------------------- 2 files changed, 167 insertions(+), 166 deletions(-) diff --git a/pyscriptjs/src/config.ts b/pyscriptjs/src/config.ts index 33eeede79f0..9ba7c93d025 100644 --- a/pyscriptjs/src/config.ts +++ b/pyscriptjs/src/config.ts @@ -1,6 +1,7 @@ +import { toml } from './toml' import { getLogger } from './logger'; import { version } from './runtime'; -import { readTextFromPath, mergeConfig, validateConfig, defaultConfig } from './utils' +import { readTextFromPath, showError } from './utils' const logger = getLogger('py-config'); @@ -32,6 +33,34 @@ export type PyScriptMetadata = { 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"] +}; + +const defaultConfig: AppConfig = { + "name": "pyscript", + "description": "default config", + "version": "0.1", + "schema_version": 1, + "type": "app", + "author_name": "anonymous coder", + "author_email": "foo@bar.com", + "license": "Apache", + "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: HTMLElement): AppConfig { const configType: string = el.hasAttribute("type") ? el.getAttribute("type") : "toml"; let srcConfig = extractFromSrc(el, configType); @@ -65,3 +94,138 @@ function extractFromInline(el: HTMLElement, configType: string) { } 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(`

${errMessage}

`); + throw Error(errMessage); + } + config = toml.parse(configText); + } + catch (err) { + const errMessage: string = err.toString(); + showError(`

config supplied: ${configText} is an invalid TOML and cannot be parsed: ${errMessage}

`); + throw err; + } + } + else if (configType === "json") { + try { + config = JSON.parse(configText); + } + catch (err) { + const errMessage: string = err.toString(); + showError(`

config supplied: ${configText} is an invalid JSON and cannot be parsed: ${errMessage}

`); + throw err; + } + } + else { + showError(`

type of config supplied is: ${configType}, supported values are ["toml", "json"].

`); + } + 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; +} diff --git a/pyscriptjs/src/utils.ts b/pyscriptjs/src/utils.ts index 555a049631f..cbca0234625 100644 --- a/pyscriptjs/src/utils.ts +++ b/pyscriptjs/src/utils.ts @@ -1,33 +1,4 @@ -import {toml} from './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 = { - "name": "pyscript", - "description": "default config", - "version": "0.1", - "schema_version": 1, - "type": "app", - "author_name": "anonymous coder", - "author_email": "foo@bar.com", - "license": "Apache", - "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": [] -} - +import type { AppConfig } from "./config"; function addClasses(element: HTMLElement, classes: Array) { for (const entry of classes) { @@ -128,19 +99,6 @@ function inJest(): boolean { return typeof process === 'object' && process.env.JEST_WORKER_ID !== undefined; } -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 globalExport(name: string, obj: any) { // attach the given object to the global object, so that it is globally // visible everywhere. Should be used very sparingly! @@ -151,126 +109,5 @@ function globalExport(name: string, obj: any) { } -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(`

${errMessage}

`); - throw Error(errMessage); - } - config = toml.parse(configText); - } - catch (err) { - const errMessage: string = err.toString(); - showError(`

config supplied: ${configText} is an invalid TOML and cannot be parsed: ${errMessage}

`); - throw err; - } - } - else if (configType === "json") { - try { - config = JSON.parse(configText); - } - catch (err) { - const errMessage: string = err.toString(); - showError(`

config supplied: ${configText} is an invalid JSON and cannot be parsed: ${errMessage}

`); - throw err; - } - } - else { - showError(`

type of config supplied is: ${configType}, supported values are ["toml", "json"].

`); - } - 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, globalExport, mergeConfig, validateConfig }; +export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, globalExport, }; From 62ddb907e2aaae451f0404f9f9935d77c4a28203 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Thu, 29 Sep 2022 19:05:03 +0200 Subject: [PATCH 09/32] WIP: start to refactor pyconfig.test.ts to test directly loadConfigFromElement instead of going through PyConfig.connectedCallback --- pyscriptjs/src/config.ts | 2 +- pyscriptjs/tests/unit/pyconfig.test.ts | 75 +++++++++++++++++++------- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/pyscriptjs/src/config.ts b/pyscriptjs/src/config.ts index 9ba7c93d025..b4d82426035 100644 --- a/pyscriptjs/src/config.ts +++ b/pyscriptjs/src/config.ts @@ -40,7 +40,7 @@ const allKeys = { "array": ["runtimes", "packages", "paths", "plugins"] }; -const defaultConfig: AppConfig = { +export const defaultConfig: AppConfig = { "name": "pyscript", "description": "default config", "version": "0.1", diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index d40e868b06a..42657528124 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -1,6 +1,7 @@ import { jest } from '@jest/globals'; import type { AppConfig, RuntimeConfig } from '../../src/config'; -import { PyConfig } from '../../src/components/pyconfig'; +import { loadConfigFromElement, defaultConfig } from '../../src/config'; + // inspired by trump typos const covfefeConfig = { name: 'covfefe', @@ -25,10 +26,22 @@ name = "covfefe" lang = "covfefe" `; -customElements.define('py-config', PyConfig); -describe('PyConfig', () => { - let instance: PyConfig; +// ideally, I would like to be able to just do "new HTMLElement" in the tests +// 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); + + +describe('loadConfigFromElement', () => { + let element: HTMLElement; const xhrMockClass = () => ({ open: jest.fn(), @@ -39,28 +52,26 @@ describe('PyConfig', () => { window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass); beforeEach(() => { - instance = new PyConfig(); + element = new FakeElement(); }); - it('should get the Config to just instantiate', async () => { - expect(instance).toBeInstanceOf(PyConfig); + it('FakeElement can be instantiated', async () => { + expect(element).toBeInstanceOf(HTMLElement); }); - 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', () => { - instance.connectedCallback(); - expect(instance.values.name).toBe('pyscript'); - expect(instance.values.author_email).toBe('foo@bar.com'); - expect(instance.values.pyscript?.time).not.toBeNull(); - // @ts-ignore - expect(instance.values.runtimes[0].lang).toBe('python'); + // XXX fix me + // it('should load the default config', () => { + // const config = loadConfigFromElement(null); + // expect(config).toBe(defaultConfig); + // }); + + it('an empty should load the default config', () => { + let config = loadConfigFromElement(element); + expect(config).toBe(defaultConfig); }); + /* it('should load the JSON config from inline', () => { instance.setAttribute('type', 'json'); instance.innerHTML = JSON.stringify(covfefeConfig); @@ -171,4 +182,30 @@ describe('PyConfig', () => { instance.close(); expect(instance.remove).toHaveBeenCalled(); }); + */ }); + + +// old tests about PyConfig.connectedCallback behavior, should be moved +// somewhere else + +import { PyConfig } from '../../src/components/pyconfig'; +customElements.define('py-config', PyConfig); + +describe('PyConfig', () => { + let instance: PyConfig; + + 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'); + }); +}) From 634be619676e1b85305df55518a86e8dcd578a93 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 10:53:54 +0200 Subject: [PATCH 10/32] WIP: allow to call loadConfigFromElement(null) --- pyscriptjs/src/config.ts | 15 +++++++++++---- pyscriptjs/tests/unit/pyconfig.test.ts | 11 ++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/pyscriptjs/src/config.ts b/pyscriptjs/src/config.ts index b4d82426035..2f669185041 100644 --- a/pyscriptjs/src/config.ts +++ b/pyscriptjs/src/config.ts @@ -62,11 +62,18 @@ export const defaultConfig: AppConfig = { export function loadConfigFromElement(el: HTMLElement): AppConfig { - const configType: string = el.hasAttribute("type") ? el.getAttribute("type") : "toml"; - let srcConfig = extractFromSrc(el, configType); - const inlineConfig = extractFromInline(el, configType); + 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); - // then merge inline config and config from src const result = mergeConfig(inlineConfig, srcConfig); result.pyscript = { "version": version, diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index 42657528124..c7ef9ce351c 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -1,6 +1,7 @@ import { jest } from '@jest/globals'; import type { AppConfig, RuntimeConfig } from '../../src/config'; import { loadConfigFromElement, defaultConfig } from '../../src/config'; +import { version } from '../../src/runtime'; // inspired by trump typos const covfefeConfig = { @@ -60,11 +61,11 @@ describe('loadConfigFromElement', () => { }); - // XXX fix me - // it('should load the default config', () => { - // const config = loadConfigFromElement(null); - // expect(config).toBe(defaultConfig); - // }); + it('should load the default config', () => { + const config = loadConfigFromElement(null); + expect(config).toBe(defaultConfig); + expect(config.pyscript.version).toBe(version); + }); it('an empty should load the default config', () => { let config = loadConfigFromElement(element); From 6ca9be57ae7774f76fb2c259d9ce918eee8dbb1f Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 11:25:08 +0200 Subject: [PATCH 11/32] WIP: more progress in porting tests, create a brand new element instead of reusing always the same one --- pyscriptjs/tests/unit/pyconfig.test.ts | 53 ++++++++++++-------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index c7ef9ce351c..2a80ee4fd1b 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -40,10 +40,16 @@ class FakeElement extends HTMLElement { } 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', () => { - let element: HTMLElement; +describe('loadConfigFromElement', () => { const xhrMockClass = () => ({ open: jest.fn(), send: jest.fn(), @@ -52,15 +58,6 @@ describe('loadConfigFromElement', () => { // @ts-ignore window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass); - beforeEach(() => { - element = new FakeElement(); - }); - - it('FakeElement can be instantiated', async () => { - expect(element).toBeInstanceOf(HTMLElement); - }); - - it('should load the default config', () => { const config = loadConfigFromElement(null); expect(config).toBe(defaultConfig); @@ -68,35 +65,35 @@ describe('loadConfigFromElement', () => { }); it('an empty should load the default config', () => { - let config = loadConfigFromElement(element); + 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', () => { - instance.setAttribute('type', 'json'); - instance.innerHTML = JSON.stringify(covfefeConfig); - instance.connectedCallback(); - // @ts-ignore - expect(instance.values.runtimes[0].lang).toBe('covfefe'); - expect(instance.values.pyscript?.time).not.toBeNull(); + const el = make_config_element({ type: 'json' }); + el.innerHTML = JSON.stringify(covfefeConfig); + const config = loadConfigFromElement(el); + expect(config.runtimes[0].lang).toBe('covfefe'); + expect(config.pyscript?.time).not.toBeNull(); // version wasn't present in `inline config` but is still set due to merging with default - expect(instance.values.version).toBe('0.1'); + expect(config.version).toBe('0.1'); }); it('should load the JSON config from src attribute', () => { - instance.setAttribute('type', 'json'); - instance.setAttribute('src', '/covfefe.json'); - instance.connectedCallback(); - // @ts-ignore - expect(instance.values.runtimes[0].lang).toBe('covfefe'); - expect(instance.values.pyscript?.time).not.toBeNull(); + const el = make_config_element({ type: 'json', src: '/covfefe.json' }); + const config = loadConfigFromElement(el); + expect(config.runtimes[0].lang).toBe('covfefe'); + expect(config.pyscript?.time).not.toBeNull(); // wonerful is an extra key supplied by the user and is unaffected by merging process - expect(instance.values.wonerful).toBe('discgrace'); + expect(config.wonerful).toBe('discgrace'); // version wasn't present in `config from src` but is still set due to merging with default - expect(instance.values.version).toBe('0.1'); + expect(config.version).toBe('0.1'); }); + /* + it('should load the JSON config from both inline and src', () => { instance.setAttribute('type', 'json'); instance.innerHTML = JSON.stringify({ version: '0.2a', wonerful: 'highjacked' }); From 798c436d8d42cf8a54d41a2dd8e650bd92179cd4 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 11:33:36 +0200 Subject: [PATCH 12/32] fix typos :) --- pyscriptjs/tests/unit/pyconfig.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index 2a80ee4fd1b..296fbce9849 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -13,13 +13,13 @@ const covfefeConfig = { lang: 'covfefe', }, ], - wonerful: 'discgrace', + wonderful: 'disgrace', }; const covfefeConfigToml = ` name = "covfefe" -wonerful = "highjacked" +wonderful = "hijacked" [[runtimes]] src = "/demo/covfefe.js" @@ -86,8 +86,8 @@ describe('loadConfigFromElement', () => { const config = loadConfigFromElement(el); expect(config.runtimes[0].lang).toBe('covfefe'); expect(config.pyscript?.time).not.toBeNull(); - // wonerful is an extra key supplied by the user and is unaffected by merging process - expect(config.wonerful).toBe('discgrace'); + // wonderful is an extra key supplied by the user and is unaffected by merging process + expect(config.wonderful).toBe('disgrace'); // version wasn't present in `config from src` but is still set due to merging with default expect(config.version).toBe('0.1'); }); @@ -96,17 +96,17 @@ describe('loadConfigFromElement', () => { it('should load the JSON config from both inline and src', () => { instance.setAttribute('type', 'json'); - instance.innerHTML = JSON.stringify({ version: '0.2a', wonerful: 'highjacked' }); + instance.innerHTML = JSON.stringify({ version: '0.2a', wonderful: 'hijacked' }); instance.setAttribute('src', '/covfefe.json'); instance.connectedCallback(); // @ts-ignore expect(instance.values.runtimes[0].lang).toBe('covfefe'); expect(instance.values.pyscript?.time).not.toBeNull(); - // config from src had an extra key "wonerful" with value "discgrace" - // inline config had the same extra key "wonerful" with value "highjacked" + // config from src had an extra key "wonderful" with value "disgrace" + // inline config had the same extra key "wonderful" with value "hijacked" // the merge process works for extra keys that clash as well - // so the final value is "highjacked" since inline takes precedence over src - expect(instance.values.wonerful).toBe('highjacked'); + // so the final value is "hijacked" since inline takes precedence over src + expect(instance.values.wonderful).toBe('hijacked'); // 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'); }); @@ -120,7 +120,7 @@ describe('loadConfigFromElement', () => { expect(instance.values.pyscript?.time).not.toBeNull(); // version wasn't present in `inline config` but is still set due to merging with default expect(instance.values.version).toBe('0.1'); - expect(instance.values.wonerful).toBe('highjacked'); + expect(instance.values.wonderful).toBe('hijacked'); }); it.failing('should NOT be able to load an inline config in JSON format with type as TOML', () => { From ef79c647fa1602c0d9f252641376cb0b568f7a41 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 11:35:21 +0200 Subject: [PATCH 13/32] WIP: one more test --- pyscriptjs/tests/unit/pyconfig.test.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index 296fbce9849..ac74f385abb 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -92,25 +92,24 @@ describe('loadConfigFromElement', () => { expect(config.version).toBe('0.1'); }); - /* it('should load the JSON config from both inline and src', () => { - instance.setAttribute('type', 'json'); - instance.innerHTML = JSON.stringify({ version: '0.2a', wonderful: 'hijacked' }); - instance.setAttribute('src', '/covfefe.json'); - instance.connectedCallback(); - // @ts-ignore - expect(instance.values.runtimes[0].lang).toBe('covfefe'); - expect(instance.values.pyscript?.time).not.toBeNull(); + const el = make_config_element({ type: 'json', src: '/covfefe.json' }); + el.innerHTML = JSON.stringify({ version: '0.2a', wonderful: 'hijacked' }); + const config = loadConfigFromElement(el); + expect(config.runtimes[0].lang).toBe('covfefe'); + expect(config.pyscript?.time).not.toBeNull(); // config from src had an extra key "wonderful" with value "disgrace" // inline config had the same extra key "wonderful" with value "hijacked" // the merge process works for extra keys that clash as well // so the final value is "hijacked" since inline takes precedence over src - expect(instance.values.wonderful).toBe('hijacked'); + expect(config.wonderful).toBe('hijacked'); // 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', () => { // type of config is TOML if not supplied instance.innerHTML = covfefeConfigToml; From 257c1e92891274a13ccbb3da2a6f243b05cc443d Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 11:38:02 +0200 Subject: [PATCH 14/32] WIP: more tests --- pyscriptjs/tests/unit/pyconfig.test.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index ac74f385abb..ba57afd76e9 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -108,25 +108,26 @@ describe('loadConfigFromElement', () => { expect(config.version).toBe('0.2a'); }); - /* - it('should be able to load an inline TOML config', () => { - // type of config is TOML if not supplied - instance.innerHTML = covfefeConfigToml; - instance.connectedCallback(); - // @ts-ignore - expect(instance.values.runtimes[0].lang).toBe('covfefe'); - expect(instance.values.pyscript?.time).not.toBeNull(); + // TOML is the default type + const el = make_config_element({}); + el.innerHTML = covfefeConfigToml; + const config = loadConfigFromElement(el); + expect(config.runtimes[0].lang).toBe('covfefe'); + expect(config.pyscript?.time).not.toBeNull(); // version wasn't present in `inline config` but is still set due to merging with default - expect(instance.values.version).toBe('0.1'); - expect(instance.values.wonderful).toBe('hijacked'); + expect(config.version).toBe('0.1'); + expect(config.wonderful).toBe('hijacked'); }); it.failing('should NOT be able to load an inline config in JSON format with type as TOML', () => { - instance.innerHTML = JSON.stringify(covfefeConfig); - instance.connectedCallback(); + const el = make_config_element({}); + el.innerHTML = JSON.stringify(covfefeConfig); + loadConfigFromElement(el); }); + /* + it.failing('should NOT be able to load an inline config in TOML format with type as JSON', () => { instance.setAttribute('type', 'json'); instance.innerHTML = covfefeConfigToml; From 07a4548f2b23a914bf769375f85ee6a6d610f5ae Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 11:46:08 +0200 Subject: [PATCH 15/32] WIP: more tests --- pyscriptjs/tests/unit/pyconfig.test.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index ba57afd76e9..79e4dc1e68c 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -126,27 +126,26 @@ describe('loadConfigFromElement', () => { loadConfigFromElement(el); }); - /* - it.failing('should NOT be able to load an inline config in TOML format with type as JSON', () => { - instance.setAttribute('type', 'json'); - instance.innerHTML = covfefeConfigToml; - instance.connectedCallback(); + const el = make_config_element({ type: 'json' }); + el.innerHTML = covfefeConfigToml; + loadConfigFromElement(el); }); it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as toml', () => { - instance.innerHTML = covfefeConfigToml; - instance.setAttribute('src', '/covfefe.json'); - instance.connectedCallback(); + const el = make_config_element({ src: '/covfefe.json' }); + el.innerHTML = covfefeConfigToml; + loadConfigFromElement(el); }); it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as json', () => { - instance.setAttribute('type', 'json'); - instance.innerHTML = covfefeConfigToml; - instance.setAttribute('src', '/covfefe.json'); - instance.connectedCallback(); + const el = make_config_element({ type: 'json', src: '/covfefe.json' }); + el.innerHTML = covfefeConfigToml; + loadConfigFromElement(el); }); + /* + it('connectedCallback should call loadRuntimes', async () => { const mockedMethod = jest.fn(); instance.loadRuntimes = mockedMethod; From 478970565dd4355a39a60a8fbea23e845322f53d Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 11:47:28 +0200 Subject: [PATCH 16/32] move the last tests to PyConfig --- pyscriptjs/tests/unit/pyconfig.test.ts | 51 ++++++++++++-------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index 79e4dc1e68c..a57f9b53117 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -143,8 +143,31 @@ describe('loadConfigFromElement', () => { el.innerHTML = covfefeConfigToml; loadConfigFromElement(el); }); +}); + - /* +// old tests about PyConfig.connectedCallback behavior, should be moved +// somewhere else + +import { PyConfig } from '../../src/components/pyconfig'; +customElements.define('py-config', PyConfig); + +describe('PyConfig', () => { + let instance: PyConfig; + + 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('connectedCallback should call loadRuntimes', async () => { const mockedMethod = jest.fn(); @@ -179,30 +202,4 @@ describe('loadConfigFromElement', () => { instance.close(); expect(instance.remove).toHaveBeenCalled(); }); - */ -}); - - -// old tests about PyConfig.connectedCallback behavior, should be moved -// somewhere else - -import { PyConfig } from '../../src/components/pyconfig'; -customElements.define('py-config', PyConfig); - -describe('PyConfig', () => { - let instance: PyConfig; - - 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'); - }); }) From 46c8aabb503fc0d4e7f031dee0dc2d260cb48035 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 14:52:28 +0200 Subject: [PATCH 17/32] another small step: PyConfig is no longer a web component but it's manually instantiated and called by main --- pyscriptjs/src/components/pyconfig.ts | 10 ++++---- pyscriptjs/src/config.ts | 6 ++--- pyscriptjs/src/main.ts | 33 +++++++++++++++++++-------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index 7a7cdf75fe3..5d2427f21b7 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -3,7 +3,6 @@ import { appConfig, addInitializer, runtimeLoaded } from '../stores'; import type { AppConfig, Runtime } from '../runtime'; import { PyodideRuntime } from '../pyodide'; import { getLogger } from '../logger'; -import { loadConfigFromElement } from '../config'; import { readTextFromPath, handleFetchError, globalExport } from '../utils' // Subscriber used to connect to the first available runtime (can be pyodide or others) @@ -34,19 +33,18 @@ globalExport('pyscript_get_config', pyscript_get_config); * the default runtime based on Pyodide is used. */ -export class PyConfig extends BaseEvalElement { +export class PyConfig { widths: Array; label: string; mount_name: string; details: HTMLElement; operation: HTMLElement; values: AppConfig; - constructor() { - super(); + constructor(config: AppConfig) { + this.values = config; } connectedCallback() { - this.values = loadConfigFromElement(this); appConfig.set(this.values); logger.info('config set:', this.values); @@ -62,7 +60,7 @@ export class PyConfig extends BaseEvalElement { } close() { - this.remove(); + //this.remove(); } loadPackages = async () => { diff --git a/pyscriptjs/src/config.ts b/pyscriptjs/src/config.ts index 2f669185041..a0c50637e2e 100644 --- a/pyscriptjs/src/config.ts +++ b/pyscriptjs/src/config.ts @@ -61,7 +61,7 @@ export const defaultConfig: AppConfig = { } -export function loadConfigFromElement(el: HTMLElement): AppConfig { +export function loadConfigFromElement(el: Element): AppConfig { let srcConfig; let inlineConfig; if (el === null) { @@ -82,7 +82,7 @@ export function loadConfigFromElement(el: HTMLElement): AppConfig { return result; } -function extractFromSrc(el: HTMLElement, configType: string) { +function extractFromSrc(el: Element, configType: string) { if (el.hasAttribute('src')) { const src = el.getAttribute('src'); @@ -93,7 +93,7 @@ function extractFromSrc(el: HTMLElement, configType: string) { } -function extractFromInline(el: HTMLElement, configType: string) { +function extractFromInline(el: Element, configType: string) { if (el.innerHTML!=='') { logger.info('loading content'); diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 9c687525b09..2c7a2cdbc9e 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -1,38 +1,51 @@ import './styles/pyscript_base.css'; +import { loadConfigFromElement } from './config'; +import type { AppConfig } from './config'; import { PyScript } from './components/pyscript'; import { PyEnv } from './components/pyenv'; import { PyLoader } from './components/pyloader'; import { PyConfig } from './components/pyconfig'; import { getLogger } from './logger'; -import { globalLoader } from './stores'; +import { globalLoader, appConfig } from './stores'; const logger = getLogger('pyscript/main'); class PyScriptApp { + config: AppConfig; + main() { + this.loadConfig(); + /* eslint-disable @typescript-eslint/no-unused-vars */ const xPyScript = customElements.define('py-script', PyScript); const xPyLoader = customElements.define('py-loader', PyLoader); - const xPyConfig = customElements.define('py-config', PyConfig); + //const xPyConfig = customElements.define('py-config', PyConfig); const xPyEnv = customElements.define('py-env', PyEnv); /* eslint-disable @typescript-eslint/no-unused-vars */ - // 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 logger.info('add py-loader'); const loader = document.createElement('py-loader'); document.body.append(loader); globalLoader.set(loader); } + + loadConfig() { + // find the tag. If not found, we get null which means + // "use the default config" + // XXX: what happens if we have multiple ones? + logger.info('searching for '); + const el = document.querySelector('py-config'); + this.config = loadConfigFromElement(el); + logger.info('config loaded:', this.config); + + // XXX kill me eventually + const py_config = new PyConfig(this.config); + py_config.connectedCallback(); + } + } From 2a185af2abd95985fe876b6b227d3046127f197d Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 17:47:45 +0200 Subject: [PATCH 18/32] fix tests which were broken after the merge --- pyscriptjs/src/config.ts | 2 +- pyscriptjs/tests/unit/pyconfig.test.ts | 32 +++++++------------------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/pyscriptjs/src/config.ts b/pyscriptjs/src/config.ts index a1923f875f6..bab576b0c8a 100644 --- a/pyscriptjs/src/config.ts +++ b/pyscriptjs/src/config.ts @@ -40,7 +40,7 @@ const allKeys = { "array": ["runtimes", "packages", "paths", "plugins"] }; -const defaultConfig: AppConfig = { +export const defaultConfig: AppConfig = { "schema_version": 1, "type": "app", "autoclose_loader": true, diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index a57f9b53117..970986d46f0 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -77,8 +77,8 @@ describe('loadConfigFromElement', () => { const config = loadConfigFromElement(el); expect(config.runtimes[0].lang).toBe('covfefe'); expect(config.pyscript?.time).not.toBeNull(); - // version wasn't present in `inline config` but is still set due to merging with default - expect(config.version).toBe('0.1'); + // 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', () => { @@ -88,8 +88,8 @@ describe('loadConfigFromElement', () => { expect(config.pyscript?.time).not.toBeNull(); // wonderful is an extra key supplied by the user and is unaffected by merging process expect(config.wonderful).toBe('disgrace'); - // version wasn't present in `config from src` but is still set due to merging with default - expect(config.version).toBe('0.1'); + // schema_version wasn't present in `config from src` but is still set due to merging with default + expect(config.schema_version).toBe(1); }); @@ -115,8 +115,8 @@ describe('loadConfigFromElement', () => { const config = loadConfigFromElement(el); expect(config.runtimes[0].lang).toBe('covfefe'); expect(config.pyscript?.time).not.toBeNull(); - // version wasn't present in `inline config` but is still set due to merging with default - expect(config.version).toBe('0.1'); + // 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'); }); @@ -150,13 +150,13 @@ describe('loadConfigFromElement', () => { // somewhere else import { PyConfig } from '../../src/components/pyconfig'; -customElements.define('py-config', PyConfig); +//customElements.define('py-config', PyConfig); describe('PyConfig', () => { let instance: PyConfig; beforeEach(() => { - instance = new PyConfig(); + instance = new PyConfig({}); }); it('should get the Config to just instantiate', async () => { @@ -178,16 +178,6 @@ describe('PyConfig', () => { expect(mockedMethod).toHaveBeenCalled(); }); - it('confirm connectedCallback happy path', async () => { - const mockedMethod = jest.fn(); - instance.loadRuntimes = mockedMethod; - instance.innerHTML = 'test'; - - instance.connectedCallback(); - - expect(instance.values['0']).toBe('test'); - }); - 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'); @@ -196,10 +186,4 @@ describe('PyConfig', () => { // @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(); - }); }) From c51621af9330f5ef5f308dec017ed6f6615866e8 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 18:42:44 +0200 Subject: [PATCH 19/32] use JSON.stringify to get a human-readable version of the config --- pyscriptjs/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 2c7a2cdbc9e..e4907f5882f 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -39,7 +39,7 @@ class PyScriptApp { logger.info('searching for '); const el = document.querySelector('py-config'); this.config = loadConfigFromElement(el); - logger.info('config loaded:', this.config); + logger.info('config loaded:\n' + JSON.stringify(this.config, null, 2)); // XXX kill me eventually const py_config = new PyConfig(this.config); From 2964124bd2403a8613ce0a765e12517d6e2c70db Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 18:46:31 +0200 Subject: [PATCH 20/32] add an integration test for paths=... --- pyscriptjs/tests/integration/test_01_basic.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pyscriptjs/tests/integration/test_01_basic.py b/pyscriptjs/tests/integration/test_01_basic.py index d194e0938b9..cb02b9b4ec2 100644 --- a/pyscriptjs/tests/integration/test_01_basic.py +++ b/pyscriptjs/tests/integration/test_01_basic.py @@ -51,3 +51,26 @@ def test_escaping_of_angle_brackets(self): """ ) assert self.console.log.lines == [self.PY_COMPLETE, "true false", "
"] + + def test_paths(self): + self.writefile("a.py", "x = 'hello from A'") + self.writefile("b.py", "x = 'hello from B'") + self.pyscript_run( + """ + + paths = ["./a.py", "./b.py"] + + + + import js + import a, b + js.console.log(a.x) + js.console.log(b.x) + + """ + ) + assert self.console.log.lines == [ + self.PY_COMPLETE, + "hello from A", + "hello from B", + ] From fe702cef8bc2a5f0fd26cd273d9820ba46d7b67a Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 19:06:13 +0200 Subject: [PATCH 21/32] add a test for packages... --- pyscriptjs/tests/integration/test_01_basic.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pyscriptjs/tests/integration/test_01_basic.py b/pyscriptjs/tests/integration/test_01_basic.py index cb02b9b4ec2..d79160f9fc6 100644 --- a/pyscriptjs/tests/integration/test_01_basic.py +++ b/pyscriptjs/tests/integration/test_01_basic.py @@ -74,3 +74,27 @@ def test_paths(self): "hello from A", "hello from B", ] + + def test_packages(self): + self.pyscript_run( + """ + + # we use asciitree because it's one of the smallest packages + # which are built and distributed with pyodide + packages = ["asciitree"] + + + + import js + import asciitree + js.console.log('hello', asciitree.__name__) + + + """ + ) + assert self.console.log.lines == [ + self.PY_COMPLETE, + "Loading asciitree", # printed by pyodide + "Loaded asciitree", # printed by pyodide + "hello asciitree", # printed by us + ] From 35a82018875850243d026bd880efd65444a696a0 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 19:18:37 +0200 Subject: [PATCH 22/32] move the loadPackages logic from pyconfig into main.ts --- pyscriptjs/src/components/pyconfig.ts | 7 ------- pyscriptjs/src/main.ts | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index 5d2427f21b7..5fa375610e1 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -48,7 +48,6 @@ export class PyConfig { appConfig.set(this.values); logger.info('config set:', this.values); - addInitializer(this.loadPackages); addInitializer(this.loadPaths); this.loadRuntimes(); } @@ -63,12 +62,6 @@ export class PyConfig { //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) diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index e4907f5882f..db2619268b1 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -2,21 +2,29 @@ import './styles/pyscript_base.css'; import { loadConfigFromElement } from './config'; import type { AppConfig } from './config'; +import type { Runtime } from './runtime'; import { PyScript } from './components/pyscript'; import { PyEnv } from './components/pyenv'; import { PyLoader } from './components/pyloader'; import { PyConfig } from './components/pyconfig'; import { getLogger } from './logger'; -import { globalLoader, appConfig } from './stores'; +import { globalLoader, appConfig, runtimeLoaded, addInitializer } from './stores'; const logger = getLogger('pyscript/main'); +let runtimeSpec: Runtime; +runtimeLoaded.subscribe(value => { + runtimeSpec = value; +}); + + class PyScriptApp { config: AppConfig; main() { this.loadConfig(); + this.initialize(); /* eslint-disable @typescript-eslint/no-unused-vars */ const xPyScript = customElements.define('py-script', PyScript); @@ -46,6 +54,15 @@ class PyScriptApp { py_config.connectedCallback(); } + initialize() { + addInitializer(this.loadPackages); + } + + loadPackages = async () => { + const env = this.config.packages; + logger.info("Loading env: ", env); + await runtimeSpec.installPackage(env); + } } From d4a3138d044150af6a50d814eafac6673745acf2 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 19:24:47 +0200 Subject: [PATCH 23/32] move the loadPaths logic from pyconfig into main.ts --- pyscriptjs/src/components/pyconfig.ts | 15 --------------- pyscriptjs/src/main.ts | 23 ++++++++++++++++++++--- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index 5fa375610e1..c582c75b835 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -48,7 +48,6 @@ export class PyConfig { appConfig.set(this.values); logger.info('config set:', this.values); - addInitializer(this.loadPaths); this.loadRuntimes(); } @@ -62,20 +61,6 @@ export class PyConfig { //this.remove(); } - 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(e, singleFile); - } - } - logger.info("All paths loaded"); - } loadRuntimes() { logger.info('Initializing runtimes'); diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index db2619268b1..7769dbd6872 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -9,6 +9,7 @@ import { PyLoader } from './components/pyloader'; import { PyConfig } from './components/pyconfig'; import { getLogger } from './logger'; import { globalLoader, appConfig, runtimeLoaded, addInitializer } from './stores'; +import { handleFetchError } from './utils' const logger = getLogger('pyscript/main'); @@ -56,13 +57,29 @@ class PyScriptApp { initialize() { addInitializer(this.loadPackages); + addInitializer(this.loadPaths); } loadPackages = async () => { - const env = this.config.packages; - logger.info("Loading env: ", env); - await runtimeSpec.installPackage(env); + 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(e, singleFile); + } + } + logger.info("All paths loaded"); + } + } From 6ee77948459282cd16251da638170f0fba07d55a Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 20:02:24 +0200 Subject: [PATCH 24/32] move the loadRuntimes() logic into main.ts --- pyscriptjs/src/components/pyconfig.ts | 14 -------------- pyscriptjs/src/main.ts | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index c582c75b835..7a34efcea10 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -47,8 +47,6 @@ export class PyConfig { connectedCallback() { appConfig.set(this.values); logger.info('config set:', this.values); - - this.loadRuntimes(); } log(msg: string) { @@ -62,16 +60,4 @@ export class PyConfig { } - 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); - } - } } diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 7769dbd6872..56aaeae2429 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -7,12 +7,14 @@ import { PyScript } from './components/pyscript'; import { PyEnv } from './components/pyenv'; import { PyLoader } from './components/pyloader'; import { PyConfig } from './components/pyconfig'; +import { PyodideRuntime } from './pyodide'; import { getLogger } from './logger'; import { globalLoader, appConfig, runtimeLoaded, addInitializer } from './stores'; import { handleFetchError } from './utils' const logger = getLogger('pyscript/main'); +// XXX this should be killed eventually let runtimeSpec: Runtime; runtimeLoaded.subscribe(value => { runtimeSpec = value; @@ -58,6 +60,7 @@ class PyScriptApp { initialize() { addInitializer(this.loadPackages); addInitializer(this.loadPaths); + this.loadRuntimes(); } loadPackages = async () => { @@ -80,6 +83,19 @@ class PyScriptApp { logger.info("All paths loaded"); } + loadRuntimes() { + logger.info('Initializing runtimes'); + for (const runtime of this.config.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); + } + } + } From e2ae49014fde57246b1f4685e0ce6745b35b0539 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 20:08:06 +0200 Subject: [PATCH 25/32] kill all the remaining references to PyConfig --- pyscriptjs/src/components/pyconfig.ts | 4 ---- pyscriptjs/src/main.ts | 11 ++++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts index 7a34efcea10..5de0dda4b8f 100644 --- a/pyscriptjs/src/components/pyconfig.ts +++ b/pyscriptjs/src/components/pyconfig.ts @@ -18,10 +18,6 @@ appConfig.subscribe(value => { const logger = getLogger('py-config'); -function pyscript_get_config() { - return appConfig_; -} -globalExport('pyscript_get_config', pyscript_get_config); /** diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 56aaeae2429..2cf9cffb21e 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -6,11 +6,10 @@ import type { Runtime } from './runtime'; import { PyScript } from './components/pyscript'; import { PyEnv } from './components/pyenv'; import { PyLoader } from './components/pyloader'; -import { PyConfig } from './components/pyconfig'; import { PyodideRuntime } from './pyodide'; import { getLogger } from './logger'; import { globalLoader, appConfig, runtimeLoaded, addInitializer } from './stores'; -import { handleFetchError } from './utils' +import { handleFetchError, globalExport } from './utils' const logger = getLogger('pyscript/main'); @@ -32,7 +31,6 @@ class PyScriptApp { /* eslint-disable @typescript-eslint/no-unused-vars */ const xPyScript = customElements.define('py-script', PyScript); const xPyLoader = customElements.define('py-loader', PyLoader); - //const xPyConfig = customElements.define('py-config', PyConfig); const xPyEnv = customElements.define('py-env', PyEnv); /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -53,8 +51,7 @@ class PyScriptApp { logger.info('config loaded:\n' + JSON.stringify(this.config, null, 2)); // XXX kill me eventually - const py_config = new PyConfig(this.config); - py_config.connectedCallback(); + appConfig.set(this.config); } initialize() { @@ -98,6 +95,10 @@ class PyScriptApp { } +function pyscript_get_config() { + return globalApp.config; +} +globalExport('pyscript_get_config', pyscript_get_config); // main entry point of execution const globalApp = new PyScriptApp(); From b9e77ae6ac305758052a550c745f84413e71b47d Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 20:24:31 +0200 Subject: [PATCH 26/32] finally kill pyconfig.ts --- pyscriptjs/src/components/pyconfig.ts | 59 -------------------------- pyscriptjs/tests/unit/pyconfig.test.ts | 43 ------------------- 2 files changed, 102 deletions(-) delete mode 100644 pyscriptjs/src/components/pyconfig.ts diff --git a/pyscriptjs/src/components/pyconfig.ts b/pyscriptjs/src/components/pyconfig.ts deleted file mode 100644 index 5de0dda4b8f..00000000000 --- a/pyscriptjs/src/components/pyconfig.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { BaseEvalElement } from './base'; -import { appConfig, addInitializer, runtimeLoaded } from '../stores'; -import type { AppConfig, Runtime } from '../runtime'; -import { PyodideRuntime } from '../pyodide'; -import { getLogger } from '../logger'; -import { readTextFromPath, handleFetchError, globalExport } 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 { - widths: Array; - label: string; - mount_name: string; - details: HTMLElement; - operation: HTMLElement; - values: AppConfig; - constructor(config: AppConfig) { - this.values = config; - } - - connectedCallback() { - appConfig.set(this.values); - logger.info('config set:', this.values); - } - - log(msg: string) { - const newLog = document.createElement('p'); - newLog.innerText = msg; - this.details.appendChild(newLog); - } - - close() { - //this.remove(); - } - - -} diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index 970986d46f0..d7ed721987f 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -144,46 +144,3 @@ describe('loadConfigFromElement', () => { loadConfigFromElement(el); }); }); - - -// old tests about PyConfig.connectedCallback behavior, should be moved -// somewhere else - -import { PyConfig } from '../../src/components/pyconfig'; -//customElements.define('py-config', PyConfig); - -describe('PyConfig', () => { - let instance: PyConfig; - - 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('connectedCallback should call loadRuntimes', async () => { - const mockedMethod = jest.fn(); - instance.loadRuntimes = mockedMethod; - - instance.connectedCallback(); - - expect(mockedMethod).toHaveBeenCalled(); - }); - - 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'); - }); -}) From 8f8b4b1d39c6472a390fae5ae1e4589b2f39a349 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 30 Sep 2022 20:28:56 +0200 Subject: [PATCH 27/32] now that the old pyconfig.ts has gone, we can reuse its name --- pyscriptjs/src/main.ts | 4 ++-- pyscriptjs/src/{config.ts => pyconfig.ts} | 0 pyscriptjs/src/utils.ts | 2 -- pyscriptjs/tests/unit/pyconfig.test.ts | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) rename pyscriptjs/src/{config.ts => pyconfig.ts} (100%) diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 2cf9cffb21e..c571c3c5f90 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -1,7 +1,7 @@ import './styles/pyscript_base.css'; -import { loadConfigFromElement } from './config'; -import type { AppConfig } from './config'; +import { loadConfigFromElement } from './pyconfig'; +import type { AppConfig } from './pyconfig'; import type { Runtime } from './runtime'; import { PyScript } from './components/pyscript'; import { PyEnv } from './components/pyenv'; diff --git a/pyscriptjs/src/config.ts b/pyscriptjs/src/pyconfig.ts similarity index 100% rename from pyscriptjs/src/config.ts rename to pyscriptjs/src/pyconfig.ts diff --git a/pyscriptjs/src/utils.ts b/pyscriptjs/src/utils.ts index cbca0234625..cb5377a351d 100644 --- a/pyscriptjs/src/utils.ts +++ b/pyscriptjs/src/utils.ts @@ -1,5 +1,3 @@ -import type { AppConfig } from "./config"; - function addClasses(element: HTMLElement, classes: Array) { for (const entry of classes) { element.classList.add(entry); diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index d7ed721987f..5bbb564f095 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals'; -import type { AppConfig, RuntimeConfig } from '../../src/config'; -import { loadConfigFromElement, defaultConfig } from '../../src/config'; +import type { AppConfig, RuntimeConfig } from '../../src/pyconfig'; +import { loadConfigFromElement, defaultConfig } from '../../src/pyconfig'; import { version } from '../../src/runtime'; // inspired by trump typos From 4b37b8beca390a3d724c95cd06022bc6b114ce25 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 1 Oct 2022 00:36:32 +0200 Subject: [PATCH 28/32] wooo, kill the appConfig store: instead of using a global variable to access the confing from Runtime, we explicitly pass it in the constructor --- pyscriptjs/src/main.ts | 8 ++--- pyscriptjs/src/pyodide.ts | 5 +-- pyscriptjs/src/runtime.ts | 46 +++++---------------------- pyscriptjs/src/stores.ts | 4 +-- pyscriptjs/tests/unit/runtime.test.ts | 4 ++- 5 files changed, 19 insertions(+), 48 deletions(-) diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index c571c3c5f90..f7af9b68a2b 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -8,7 +8,7 @@ import { PyEnv } from './components/pyenv'; import { PyLoader } from './components/pyloader'; import { PyodideRuntime } from './pyodide'; import { getLogger } from './logger'; -import { globalLoader, appConfig, runtimeLoaded, addInitializer } from './stores'; +import { globalLoader, runtimeLoaded, addInitializer } from './stores'; import { handleFetchError, globalExport } from './utils' const logger = getLogger('pyscript/main'); @@ -49,9 +49,6 @@ class PyScriptApp { const el = document.querySelector('py-config'); this.config = loadConfigFromElement(el); logger.info('config loaded:\n' + JSON.stringify(this.config, null, 2)); - - // XXX kill me eventually - appConfig.set(this.config); } initialize() { @@ -83,7 +80,8 @@ class PyScriptApp { loadRuntimes() { logger.info('Initializing runtimes'); for (const runtime of this.config.runtimes) { - const runtimeObj: Runtime = new PyodideRuntime(runtime.src, runtime.name, runtime.lang); + 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', () => { diff --git a/pyscriptjs/src/pyodide.ts b/pyscriptjs/src/pyodide.ts index 8a81a56042e..1c113bae667 100644 --- a/pyscriptjs/src/pyodide.ts +++ b/pyscriptjs/src/pyodide.ts @@ -1,4 +1,4 @@ -import { Runtime, RuntimeConfig } from './runtime'; +import { Runtime } from './runtime'; import { getLastPath } from './utils'; import { getLogger } from './logger'; import type { PyodideInterface } from 'pyodide'; @@ -16,12 +16,13 @@ export class PyodideRuntime extends Runtime { globals: any; constructor( + config, src = 'https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js', name = 'pyodide-default', lang = 'python', ) { logger.info('Runtime config:', { name, lang, src }); - super(); + super(config); this.src = src; this.name = name; this.lang = lang; diff --git a/pyscriptjs/src/runtime.ts b/pyscriptjs/src/runtime.ts index 585a1c7f2a6..722f61aebf3 100644 --- a/pyscriptjs/src/runtime.ts +++ b/pyscriptjs/src/runtime.ts @@ -1,3 +1,4 @@ +import type { AppConfig } from './pyconfig'; import type { PyodideInterface } from 'pyodide'; import type { PyLoader } from './components/pyloader'; import { @@ -8,7 +9,6 @@ import { postInitializers, Initializer, scriptsQueue, - appConfig, } from './stores'; import { createCustomElements } from './components/elements'; import type { PyScript } from './components/pyscript'; @@ -19,33 +19,6 @@ const logger = getLogger('pyscript/runtime'); export const version = "<>"; export type RuntimeInterpreter = PyodideInterface | null; -export interface AppConfig extends Record { - name?: string; - description?: string; - version?: string; - schema_version?: number; - type?: string; - author_name?: string; - author_email?: string; - license?: string; - autoclose_loader?: boolean; - runtimes?: Array; - packages?: Array; - paths?: Array; - plugins?: Array; - pyscript?: PyScriptMetadata; -} - -export type PyScriptMetadata = { - version?: string; - time?: string; -} - -export type RuntimeConfig = { - src?: string; - name?: string; - lang?: string; -}; let loader: PyLoader | undefined; globalLoader.subscribe(value => { @@ -67,15 +40,6 @@ scriptsQueue.subscribe((value: PyScript[]) => { 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 @@ -95,6 +59,7 @@ For an example implementation, refer to the `PyodideRuntime` class in `pyodide.ts` */ export abstract class Runtime extends Object { + config: AppConfig; abstract src: string; abstract name?: string; abstract lang?: string; @@ -104,6 +69,11 @@ export abstract class Runtime extends Object { * */ abstract globals: any; + constructor(config: AppConfig) { + super(); + this.config = config; + } + /** * loads the interpreter for the runtime and saves an instance of it * 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 createCustomElements(); - if (appConfig_ && appConfig_.autoclose_loader) { + if (this.config.autoclose_loader) { loader?.close(); } diff --git a/pyscriptjs/src/stores.ts b/pyscriptjs/src/stores.ts index 2f276f6d52c..78a5dc42f9c 100644 --- a/pyscriptjs/src/stores.ts +++ b/pyscriptjs/src/stores.ts @@ -1,7 +1,8 @@ import { writable } from 'svelte/store'; import type { PyLoader } from './components/pyloader'; 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'; export type Initializer = () => Promise; @@ -29,7 +30,6 @@ export const scriptsQueue = writable([]); export const initializers = writable([]); export const postInitializers = writable([]); export const globalLoader = writable(); -export const appConfig = writable(); export const addToScriptsQueue = (script: PyScript) => { scriptsQueue.update(scriptsQueue => [...scriptsQueue, script]); diff --git a/pyscriptjs/tests/unit/runtime.test.ts b/pyscriptjs/tests/unit/runtime.test.ts index 5398eefebac..06fdde0b19b 100644 --- a/pyscriptjs/tests/unit/runtime.test.ts +++ b/pyscriptjs/tests/unit/runtime.test.ts @@ -1,3 +1,4 @@ +import type { AppConfig } from '../../src/pyconfig'; import { Runtime } from '../../src/runtime'; import { PyodideRuntime } from '../../src/pyodide'; @@ -8,7 +9,8 @@ global.TextDecoder = TextDecoder describe('PyodideRuntime', () => { let runtime: PyodideRuntime; beforeAll(async () => { - runtime = new PyodideRuntime(); + const config: AppConfig = {}; + runtime = new PyodideRuntime(config); /** * Since import { loadPyodide } from 'pyodide'; * is not used inside `src/pyodide.ts`, the function From c0577d6511887f0d6583ba6328caea7a46d10e98 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Mon, 3 Oct 2022 13:12:14 +0200 Subject: [PATCH 29/32] Update pyscriptjs/src/pyconfig.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Rosado --- pyscriptjs/src/pyconfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscriptjs/src/pyconfig.ts b/pyscriptjs/src/pyconfig.ts index bab576b0c8a..fe16737042e 100644 --- a/pyscriptjs/src/pyconfig.ts +++ b/pyscriptjs/src/pyconfig.ts @@ -88,7 +88,7 @@ function extractFromSrc(el: Element, configType: string) { function extractFromInline(el: Element, configType: string) { - if (el.innerHTML!=='') + if (el.innerHTML !== '') { logger.info('loading content'); return validateConfig(el.innerHTML, configType); From c65a8bac8f51c27a6fe903e94bb4409febc48c96 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Mon, 3 Oct 2022 13:10:00 +0200 Subject: [PATCH 30/32] improve this comment --- pyscriptjs/src/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index f7af9b68a2b..edc570f6c3f 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -44,7 +44,8 @@ class PyScriptApp { loadConfig() { // find the tag. If not found, we get null which means // "use the default config" - // XXX: what happens if we have multiple ones? + // XXX: we should actively complain if there are multiple + // and show a big error. PRs welcome :) logger.info('searching for '); const el = document.querySelector('py-config'); this.config = loadConfigFromElement(el); From 0dca19809fa5bff07d7149f99b615c6b275f9792 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Mon, 3 Oct 2022 16:34:14 +0200 Subject: [PATCH 31/32] try to use an older version of eslint --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ecbe38e5b16..f1d62d3c40d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - --py310-plus - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.24.0 + rev: v8.23.1 hooks: - id: eslint files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx From 7239e2ddf84f4d0a3b44557ffcb9d2f144bb2391 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 4 Oct 2022 12:59:02 +0200 Subject: [PATCH 32/32] use expect().toThrow instead of it.failing() --- pyscriptjs/tests/unit/pyconfig.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyscriptjs/tests/unit/pyconfig.test.ts b/pyscriptjs/tests/unit/pyconfig.test.ts index f600e39bb3e..5c288706ccb 100644 --- a/pyscriptjs/tests/unit/pyconfig.test.ts +++ b/pyscriptjs/tests/unit/pyconfig.test.ts @@ -120,28 +120,28 @@ describe('loadConfigFromElement', () => { 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', () => { const el = make_config_element({}); el.innerHTML = JSON.stringify(covfefeConfig); - loadConfigFromElement(el); + 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', () => { const el = make_config_element({ type: 'json' }); el.innerHTML = covfefeConfigToml; - loadConfigFromElement(el); + 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', () => { const el = make_config_element({ src: '/covfefe.json' }); el.innerHTML = covfefeConfigToml; - loadConfigFromElement(el); + 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', () => { const el = make_config_element({ type: 'json', src: '/covfefe.json' }); el.innerHTML = covfefeConfigToml; - loadConfigFromElement(el); + expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError); }); it('should error out when passing an invalid JSON', () => {