Provide Visible Error if <py-env> paths is used in a local HTML file (#311)

* Add onscreen error when using py-env paths in local HTTP files without file server

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove redundant code, fix error handling, add 404 error

* Lint and Format

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* manage errors loading files

* use handleFetchError for handling fetch errors in env

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Fabio Pliger <fabio.pliger@gmail.com>
This commit is contained in:
Jeff Glass
2022-05-17 23:48:02 -05:00
committed by GitHub
parent 39774a83c5
commit b767a78b05
8 changed files with 188 additions and 125 deletions

View File

@@ -1,37 +1,39 @@
<style global>
.spinner::after {
content: '';
box-sizing: border-box;
width: 40px;
height: 40px;
position: absolute;
top: calc(40% - 20px);
left: calc(50% - 20px);
border-radius: 50%;
}
.spinner.smooth::after {
border-top: 4px solid rgba(255, 255, 255, 1);
border-left: 4px solid rgba(255, 255, 255, 1);
border-right: 4px solid rgba(255, 255, 255, 0);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.label {
text-align: center;
width: 100%;
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 0.8rem;
margin-top: 6rem;
}
</style>
<script lang="ts">
import Tailwind from './Tailwind.svelte';
</script>
<style global>
.spinner::after {
content: '';
box-sizing: border-box;
width: 40px;
height: 40px;
position: absolute;
top: calc(40% - 20px);
left: calc(50% - 20px);
border-radius: 50%;
}
.spinner.smooth::after {
border-top: 4px solid rgba(255, 255, 255, 1.0);
border-left: 4px solid rgba(255, 255, 255, 1.0);
border-right: 4px solid rgba(255, 255, 255, 0.0);
animation: spinner .6s linear infinite;
}
@keyframes spinner {
to {transform: rotate(360deg);}
}
.label {
text-align: center;
width: 100%;
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 0.8rem;
margin-top: 6rem;
}
</style>
<Tailwind />

View File

@@ -1,15 +1,24 @@
import * as jsyaml from 'js-yaml';
import { BaseEvalElement } from './base';
import { initializers, loadedEnvironments, mode, postInitializers, pyodideLoaded, scriptsQueue, globalLoader, appConfig, Initializer } from '../stores';
import {
initializers,
loadedEnvironments,
mode,
postInitializers,
pyodideLoaded,
scriptsQueue,
globalLoader,
appConfig,
Initializer,
} from '../stores';
import { loadInterpreter } from '../interpreter';
import type { PyScript } from './pyscript';
const DEFAULT_RUNTIME = {
src: "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js",
name: "pyodide-default",
lang: "python"
}
src: 'https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js',
name: 'pyodide-default',
lang: 'python',
};
export type Runtime = {
src: string;
@@ -28,107 +37,103 @@ let appConfig_: AppConfig = {
autoclose_loader: true,
};
appConfig.subscribe( (value:AppConfig) => {
if (value){
appConfig.subscribe((value: AppConfig) => {
if (value) {
appConfig_ = value;
}
console.log("config set!")
console.log('config set!');
});
let initializers_: Initializer[];
initializers.subscribe( (value:Initializer[]) => {
initializers.subscribe((value: Initializer[]) => {
initializers_ = value;
console.log("initializers set")
console.log('initializers set');
});
let postInitializers_: Initializer[];
postInitializers.subscribe( (value:Initializer[]) => {
postInitializers.subscribe((value: Initializer[]) => {
postInitializers_ = value;
console.log("post initializers set")
console.log('post initializers set');
});
let scriptsQueue_: PyScript[];
scriptsQueue.subscribe( (value: PyScript[]) => {
scriptsQueue.subscribe((value: PyScript[]) => {
scriptsQueue_ = value;
console.log("post initializers set")
console.log('post initializers set');
});
let mode_: string;
mode.subscribe( (value:string) => {
mode.subscribe((value: string) => {
mode_ = value;
console.log("post initializers set")
console.log('post initializers set');
});
let pyodideReadyPromise;
let loader;
globalLoader.subscribe(value => {
loader = value;
});
export class PyodideRuntime extends Object{
export class PyodideRuntime extends Object {
src: string;
constructor(url:string) {
constructor(url: string) {
super();
this.src = url;
}
async initialize(){
loader.log("Loading runtime...")
pyodideReadyPromise = loadInterpreter(this.src);
const pyodide = await pyodideReadyPromise;
const newEnv = {
id: 'a',
promise: pyodideReadyPromise,
runtime: pyodide,
state: 'loading',
};
pyodideLoaded.set(pyodide);
async initialize() {
loader.log('Loading runtime...');
pyodideReadyPromise = loadInterpreter(this.src);
const pyodide = await pyodideReadyPromise;
const newEnv = {
id: 'a',
promise: pyodideReadyPromise,
runtime: pyodide,
state: 'loading',
};
pyodideLoaded.set(pyodide);
// Inject the loader into the runtime namespace
pyodide.globals.set("pyscript_loader", loader);
// Inject the loader into the runtime namespace
pyodide.globals.set('pyscript_loader', loader);
loader.log("Runtime created...")
loadedEnvironments.update((value: any): any => {
value[newEnv['id']] = newEnv;
});
loader.log('Runtime created...');
loadedEnvironments.update((value: any): any => {
value[newEnv['id']] = newEnv;
});
// now we call all initializers before we actually executed all page scripts
loader.log("Initializing components...")
for (const initializer of initializers_) {
await initializer();
}
// now we can actually execute the page scripts if we are in play mode
loader.log("Initializing scripts...")
if (mode_ == 'play') {
for (const script of scriptsQueue_) {
script.evaluate();
// now we call all initializers before we actually executed all page scripts
loader.log('Initializing components...');
for (const initializer of initializers_) {
await initializer();
}
scriptsQueue.set([]);
}
// now we call all post initializers AFTER we actually executed all page scripts
loader.log("Running post initializers...");
if (appConfig_ && appConfig_.autoclose_loader) {
loader.close();
console.log("------ loader closed ------");
}
setTimeout(() => {
for (const initializer of postInitializers_) {
initializer();
// now we can actually execute the page scripts if we are in play mode
loader.log('Initializing scripts...');
if (mode_ == 'play') {
for (const script of scriptsQueue_) {
script.evaluate();
}
scriptsQueue.set([]);
}
}, 3000);
// now we call all post initializers AFTER we actually executed all page scripts
loader.log('Running post initializers...');
if (appConfig_ && appConfig_.autoclose_loader) {
loader.close();
console.log('------ loader closed ------');
}
setTimeout(() => {
for (const initializer of postInitializers_) {
initializer();
}
}, 3000);
}
}
export class PyConfig extends BaseEvalElement {
shadow: ShadowRoot;
wrapper: HTMLElement;
@@ -149,23 +154,23 @@ export class PyConfig extends BaseEvalElement {
this.innerHTML = '';
const loadedValues = jsyaml.load(this.code);
if (loadedValues === undefined){
if (loadedValues === undefined) {
this.values = {
autoclose_loader: true,
};
}else{
} else {
this.values = Object.assign({}, ...loadedValues);
}
if (this.values.runtimes === undefined){
if (this.values.runtimes === undefined) {
this.values.runtimes = [DEFAULT_RUNTIME];
}
appConfig.set(this.values);
console.log("config set", this.values);
console.log('config set', this.values);
this.loadRuntimes();
}
log(msg: string){
log(msg: string) {
const newLog = document.createElement('p');
newLog.innerText = msg;
this.details.appendChild(newLog);
@@ -175,10 +180,10 @@ export class PyConfig extends BaseEvalElement {
this.remove();
}
loadRuntimes(){
console.log("Initializing runtimes...")
loadRuntimes() {
console.log('Initializing runtimes...');
for (const runtime of this.values.runtimes) {
const script = document.createElement("script"); // create a script DOM node
const script = document.createElement('script'); // create a script DOM node
const runtimeSpec = new PyodideRuntime(runtime.src);
script.src = runtime.src; // set its src to the provided URL
script.addEventListener('load', () => {

View File

@@ -2,6 +2,7 @@ import * as jsyaml from 'js-yaml';
import { pyodideLoaded, addInitializer } from '../stores';
import { loadPackage, loadFromFile } from '../interpreter';
import { handleFetchError } from '../utils';
// Premise used to connect to the first available pyodide interpreter
let pyodideReadyPromise;
@@ -62,7 +63,12 @@ export class PyEnv extends HTMLElement {
async function loadPaths() {
for (const singleFile of paths) {
console.log(`loading ${singleFile}`);
await loadFromFile(singleFile, runtime);
try {
await loadFromFile(singleFile, runtime);
} catch (e) {
//Should we still export full error contents to console?
handleFetchError(e, singleFile);
}
}
console.log('paths loaded');
}

View File

@@ -26,7 +26,7 @@ export class PyLoader extends BaseEvalElement {
this.details = document.getElementById('pyscript-operation-details');
}
log(msg: string){
log(msg: string) {
const newLog = document.createElement('p');
newLog.innerText = msg;
this.details.appendChild(newLog);

View File

@@ -47,7 +47,7 @@ function getEditorTheme(el: BaseEvalElement): string {
return initialTheme;
}
return initialTheme = el.getAttribute('theme');
return (initialTheme = el.getAttribute('theme'));
}
export class PyRepl extends BaseEvalElement {

View File

@@ -1,9 +1,9 @@
import { getLastPath } from './utils';
import { getLastPath, handleFetchError } from './utils';
let pyodideReadyPromise;
let pyodide;
const loadInterpreter = async function (indexUrl:string): Promise<any> {
const loadInterpreter = async function (indexUrl: string): Promise<any> {
console.log('creating pyodide runtime');
// eslint-disable-next-line
// @ts-ignore
@@ -11,7 +11,7 @@ const loadInterpreter = async function (indexUrl:string): Promise<any> {
// indexURL: indexUrl,
stdout: console.log,
stderr: console.log,
fullStdLib: false
fullStdLib: false,
});
// now that we loaded, add additional convenience functions
@@ -24,35 +24,47 @@ const loadInterpreter = async function (indexUrl:string): Promise<any> {
// file from the same location
const loadedScript: HTMLScriptElement = document.querySelector(`script[src$='pyscript.js'], script[src$='pyscript.min.js']`);
const scriptPath = loadedScript.src.substring(0, loadedScript.src.lastIndexOf('/'));
await pyodide.runPythonAsync(await (await fetch(`${scriptPath}/pyscript.py`)).text());
const pyScriptPath = `${scriptPath}/pyscript.py`;
console.log(scriptPath);
try {
await pyodide.runPythonAsync(await (await fetch(pyScriptPath)).text());
} catch (e) {
//Should we still export full error contents to console?
handleFetchError(e, pyScriptPath);
}
console.log('done setting up environment');
return pyodide;
};
const loadPackage = async function (package_name: string[] | string, runtime: any): Promise<any> {
const micropip = pyodide.globals.get('micropip');
await micropip.install(package_name);
micropip.destroy();
if (package_name.length > 0){
const micropip = pyodide.globals.get('micropip');
await micropip.install(package_name);
micropip.destroy();
}
};
const loadFromFile = async function (s: string, runtime: any): Promise<any> {
const filename = getLastPath(s);
await runtime.runPythonAsync(
`
from pyodide.http import pyfetch
from js import console
response = await pyfetch("` +
from pyodide.http import pyfetch
from js import console
try:
response = await pyfetch("` +
s +
`")
content = await response.bytes()
with open("` +
except Exception as err:
console.warn("PyScript: Access to local files (using 'Paths:' in py-env) is not available when directly opening a HTML file; you must use a webserver to serve the additional files. See https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062 on starting a simple webserver with Python.")
raise(err)
content = await response.bytes()
with open("` +
filename +
`", "wb") as f:
f.write(content)
`,
f.write(content)
`,
);
};

View File

@@ -25,7 +25,7 @@ const xPyConfig = customElements.define('py-config', PyConfig);
// As first thing, loop for application configs
const config: PyConfig = document.querySelector('py-config');
if (!config){
if (!config) {
const loader = document.createElement('py-config');
document.body.append(loader);
}

View File

@@ -43,4 +43,42 @@ function guidGenerator(): string {
return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
}
export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator };
/*
* Display a page-wide error message to show that something has gone wrong with
* PyScript or Pyodide during loading. Probably not be used for issues that occur within
* Python scripts, since stderr can be routed to somewhere in the DOM
*/
function showError(msg: string): void {
const warning = document.createElement('div');
warning.style.backgroundColor = 'LightCoral';
warning.style.alignContent = 'center';
warning.style.margin = '4px';
warning.style.padding = '4px';
warning.innerHTML = msg;
document.body.prepend(warning);
}
function handleFetchError(e: Error, singleFile: string){
//Should we still export full error contents to console?
console.warn('Caught an error in loadPaths:\r\n' + e);
let errorContent;
if (e.message.includes('TypeError: Failed to fetch')) {
errorContent = `<p>PyScript: Access to local files
(using "Paths:" in &lt;py-env&gt;)
is not available when directly opening a HTML file;
you must use a webserver to serve the additional files.
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
on starting a simple webserver with Python.</p>`;
} else if (e.message.includes('404')) {
errorContent =
`<p>PyScript: Loading from file <u>` +
singleFile +
`</u> failed with error 404 (File not Found). Are your filename and path are correct?</p>`;
} else {
errorContent =
'<p>PyScript encountered an error while loading from file: ' + e.message + '</p>';
}
showError(errorContent);
}
export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError };