Aller au contenu

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 (voir copy_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'option use_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.

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 :

  1. De ne pas générer de pages à partir des fichiers de remarques (visibles ou non).
  2. 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 :

    1. Télécharger le fichier sous forme de bytes
    2. Convertir les bytes avec l'encodage désiré dans pyodide.
  • Pour tĂ©lĂ©charger :

    1. Convertir dans pyodide la chaîne de caractères en bytes.
    2. Passer le contenu sous cette forme Ă  la couche JS qui enregistrera le fichier sur le disque de l'utilisateur.
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 :

  1. L'utilisateur déclenche les exécutions d'un IDE avec Ctrl+S ou Ctrl+Enter.
  2. L'utilisateur annule le téléversement, ou téléverse un fichier.
  3. 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:
  • 'txt' : renvoie le contenu au format str (lu avec l'encodage par dĂ©faut)
  • 'img' : renvoie le contenu sous forme de DataURL, qui peut ĂŞtre insĂ©rĂ©e directement dans une balise <img>
  • 'bytes' : renvoie le contenu au format bytes
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
# --- PYODIDE:env --- #
def upload(content:str):
    print("This will show up in the terminal!")
    return content[:50]+' [...]'

print("env!")
data = await pyodide_uploader_async(upload)
# `data` available NOW!

# --- PYODIDE:code --- #
print("From `PYODIDE-code`, data is now:")
print(data)

# --- PYODIDE:post --- #
print("post!")

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

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
# --- PYODIDE:env --- #
print("env!") ; DataURL = str

def upload(img:DataURL):
    import js
    js.document.getElementById("img-target").src = img
    return img

data = await pyodide_uploader_async(upload, read_as='img')
# `data` available NOW!

# --- PYODIDE:code --- #
print("From `PYODIDE-code`, data is now:")
print(data[:150])

# --- PYODIDE:post --- #
print("post!")

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

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
# --- PYODIDE:env --- #
if 'ENC' not in globals():          # define once only
    ENC = "utf-16"

def upload(txt_as_bytes:bytes, name):
    print('Decoding', name, 'as', ENC)
    return txt_as_bytes.decode(ENC)[:50] + "[...]"

print("env!")
data = await pyodide_uploader_async(upload, read_as='bytes', multi=True, with_name=True)
# `data` is available NOW!

# --- PYODIDE:code --- #
print('---')
print(*(s[:50]+'...' for s in data), sep='\n')
print("\n(you can modify `ENC` from the terminal)")

# --- PYODIDE:post --- #
print("post!")

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

Contenu du fichier python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# --- PYODIDE:env --- #
print("env!")

if 'data_sync' not in globals():     # define once only
    data_sync = None

def upload(content:str):
    global data_sync
    data_sync = content[:30]+' [...]'  # available on next run
    print("This is run from an event and will show up in the browser's console only! (F12)")

pyodide_uploader(upload)  # This is synch, but upload is run later from an async context!
print('Not waiting...')

# --- PYODIDE:code --- #
print("data:", data_sync or "NOTHING YET!")      # you see content of previous run!

# --- PYODIDE:post --- #
print("post!")
print("Regarder dans la console! (F12)")

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

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 :

    1. Télécharger le fichier sous forme de bytes
    2. Convertir les bytes avec l'encodage désiré dans pyodide.
  • Pour tĂ©lĂ©charger :

    1. Convertir dans pyodide la chaîne de caractères en bytes.
    2. Passer le contenu sous cette forme Ă  la couche JS qui enregistrera le fichier sur le disque de l'utilisateur.

Exemple :

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier