Gestion de fichiers
Sont discutés ici :
- Le téléchargement depuis l'environnement Pyodide de fichiers stockés en ligne, sur un serveur.
- La création de fichiers dans l'environnement de Pyodide.
- Les échanges de fichiers Pyodide/utilisateur pour :
- téléverser (upload) les contenus d'un ou plusieurs fichiers de la machine de l'utilisateur vers l'environnement de Pyodide.
- télécharger (download) sur la machine de l'utilisateur des données construites lors des exécutions dans Pyodide.
Ne sont donc pas discutés ici les fichiers markdown ou les fichiers pythons utilisés pour les macros IDE
, terminal
, ... (les concernant, voir les pages dédiées dans la section Rédacteurs).
Les codes python pour les échanges de fichiers doivent être dans des sections asynchrones
Les échanges de fichiers nécessitent dans leur grande majorité de recourir à des appels asynchrones comme await fetch(...)
. En python, ce type d'appels ne peut normalement pas ĂŞtre fait en dehors d'une fonction async (async def func(...)
), alors qu'il est laborieux d'appeler des fonction async depuis du code synchrone (beaucoup plus qu'en javascript).
Les sections d'environnements et la section secrets
des fichiers pythons utilisés pour les IDE, les terminaux ou les py_btn
sont exécutées via un mode async
de pyodide, qui permet de mettre des appels asynchrones en dehors des fonctions.
Système de fichiers#
Contexte#
L'environnement de Pyodide dispose de son propre système de fichiers, avec son disque virtuel.
Il est donc possible d'utiliser les fonctionnalités I/O dans Pyodide comme on le ferait sur une machine "normale" (with open(...)
, pathlib.Path
, ...).
Limitations liées au système de fichiers dans Pyodide
-
Le système de fichiers est vierge au démarrage de l'environnement.
Si l'utilisateur doit manipuler des fichiers pour un exercice, il faut donc les créer manuellement ou les copier depuis le serveur au début des exécutions (voircopy_from_server
plus bas). -
Ne pas oublier que l'intégralité de l'environnement, disque virtuel compris, tourne en mémoire vive, par l'intermédiaire des onglets du navigateur. Ceci implique qu'il faut éviter de manipuler de gros fichiers via Pyodide, sous peine de faire crasher le navigateur de l'utilisateur.
Il faut également garder en tête que la quantité de RAM disponible peut varier drastiquement selon la machine, l'OS et/ou le navigateur utilisés.
Ne pas confondre avec les fichiers sur le serveur
Lors d'un serve/build de mkdocs, les fichiers markdown du docs_dir
(généralement nommé docs
) sont convertis en pages html, puis ces fichiers et tous les fichiers autres que markdown qui ne sont pas exclus de la construction du site sont également ajoutés.
L'ensemble de ces fichiers est ensuite présent sur le serveur une fois le site en ligne.
-
Ces fichiers sont sur le serveur, et non dans l'environnement Pyodide lui-même. L'environnement Pyodide étant dans le navigateur du client.
-
Depuis Pyodide, il est possible d'extraire des fichiers du serveur, pour les recréer dans le disque virtuel, sur le navigateur du client (voir les sections suivantes, notamment concernant la fonction
copy_from_server
).
Extraction de fichiers d'un serveur#
Il est possible de récupérer le contenu de fichiers hébergés en ligne, sur n'importe quel serveur, depuis les sections d'environnement (env
, env_term
, post_term
ou post
) avec ce type de code :
# --- PYODIDE:env --- #
from js import fetch
url_fichier = "zoo_traduction.csv"
# Voir ci-dessous concernant la localisation du fichier !
reponse = await fetch(url_fichier)
data = await reponse.text()
data
peut ensuite être converti par vos soins, pour correspondre à l'utilisation souhaitée.
Il est également possible de récupérer le contenu brut du fichier en utilisant soit js.fetch
, soit pyodide.http.pyfetch
:
from js import fetch
reponse = await fetch("zoo_traduction.csv")
data = (await reponse.arrayBuffer()).to_bytes()
from pyodide.http import pyfetch
reponse = await pyfetch("zoo_traduction.csv")
data = await reponse.bytes()
Gérer les adresses relatives#
Si vous utilisez des adresses absolues pour les requêtes d'extractions de fichiers, aucun problème.
Si par contre vous utilisez des adresses relatives, pour extraire des fichiers sur serveur du site construit, il faut être attentif à différentes choses :
Le problème des adresses de fichiers relatives
-
L'adresse du fichier doit être donnée par rapport au dossier contenant la page html sur laquelle l'utilisateur se trouve. L'adresse est donc définie par rapport au fichier markdown source, et surtout pas par rapport au fichier contenant le code python.
-
Si le fichier
mkdocs.yml
utilise l'optionuse_directory_urls: true
(ce qui est la valeur par défaut), le dossier de la page html en cours change selon que le fichier markdown source est nomméindex.md
ou autrement.
Par exemple, voici les adresses relatives à utiliser pour accéder aux fichiers .txt
depuis les fichiers markdown voisins, pour cette hiérarchie sur le dépôt :
├── prog_dyn
│ ├── index.md
│ ├── knapsack.txt
│ └── exo.py
└── DpR
├── closest_points.md
├── points.txt
└── exo.py
use_directory_urls |
Fichier markdown source, sur le dépôt |
Répertoire actif sur la page web | Chemin relatif url_fichier dans le code python |
---|---|---|---|
true |
/prog_dyn/index.md |
/prog_dyn |
"knapsack.txt" |
true |
/DpR/closest_points.md |
/DpR/closest_points |
"../points.txt" |
false |
/prog_dyn/index.md |
/prog_dyn |
"knapsack.txt" |
false |
/DpR/closest_points.md |
/DpR |
"points.txt" |
Création et copie de fichiers: copy_from_server
#
Après extraction d'un fichier sur un serveur, il est possible de recréer le même fichier dans l'environnement Pyodide.
Ceci peut s'avérer particulièrement utile pour travailler ensuite sur des images.
Le thème propose une fonction dédiée à ce type de tâche, await copy_from_server(...)
, permettant de récupérer automatiquement un fichier sur un serveur, et de le recréer sur le disque virtuel.
# --- PYODIDE:env --- #
fichier = "image.jpg" # Voisin d'un fichier `index.md`
await copy_from_server(fichier) # Recrée `image.jpg` à la racine dans pyodide
Le principe de fonctionnement est de récupérer les données au format bytes
(afin d'éviter tous problèmes liés aux encodages), puis d'écrire directement le fichier correspondant sur le disque virtuel :
# --- PYODIDE:env --- #
from pyodide.http import pyfetch
fichier = "image.jpg" # Voisin d'un fichier `index.md`
reponse = await pyfetch(url_fichier)
with open(fichier, 'wb') as pyodide_file:
pyodide_file.write(await reponse.bytes())
Un fichier image.jpg
identique à celui stocké sur le serveur de la documentation existe alors à la racine du disque virtuel dans Pyodide.
Spécifications de la fonction asynchrone copy_from_server(...)
async def copy_from_server(
src: str,
dest: str=".",
name: str="",
):
"""
Récupère le fichier à l'adresse `src` (absolue ou relative au dossier de la
page en cours), et crĂ©e son Ă©quivalent sur le disque virtuel de pyodide Ă
l'adresse `dest/nom_de_fichier`.
`nom_de_fichier` est l'argument `name`, ou si celui-ci n'est pas renseigné,
le nom de fichier à la fin de `src` est utilisé à la place.
Exemple :
await copy_from_server("../other/img.jpg", "work/black_white")
=> Crée le fichier `work/black_white/img.jpg` sur le disque virtuel.
"""
Argument | RĂ´le |
---|---|
src |
Adresse du fichier source. Peut ĂŞtre une adresse relative ou absolue. |
dest |
Dossier de destination du fichier dans pyodide. Par défaut, le répertoire de travail de l'environnement est utilisé. |
name |
Si l'argument name est utilisé, ce sera le nom utilisé pour le fichier dans pyodide. Sinon, le nom de fichier sera extrait de src . |
- Si
src
est une adresse relative, attention au nom du fichier markdown ,index.md
ou autre, qui peut modifier le chemin relatif sur le site construit. - Si
dest
est utilisé, les éventuels répertoires intermédiaires manquant sont créés automatiquement. - Si un fichier du même nom existe déjà sur le disque virtuel, il est écrasé automatiquement.
Récupérer des fichiers python sur le serveur#
Par défaut, les fichiers pythons ne sont pas sur le serveur !
La configuration par défaut du thème comporte ce réglage dans le fichier mkdocs.yml
:
exclude_docs: |
**/*_REM.md
**/*.py
Ceci permet :
- De ne pas générer de pages à partir des fichiers de remarques (visibles ou non).
- D'exclure du site construit tous les fichiers python, afin qu'un utilisateur ne puisse pas simplement accéder aux codes des exercices en téléchargeant les fichiers depuis le serveur.
La contre-partie de ceci est qu'il n'est donc pas possible, par défaut, de laisser un fichier python dans un dossier de la documentation pour ensuite le télécharger lors des exécutions, en utilisant par exemple copy_from_server
.
Diverses solutions sont utilisables pour rendre certains fichiers python disponibles au téléchargement.
Modifier la règle mkdocs.yml:exclude_docs
avec :
```yaml
exclude_docs: |
**/*_REM.md
**/*.py
!**/*_upload.py
```
Tous les fichiers python finissant en `_upload.py` seront maintenant disponibles sur le site construit.
Si la présence d'un suffixe comme _upload
est problématique, il est également possible d'ajouter des règles spécifiques à un fichier dans exclude_docs
.
Il est alors conseillé de renseigner le chemin complet du fichier, relatif au docs_dir
:
exclude_docs: |
**/*_REM.md
**/*.py
!chapitre_X/prog_dynamique/script_sous_chaines_communes.py
Ces fichiers peuvent ensuite être utilisés, soit en mettant un lien vers le fichier à télécharger dans la documentation, soit en les téléchargeant directement depuis pyodide afin d'utiliser leur contenu dans un exercice. On utilise alors copy_from_server
, en modifiant le nom du fichier enregistré si besoin :
Pour un fichier data_upload.py
présent dans le même dossier que le fichier source index.md
(rappel : attention aux adresses relatives, qui peuvent changer selon le nom du fichier markdown...) :
# --- PYODIDE:env --- #
await copy_from_server('data_upload.py')
# --- PYODIDE:code --- #
import data_upload
Il est aussi possible de renommer le fichier téléchargé :
# --- PYODIDE:env --- #
await copy_from_server('data_upload.py', name='my_data.py')
# --- PYODIDE:code --- #
import my_data
Échanges de fichiers avec l'utilisateur #
Moyennant certaines limitations, il est possible de proposer Ă l'utilisateur de :
- Téléverser son propre contenu dans l'environnement, potentiellement pour l'utiliser ensuite lors des exécutions (uniquement depuis une section asynchrone).
- Télécharger des données construites durant l'exécution du code (un graphe mermaid, du texte généré, une image, ...).
Ces opérations sont par essence "asynchrones" car elles utilisent des évènements du DOM, ce qui impacte notamment la façon de réaliser les téléversements de fichiers dans l'environnement.
Téléverser dans pyodide #
But
L'utilisateur injecte des données issues d'un de ses fichiers dans l'environnement.
Il est possible de téléverser des contenus au format chaîne de caractères (str
) en utilisant l'encodage par défaut (utile pour les fichiers py
, txt
, csv
, ...), ou bien au format bytes
(fichiers png
, jpeg
, zip
, ...).
Deux fonctions (décrites plus bas) permettent de gérer les téléversements : elles partagent les mêmes arguments, mais n'ont pas exactement les mêmes comportements ni les mêmes types de sortie.
Gérer l'encodage des fichiers textes
Le passage des données entre la couche javascript et la couche python à travers pyodide comporte certaines limitations.
Aussi, s'il est nécessaire d'avoir un contrôle fin de l'encodage utilisé pour lire ou écrire un fichier de données, il faut procéder comme suit :
-
Pour téléverser :
- Télécharger le fichier sous forme de
bytes
- Convertir les
bytes
avec l'encodage désiré dans pyodide.
- Télécharger le fichier sous forme de
-
Pour télécharger :
- Convertir dans pyodide la chaîne de caractères en
bytes
. - Passer le contenu sous cette forme Ă la couche JS qui enregistrera le fichier sur le disque de l'utilisateur.
- Convertir dans pyodide la chaîne de caractères en
Bug possible avec les raccourcis clavier
Contexte
Les navigateurs mettent en place diverses sécurités pour protéger l'utilisateur contre des sites frauduleux, dont l'une qui peut potentiellement empêcher le bon fonctionnement du téléversement :
Le téléversement repose sur l'activation d'un évènement input.click()
via le code lui-même. Or, un navigateur n'autorise pas un programme à faire deux fois de suite ce type d'opération s'il n'y a eu aucune "action utilisateur" dans la page entre les deux, une action utilisateur étant typiquement un clic dans la page.
Le problème
Les raccourcis clavier ne sont pas considérés comme des "actions utilisateur". Donc le téléversement n'est pas effectué dans la situation suivante :
- L'utilisateur déclenche les exécutions d'un IDE avec Ctrl+S ou Ctrl+Enter.
- L'utilisateur annule le téléversement, ou téléverse un fichier.
- L'utilisateur réactive un raccourci clavier pour relancer les exécutions sans faire d'autre action dans la page.
Solution partielle
Le comportement par défaut ne permet pas d'avertir l'utilisateur du problème car aucune erreur n'est levée durant les exécutions, mais une façon alternative de déclencher l'évènement a permis de forcer la levée d'une erreur dans ce cas. Cette approche n'est cependant pas compatible avec tous les navigateurs (en fait, à l'heure actuelle, seul Safari n'est pas compatible).
-
Pour un navigateur compatible :
L'utilisateur verra un message dans le terminal lui disant de cliquer dans la page avant de réutiliser le raccourci, ou de lancer les exécutions via un bouton plutôt qu'un raccourci.
Le terminal restera utilisable normalement, dans ce cas. -
Pour un navigateur NON compatible :
Dans ce cas, l'utilisateur peut se retrouver avec une page non fonctionnelle, et aucune indication sur le fait qu'il y a un problème. Il devra obligatoirement recharger la page.
Asynchrone#
C'est cette version qu'il faut utiliser en priorité car elle permet d'utiliser le contenu du fichier téléversé dans la suite des exécutions.
Mais comme son exécution est async
, cette fonction ne peut être appelée que depuis des sections d'environnement, comme env
, env_term
, post_term
ou post
.
async def pyodide_uploader_async(
cbk: Callable[[...], T],
*,
read_as: Literal['txt','img','bytes'] = 'txt',
with_name: bool = False,
multi: bool = False,
) -> T | Tuple[T] :
...
Synchrone#
Cette version peut être exécutée depuis n'importe quelle section, mais elle ne sera exécutée qu'après la fin des exécutions en cours. Ceci signifie que le contenu téléversé ne sera PAS disponible pour les exécutions en cours, mais seulement pour la fois suivante.
def pyodide_uploader(
cbk: Callable[[...], None],
*,
read_as: Literal['txt','img','bytes'] = 'txt',
with_name: bool = False,
multi: bool = False,
) -> None :
...
Préférez la version async
!
La gestion de données provenant de deux exécutions différentes est particulièrement propice aux bugs, en particulier si l'utilisateur peut interagir avec l'environnement entre ces exécutions.
N'utilisez la version synchrone que s'il n'existe pas d'autre solution.
Entrées/sorties des fonctions#
Argument | Type | RĂ´le |
---|---|---|
cbk |
Callable |
Routine qui reçoit en argument le contenu d'un fichier téléversé au format désiré (str ou bytes ), avec éventuellement le nom du fichier d'origine. Cette fonction a en charge les éventuelles conversions à appliquer au contenu du fichier. Elle peut ou non renvoyer un résultat, selon l'uploader utilisé (voir ci-dessous). |
read_as |
str='txt' |
Permet de choisir sous quelle forme le contenu du fichier va être lu et passé à cbk :
|
with_name |
bool=False |
Si vrai, la routine cbk recevra un second argument, filename , indiquant le nom du fichier source. |
multi |
bool=False |
Si vrai, l'utilisateur aura le droit de sélectionner plusieurs fichiers à la fois. La routine cbk sera appelée une fois par fichier. |
Signatures pour cbk
, selon l'appel utilisé pour l'uploader :
Uploader | with_name |
Signature de cbk |
---|---|---|
async | False |
cbk(data) -> T|tuple[T,...] |
async | True |
cbk(data, filename) -> T|tuple[T,...] |
sync | False |
cbk(data) -> None |
sync | True |
cbk(data, filename) -> None |
Le type des données reçues via l'argument data
dépend de l'argument read_as
passé à l'uploader
:
uploader(..., read_as=...) |
Type de data |
---|---|
'txt' |
str |
'img' |
str |
'bytes' |
bytes |
Les types de sortie de la routine cbk
diffèrent également selon l'uploader utilisé et l'argument multi
:
Fonction | multi |
Type renvoyé par cbk |
---|---|---|
pyodide_uploader_async |
False |
T (au choix du rédacteur) |
pyodide_uploader_async |
True |
tuple[T,...] |
pyodide_uploader |
True|False |
None |
- Avec
pyodide_uploader_async
:cbk
peut renvoyer le rĂ©sultat du traitement des donnĂ©es provenant du fichier tĂ©lĂ©versĂ© par l'utilisateur. Ces rĂ©sultats seront ensuite renvoyĂ©s par l'appel initial Ăpyodide_uploader_async
, les rendant disponibles dans l'environnement python pour la suite des exécutions. - Avec
pyodide_uploader
:cbk
ne renvoie pas de résultat et la routine devra muter l'environnement global pour que les données restent disponibles pour l'exécution suivante.
Exemples#
Voici des exemples fonctionnels de chaque version, pour se rendre compte de leur mode de fonctionnement :
Téléversement du contenu d'un fichier au format chaîne de caractères :
Contenu du fichier python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Téléversement d'une image au format bytes et insertion dans la page :
Contenu du fichier python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
# Tests
(insensible Ă la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Image téléversée insérée ci-dessous :
Le code html initial de la balise ci-dessus est le suivant :
<div style="border:solid gray;width:100%;min-height:10px;display:flex;justify-content:center">
<img id="img-target" />
</div>
Ouverture d'un fichier texte avec un encodage spécifique :
Contenu du fichier python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
# Tests
(insensible Ă la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Contenu du fichier python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
# Tests
(insensible Ă la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Télécharger depuis pyodide #
But
L'utilisateur récupère des données issues de l'environnement en récupérant un fichier dans son répertoire de téléchargement.
La fonction de téléchargement est synchrone et peut être utilisée depuis n'importe quelle section.
De la même façon que pour le téléchargement des contenus des éditeurs, le navigateur impose que les fichiers soient enregistrés dans le répertoire de téléchargement, et seul le nom de fichier peut être choisi.
La signature de la fonction est la suivante :
pyodide_downloader(
content: str|bytes|list[int]|bytearray,
filename: str,
type_mime: str="text/plain"
)
Argument | RĂ´le |
---|---|
content |
Le contenu du fichier. Si content est une liste d'entiers, elle sera automatiquement convertie en bytes .Le type de contenu doit être cohérent avec la valeur utilisée pour l'argument type_mime . |
filename |
Nom du fichier (le navigateur peut y ajouter des numéros de version, si un fichier du même nom existe déjà dans le répertoire de téléchargement). |
type_mime |
Définit le type de fichier créé. Ce type doit impérativement être cohérent avec le contenu fourni en premier argument pour que le fichier téléchargé puisse ensuite être ouvert par l'utilisateur. La liste des types MIME possible est consultable sur MDN. Un des scripts du thème permet d'ouvrir la page en question directement dans le navigateur : python -m pyodide_mkdocs_theme --mime . |
Gérer l'encodage des fichiers textes
Le passage des données entre la couche javascript et la couche python à travers pyodide comporte certaines limitations.
Aussi, s'il est nécessaire d'avoir un contrôle fin de l'encodage utilisé pour lire ou écrire un fichier de données, il faut procéder comme suit :
-
Pour téléverser :
- Télécharger le fichier sous forme de
bytes
- Convertir les
bytes
avec l'encodage désiré dans pyodide.
- Télécharger le fichier sous forme de
-
Pour télécharger :
- Convertir dans pyodide la chaîne de caractères en
bytes
. - Passer le contenu sous cette forme Ă la couche JS qui enregistrera le fichier sur le disque de l'utilisateur.
- Convertir dans pyodide la chaîne de caractères en
Exemple :
# Tests
(insensible Ă la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
# Tests
(insensible Ă la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)