javascriptpythonace-editorpyodide

How can I run python files that I wrote into Pyodides file system?


I am writing a Pyodide based text editor that will run test scripts, when a user types into the editor, using ace-editor, I grab whatever the user types and write it into a python file using pyodides pyodide.FS.writeFile() function, I do the same with the premade test file I have, however, I am struggling to understand if pyodide can run the files I write into pyodides virtual file system as I don't see a reason why it shouldnt. Here is the javascript file that handles this below, keep in mind I am using ace:

//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();
});

The main thing to pay attention to is the runCode() function as that is where the changes should be made. I first grab whatever the user wrote, put it into pyodides virtual file system, do the same with a premade test file, then in the variable testCodeString I want to just run the test file. I tried import test but that didnt work. How would I be able to accomplish this?


Solution

  • After looking into how Pyodide handles packages, local files can be written to the file system to be run by using the exec keyword, however, to ensure that test output will change when editing code, then you need to clear sys.modules like so in the code:

    
    console.logs = [];
    
    var data = editor.getValue(); 
    var testData = code_to_run;
    window.pyodide.FS.writeFile("challenge.py", data);
    window.pyodide.FS.writeFile("test.py", testData);
            
    let promise = new Promise((resolve, reject) => {
        window.pyodide.runPython(`
            exec(open('test.py').read())
            import sys
            sys.modules.pop("challenge", None)
            sys.modules.pop("test", None)
    `)
    resolve(true)
       }).catch(err => {
            console.log(err);
            appendOutput(console.logs.join('\n')); 
       });
            
    let result = await promise;
    if (result) { 
        appendOutput(console.logs.join('\n')); 
    }