Skip to content

Commit

Permalink
Merge pull request #616 from live-codes/python-wasm-improvements
Browse files Browse the repository at this point in the history
Python-wasm improvements
  • Loading branch information
hatemhosny authored Jul 27, 2024
2 parents bcb8a80 + 15665fa commit cc0cc02
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 42 deletions.
21 changes: 18 additions & 3 deletions docs/docs/languages/python-wasm.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
toc_max_heading_level: 4
---

# Python (Wasm)

import RunInLiveCodes from '../../src/components/RunInLiveCodes.tsx';
Expand Down Expand Up @@ -25,23 +29,34 @@ In addition, since the Python code is running on the client-side, it has access

### Loading Modules

Modules can just be imported in code without the need for any explicit installs. The modules are automatically loaded using [micropip](https://micropip.pyodide.org).
Most of the modules in the Python standard library and many external packages can be used directly without explicit installs.

### Standard Library
#### Standard Library

Most of the Python standard library is functional, except for the modules [listed here](https://pyodide.org/en/stable/usage/wasm-constraints.html).

### External Packages
#### External Packages

Pyodide allows using many external packages (all pure Python packages on PyPI and many general-purpose and scientific [packages built in Pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html)).

Most of the time, a [distribution package provides one single import package](https://packaging.python.org/en/latest/discussions/distribution-package-vs-import-package/) (or non-package module), with a matching name. For example, `pip install numpy` lets you `import numpy`. In these cases, modules can just be imported in code without the need for any explicit installs. The modules are automatically loaded using [micropip](https://micropip.pyodide.org).

Example:

<!-- prettier-ignore -->
export const libParams = { pyodide: `import snowballstemmer\nstemmer = snowballstemmer.stemmer('english')\nprint(stemmer.stemWords('go goes going gone'.split()))\n`, languages: 'pyodide', console: 'full', compiled: 'none' };

<RunInLiveCodes params={libParams} code={libParams.pyodide} language="python" formatCode={false}></RunInLiveCodes>

However, modules with different import names (e.g. `pkg_resources` module from `setuptools` package) need to be explicitly installed using [micropip](https://micropip.pyodide.org).

Example:

<!-- prettier-ignore -->
export const micropipParams = { pyodide: `import micropip\nawait micropip.install("setuptools")\n\nimport pkg_resources\nprint(pkg_resources.get_distribution("setuptools").version)\n`, languages: 'pyodide', console: 'full', compiled: 'none' };

<RunInLiveCodes params={micropipParams} code={micropipParams.pyodide} language="python" formatCode={false}></RunInLiveCodes>

In addition, [micropip](https://micropip.pyodide.org) can be used to load external packages from custom URLs. See [examples](https://micropip.pyodide.org/en/stable/project/usage.html#examples).

### JavaScript Interoperability
Expand Down
39 changes: 24 additions & 15 deletions src/livecodes/languages/python-wasm/lang-python-wasm-script.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/* eslint-disable no-underscore-dangle */
import { pyodideBaseUrl } from '../../vendors';
import { fontAwesomeUrl, pyodideBaseUrl } from '../../vendors';

declare const loadPyodide: any;

if (livecodes.pyodideLoading === undefined) {
livecodes.pyodideLoading = new Promise((resolve) => {
livecodes.resolveLoading = resolve;
});
const script = document.createElement('script');
script.src = `${pyodideBaseUrl}pyodide.js`;
document.head.append(script);
Expand All @@ -18,22 +21,17 @@ window.addEventListener('load', async () => {
// already loaded
if (livecodes.pyodideLoading === false) return;
// still loading
if (livecodes.pyodideLoading) {
if (livecodes.pyodide && livecodes.pyodideLoading) {
await livecodes.pyodideLoading;
return;
}
// start loading
livecodes.pyodideLoading = new Promise<void>(async (resolve) => {
livecodes.pyodide = await loadPyodide({
indexURL: pyodideBaseUrl,
});
await livecodes.pyodide.loadPackage('micropip');
livecodes.micropip = livecodes.pyodide.pyimport('micropip');
livecodes.pyodideLoading = false;
livecodes.excludedPackages = [];
resolve();
});
await livecodes.pyodideLoading;
livecodes.pyodide = await loadPyodide({ indexURL: pyodideBaseUrl });
await livecodes.pyodide.loadPackage('micropip');
livecodes.micropip = livecodes.pyodide.pyimport('micropip');
livecodes.pyodideLoading = false;
livecodes.excludedPackages = [];
livecodes.resolveLoading?.();
}

async function cleanUp() {
Expand All @@ -52,6 +50,12 @@ window.addEventListener('load', async () => {
}

async function prepareEnv() {
// needed for matplotlib icons
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = fontAwesomeUrl;
document.head.append(stylesheet);

await pyodideReady;
const patchInput = `
from js import prompt
Expand All @@ -65,17 +69,22 @@ __builtins__.input = input
}

async function loadPackagesInCode(code: string) {
const pkgMap = {
skimage: 'scikit-image',
sklearn: 'scikit-learn',
};
const packages = [...livecodes.pyodide.pyodide_py.code.find_imports(code)];
const newPackages = packages.filter(
(p) => !(p in livecodes.pyodide.loadedPackages) && !livecodes.excludedPackages.includes(p),
);
for (const p of newPackages) {
const pkg = (pkgMap as any)[p] ?? p;
try {
await livecodes.micropip.install(p);
await livecodes.micropip.install(pkg);
} catch (err) {
// in Pyodide v0.26.x this needs to be done,
// otherwise the following micropip installs do not resolve
// livecodes.excludedPackages.push(p);
// livecodes.excludedPackages.push(pkg);
}
}
}
Expand Down
51 changes: 27 additions & 24 deletions src/livecodes/templates/starter/python-wasm-starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const pythonWasmStarter: Template = {
language: 'html',
content: `
<h1 id="title">Hello, World!</h1>
<div id="plot">Loading...</div>
<div id="loading">Loading...</div>
<div id="plots"></div>
`.trimStart(),
},
style: {
Expand All @@ -28,6 +29,7 @@ import pandas as pd
import matplotlib.pyplot as plt
from io import StringIO
def load_data(url):
req = XMLHttpRequest.new()
req.open("GET", url, False)
Expand All @@ -38,44 +40,45 @@ def load_data(url):
def prepare_data(dataframe):
def add_species_id(x):
if x == 'setosa':
return 0
elif x == 'versicolor':
return 1
if x == "setosa":
return 0
elif x == "versicolor":
return 1
return 2
df = dataframe.copy()
df['species_id'] = df['species'].apply(add_species_id)
df["species_id"] = df["species"].apply(add_species_id)
return df
def showPlot(figure, selector):
iconStyles = document.createElement('link')
iconStyles.rel = 'stylesheet'
iconStyles.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'
document.head.appendChild(iconStyles)
el = document.querySelector(selector)
el.innerHTML = ''
document.pyodideMplTarget = el
figure.canvas.show()
df = pd.read_csv(load_data("https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv"))
data = load_data("https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv")
df = pd.read_csv(data)
df = prepare_data(df)
formatter = plt.FuncFormatter(lambda i, *args: df['species'].unique()[int(i)])
formatter = plt.FuncFormatter(lambda i, *args: df["species"].unique()[int(i)])
fig = plt.figure(figsize=(6, 4))
plt.scatter(df[df.columns[0]], df[df.columns[1]], c=df['species_id'])
plt.scatter(df[df.columns[0]], df[df.columns[1]], c=df["species_id"])
plt.colorbar(ticks=[0, 1, 2], format=formatter)
plt.xlabel(df.columns[0])
plt.ylabel(df.columns[1])
plt.title('Iris dataset')
plt.title("Iris dataset")
plt.tight_layout()
showPlot(fig, '#plot')
title = document.getElementById('title')
name = 'Python'
# render plots in a specific DOM element
# plots = document.querySelector("#plots")
# document.pyodideMplTarget = plots
plt.show()
title = document.getElementById("title")
name = "Python"
title.innerHTML = f"Hello, {name}!"
loading = document.getElementById("loading")
loading.innerHTML = ""
# avoid leaving figures open
plt.close("all")
`.trimStart(),
},
};
2 changes: 2 additions & 0 deletions src/livecodes/vendors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export const fontAstigmataUrl = /* @__PURE__ */ getUrl(
'gh:hatemhosny/astigmata-font@6d0ee00a07fb1932902f0b81a504d075d47bd52f/index.css',
);

export const fontAwesomeUrl = /* @__PURE__ */ getUrl('[email protected]/css/font-awesome.min.css');

export const fontCascadiaCodeUrl = /* @__PURE__ */ getUrl(
'@fontsource/[email protected]/index.css',
);
Expand Down

0 comments on commit cc0cc02

Please sign in to comment.