Monday, February 20, 2023

Loading multiple Python versions with Pyodide

As described in my last post, dis-this.com is an online tool for disassembling Python code. After I shared it last week in the Python forum, Guido asked if I could add a feature to switch Python versions, to see the difference in disassembly across versions. I was able to get it working for versions 3.9 - 3.11, but it was a little tricky due to the way Pyodide is designed.

I'm sharing my learnings here for anyone else building a similar tool in Pyodide.

Pyodide version ↔ Python version

Pyodide doesn't formally support multiple Python versions at once. The latest Pyodide version has the latest Python version that they've been able to support, as well as other architectural improvements. To get to older Python versions, you need to load older Pyodide versions that happened to support that version.

Here's a JS object mapping Python versions to Pyodide versions:

const versionMap = {
    '3.11': 'dev',
    '3.10': 'v0.22.1',
    '3.9': 'v0.19.1',
};

As you can see, 3.11 doesn't yet map to a numbered release version. According to the repository activity, v0.23 will be the numbered release once it's out. I will need to update that in the future.

Once I know what Python version the user wants, I append the script tag and call loadPyodide once loaded:

const scriptUrl = `https://cdn.jsdelivr.net/pyodide/${pyodideVersion}/full/pyodide.js`;
const script = document.createElement('script');
script.src = scriptUrl;
script.onload = async () => {
    pyodide = await loadPyodide({
        indexURL: `https://cdn.jsdelivr.net/pyodide/${pyodideVersion}/full/`,
        stdout: handleStdOut,
    });
    // Enable the UI for interaction
    button.removeAttribute('disabled');
};

Loading multiple Pyodide versions in same page

For dis-this, I want users to be able to change the Python version and see the disassembly in that different version. For example, they could start on 3.11 and then change to 3.10 to compare the output.

Originally, I attempted just calling the above code with the new Pyodide version. Unfortunately, that resulted in some funky errors. I figured it related to Pyodide leaking globals into the window object, so my next attempt was deleting those globals before loading a different Pyodide version. That actually worked a lot better, but still failed sometimes.

So, to be on the safe side, I made it so that changing the version number reloads the page. The website already supports encoding the state in the URL (via the permalink), so it wasn't actually too much work to add the "&version=" parameter to the URL and reload.

This code listens to the dropdown's change event and reloads the window to the new permalink:

document.getElementById('version-select').addEventListener('change', async () => {
    pythonVersion = document.getElementById('version-select').value;
    permalink.setAttribute('version', pythonVersion);
    window.location.search = permalink.path;
});

That permalink element is a Web Component that knows how to compute the correct path. Once the page reloads, this code grabs the version parameter from the URL:

wpythonVersion = new URLSearchParams(window.location.search).get('version') || '3.11';

The codebase for dis-this.com is relatively small, so you can also look through it yourself or fork it if you're creating a similar tool.

No comments: