如何在Pyodide虚拟文件系统中运行写入的Python测试文件?
问题
我正在开发一款基于Pyodide的文本编辑器,支持运行测试脚本。使用Ace Editor时,会获取用户输入的内容,通过pyodide.FS.writeFile()写入虚拟文件系统的/challenge.py;同时将预制测试文件写入/test.py。但尝试通过import test运行测试文件时失败,需要修改runCode()函数实现运行该测试文件的功能,相关JavaScript代码如下:
//fileSaver is used to save the code to a file and download it const fileSaver = require('file-saver'); // Setup ace variables and the output pane for pyodide var editor = ace.edit("editor"); var output_pane; // The following line will essentially be the "file path" input for the RST directive, or // we can figure out how to pass arguments into an iframe if thats even possible var testFilePath = '/_static/test_files/test.py'; loadPyodide().then((pyodide) => { // pyodide is now ready to use... globalThis.pyodide = pyodide; appendOutput('Python ready.\n'); }); function appendOutput(msg) { // used to add program output to the textarea output_pane.value = output_pane.value + '\n' + msg; output_pane.scrollTop = output_pane.scrollHeight; } function configEditor(){ // configure the ace editor to make it usable editor = ace.edit("editor"); editor.setTheme("ace/theme/xcode"); editor.session.setMode("ace/mode/python"); editor.setShowPrintMargin(false); editor.setBehavioursEnabled(true); editor.setFontSize(13); //Fix indentation issue with ace editor, not really the best solution but it works var code = editor.getValue(); var lines = code.split("\n"); var minIndent = null; for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line.trim().length > 0) { var indent = line.search(/\S/); if (minIndent === null || (indent !== -1 && indent < minIndent)) { minIndent = indent; } } } if (minIndent !== null) { for (var i = 0; i < lines.length; i++) { if (lines[i].trim().length > 0) { lines[i] = lines[i].slice(minIndent); } } code = lines.join('\n'); editor.setValue(code); } } function openCode(filePathToUse) { getCode(filePathToUse) .then(code => { var modelist = ace.require("ace/ext/modelist"); var modeName = modelist.getModeForPath(filePathToUse).mode; editor.session.setMode(modeName); editor.session.setValue(code); }) .catch(error => { console.error('Error occurred while opening the code:', error); }); } async function runCode(code_to_run) { // Run the code thats within the editor so students can test if(code_to_run == editor.getValue()){ console.logs = []; let promise = new Promise((resolve, reject) => { window.pyodide.runPython(code_to_run) resolve(true) }).catch(err => { console.log(err); appendOutput(console.logs.join('\n')); }); let result = await promise; if (result) { appendOutput(console.logs.join('\n')); } } else { // run the code from the editor and display the output in the textarea let data = editor.getValue(); let testData = code_to_run; window.pyodide.FS.writeFile("/challenge.py", data); window.pyodide.FS.writeFile("/test.py", testData); let challengeFile = pyodide.FS.readFile("/challenge.py"); let testFile = pyodide.FS.readFile("/test.py"); console.log(challengeFile); console.log(testFile); console.logs = []; let promise = new Promise((resolve, reject) => { window.pyodide.runPython(testCodeString) resolve(true) }).catch(err => { console.log(err); appendOutput(console.logs.join('\n')); }); let result = await promise; if (result) { appendOutput(console.logs.join('\n')); } } } function saveCode(code) { var blob = new Blob([code], { type: "text/plain;charset=utf-8" }); window.saveAs(blob, 'challenge.py'); } //make a function getCode that takes in a file path and returns the code in that file as a string to use in ace async function getCode(codeToGet) { try { const response = await fetch(codeToGet); const data = await response.text(); return data; } catch (error) { console.error('Error occurred while opening the code:', error); } } //codeToSwitch will be a file path function switchFile(codeToSwitch) { getCode(codeToSwitch) .then(code => { var EditSession = ace.require("ace/edit_session").EditSession; var oldSession = editor.getSession(); //change to new file var newSession = new EditSession(code, "ace/mode/python"); editor.setSession(newSession); }) .catch(error => { console.error('Error occurred while opening the code:', error); }); } document.addEventListener('DOMContentLoaded', (event) => { output_pane = document.getElementById("output"); // Add event listeners for downloading code document.getElementById("downloadButton").addEventListener('click', function () { saveCode(editor.getValue()); }); // Add event listeners for switching files document.getElementById("switchButton").addEventListener('click', function () { switchFile(testFilePath); }); document.getElementById("runButton").addEventListener('click', function () { runCode(editor.getValue()); }); // Add event listeners for running code document.getElementById("run_code").addEventListener('click', function () { //Run the getcode function to get the code from the editor getCode(testFilePath) .then(code => { runCode(code); }) .catch(error => { console.error('Error occurred while opening the code:', error); }); }); // Capture the output from Pyodide and add it to an array console.stdlog = console.log.bind(console); console.logs = []; console.log = function(){ console.logs.push(Array.from(arguments)); console.stdlog.apply(console, arguments); } configEditor(); });
重点关注runCode()函数,已将用户代码和测试文件写入虚拟文件系统,需要在testCodeString变量中实现运行测试文件的功能。
解决方案
问题核心是Pyodide的Python环境默认不会将根目录/加入模块搜索路径,导致无法找到/test.py文件。修改runCode()函数的else分支即可解决:
- 定义
testCodeString变量:构造Python代码,先将根目录添加到sys.path,再导入并执行测试文件。 - 优化错误处理:在Promise中用
try-catch包裹runPython调用,确保错误能被捕获并显示。
修改后的runCode()函数完整代码如下:
async function runCode(code_to_run) { // Run the code thats within the editor so students can test if(code_to_run == editor.getValue()){ console.logs = []; let promise = new Promise((resolve, reject) => { try { window.pyodide.runPython(code_to_run); resolve(true); } catch (err) { console.error(err); reject(err); } }).catch(err => { appendOutput(`执行错误: ${err.message}`); appendOutput(console.logs.join('\n')); }); let result = await promise; if (result) { appendOutput(console.logs.join('\n')); } } else { // run the code from the editor and display the output in the textarea let data = editor.getValue(); let testData = code_to_run; window.pyodide.FS.writeFile("/challenge.py", data); window.pyodide.FS.writeFile("/test.py", testData); console.logs = []; // 构造执行测试文件的Python代码 const testCodeString = ` import sys # 将根目录添加到Python模块搜索路径 sys.path.insert(0, '/') # 导入测试文件,执行其中的测试逻辑 import test `; let promise = new Promise((resolve, reject) => { try { window.pyodide.runPython(testCodeString); resolve(true); } catch (err) { console.error(err); reject(err); } }).catch(err => { appendOutput(`测试执行错误: ${err.message}`); appendOutput(console.logs.join('\n')); }); let result = await promise; if (result) { appendOutput(console.logs.join('\n')); } } }
关键说明
sys.path.insert(0, '/'):强制让Python优先从根目录搜索模块,确保能找到/test.py。- 错误处理优化:原来的Promise未捕获
runPython的异常,修改后用try-catch包裹,将错误信息显示到输出面板,方便调试。 - 如果测试文件需要调用
challenge.py中的代码,确保测试文件里的导入路径正确(比如import challenge,因为/challenge.py也在根目录,添加路径后同样能被找到)。
内容的提问来源于stack exchange,提问作者Cr3
相关产品推荐
相关产品推荐

