holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.62k stars 500 forks source link

Make Pyodide init_doc + write_doc idempotent #5810

Open MarcSkovMadsen opened 10 months ago

MarcSkovMadsen commented 10 months ago

I'm trying to figure out how I can make a code sandbox/ Panel playground with "hot reload".

To support templates and extensions I need to run my Panel app inside an iframe. Now I'm trying to figure out how I can hot reload the app in my iframe. I cannot figure out how to do this because I experience different issues reported with Pyodide in https://github.com/pyodide/pyodide/issues/4279.

BUT. Now I'm trying to do hot reload when the template, extensions and root do not change with init_doc and write_doc. But I get

File "/lib/python3.11/site-packages/panel/io/pyodide.py", line 139, in _doc_json
    render_items_json[0].update({
    ~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range

I've been trying to understand why this happens. But its hard for me to understand the Panel pyodie code and even harder to debug as its running inside the browser.

Reproducible example

Start from the app.py file.

import panel as pn

pn.extension(template="fast")

button = pn.widgets.Button(name="Update")
button.js_on_click(code="update()")
pn.Row(button, pn.bind(lambda c: c, button.param.clicks)).servable()

Convert it

panel convert app.py

Update the <script> section to

    async function main() {
      let pyodide = await loadPyodide();
      window.pyodide=pyodide
      await pyodide.loadPackage("micropip");
      await pyodide.runPythonAsync(`
        import micropip
        await micropip.install(['https://cdn.holoviz.org/panel/wheels/bokeh-3.3.0-py3-none-any.whl', 'https://cdn.holoviz.org/panel/1.3.1/dist/wheels/panel-1.3.1-py3-none-any.whl', 'pyodide-http==0.2.1']);
      `);
      code = `
    import asyncio

    from panel.io.pyodide import init_doc, write_doc

    init_doc()

    import panel as pn

    pn.extension(template="fast")

    button = pn.widgets.Button(name="Update")
    button.js_on_click(code="update()")
    pn.Row(button, pn.bind(lambda c: c, button.param.clicks)).servable()

    await write_doc()`
      await pyodide.runPythonAsync(code);
    }
    async function update(){
      console.log("updating")
      code=code.replace("Update", "Update.")
      await window.pyodide.runPythonAsync(code).then(()=>{console.log("finished")});
    }
    main();
app.html ```html Panel Application
```

Serve the app.html file with python -m http.server and open it at http://localhost:8000/app.html.

The app loads well.

image

But when clicking the button I see

image

pyodide.asm.js:9  Uncaught (in promise) PythonError: Traceback (most recent call last):
  File "/lib/python311.zip/_pyodide/_base.py", line 571, in eval_code_async
    await CodeRunner(
  File "/lib/python311.zip/_pyodide/_base.py", line 396, in run_async
    await coroutine
  File "<exec>", line 16, in <module>
  File "/lib/python3.11/site-packages/panel/io/pyodide.py", line 514, in write_doc
    docs_json, render_items, root_ids = _doc_json(pydoc, root_els)
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lib/python3.11/site-packages/panel/io/pyodide.py", line 139, in _doc_json
    render_items_json[0].update({
    ~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range
MarcSkovMadsen commented 10 months ago

It works when not using template="fast"

If I remove template="fast" in app.py and go through the steps again, it works. I've clicked the button multiple times. Each time pyodide has successfully run the python code and the button gets an additional ..

image

app.html ```html Panel Application
```
MarcSkovMadsen commented 10 months ago

It can work for template='fast' if I update by hand instead of with init_doc and write_doc.

If I hand make the <script> to specifically update the div for the .servable() element, it can work. You can see I've been clicking multiple times and each time the component has been rewritten and an additional . added to the button name.

image

    async function main() {
      let pyodide = await loadPyodide();
      window.pyodide = pyodide
      await pyodide.loadPackage("micropip");
      await pyodide.runPythonAsync(`
        import micropip
        await micropip.install(['https://cdn.holoviz.org/panel/wheels/bokeh-3.3.0-py3-none-any.whl', 'https://cdn.holoviz.org/panel/1.3.1/dist/wheels/panel-1.3.1-py3-none-any.whl', 'pyodide-http==0.2.1']);
      `);
      code = `
    import panel as pn

    pn.extension(template="fast")

    button = pn.widgets.Button(name="Update")
    button.js_on_click(code="update()")
    pn.Row(button, pn.bind(lambda c: c, button.param.clicks)).servable(target="fb4cec10-fd14-4f89-bdfe-b56b70da57e6")
    `
    let myDiv = document.getElementById("fb4cec10-fd14-4f89-bdfe-b56b70da57e6");
    while (myDiv.firstChild) {  
      myDiv.removeChild(myDiv.firstChild);  
    }
    await pyodide.runPythonAsync(code).then(()=>{document.getElementsByTagName("body")[0].className=""});
    }
    async function update(){
      console.log("updating")
      code=code.replace("Update", "Update.")
      let myDiv = document.getElementById("fb4cec10-fd14-4f89-bdfe-b56b70da57e6");
      while (myDiv.firstChild) {  
        myDiv.removeChild(myDiv.firstChild);  
      }
      await window.pyodide.runPythonAsync(code).then(()=>{console.log("finished")});
    }
    main();
app.html ```html Panel Application
```