Repository: pyscript/pyscript
File: pyscriptjs/src/pyodide.ts (TypeScript)

Code before:
                                           logger.info.bind(logger),
                                           logger.info.bind(logger));
    }

    async installPackage(package_name: string | string[]): Promise<void> {
        if (package_name.length > 0) {
            logger.info(`micropip install ${package_name.toString()}`);
            const micropip = this.globals.get('micropip');
            await micropip.install(package_name);
            micropip.destroy();
        }
    }

    async loadFromFile(path: string): Promise<void> {
        const filename = getLastPath(path);
        try {
            const buffer = await fetch(path).then(res => res.arrayBuffer());
            const data = new Uint8Array(buffer);
            const stream = this.interpreter.FS.open(filename, 'w');
            this.interpreter.FS.write(stream, data, 0, data.length, 0);
            this.interpreter.FS.close(stream);
        }
        catch (err) {
            console.warn("PyScript: Access to local files (using 'paths:' in py-config) 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.")
        }
    }
}


Review comment (bug):
I know that this is a problem which was already present before, but this looks like a good occasion to improve it.
There are many things which could go wrong in the `try` clause, and this message is not always the most appropriate one (e.g., if you make a typo in your `paths=[...]` you would get this, probably).

I think that:

1. if we cannot fetch a file, we should abort the rest of the execution. Errors should not pass silently.

2. corollary of the above, if we abort the execution we should show a big red message on the PAGE, not on the console. I expect that many pyscript beginners don't even know how to look at the console.

3. The "access to local files" message should be shown only if we know that we are trying to access a local file (e.g. by looking at the protocol of the URL and/or `window.location.href`).



Code after:
import { Runtime } from './runtime';
import { getLastPath } from './utils';
import { getLogger } from './logger';
import type { PyodideInterface } from 'pyodide';
// eslint-disable-next-line
// @ts-ignore
import pyscript from './python/pyscript.py';

const logger = getLogger('pyscript/pyodide');

export class PyodideRuntime extends Runtime {
    src: string;
    name?: string;
    lang?: string;
    interpreter: PyodideInterface;
    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(config);
        this.src = src;
        this.name = name;
        this.lang = lang;
    }

    /**
     * Although `loadPyodide` is used below,
     * notice that it is not imported i.e.
     * import { loadPyodide } from 'pyodide';
     * is not used at the top of this file.
     *
     * This is because, if it's used, loadPyodide
     * behaves mischievously i.e. it tries to load
     * `pyodide.asm.js` and `pyodide_py.tar` but
     * with paths that are wrong such as:
     *
     * http://127.0.0.1:8080/build/pyodide_py.tar
     * which results in a 404 since `build` doesn't
     * contain these files and is clearly the wrong
     * path.
     */
    async loadInterpreter(): Promise<void> {
        logger.info('Loading pyodide');
        // eslint-disable-next-line
        // @ts-ignore
        this.interpreter = await loadPyodide({
            stdout: console.log,
            stderr: console.log,
            fullStdLib: false,
        });

        this.globals = this.interpreter.globals;

        // XXX: ideally, we should load micropip only if we actually need it
        await this.loadPackage('micropip');

        logger.info('importing pyscript.py');
        await this.run(pyscript);

        logger.info('pyodide loaded and initialized');
    }

    async run(code: string): Promise<any> {
        return await this.interpreter.runPythonAsync(code);
    }

    registerJsModule(name: string, module: object): void {
        this.interpreter.registerJsModule(name, module);
    }

    async loadPackage(names: string | string[]): Promise<void> {
        logger.info(`pyodide.loadPackage: ${names.toString()}`);
        await this.interpreter.loadPackage(names,
                                           logger.info.bind(logger),
                                           logger.info.bind(logger));
    }

    async installPackage(package_name: string | string[]): Promise<void> {
        if (package_name.length > 0) {
            logger.info(`micropip install ${package_name.toString()}`);
            const micropip = this.globals.get('micropip');
            await micropip.install(package_name);
            micropip.destroy();
        }
    }

    async loadFromFile(path: string): Promise<void> {
        const filename = getLastPath(path);
        const response = await fetch(path);
        const buffer = await response.arrayBuffer();
        const data = new Uint8Array(buffer);
        const stream = this.interpreter.FS.open(filename, 'w');
        this.interpreter.FS.write(stream, data, 0, data.length, 0);
        this.interpreter.FS.close(stream);
    }
}