Executing Python via newer versions of Pyodide in React leads to an error

Question:

I am a React and Pyodide newbie. I am trying to get the following code to work in a way that I can utilize some OpenCV functionality. The code below works and I can use numpy, etc in the calculations. However, OpenCV is not available in Pyodide v0.18.1. It is available in all the subsequent versions (in one way or another). However, when I change the version number to anything above 0.18.1, I get an error in loadPyodide().

import React, { useState } from 'react';

let pyodidePromise = null;
let pyodideLoading = false;

const loadPyodide = async () => {
  if (!pyodidePromise && !pyodideLoading) {
    pyodideLoading = true;
    pyodidePromise = window.loadPyodide({
      indexURL: "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/"
    });
    const pyodide = await pyodidePromise;
    await pyodide . loadPackage(['numpy']); //, 'opencv-python']);
    pyodideLoading = false;
  }
  return pyodidePromise;
};

const runPythonScript = async (code) => {
  const pyodide = await loadPyodide();
  return await pyodide.runPythonAsync(code);
};


const ImageUploader = () => {

...

  const callPython = async () => {
...
    const scriptText = await (await fetch(script)).text();
    const out = await runPythonScript(`${scriptText}nmain(${JSON.stringify(args)})`);
...
  };
...

For example when I change to v0.22.1, I get the following error (which may be the same error in all versions above v0.18.1).

load-pyodide.js:23 
       GET https://cdn.jsdelivr.net/pyodide/v0.21.1/full/packages.json 404
initializePackageIndex @ load-pyodide.js:23
loadPyodide @ pyodide.js:215
loadPyodide @ App.js:12
runPythonScript @ App.js:24
callPython @ App.js:59
await in callPython (async)
callCallback @ react-dom.development.js:4164
invokeGuardedCallbackDev @ react-dom.development.js:4213
invokeGuardedCallback @ react-dom.development.js:4277
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:4291
executeDispatch @ react-dom.development.js:9041
processDispatchQueueItemsInOrder @ react-dom.development.js:9073
processDispatchQueue @ react-dom.development.js:9086
dispatchEventsForPlugins @ react-dom.development.js:9097
(anonymous) @ react-dom.development.js:9288
batchedUpdates$1 @ react-dom.development.js:26140
batchedUpdates @ react-dom.development.js:3991
dispatchEventForPluginEventSystem @ react-dom.development.js:9287
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ react-dom.development.js:6465
dispatchEvent @ react-dom.development.js:6457
dispatchDiscreteEvent @ react-dom.development.js:6430
VM60:1 
        
       Uncaught (in promise) SyntaxError: Unexpected token '<', "<html>
<he"... is not valid JSON
await (async)
loadPyodide @ pyodide.js:215
loadPyodide @ App.js:12
runPythonScript @ App.js:24
callPython @ App.js:59
await in callPython (async)
callCallback @ react-dom.development.js:4164
invokeGuardedCallbackDev @ react-dom.development.js:4213
invokeGuardedCallback @ react-dom.development.js:4277
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:4291
executeDispatch @ react-dom.development.js:9041
processDispatchQueueItemsInOrder @ react-dom.development.js:9073
processDispatchQueue @ react-dom.development.js:9086
dispatchEventsForPlugins @ react-dom.development.js:9097
(anonymous) @ react-dom.development.js:9288
batchedUpdates$1 @ react-dom.development.js:26140
batchedUpdates @ react-dom.development.js:3991
dispatchEventForPluginEventSystem @ react-dom.development.js:9287
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ react-dom.development.js:6465
dispatchEvent @ react-dom.development.js:6457
dispatchDiscreteEvent @ react-dom.development.js:6430
pyodide.asm.js:10 
        
       Uncaught (in promise) TypeError: Cannot set properties of undefined (setting 'tests')
    at pyodide.asm.js:10:114344
    at loadPyodide (pyodide.js:227:9)
    at async loadPyodide (App.js:16:1)
    at async runPythonScript (App.js:24:1)
    at async callPython (App.js:59:1)

What am I doing wrong? (This is my fourth day trying to understand this.)


edit

I incorporated it into my code based on the example below from @TachyonicBytes. It is simpler than what I previously had and works!!!

I am including a version of the simplified example from TachyonicBytes, but with imports of numpy and openCV. A simple example like this is what I have been looking for on the net for a week and could not find.

import React, { useEffect, useRef, useState } from "react";
import {useScript} from 'usehooks-ts'

const App = () => {
  const pyodideStatus = useScript("https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js", {
    removeOnUnmount: false,
  })
  const [pyodide, setPyodide] = useState(null);
  const [pyodideLoaded, setPyodideLoaded] = useState(false);

  useEffect(() => {
    if (pyodideStatus === "ready") {
      setTimeout(()=>{
        (async function () {
          const indexUrl = `https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js`
          const pyodide = await globalThis.loadPyodide({indexUrl});
          setPyodide(pyodide);
          await pyodide.loadPackage(["numpy","opencv-python"]);
          setPyodideLoaded(true);
        })();
      }, 1000)
    }
  }, [pyodideStatus]); // evaluate python code with pyodide and set output

  async function callThis() {
    const myPython = `
import numpy as np
import cv2
def func():
    return np.array2string(5*np.array((1,2,3)))+" |  numpy version: "+np.__version__+"  |  opecv version: "+cv2.__version__
func() `;
    if (pyodideLoaded) {
      console.log(pyodide);
      let element = document.getElementById("replace");
      element.innerHTML = pyodide.runPython(myPython);
      console.log(element.innerHTML)
    } else {
      console.log("Pyodide not loaded yet");
    }
  }

  return (
    <div>
      <div id="replace">Replace this</div>
      <button onClick={callThis}>
        Execute Python
      </button>
    </div> );
};

export default App;
Asked By: GaryK

||

Answers:

I answered another question about pyodide that contained a working project with the 21 version.

The main part of instantiating pyodide is this:

function Pyodide({
  pythonCode,
  loadingMessage = "loading…",
  evaluatingMessage = "evaluating…",
}) {
  const indexURL = "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/";
  const pyodide = useRef(null);
  const [isPyodideLoading, setIsPyodideLoading] = useState(true);
  const [pyodideOutput, setPyodideOutput] = useState(evaluatingMessage); // load pyodide wasm module and initialize it

  useEffect(() => {
    setTimeout(()=>{
      (async function () {
        pyodide.current = await globalThis.loadPyodide({ indexURL });
        setIsPyodideLoading(false);
      })();
    }, 1000)
  }, [pyodide]); // evaluate python code with pyodide and set output
...

You can look into the repo from that question to learn more.

I can’t tell exactly what you are doing wrong though. The error seems not to be in loadPyodide itself, but in the callPython function. From SyntaxError: Unexpected token '<', "<html> I would assume that the args you are trying to serialize is not actually a JSON. There is also code for calling python in the links above.

If it still does not work, please update the question with what exactly the value of args is, so we can debug further.

Edit:

The invalid hook call error that you get is because you are trying to call a component directly as code, as opposed to using it in JSX. I modified the code to work as you want. The main problem is that you have to use the useScript hook to instantiate pyodide. The other problem is that you have to use the useEffect hook in order to start loading pyodide. I expect the following code to do what you want, so you can start improving it further:

import React, { useEffect, useRef, useState } from "react";
import {useScript} from 'usehooks-ts'

const App = () => {
  const pyodideStatus = useScript("https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js", {
    removeOnUnmount: false,
  })
  const [pyodide, setPyodide] = useState(null);
  const [pyodideLoaded, setPyodideLoaded] = useState(false);

  useEffect(() => {
    if (pyodideStatus === "ready") {
      setTimeout(()=>{
        (async function () {
          const indexUrl = `https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js`
          const pyodide = await globalThis.loadPyodide({indexUrl});
          setPyodide(pyodide);
          setPyodideLoaded(true);
        })();
      }, 1000)
    }
  }, [pyodideStatus]); // evaluate python code with pyodide and set output

  async function callThis() {
    const myPython = `
def func():
    return 5 + 7
func() `;
    if (pyodideLoaded) {
      console.log(pyodide);
      let element = document.getElementById("replace");
      element.innerHTML = pyodide.runPython(myPython);
    } else {
      console.log("Pyodide not loaded yet");
    }
  }

  return (
    <div>
      <div id="replace">Replace this</div>
      <button onClick={callThis}>
        Execute Python
      </button>
    </div> );
};

export default App;

It is the App.js from that repo, modified a little bit. Don’t forget to npm install usehooks-ts.

Answered By: TachyonicBytes
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.