fix: vendor openpyxl so Excel I/O works in the Pyodide code interpreter (#30140)

openpyxl has been listed as a Pyodide package since March, but it is not part of the Pyodide distribution, so the build never wrote a wheel for it and never warned about it. In the browser code interpreter every Excel operation failed with ModuleNotFoundError: import openpyxl, pd.read_excel() and DataFrame.to_excel() alike.

It now takes the PyPI wheel path together with its dependency et-xmlfile, and the code interpreter installs it when a snippet imports openpyxl or calls pandas' Excel helpers, which need it without importing it by name.

Injected lock entries are now keyed by the canonical dashed package name, the only spelling pyodide resolves them by. As a side effect black's mypy-extensions now resolves from the bundled wheel too, so formatting Python in the editor no longer reaches out to PyPI at runtime.

Verified offline against a built static/pyodide with every network call blocked: a to_excel then read_excel roundtrip loads openpyxl and et-xmlfile from the local directory and returns the frame.

Fixes #30130
This commit is contained in:
Classic298
2026-09-18 10:38:02 -05:00
committed by GitHub
parent 95406fd28d
commit 655337f7bf
4 changed files with 45 additions and 14 deletions
+11 -10
View File
@@ -14,15 +14,17 @@ const packages = [
'seaborn',
'pytz',
'black',
'openai',
'openpyxl'
'openai'
];
// Pure-Python packages whose wheels must be downloaded from PyPI and saved into
// static/pyodide/ so that the browser can install them offline via micropip.
// Packages already provided by the Pyodide distribution (click, platformdirs,
// typing_extensions, etc.) do NOT need to be listed here.
const pypiPackages = ['black', 'pathspec', 'mypy_extensions', 'pytokens'];
// Spell them canonically (dashed): that is the only form pyodide resolves lock entries by.
const pypiPackages = ['black', 'pathspec', 'mypy-extensions', 'pytokens', 'openpyxl', 'et-xmlfile'];
const pypiDepends = { openpyxl: ['et-xmlfile'] };
import { loadPyodide } from 'pyodide';
import { setGlobalDispatcher, ProxyAgent } from 'undici';
@@ -175,19 +177,18 @@ async function downloadPyPIWheels() {
}
// Inject into pyodide-lock.json so micropip resolves locally
const normalizedName = pkg.replace(/-/g, '_');
if (!lockData.packages[normalizedName]) {
lockData.packages[normalizedName] = {
name: normalizedName,
if (!lockData.packages[pkg]) {
lockData.packages[pkg] = {
name: pkg,
version: version,
file_name: wheel.filename,
install_dir: 'site',
sha256: wheel.digests?.sha256 || '',
package_type: 'package',
imports: [normalizedName],
depends: []
imports: [pkg.replace(/-/g, '_')],
depends: pypiDepends[pkg] || []
};
console.log(` Added ${normalizedName}==${version} to pyodide-lock.json`);
console.log(` Added ${pkg}==${version} to pyodide-lock.json`);
}
}
@@ -233,7 +233,9 @@
/\bimport\s+seaborn\b|\bfrom\s+seaborn\b/.test(code) ? 'seaborn' : null,
/\bimport\s+sympy\b|\bfrom\s+sympy\b/.test(code) ? 'sympy' : null,
/\bimport\s+tiktoken\b|\bfrom\s+tiktoken\b/.test(code) ? 'tiktoken' : null,
/\bimport\s+pytz\b|\bfrom\s+pytz\b/.test(code) ? 'pytz' : null
/\bimport\s+pytz\b|\bfrom\s+pytz\b/.test(code) ? 'pytz' : null,
/\bimport\s+openpyxl\b|\bfrom\s+openpyxl\b/.test(code) ? 'openpyxl' : null,
/\.(read|to)_excel\(|\.Excel(Writer|File)\(/.test(code) ? 'openpyxl' : null
].filter(Boolean);
console.log(packages);
+3 -1
View File
@@ -311,7 +311,9 @@
/\bimport\s+seaborn\b|\bfrom\s+seaborn\b/.test(code) ? 'seaborn' : null,
/\bimport\s+sympy\b|\bfrom\s+sympy\b/.test(code) ? 'sympy' : null,
/\bimport\s+tiktoken\b|\bfrom\s+tiktoken\b/.test(code) ? 'tiktoken' : null,
/\bimport\s+pytz\b|\bfrom\s+pytz\b/.test(code) ? 'pytz' : null
/\bimport\s+pytz\b|\bfrom\s+pytz\b/.test(code) ? 'pytz' : null,
/\bimport\s+openpyxl\b|\bfrom\s+openpyxl\b/.test(code) ? 'openpyxl' : null,
/\.(read|to)_excel\(|\.Excel(Writer|File)\(/.test(code) ? 'openpyxl' : null
].filter(Boolean);
const worker = getOrCreateWorker();
+28 -2
View File
@@ -5131,8 +5131,8 @@
],
"depends": []
},
"mypy_extensions": {
"name": "mypy_extensions",
"mypy-extensions": {
"name": "mypy-extensions",
"version": "1.1.0",
"file_name": "mypy_extensions-1.1.0-py3-none-any.whl",
"install_dir": "site",
@@ -5154,6 +5154,32 @@
"pytokens"
],
"depends": []
},
"openpyxl": {
"name": "openpyxl",
"version": "3.1.5",
"file_name": "openpyxl-3.1.5-py2.py3-none-any.whl",
"install_dir": "site",
"sha256": "5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2",
"package_type": "package",
"imports": [
"openpyxl"
],
"depends": [
"et-xmlfile"
]
},
"et-xmlfile": {
"name": "et-xmlfile",
"version": "2.0.0",
"file_name": "et_xmlfile-2.0.0-py3-none-any.whl",
"install_dir": "site",
"sha256": "7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa",
"package_type": "package",
"imports": [
"et_xmlfile"
],
"depends": []
}
}
}