You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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分支即可解决:

  1. 定义testCodeString变量:构造Python代码,先将根目录添加到sys.path,再导入并执行测试文件。
  2. 优化错误处理:在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 22:54:59