mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-22 19:53:00 -05:00
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:
@@ -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 />
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
`,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 <py-env>)
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user