9
votes

Comment rediriger / rendre la sortie Pyodide dans le navigateur?

Je suis récemment tombé sur le projet Pyodide .

J'ai créé une petite démo en utilisant Pyodide, mais bien que j'aie passé beaucoup de temps à regarder la source, il ne m'est pas (encore) évident de rediriger la sortie print de python (autre que de modifier la source CPython), et aussi, comment rediriger la sortie de matplotlib.pyplot vers le navigateur.

Depuis le code source, FigureCanvasWasm a une méthode show () avec le backend approprié pour le traçage dans le canevas du navigateur - cependant, c'est Je ne sais pas comment instancier cette classe et invoquer sa méthode show () ou, en fait, s'il existe un autre moyen plus évident de rediriger les graphiques vers le canevas.

Mes questions sont donc :

  1. Comment rediriger les messages print ()
  2. Comment forcer pyodide à tracer les figures matplotlib dans le navigateur?

Voici ma page de test:

<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
    <title>Demo</title>
    <script src="../../pyodide/build/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
      pyodide.loadPackage(['matplotlib']).then(() => {
          pyodide.runPython(`
                  import matplotlib.pyplot as plt
                  plt.plot([1, 2, 3, 4])
                  plt.ylabel('some numbers')
                  #fig = plt.gcf()
                  #fig.savefig(imgdata, format='png')                  
                  print('Done from python!')`
          );
          //var image = pyodide.pyimport('imgdata');
          //console.log(image);
      });});

    </script>
<html>


0 commentaires

3 Réponses :


9
votes

Tout d'abord, voyons si nous pouvons faire apparaître n'importe quoi dans le navigateur; par exemple. une chaîne normale. Les variables Python sont stockées dans le pyodide.globals attribut. Par conséquent, nous pouvons prendre l'objet python à partir de là et le placer dans un élément

sur la page.
<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
    <title>Demo</title>
    <script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
      pyodide.loadPackage(['matplotlib']).then(() => {
          pyodide.runPython(`
                import matplotlib.pyplot as plt
                import io, base64

                fig, ax = plt.subplots()
                ax.plot([1,3,2])

                buf = io.BytesIO()
                fig.savefig(buf, format='png')
                buf.seek(0)
                img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')`
          );

          document.getElementById("pyplotfigure").src=pyodide.globals.img_str

      });});

    </script>

    <div id="textfield">A matplotlib figure:</div>
    <div id="pyplotdiv"><img id="pyplotfigure"/></div>
<html>

Maintenant, je suppose que nous pouvons faire la même chose avec un figure de matplotlib. Ce qui suit affichera une image png enregistrée dans le document.

<!doctype html>
<meta charset="utf-8">
<html>
<head>
    <title>Demo</title>
    <script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
          pyodide.runPython(`my_string = "This is a python string." `);

          document.getElementById("textfield").innerText = pyodide.globals.my_string;
      });

    </script>

    <div id="textfield"></div>
<html>

Je n'ai pas encore examiné les backends.wasm_backend , donc cela peut permettre pour une manière plus automatisée de ce qui précède.


2 commentaires

+1 C'est un début fantastique . C'est une superbe spéléologie! ... :) cependant, je recherche un niveau légèrement plus bas de faire cela (dans les coulisses - pour ainsi dire), afin qu'un utilisateur puisse simplement exécuter des scripts python "normaux" sans avoir à recourir aux mécanismes que vous employé. Idéalement, je souhaite intercepter les instructions print () et tous les messages envoyés à la console.


Voici chacun de ces exemples en direct sur codepen: Exemple 1 , Exemple 2



5
votes

Lors de l'utilisation du backend wasm, la propriété canvas d'une figure est une instance de FigureCanvasWasm . L'appel de la méthode show () du canevas devrait être suffisant pour afficher la figure dans le navigateur. Malheureusement, un bug mineur dans la méthode create_root_element () du canevas empêche l'affichage de la figure. Cette méthode crée un élément div qui contiendra la figure. Il essaie d'abord de créer un élément div de sortie iodure. Si cela échoue, un élément div HTML brut est créé. Cet élément n'est cependant jamais annexé au document et reste donc invisible.

Voici les lignes de code de FigureCanvasWasm où cela se produit

var stdout = pyodide.runPython("sys.stdout.getvalue()")
var div = document.createElement('div');
div.innerText = stdout;
document.body.appendChild(div);

Le commentaire suggère que le code sans iodure est un stub qui a besoin à étendre, en remplaçant la méthode. Cela nécessiterait de sous-classer FigureCanvasWasm , en l'installant en tant que module pyodide et en configurant matplotlib pour utiliser ce backend.

Il existe cependant un raccourci, car python permet de surcharger une méthode d'une instance, sans modifier la classe, selon la question 394770 . Mettre le code suivant dans votre document HTML donne une figure dans le navigateur

pyodide.runPython(`
    print("Hello, world!")
`);

Au départ, la barre d'outils n'affichait pas d'icônes. J'ai dû télécharger, décompresser et installer fontawesome à côté de pyodide et inclure la ligne suivante dans l'en-tête pour les obtenir p>

pyodide.runPython(`
    import sys
    import io
    sys.stdout = io.StringIO()
`);

Modifier: À propos de la première partie de votre question, redirigeant le flux de sortie vers le navigateur, vous pouvez jeter un œil à la façon dont cela est fait dans console.html .

Il remplace sys.stdout par un objet StringIO

<link rel="stylesheet" href="font-awesome-4.7.0/css/font-awesome.min.css">

Ensuite, exécutez le code python (qui peut être complètement inconscient du fait qu'il s'exécute dans un contexte wasm)

import numpy as np
from matplotlib import pyplot as plt
from js import document

x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)

f = plt.figure()
plt.plot(x,y)

# ordinary function to create a div
def create_root_element1(self):
    div = document.createElement('div')
    document.body.appendChild(div)
    return div

#ordinary function to find an existing div
#you'll need to put a div with appropriate id somewhere in the document
def create_root_element2(self):
    return document.getElementById('figure1')

#override create_root_element method of canvas by one of the functions above
f.canvas.create_root_element = create_root_element.__get__(
    create_root_element1, f.canvas.__class__)

f.canvas.show()

Enfin, envoyez le contenu du tampon stdout vers un élément de sortie

def create_root_element(self):
    # Designed to be overridden by subclasses for use in contexts other
    # than iodide.
    try:
        from js import iodide
        return iodide.output.element('div')
    except ImportError:
        return document.createElement('div')


0 commentaires

0
votes

J'ai créé un shell interactif simple pour Python. Lisez mon tutoriel si vous avez besoin d'informations plus détaillées.

<!DOCTYPE html>

<head>
    <script type="text/javascript">
        // this variable should be changed if you load pyodide from different source
        window.languagePluginUrl = 'https://pyodide-cdn2.iodide.io/v0.15.0/full/';
    </script>

    <script src="https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js"></script>
</head>

<body>
Status: <strong id='status'>Initializing...</strong>
<br><br>
mu:
<input id='mu' value='1' type="number">
<br><br>
sigma:
<input id='sigma' value='1' type="number">
<br><br>
<button onclick='pyodide.globals.generate_plot_img()'>Plot</button>
<br>
<img id="fig" />
</body>

</html>
let python_code = `
from js import document
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import io, base64

def generate_plot_img():
  # get values from inputs
  mu = int(document.getElementById('mu').value)
  sigma = int(document.getElementById('sigma').value)
  # generate an interval
  x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100)
  # calculate PDF for each value in the x given mu and sigma and plot a line 
  plt.plot(x, stats.norm.pdf(x, mu, sigma))
  # create buffer for an image
  buf = io.BytesIO()
  # copy the plot into the buffer
  plt.savefig(buf, format='png')
  buf.seek(0)
  # encode the image as Base64 string
  img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')
  # show the image
  img_tag = document.getElementById('fig')
  img_tag.src = img_str
  buf.close()
`

languagePluginLoader.then(()=>pyodide.runPythonAsync(python_code).then(()=>document.getElementById('status').innerHTML='Done!'))

Voici l'exemple pour matplotlib . Notez que cela chargera un tas de dépendances qui prendra jusqu'à plusieurs minutes.

<!DOCTYPE html>

<head>
    <script type="text/javascript">
        // this variable should be changed if you load pyodide from different source
        window.languagePluginUrl = 'https://pyodide-cdn2.iodide.io/v0.15.0/full/';
    </script>

    <script src="https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js"></script>
</head>

<body>
    Output:
    </div>
    <textarea id='output' style='width: 100%;' rows='10' disabled></textarea>
    <textarea id='code' value=''  rows='2'></textarea>
    <button id='run' onclick='evaluatePython()'>Run</button>
    <p>You can execute any Python code. Just enter something in the box above and click the button (or Ctrl+Enter).</p>
    <div><a href='https://github.com/karray/truepyxel/demo.html'>Source code</a></div>
</body>

</html>
const output = document.getElementById("output")
const code = document.getElementById("code")

code.addEventListener("keydown", function (event) {
    if (event.ctrlKey && event.key === "Enter") {
        evaluatePython()
    }
})

function addToOutput(s) {
    output.value += `>>>${code.value}\n${s}\n`
    output.scrollTop = output.scrollHeight
    code.value=''
}

output.value = 'Initializing...\n'
// init pyodide
languagePluginLoader.then(() => { output.value += 'Ready!\n' })

function evaluatePython() {
    pyodide.runPythonAsync(code.value)
        .then(output => addToOutput(output))
        .catch((err) => { addToOutput(err) })
}


0 commentaires