Skip to content

Commit

Permalink
xlwings Lite: Replace PyScript with (updated) Pyodide (#233)
Browse files Browse the repository at this point in the history
Pyscript removed and Pyodide upgraded from 0.26.4 to 0.27.2
  • Loading branch information
fzumstein authored Feb 6, 2025
1 parent 03a574c commit d04c0b4
Show file tree
Hide file tree
Showing 84 changed files with 440 additions and 1,002 deletions.
2 changes: 0 additions & 2 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ When debugging the alert/dialog window, you need to open up a separate instance

## xlwings Lite (Wasm)

Make sure to install the correct version of Pyodide (`npm i pyodide@x.x.x`) for the given version of PyScript. To find out, use the CDN version of PyScript and check out the console logs where it prints the version of Pyodide.

For the offline usage of Pyodide, the following packages are always required to be copied over from the pyodide release package:

- micropip
Expand Down
2 changes: 1 addition & 1 deletion app/custom_functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
try:
from ..config import settings
except ImportError:
# PyScript doesn't work with parent relative imports
# xlwings Lite
from config import settings


Expand Down
2 changes: 1 addition & 1 deletion app/custom_scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
try:
from ..config import settings
except ImportError:
# PyScript doesn't work with parent relative imports
# xlwings Lite
from config import settings

if settings.enable_examples:
Expand Down
15 changes: 5 additions & 10 deletions app/lite/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
# Via xlwings Server
from .. import custom_functions, custom_scripts
except ImportError:
# Via PyScript
# xlwings Lite
import custom_functions
import custom_scripts
import js # type: ignore
import pyodide_js # type: ignore
import xlwings as xw
from pyscript import ffi, window # type: ignore
from pyodide.ffi import to_js # type: ignore
from xlwings.server import (
custom_functions_call as xlwings_custom_functions_call,
custom_scripts_call as xlwings_custom_scripts_call,
Expand Down Expand Up @@ -40,10 +41,7 @@ async def custom_functions_call(data):
except Exception as e:
result = {"error": str(e), "details": traceback.format_exc()}
# Note: converts None to undefined
return ffi.to_js(result)


window.custom_functions_call = custom_functions_call
return to_js(result, dict_converter=js.Object.fromEntries)


async def custom_scripts_call(data, script_name):
Expand All @@ -57,7 +55,4 @@ async def custom_scripts_call(data, script_name):
result = book.json()
except Exception as e:
result = {"error": str(e), "details": traceback.format_exc()}
return ffi.to_js(result)


window.custom_scripts_call = custom_scripts_call
return to_js(result, dict_converter=js.Object.fromEntries)
2 changes: 1 addition & 1 deletion app/lite/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
xlwings==0.33.6
pandas==2.2.0
pandas==2.2.3
python-dotenv==1.0.1
8 changes: 1 addition & 7 deletions app/routers/xlwings.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async def custom_scripts_sheet_buttons():

if settings.enable_lite:

@router.get("/pyscript.json")
@router.get("/pyodide.json")
async def get_pyscript_config():
# requirements.txt
packages = (
Expand Down Expand Up @@ -176,10 +176,4 @@ def scan_directory(
scan_directory(settings.base_dir, directory, prepend_dir_name=True)
)
response = {"packages": packages, "files": files}

# Interpreter
if not settings.cdn_pyodide:
response["interpreter"] = (
f"{settings.static_url_path}/vendor/pyodide/pyodide.mjs"
)
return response
7 changes: 4 additions & 3 deletions app/static/js/core/custom-functions-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,15 +254,16 @@ async function makeServerCall(body) {
}

async function makeLiteCall(body) {
await xlwings.pyscriptAllDone;
await xlwings.pyodideReadyPromise;
try {
let result = await window.custom_functions_call(body);
let result = await globalThis.liteCustomFunctionsCall(body);
if (result.error) {
console.error(result.details);
throw new Error(result.error);
showError(result.error);
}
return result;
} catch (error) {
console.error(error);
showError(error);
}
}
Expand Down
88 changes: 88 additions & 0 deletions app/static/js/core/xlwingsjs/lite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
async function initPyodide() {
if (config.onLite === false) {
return;
}
const globalStatusAlert = document.querySelector("#global-status-alert");
if (globalStatusAlert) {
globalStatusAlert.classList.remove("d-none");
globalStatusAlert.querySelector("span").innerHTML = `
<div class="spinner-border spinner-border-sm text-alert me-1" role="status">
<span class="visually-hidden">Loading...</span>
</div>
Loading...
`;
}

let pyodide = await loadPyodide();
const pyodideConfigResponse = await fetch(
config.appPath + "/xlwings/pyodide.json",
);
const pyodideConfigData = await pyodideConfigResponse.json();
// Install dependencies
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
let packages = pyodideConfigData["packages"];
await micropip.install(packages);
// Python files
const files = pyodideConfigData["files"];
function createDirectories(files) {
const createdDirs = new Set();
Object.values(files).forEach((localPath) => {
const parts = localPath.split("/");
if (parts.length > 1) {
const dirPath = parts.slice(0, parts.length - 1).join("/");
if (!createdDirs.has(dirPath)) {
try {
// TODO: does this work for nested dirs? Also, ./ is probably wrong
if (dirPath !== ".") {
pyodide.FS.mkdir(dirPath);
}
} catch (err) {
console.log(err);
}
try {
if (dirPath !== ".") {
pyodide.FS.mount(pyodide.FS.filesystems.MEMFS, {}, dirPath);
}
} catch (err) {
console.log(err);
}
createdDirs.add(dirPath);
}
}
});
}
createDirectories(files);
for (const [endpoint, localPath] of Object.entries(files)) {
const response = await fetch(config.appPath + endpoint);
const content = await response.text();
pyodide.FS.writeFile(localPath, content);
}
try {
// Entrypoint
let mainText = pyodide.FS.readFile("./main.py", { encoding: "utf8" });
await pyodide.runPythonAsync(mainText);
// Functions
const liteCustomFunctionsCall = pyodide.globals.get(
"custom_functions_call",
);
const liteCustomScriptsCall = pyodide.globals.get("custom_scripts_call");
// You can't simply export custom_functions_call as it will be null when used in
// custom-functions-code.js (it's only assigned the function here).
globalThis.liteCustomFunctionsCall = liteCustomFunctionsCall;
globalThis.liteCustomScriptsCall = liteCustomScriptsCall;
} catch (err) {
console.log(err);
}

// Loading status
if (globalStatusAlert) {
globalStatusAlert.classList.add("d-none");
}

return pyodide;
}

// Call as follows:
// let pyodide = await pyodideReadyPromise;
export let pyodideReadyPromise = initPyodide();
45 changes: 5 additions & 40 deletions app/static/js/core/xlwingsjs/xlwings.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,19 @@ import {
getDateFormat,
} from "./utils.js";
export { getActiveBookName, getCultureInfoName, getDateFormat };
import { pyodideReadyPromise } from "./lite.js";

// Prints the supported API versions into the Console
printSupportedApiVersions();

// xlwings Lite
let pyscriptAllDone = new Promise((resolve) => {
if (config.onLite === false) {
resolve(false);
} else {
window.addEventListener(
"py:all-done",
() => {
// Hide status alert when py:all-done fires
const globalStatusAlert = document.querySelector(
"#global-status-alert",
);
if (globalStatusAlert) {
globalStatusAlert.classList.add("d-none");
}
resolve(true);
},
{ once: true },
);
}
});

// Namespace
const xlwings = {
runPython,
getAccessToken,
getActiveBookName,
getBookData,
runActions,
pyscriptAllDone,
pyodideReadyPromise,
getCultureInfoName,
getDateFormat,
};
Expand All @@ -51,20 +30,6 @@ globalThis.xlwings = xlwings;
document.addEventListener("DOMContentLoaded", init);

export function init() {
// Pyscript status
if (config.onLite) {
const globalStatusAlert = document.querySelector("#global-status-alert");
if (globalStatusAlert) {
globalStatusAlert.classList.remove("d-none");
globalStatusAlert.querySelector("span").innerHTML = `
<div class="spinner-border spinner-border-sm text-alert me-1" role="status">
<span class="visually-hidden">Loading...</span>
</div>
Loading...
`;
}
}

const elements = document.querySelectorAll("[xw-click]");
elements.forEach((element) => {
element.addEventListener("click", async (event) => {
Expand Down Expand Up @@ -135,8 +100,8 @@ export async function runPython(
);
let rawData;
if (config.onLite) {
await pyscriptAllDone;
rawData = await window.custom_scripts_call(payload, scriptName);
await pyodideReadyPromise;
rawData = await globalThis.liteCustomScriptsCall(payload, scriptName);
if (rawData.error) {
console.error(rawData.details);
throw new Error(rawData.error);
Expand All @@ -159,7 +124,7 @@ export async function runPython(
// console.log(rawData);

// Run Functions
// Note that PyScript returns undefined, so use != and == rather than !== and ===
// Note that Pyodide returns undefined, so use != and == rather than !== and ===
if (rawData != null) {
await runActions(rawData, context);
}
Expand Down
Loading

0 comments on commit d04c0b4

Please sign in to comment.