Aller au contenu

Les IDEs#

un IDE

Les IDEs proposés par pyodide-mkdocs-theme sont un environnement clef en main avec :

  • Un éditeur de code, très similaire à ce qu'on peut trouver dans des éditeurs de type VSC.
  • Un terminal, qui fonctionne de la même manière qu'une console interactive python.
  • Un jeu de boutons et fonctionnalités permettant de faire tourner l'ensemble, avec pyodide travaillant en sous-main.

Rappel RGPD

L'intégralité de l'environnement tourne dans le navigateur, côté client. Il n'y a aucune donnée renvoyée au serveur !

Vue d'ensemble#

Contenus / sections#

Pour fonctionner, un IDE a besoin de différents snippets/sections python fournis par le rédacteur via un ou plusieurs fichiers pythons. Ces fichiers sont spécifiques à un exercice/IDE.


Les différents codes python sont identifiés par des noms de sections, chaque section ayant un rôle spécifique :

Section Exécution Rôle
env async Code d'environnement : "setup".
Non visible par l'utilisateur, ce code permet de définir des choses nécessaires au code de l'utilisateur ou aux tests, comme une classe à utiliser, des fonctions de tests "custom", ...
code sync Code initial proposé à l'utilisateur.
corr sync Solution permettant de passer tous les tests.
L'utilisateur ne peut pas voir ce code, sauf une fois que la solution a été révélée, soit après avoir réussi lui-même l'exercice, soit après avoir consommé tous les essais disponibles.
Ce code n'est en fait jamais exécuté dans l'IDE lui-même, mais il est possible de l'exécuter à la place du code de l'éditeur, pour tester le code de correction durant le développement en local.
tests sync Code original des tests publics.
Visible sous le contenu de la section code, dans l'éditeur.
secrets sync Code des tests cachés, lors des validations.
L'utilisateur ne peut pas voir le contenu de ces tests.
Ne pas y recopier le contenu de la section tests ! (voir le déroulement de la validation)
post async Code d'environnement : "teardown".
Non visible par l'utilisateur, ce code permet d'appliquer des éventuelles étapes de nettoyage de l'environnement.
À ne pas utiliser pour des tests !
ignore Un type de section spécial, qui permet de stocker dans les fichiers du contenu qui doit être ignoré pour le site construit.
La section ignore est la seule qui peut être présente plusieurs fois dans le fichier.
Ces sections sont utiles pour laisser des commentaires pour les rédacteurs.
  • Toutes les sections sont optionnelles.
  • La section ignore peut être utilisée autant de fois qu'on le souhaite dans le fichier.
  • Les autres sections ne doivent être utilisées qu'une seule fois.



En dehors des codes python, un IDE peut aussi utiliser deux autres fichiers markdown, {exo}_REM.md et {exo}_VIS_REM.md, contenant des remarques que le rédacteur voudrait joindre à la correction (ici, {exo} est le nom du fichier python contenant les codes nécessaire, sans l'extension .py. Voir plus bas).

alt

Les contenus de ces deux fichiers sont affichés sous l'IDE lorsque l'utilisateur passe tous les tests ou consomme tous les essais disponibles. La différence essentielle entre les deux est que :

  • {exo}_REM.md est affiché avec la correction (section corr), dans une admonition repliée. Ceci afin que l'utilisateur n'ai pas la solution visible de suite, s'il souhaite poursuivre ses essais.
  • {exo}_VIS_REM.md est affiché sous l'admonition repliée, et est donc tout de suite visible. C'est l'endroit idéal pour donner par exemple un lien pour faire l'exercice suivant.

En termes de types de contenus, ces fichiers ne doivent pas contenir d'appels de macros ni de graphes mermaid, mais il est possible d'y utiliser des syntaxes Latex.




Changer le commentaire séparant le code utilisateur des tests publics, dans l'éditeur

Par défaut, la chaîne "\n# Tests\n" est utilisée comme séparateur.
Il est possible de la modifier, en utilisant la personnalisation des messages, en modifiant l'entrée "tests".

Contraintes sur l'écriture de commentaires dans les tests publics (section tests)

Le bouton ### dans le coin supérieur droit de chaque éditeur ainsi que le raccourci Ctrl+I permettent d'activer ou désactiver le code contenu dans les tests publics (v. l'aide utilisateur).

Afin de garantir le bon fonctionnement de cet outil, il est impératif que tout commentaire écrit par vos soins dans les tests publics ait un espace après le # :

Bien commenter ses tests publics
# Tests

# Ceci est un commentaire et le restera même après un Ctrl+I

assert True     # Ctrl+I désactiverait cette ligne et activerait la suivante.
#assert False   # Ceci n'est pas un commentaire, mais du code désactivé.
L'existence d'une section secrets impacte certains éléments de l'interface
  • Le bouton de validation n'est visible que si du contenu secrets est présent.

  • L'indication du nombre d'essais disponibles, affiché en bas à droite de l'IDE devient \(\infty\) s'il n'y a pas de contenu secrets :

Avec une section secrets
Sans section secrets

Validité des contenus
À propos de la section env

Toute première à être exécutée, cette section permet de mettre en place des ressources pour l'exercice. Par exemple :

  • ajout de fonctions ou de classes qui pourraient servir à l'utilisateur, ou aux tests
  • remplacement de fonctions dans le scope global (si cela est fait dans le but d'imposer des restrictions sur le code utilisé, préférer l'argument SANS des macros IDE proposé par le thème),
  • ...

Elle est par ailleurs lancée en utilisant le mode asynchrone de pyodide, ce qui permet également :


Concernant les erreurs levées lors de l'exécution de la section env :

Il est possible d'y faire des tests avant que le code de l'utilisateur n'ait été exécuté (et donc avant qu'il ait une chance d'altérer l'environnement, pour l'exécution en cours).

  • Si AssertionError est levée alors que l'utilisateur lançait une validation, un essai est décompté si l'option decrease_attempts_on_user_code_failure est à "editor" (c'est le cas par défaut).
    Dans cette situation, le code de l'utilisateur et les tests ne seront pas exécutés, mais la section post le sera.

  • Si une autre erreur est levée, les exécutions sont complètement stoppées.
    Ceci est considéré comme une situation anormale, donc aucune des sections suivantes ne sera exécutée, pas même la section post. Le compteur d'essais n'est pas non plus mis à jour si cela arrive durant une validation.

    Protéger le code env contre les erreurs !

    Si vous mettez en place de la logistique consommatrice de ressources, qu'il faut impérativement gérer à la fin des exécutions, il faut que vous soyez sûr que l'exécution du code de la section env ne lèvera pas d'erreur.

    À noter que le problème ne se pose que lorsque la section elle-même est exécutée, et pas lorsque du code qui y a été défini lève une erreur quand il est appelé plus tard : il n'y a aucun problème avec une fonction définie dans env qui crasherait lors d'un appel par l'utilisateur, par exemple (ce n'est plus la section env qui est en cours d'exécution, à ce moment-là).

À propos de la section post

Cette section est exécutée même si le code utilisateur ou les tests ont levé une erreur quelconque.

Elle est par ailleurs lancée en utilisant le mode asynchrone de pyodide, et permet donc typiquement de disposer de certaines ressources qui ont été mises en place depuis la section env:

  • gestion de fichiers,
  • remplacement de fonctions dans le scope global,
  • ...

Il est cependant à noter que :

  • Cette section n'est pas disponible avec l'organisation des fichiers originale de pyodide-mkdocs, utilisant jusqu'à 3 fichiers python.
  • Cette section ne sera PAS exécutée si la section env a levé une erreur autre que AssertionError.
  • Ne pas faire de tests depuis cette section. La section post est effectivement exécutée après la logique de validation du code de l'utilisateur.

Créer un IDE#

La déclaration d'un IDE dans une page de la documentation se fait par simple appel d'une des macros IDE ou IDEv, en lui passant le nom du fichier python à utiliser (sans extension).

[...énoncé, avec tout le markdown qui vous fait envie...]

{{ IDE('exo') }}

La macro se charge de récupérer tous les contenus nécessaires et de les insérer là où approprié.


Il y a en fait de nombreuses façons différentes d'organiser les fichiers et leurs contenus : voir l'argument py_name, plus bas dans la page, et la section sur l'organisation des fichiers sur le disque.

Déroulement des exécutions#

Installation automatiques des bibliothèques python manquantes, lors des exécutions

Exécuter dans pyodide "une section de code python" ou une commande du terminal implique toujours les étapes suivantes :

  1. Recherche des éventuels bibliothèques manquantes pour exécuter le code en question.
  2. Installation des bibliothèques manquantes (saufs restrictions sur les packages) .
  3. Exécution du code lui-même.

Voir ici les détails sur le fonctionnement des bibliothèques dans l'environnement du thème.


Voici une description des grandes lignes des exécutions réalisées sur le site construit par mkdocs, lorsque l'utilisateur appuie sur les boutons suivants d'un IDE :

  1. Le message "Script lancé..." apparaît dans le terminal.
  2. Exécution du code de la section env.
  3. Exécution du contenu actuel de l'éditeur.
  4. Affichage de la sortie standard.
  5. Si aucune erreur n'a été rencontrée
    • Si l'IDE n'a pas de bouton de validation,"Terminé sans erreur." est affiché.
    • Sinon, le message "Éditeur: OK" est affiché dans le terminal, suivi d'un message rappelant de faire une validation.
  6. Exécution du code de la section post.

img

  1. Le message "Script lancé..." apparaît dans le terminal.
  2. Exécution du code de la section env.
  3. Exécution du contenu actuel de l'éditeur.
  4. Affichage de la sortie standard.
  5. Si pas d'erreur jusque là, validation avec :
    • Exécution de la version originale des tests publics (section tests), au cas où l'utilisateur les aurait modifiés ou désactivés dans l'éditeur.
    • Exécution des tests de la section secrets.
  6. Pas d'affichage de la sortie standard, si elle est désactivée pendant la validation. (ce qui est le réglage par défaut).
  7. Si une erreur a été rencontrée, le compteur d'essais est diminué de 1 (pour modifier ce comportement, voir ici).
  8. Si le compteur d'essais est arrivé à zéro ou si l'utilisateur a passé tous les tests avec succès, une admonition apparaît sous l'IDE, contenant la correction si elle existe (section corr) et les éventuelles remarques ({exo}_REM.md / Ce comportement est modulé et modifiable selon les contenus existant et différentes options ou arguments, comme par exemple l'argument MODE des IDEs).
  9. Exécution du code de la section post.

img

Changement des conditions de décomptes des essais

Il est possible de contrôler finement à partir de quelle étape des validations une erreur sera considérer comme comptant pour un essai consommé.
Ceci se fait en modifiant l'option ides.decrease_attempts_on_user_code_failure dans la configuration du plugin, ou dans les méta-données. Cette option prend le nom de l'étape à partir de laquelle une erreur sera considérée comme comptant pour un essai.

plugins:
    - pyodide_macros:
        ides:
            decrease_attempts_on_user_code_failure: "secrets"

Par défaut, "editor" est utilisé, ce qui veut dire que n'importe quelle erreur provoquée par le code de l'éditeur de l'IDE consommera un essai (même une erreur de syntaxe).

Attention aux conditions d'accessibilité des corrections et remarques

  • Utiliser une valeur autre que "editor" implique qu'un utilisateur qui n'arriverait pas à résoudre des problèmes de syntaxe dans le code ne pourra jamais accéder à la correction.

  • Avec "public", il suffit à un utilisateur de désactiver les tests ou même de supprimer tout le contenu de l'éditeur pour pouvoir faire décroître le compteur d'essais à volonté.

  • Avec "secrets", le compteur d'essais ne peut décroître que si le code ne contient pas d'erreurs de syntaxe et passe les tests publics (ce qui est faisable en hard-codant les réponses des tests publics).

Réglage du niveau de feedback donné à l'utilisateur, durant les tests de validation

Il est en fait possible de changer le comportement du thème, concernant l'affichage ou non de la sortie standard et le formatage automatique des messages d'erreurs, lors des tests de validation.

Ceci peut se faire avec les options suivantes du fichier mkdocs.yml (ou via les fichier .meta.pmt.yml) :

plugins:
    - pyodide_macros:
        ides:
            deactivate_stdout_for_secrets: true             # défaut: true
            show_only_assertion_errors_for_secrets: true    # défaut: false


  • L'option deactivate_stdout_for_secrets permet de réactiver l'affichage de la sortie standard dans les tests de validation, en passant la valeur à false.

  • L'option show_only_assertion_errors_for_secrets permet quant à elle de réduire au minimum les informations accessibles à l'utilisateur lors d'une erreur. Si cette option est passée à true :

    • La traceback est totalement supprimée.
    • Si l'erreur n'est pas de type AssertionError, seul le type d'erreur est affiché dans la console (ex: IndexError has been raised.).

      No feedback = BAD feedback !

      Ceci peut permettre d'éviter que l'utilisateur n'ait accès à des informations sur l'environnement de tests (les fonctions de tests "custom", en l'occurrence), mais garder en tête que ce réglage rend le débogage très compliqué, voire impossible, côté utilisateur.

      Une solution plus souple, dans ce type de cas, est d'attraper les erreurs depuis les fonctions de tests et de décider ce que vous en faites à ce moment-là.

Voici un récapitulatif du déroulement des exécutions des différentes sections, avec les embranchements logiques utilisés dans le code pour les tests publics (bouton "play") et les validations (bouton "check").

env Autres erreurs contenu éditeur + restrictions tests + secrets Corr/REMs && révélable? – restrictions "Bravo!" "Terminé" ou "Erreur" "Dommage" post Révèle corr/REMs Exécution async Exécution sync A B A B A A B B AssertionError non oui Déjà vus essais > 0 essais = 0 Tests publics Validations Succès Erreurs

Le terminal d'un IDE suit le même mode d'exécution que les terminaux isolés, dont voici le résumé :

env_term post (1x) commande env (1x) + restrictions – restrictions post_term Exécution async Exécution sync commandes multilignes

Les particularités des commandes multilignes sont décrites dans la page dédiée aux terminaux.

Fichiers pour un IDE#

En utilisant les désignations suivantes :

  • Le fichier markdown dans lequel une macro IDE ou IDEv est appelée est le fichier source.
  • Les contenus nécessaires au fonctionnement de cet IDE sont stockés dans des fichiers python dits annexes.
  • La chaîne {exo} (à choisir par vos soins) est un préfixe commun à tous les fichiers annexes.


Les macros IDE et IDEv du thème Pyodide-Mkdocs supportent différentes façons d'organiser ces fichiers et leur contenu.
Quelle que soit l'organisation choisie, tous les fichiers annexes utilisés doivent toujours se trouver dans un même dossier (qui n'est pas forcément celui du fichier source !).

Fichier Désignation
{...}.md Fichier source, depuis lequel les macros sont appelées.
{exo}.py Fichier annexe, dit "principal".
{exo}_REM.md Fichier annexe de remarques, affiché avec le code de la correction (dans une admonition repliée).
{exo}_VIS_REM.md Fichier annexe de remarques affichées sous l'admonition repliées donc visibles dès que la solution est révélée.
{exo}_corr.py
{exo}_test.py
[ OBSOLÈTES ] Fichiers annexes, originellement utilisés pour le projet pyodide-mkdocs original, mais encore utilisables avec le thème, si vous n'optez pas pour l'organisation avec un fichier python unique.

Organisation des contenus#

Voici une description de quels contenus placer dans quels fichiers,

Sont présentées ici :

  • L'organisation idoine liée au thème (version n'utilisant qu'un seul fichier python). C'est l'organisation conseillée.
  • L'organisation hérité du projet original, pyodide-mkdocs, utilisant 3 fichiers python. Cette organisation est toujours supportée, mais certaines des fonctionnalités proposées par le thème ne seront pas disponibles avec cette organisation des fichiers.


Tous les codes pythons sont regroupés dans un unique fichier {exo}.py, chaque section y étant annoncée avec un séparateur spécifique :

# ------ PYODIDE:{section} ------ #

Dans ces séparateurs :

  • Les espaces sont optionnels (quel que soit leur nombre et leur placement).
  • Les # au début et à la fin sont obligatoires.
  • Il faut au moins un trait d'union de chaque côté du texte central.


Validité des contenus

Toutes les sections sont optionnelles, mais pour être considéré valide, un fichier python doit respecter les règles suivantes :

  • Les noms des sections doivent être ceux décrits dans le tableau en haut de cette page. Tout autre nom lèvera une erreur.
  • Chaque section ne peut être déclarée qu'une fois (sauf la section ignore, qui peut être réutilisée autant de fois que nécessaire).
  • Quand une section est définie, elle ne peut pas être vide. Si vous ne souhaitez pas utiliser une section, supprimez son contenu ainsi que le commentaire annonçant cette section, ou bien ajoutez un commentaire dans la section en question.
  • L'ordre des sections dans le fichier n'a techniquement aucune importance pour les exécution dans Pyodide.
  • Selon la configuration de la section ides du plugin pyodide_macros, des contraintes peuvent être imposées ou non sur l'existence de certaines sections ou fichiers par rapport à d'autres.

    Si toutes les options sont laissées par défaut, une erreur est levée dans les cas suivants :

    Présence
    secrets
    Présence
    corr ou REMarques
    IDE(..., MAX=...) Option de configuration correspondant
    / ides.forbid_secrets_without_corr_or_REMs
    / ides.forbid_hidden_corr_and_REMs_without_secrets
    \(\infty\) ides.forbid_corr_and_REMs_with_infinite_attempts


Exemple de code python pour un fichier {exo}.py

Sur une machine où le thème est installé, le contenu de cet exemple est également accessible via un terminal en utilisant la commande :

python -m pyodide_mkdocs_theme --py


Modèle de base pour le fichier python
# --- PYODIDE:ignore --- #
"""
Les sections `ignore` sont... ignorées. Vous pouvez les utiliser pour laisser
des commentaires dans vos fichiers, ou y archiver du code python qui ne sera
pas utilisé pour le site construit.
---------------------------------------------------------------------------

La section `env` (ci-dessous) est exécutée avant le code utilisateur.
Son contenu n'est pas visible de l'utilisateur mais tout ce qui y est défini
est ensuite disponible dans l'environnement.
Si le code de la section ENV lève une erreur, rien d'autre ne sera exécuté.
"""
# --- PYODIDE:env --- #

class Stack:
    """ (Interface à décrire dans l'énoncé) """
    def __init__(self): self.__stk=[]
    def push(self, v): self.__stk.append(v)
    def pop(self): return self.__stk.pop()
    def is_empty(self): return not self.__stk



# --- PYODIDE:ignore --- #
"""
La section `code` est l'état initial du code fourni à l'utilisateur dans
l'éditeur, à l'exclusion des tests publics (voir section `tests`).
"""
# --- PYODIDE:code --- #

def est_pair(n):
    ...



# --- PYODIDE:ignore --- #
"""
La section `corr` contient le code qui sera affiché dans la correction, sous l'IDE.
"""
# --- PYODIDE:corr --- #

def est_pair(n):
    return not n%2



# --- PYODIDE:ignore --- #
"""
La section `tests` contient les tests publics qui seront affichés sous le code
utilisateur, dans l'éditeur.
"""
# --- PYODIDE:tests --- #

assert est_pair(3) is False
assert est_pair(24) is True



# --- PYODIDE:ignore --- #
"""
La section `secrets` contient les tests secrets pour les validations. Ces tests ne sont
pas visibles par l'utilisateur.

ATTENTION :
    Il est impératif d'utiliser des messages dans les assertions des tests secrets,
    sinon l'utilisateur ne peut pas déboguer son code car `print` est désactivé
    durant ces tests ! (sauf si... => Voir les options de configuration)
    À vous de choisir le niveau d'information que vous voulez fournir dans le message.

Par ailleurs, il est conseillé d'utiliser une fonction pour éviter que des variables
des tests ne se retrouvent dans l'environnement global.
"""
# --- PYODIDE:secrets --- #

def tests():
    for n in range(100):
        val = est_pair(n)
        exp = n%2 == 0

        msg = f"est_pair({n})"                           # Minimum vital
        msg = f"est_pair({n}): valeur renvoyée {val}"    # Conseillé
        msg = f"est_pair({n}): {val} devrait être {exp}" # La totale

        assert val == exp, msg

tests()         # Ne pas oublier d'appeler la fonction de tests... ! x)
del tests       # Si vous ne voulez pas laisser de traces...


# --- PYODIDE:post --- #
# La section post contient du code de "nettoyage", à appliquer systématiquement
# après que le code et les tests aient été lancés.
# Ce contenu est exécuté même si une erreur a été levée précédemment, SAUF si
# cette erreur provient de la section ENV.

alt

Le fichier {exo}_REM.md contient des informations à afficher avec le code de la correction pour l'IDE en cours. Ces informations sont insérées dans une admonition repliée.

Le fichier {exo}_VIS_REM.md est quant à lui inséré après l'admonition repliée, ce qui fait que son contenu est directement visible lorsque la solution devient disponible pour l'utilisateur.

Rappel

Le contenu de ces fichiers REM doit être statique (pas d'appels de macros).

Équivalent aux sections env, code et tests.


Il doit contenir, dans cet ordre:

  • Une éventuelle section HDR (équivalent env) en l'insérant entre deux séparateurs comme décrit ci-dessous.
  • Le code utilisateur (équivalent code).
  • Le TestsToken (insensible à la casse ou aux espaces), si des tests publics sont donnés.
  • Le code des tests publics (équivalent tests).


Le séparateur pour la section HDR a la forme suivante :

# ------ HDR ------ #

Ce séparateur suit les mêmes règles que les séparateurs de sections du fichier python unique du thème :

  • Les espaces sont optionnels (quel que soit leur nombre).
  • Les # au début et à la fin sont obligatoires.
  • il faut au moins un trait d'union entre un # et le texte central.


Exemple de code python pour un fichier {exo}.py
# --- HDR --- #

def helper(x):
    """ For whatever you need """

# --- HDR --- #

def est_pair(n):
    ...

# Tests

assert est_pair(3) is False
assert est_pair(24) is True

Séparateur alternatif

Il est en fait possible d'utiliser ENV à la place de HDR dans le séparateur :

# ------ ENV ------ #

Équivalent à la section secrets.


Contient les tests secrets pour l'IDE.


Exemple de code python pour un fichier {exo}_test.py
def tests():
    for n in range(100):
        # Il est conseillé d'utiliser une fonction pour éviter que des
        # variables des tests se retrouvent dans l'environnement global.
        val = est_pair(n)
        exp = n%2 == 0

        msg = f"est_pair({n})"                           # Minimum vital
        msg = f"est_pair({n}): valeur renvoyée {val}"    # Conseillé
        msg = f"est_pair({n}): {val} devrait être {exp}" # La totale

        assert val == exp, msg

tests()         # Ne pas oublier d'appeler la fonction de tests... ! x)

Équivalent à la section corr.


Contient la correction pour l'IDE.


Exemple de code python pour un fichier {exo}_corr.py
def est_pair(n):
    return not n%2

alt

Le fichier {exo}_REM.md contient des informations à afficher avec le code de la correction pour l'IDE en cours. Ces informations sont insérées dans une admonition repliée.

Le fichier {exo}_VIS_REM.md est quant à lui inséré après l'admonition repliée, ce qui fait que son contenu est directement visible lorsque la solution devient disponible pour l'utilisateur.

Rappel

Le contenu de ces fichiers REM doit être statique (pas d'appels de macros).


Attention


(Rappels : secrets = {exo}_test.py, corr = {exo}_corr.py)

Organisation des fichiers#

Tous les fichiers annexes pour un IDE donné doivent être stockés au même endroit, mais pas obligatoirement dans le dossier du fichier markdown source (celui depuis lequel les macros IDE sont appelées).

Par ailleurs, la macro est insensible à la configuration des noms pages sur le site final/construit : vous pouvez nommer le fichier source comme bon vous semble (sujet.md, {exo}.md, index.md, ou autres...).


Voici trois façons différentes d'organiser les fichiers, qui sont compatibles avec le thème :

C'est l'organisation la plus simple.

  • Fichier source : docs/.../paire_ou_pas/index.md
  • Appel de macro : {{ IDE('exo') }}
...
├── paire_ou_pas
│   ├── index.md
│   ├── exo.py
│   ├── exo_REM.md
│   └── exo_VIS_REM.md
...
├── paire_ou_pas
│   ├── index.md
│   ├── exo.py
│   ├── exo_corr.py
│   ├── exo_test.py
│   ├── exo_REM.md
│   └── exo_VIS_REM.md

Le premier argument de la macro est en fait un chemin relatif depuis le répertoire du fichier markdown source.

Donc si la page contient un grand nombre d'IDEs, il peut être intéressant de placer les fichiers pour chaque IDE dans des sous-dossiers différents.

  • Fichier source : docs/.../prog_dynamique/index.md
  • Appels de macro : {{ IDE('partie1/exo') }} ... {{ IDE('partie2/exo') }}
...
├── prog_dynamique
│   ├── index.md
│   │   ├── partie1
│   │   │   ├── exo.py
│   │   │   ├── exo_REM.md
│   │   │   └── exo_VIS_REM.md
│   │   ├── partie2
│   │   │   ├── exo.py
│   │   │   ├── exo_REM.md
│   │   │   └── exo_VIS_REM.md
...
...
├── prog_dynamique
│   ├── index.md
│   │   ├── partie1
│   │   │   ├── exo.py
│   │   │   ├── exo_corr.py
│   │   │   ├── exo_test.py
│   │   │   ├── exo_REM.md
│   │   │   └── exo_VIS_REM.md
│   │   ├── partie2
│   │   │   ├── exo.py
│   │   │   ├── exo_corr.py
│   │   │   ├── exo_test.py
│   │   │   ├── exo_REM.md
│   │   │   └── exo_VIS_REM.md
...

Les deux cas précédents partent plutôt du principe que chaque fichier source dispose de son propre dossier.

On peut aussi envisager de travailler avec différents exercices (fichiers markdown source), tous contenus dans le même dossier, et leurs fichiers annexes dans des sous-dossiers, tous eux-mêmes contenus dans un sous-dossier scripts.

Concrètement, les appels de macros seraient de la forme :

- `{{ IDE('exo1') }}` dans le fichier `chapitre1.md`
- `{{ IDE('exo1') }}` dans le fichier `chapitre2.md`
docs
├── chapitre1.md     => corps du chapitre que vous souhaitez écrire
├── chapitre2.md
├── chapitre3.md
├── scripts
│   ├── chapitre1    => dossier contenant les exercices à intégrer à vos IDEs
│   │   ├── exo1.py
│   │   ├── exo1_REM.md
│   │   ├── exo1_VIS_REM.md
│   │   ├── exo2.py
│   │   ├── exo2_REM.md
│   │   ├── exo2_VIS_REM.md
│   │   ├── exo3.py
│   │   ├── exo3_REM.md
│   │   └── exo3_VIS_REM.md
│   ├── chapitre2
│   │   ├── exo1.py
│   │   ├── ...
...
docs
├── chapitre1.md     => corps du chapitre que vous souhaitez écrire
├── chapitre2.md
├── chapitre3.md
├── scripts
│   ├── chapitre1    => dossier contenant les exercices à intégrer à vos IDEs
│   │   ├── exo1.py
│   │   ├── exo1_corr.py
│   │   ├── exo1_test.py
│   │   ├── exo1_REM.md
│   │   ├── exo1_VIS_REM.md
│   │   ├── exo2.py
│   │   ├── exo2_corr.py
│   │   ├── exo2_test.py
│   │   ├── exo2_REM.md
│   │   ├── exo2_VIS_REM.md
│   │   ├── exo3.py
│   │   ├── exo3_corr.py
│   │   ├── exo3_test.py
│   │   ├── exo3_REM.md
│   │   └── exo3_VIS_REM.md
│   ├── chapitre2
│   │   ├── exo1.py
│   │   ├── ...
...

Mélanges de fichiers#

Si le fichier annexe principal {exo}.py est reconnu comme un fichier "thème" (donc avec des séparateurs de sections # --- PYODIDE:{section} --- #), son contenu est prioritaire sur d'éventuelles fichiers {exo}_corr.py ou {exo}_test.py. Ceux-ci seront tout bonnement ignorés s'ils existent.

Modifications vs localStorage#

Les sites construits via le thème enregistrent les codes utilisateurs dans le localStorage associé au site, afin que les utilisateurs puissent retrouver leurs modifications lorsqu'ils se reconnectent au site depuis le même couple machine/navigateur.

L'enregistrement de leur code est effectué dans les cas suivants :

  • Lorsqu'une validation ou les tests publics sont exécutés.
  • Lorsque l'enregistrement est explicitement demandé par l'utilisateur (via le bouton sous l'IDE).
  • Toutes les 30 frappes de caractères dans l'éditeur.


Cette sauvegarde peut parfois poser des problèmes lorsque l'auteur du site fait une mise à jour des tests publics ou du code proposé initialement à l'utilisateur (sections code et tests).

Afin que l'utilisateur ne puisse pas se retrouver avec un code qui ne correspond plus aux spécifications actuelles du problème sans le savoir, le thème garde une trace du contenu initial attendu pour un éditeur. S'il constate que la version référencée dans le localStorage est différente du code que l'utilisateur obtiendrait actuellement après un reset, un message est automatiquement affiché dans le terminal au chargement de la page, indiquant à l'utilisateur qu'il devrait copier ses modifications puis réinitialiser l'IDE.


Remarques

  • Le message n'apparaît que tant qu'aucune action utilisateur ne déclenche de nouvelle sauvegarde dans le localStorage.
  • Réinitialiser l'IDE (bouton reset) met également à jour le localStorage, mais avec la version initiale du code de l'éditeur.
  • Cette fonctionnalité a été introduite en version 2.5.0 du thème.
    Les informations nécessaires pour décider d'afficher ou non le message demandant de réinitialiser l'IDE n'existaient pas avant cela, et la transition se fait automatiquement, côté utilisateur.
    Il a par contre été décidé que lors de cette transition, le code actuellement stocké dans le localStorage serait considéré comme "à jour", afin de ne pas avoir tous les IDEs du site demandant à l'utilisateur de faire la mise à jour (95% d'entre eux étant probablement des faux positifs).

La macro IDE#

Voici les specifications des arguments des macros IDE ou IDEv.

Signature#

La signature complète pour un appel aux macros IDE ou IDEv est du type :

{{ IDE(
    py_name:   str = '',
    *,
    ID:   None|int = None,
    SANS:      str = '',
    WHITE:     str = '',
    REC_LIMIT: int = -1,
    MERMAID:  bool = False,
    AUTO_RUN: bool = False,
    MAX:   int|'+' = 5,
    LOGS:     bool = True,
    MODE: None|str = None,
    MIN_SIZE:  int = 3,
    MAX_SIZE:  int = 30,
    TERM_H:    int = 10,
    TEST:      str = '',
    TWO_COLS: bool = False,
    STD_KEY:   str = '',
    EXPORT:   bool = False,
) }}


L'astérisque seule dans IDE(..., *, ...) ?

Cette astérisque fait qu'il n'est pas possible d'utiliser les arguments autres que py_name sans renseigner le nom de l'argument en plus de la valeur. Tous les arguments suivant sont donc arguments nommés.

Modifier les valeurs par défaut des arguments

La configuration du plugin pyodide_macros comporte, outre les réglages du plugin lui-même, les valeurs par défaut pour la quasi totalité des macros proposées par le thème.

Ces réglages peuvent être modifiés de différentes façons pour affecter un fichier, un dossier ou une hiérarchie de dossiers :

  1. Via le fichier mkdocs.yml du thème, bien sûr (dans la section plugins.pyodide_macros).
  2. Via des fichiers .meta.pmt.yml, qui peuvent être placés n'importe où dans la hiérarchie de la documentation et qui affectent tous les fichiers "descendants".
  3. Via les métadonnées placées dans les entêtes des pages markdown elles-mêmes.

py_name#

Type Défaut Résumé
str '' Chemin relatif (sans l'extension du fichier) vers le fichier {exo}.py et les éventuels autres fichiers annexes, sur lesquels baser l'IDE.

Chemin relatif au dossier contenant le ficher markdown source permettant d'accéder au fichier python annexe principal pour l'IDE.

  • Si l'argument n'est pas renseigné ou est une chaîne vide, l'IDE sera créé vide (ex : bac à sable).
  • Le chemin ne donne que le préfixe commun des fichiers annexes pour cet IDE, et il faut donc omettre l'extension : si le fichier annexe principal est .../exercice.py, l'argument py_name doit être ".../exercice".
  • Une erreur est levée si un chemin est donné mais qu'aucun fichier python ne peut être trouvé pour les différentes organisations de fichiers supportées.

ID#

Type Défaut Résumé
None|int None À utiliser pour différencier deux IDEs utilisant les mêmes fichiers annexes, afin de différencier leurs sauvegardes (nota: \(ID \ge 0\)).

Pour pouvoir sauvegarder, télécharger et téléverser des codes dans ou depuis les IDEs, l'id html de chaque IDE doit être unique. Or, ceux-ci sont construits à partir de la localisation du fichier python principal sur le disque.

Le thème vérifie notamment l'unicité des id générés pour tout le site et lèverait une erreur si le même fichier python (argument py_name) est utilisé plusieurs fois dans différents IDEs.

Afin de pouvoir utiliser plusieurs fois les mêmes fichiers, l'argument ID peut être utilisé afin de différencier les IDEs.

Exemple :

Fichier 1: docs/index.md
     {{ IDE('exemples/ex1', ID=1)}} 

Fichier 2: docs/aide.md
     {{ IDE('exemples/ex1', ID=2)}} 

SANS#

Type Défaut Résumé
str '' Pour interdire des fonctions builtins, des modules, des accès d'attributs ou de méthodes, ou des mots clefs : chaîne de noms séparés par des virgules et/ou espaces. Les mots clefs viennent en dernier, après le séparateur AST:.

Voici le détail des syntaxes et contraintes sur l'utilisation de l'arguent SANS :

  • La chaîne de l'argument SANS doit contenir des identifiants, séparés par des espaces, des virgules ou des points-virgules.
  • Les restrictions d'attributs et méthodes doivent être préfixées d'un point.
  • Les mots clefs et opérateurs doivent être renseignés en dernier, après avoir ajouté le séparateur "AST:" : SANS="... AST: mot_clefs".

    Syntaxe Ce qui est interdit
    identifiant Interdit la fonction builtin correspondant, ou si aucune fonction n'est trouvée, interdit le module/package correspondant.
    Les imports sont couverts quelle que soit la méthode utilisée : import, from, aliasing, ...
    .method
    .attribut
    Interdit les accès d'attributs correspondant à ce nom lève une erreur s'ils sont trouvés dans le code.
    keyword ou operator Interdit des opérateurs et des mots clefs ou assimilés.
    Ceux-ci sont différenciés des identifiants de fonctions ou des modules en les renseignant après le séparateur AST:.

Exemple d'utilisation :

{{ IDE('exo', SANS="sorted, max ; .find .index AST: for") }}

Cet appel de macro interdit :

  • Les fonctions sorted et max.
  • Les appels de méthodes .find et .index.
  • Tous types de boucles for (boucles normales et comprehensions).


Informations plus détaillées disponibles dans la page détaillant les exécutions.


Sections/code appliquant les différents types de restrictions
Codes Méthodes
+
Mots clefs
Fonctions
+
Imports
Code de l'IDE
(code+tests)
Validations
(tests+secrets)
Commande terminal

Cela signifie que l'on ne peut normalement pas utiliser les éléments interdits dans les tests secrets pour vérifier les réponses de l'utilisateur (il existe en fait un moyen... Voir la section des utilisations avancées).

Liste des mots clefs et opérateurs supportés (avec AST:)

But recherché avec les restrictions

Le but de pyodide-mkdocs-theme n'est pas de construire des restrictions ultra strictes. En effet :

D'un point de vue pédagogique :

  • Le thème est conçu pour construire des sites permettant aux élèves de s'entraîner, pas pour les évaluer.

D'un point de vue technique :

  • Python étant ce qu'il est, il est toujours possible de contourner des restrictions, si on est suffisamment persistant et qu'on en sait suffisamment sur son fonctionnement.
  • L'intégralité du runtime étant côté client, même en verrouillant complètement la partie python, l'utilisateur peut toujours mettre en échec des restrictions en travaillant dans la couche javascript.
  • De toute façon, le contenu des corrections est visible au bout de quelques clics...

Ces restrictions, même si elles présentent déjà un certain niveau de fiabilité (au moins face à des élèves de lycée), sont donc plus à voir comme des outils pédagogiques permettant de s'assurer qu'un élève ne va pas utiliser telle ou telle chose par erreur. Encore une fois, une personne motivée et avec une certaine expérience de python finira par trouver comment contourner les restrictions.

Efficacité des interdictions

La logique des interdictions fonctionne bien du moment que le contexte d'exécution est restreint.
En l'occurrence, les restrictions appliquées à un IDE sont valables pour son éditeur et son terminal.

En revanche tout autre IDE ou terminal apparaissant dans la même page est un autre point d'entrée vers l'environnement pyodide qui, rappelons-le, est commun à toute la page.
En conséquence, si un IDE ou un terminal comporte des restrictions, tous les IDEs et terminaux isolés de la page doivent avoir les mêmes.

Les fichiers .meta.pmt.yml ou les métadonnées dans les entêtes des fichiers markdown peuvent être mis à profit pour régler ce type de paramètres pour un groupe de fichiers ou une page entière.

WHITE#

Type Défaut Résumé
str '' ("White list") Ensemble de noms de modules/packages à pré-importer avant que les interdictions ne soient mises en place (voir argument SANS. L'argument WHITE est normalement obsolète).
Cet argument est normalement obsolète.

Permet de déclarer un ensemble de modules/packages à préimporter avant que les restrictions d'imports ne soient mises en place.

La syntaxe est la même que celle de l'argument SANS: des identifiants séparés par des espaces, des virgules ou des points-virgules.

Exemple: {{ IDE(..., SANS="sys", WHITE="math") }}

Le problème que WHITE essaie de résoudre...

Certains modules de "bas niveau" sont utilisés pour importer d'autres modules, même dans des cas inattendus (ex: math qui importe sys). Si le module sys est interdit (ce qui est une très mauvaise idée !), il peut devenir nécessaire de préimporter d'autres modules que l'utilisateur pourrait avoir besoin d'utiliser dans son code.

Voir la page concernant les restrictions pour plus de détails.

WHITE n'a pas pour but de remplacer du code python !

Cet argument est un ersatz d'une version antérieure du thème.

Il a été conservé dans l'éventualité où un rédacteur se retrouverait tout de même confronté à un problème d'import interdit pour de mauvaises raisons, mais des changements intervenus plus tard dans le développement ont normalement rendu cet argument obsolète : réaliser les préimports depuis la section env devrait suffire à éviter ce type de problèmes.

Efficacité des interdictions

La logique des interdictions fonctionne bien du moment que le contexte d'exécution est restreint.
En l'occurrence, les restrictions appliquées à un IDE sont valables pour son éditeur et son terminal.

En revanche tout autre IDE ou terminal apparaissant dans la même page est un autre point d'entrée vers l'environnement pyodide qui, rappelons-le, est commun à toute la page.
En conséquence, si un IDE ou un terminal comporte des restrictions, tous les IDEs et terminaux isolés de la page doivent avoir les mêmes.

Les fichiers .meta.pmt.yml ou les métadonnées dans les entêtes des fichiers markdown peuvent être mis à profit pour régler ce type de paramètres pour un groupe de fichiers ou une page entière.

REC_LIMIT#

Type Défaut Résumé
int -1 Pour imposer une profondeur de récursion maximale. Nota: ne jamais descendre en-dessous de 20. La valeur par défaut, -1, signifie que l'argument n'est pas utilisé.

Si cet argument est utilisé avec une valeur positive, la profondeur de récursion sera limitée et une erreur sera levée si elle est atteinte durant les exécutions.

Limitations, particularités, conseils pour cet argument...
  • Ne pas oublier que ce réglage affecte la stack, qui est globale. Ce qui veut dire que le premier appel de fonction de l'utilisateur n'est pas à une profondeur 0 ou 1.

  • Pour cette raison, une erreur est levée par la macro si une valeur inférieure à 20 est utilisée pour REC_LIMIT, car l'environnement lui-même ne pourrait pas faire tourner le code de l'utilisateur et les tests.

  • Côté utilisateur, la fonction sys.setrecursionlimit est désactivée lorsque cette fonctionnalité est utilisée.

    • Le procédé utilisé est le même que pour les restrictions de code.
    • Ce réglage affecte donc également les différents types de tests et le terminal de l'IDE.
  • Ne surtout pas activer cette fonctionnalité si vous utilisez des structures de données récursives, avec des implantations pour __str__ ou __repr__ : l'utilisateur se retrouverait probablement avec un crash dû à la restriction à des moments inopportuns :

    • Sur un print (même si la sortie standard n'est pas affichée !)
    • Lors de la construction d'un message d'erreur affichant une structure de données.

MERMAID#

Type Défaut Résumé
bool False Signale qu'un rendu de graphe mermaid sera attendu à un moment ou un autre des exécutions.
Nota : l'extension markdown pymdownx.superfences doit être configurée pour accepter les blocs de code mermaid. Voir la configuration par défaut du mkdocs.yml via les scripts du thème, par exemple avec : python -m pyodide_mkdocs_theme --yml.

Le rôle de cet argument est un peu particulier : son but est de signaler au thème que cette page devra intégrer la logistique javascript et pyodide pour construire dynamiquement des graphes mermaid dans la page.

Ce "signalement" est en fait global à la page entière de la documentation, et il n'est donc pas nécessaire de l'utiliser pour chaque IDE d'une page : une seule fois par page est suffisant.

Voir la page dédiée à l'utilisation dynamique de mermaid dans pyodide pour plus d'informations.

AUTO_RUN#

Type Défaut Résumé
bool False Lance automatiquement le code après avoir affiché la page (lance uniquement les tests publics).

LOGS#

Type Défaut Résumé
bool True Durant des tests de validation, si LOGS est True, le code complet d'une assertion est utilisé comme message d'erreur, quand l'assertion a été écrite sans message.

Lors d'une assertion échouée durant une validation, si le code de l'assertion n'a aucun message d'erreur, le thème peut en construire un automatiquement. Selon le type d'exercices que vous rédigez, ou si l'exercice provient d'un ancien site utilisant pyodide-mkdocs vous pourriez souhaiter que les messages d'erreur automatiques soient construits ou non, pour ces assertions sans messages.


Migration depuis pyodide-mkdocs : BREAKING CHANGE

pyodide-mkdocs pratiquait une sorte d'introspection du code pour reconstruire un message en injectant automatiquement des informations/bouts de codes provenant de différents endroits. Il reconstruisait notamment les valeurs utilisées pour les arguments.

La logique employée posait de nombreux problèmes et le comportement n'était pas modulable, ce qui rendait les tests secrets "pas très secrets". Au final, il a été décidé de supprimer cette fonctionnalité et d'en faire une version plus robuste, mais aussi beaucoup plus simple.

Par défaut, le thème va reconstruire des messages mais ils seront probablement insuffisants pour donner un feedback digne de ce nom aux utilisateurs, si le code n'a pas été rédigé en prévision de cela.
Concrètement :

assert user_func(42) == [
    [0,1,2],
    [1,2,3],
    [3,4,5],
]

Si cette assertion échoue, le code entier de l'assertion sera ajoutée au message :

...
AssertionError: assert user_func(42) == [
    [0,1,2],
    [1,2,3],
    [3,4,5],
]
>>>
actual = user_func(42)
expected = [
    [0,1,2],
    [1,2,3],
    [3,4,5],
]
assert actual == expected

Si cette assertion échoue, la même chose se produit, mais cette fois le message ne contient aucune information utile pour l'utilisateur !

...
AssertionError: assert actual == expected
>>>

Notez que ce changement de comportement par rapport à pyodide-mkdocs concerne aussi les tests publics.

MAX#

Type Défaut Résumé
int|'+' 5 Nombre maximal d'essais de validation avant de rendre la correction et/ou les remarques disponibles.
  • En l'absence de correction et de fichiers de remarques, le compteur d'essais sera automatiquement passé à \(\infty/\infty\).
  • Il est possible d'imposer un nombre d'essais infini en passant 1000 ou "+" en argument.
    Ceci impliquerait que la seule façon pour l'utilisateur de voir la solution et/ou les remarques serait de passer tous les tests avec succès.
Erreur levée pour les contenus corr/REM avec des compteurs d'essais à l'\(\infty\)

Par défaut, le thème considère que du contenu corr ou des remarques (visibles ou non) qui seraient "cachés" par un compteur d'essais réglé à l'\(\infty\) est une situation non désirée. Une erreur est donc levée durant le build si la situation est rencontrée.

Si c'est effectivement le but recherché, il faut alors modifier l'option ides.forbid_corr_and_REMs_with_infinite_attempts du plugin, soit via le fichier mkdocs.yml, soit via la configuration des métadonnées (fichiers .meta.pmt.yml ou entêtes de pages) :

plugins:
    pyodide_macros:
        ides:
            forbid_corr_and_REMs_with_infinite_attempts: false  # (défaut : true)

MODE#

Type Défaut Résumé
None|str None Change le mode d'exécution des codes python. Les modes disponibles sont :
  • None : exécutions normales.
  • 'delayed_reveal' : pour des IDEs n'ayant pas de tests (pas de section tests ni secrets) mais dont on ne veut pas que la solution s'affiche dès la première exécution (typiquement, des exercices turtle ou p5). Chaque validation fait décroître le nombre d'essais et les solutions et remarques, si elles existent, sont révélées une fois tous les essais consommés (une erreur est levée durant le build, si l'IDE a des sections tests ou secrets, ou s'il a un nombre d'essais infini).
  • 'no_reveal' : exécutions normales, mais les solutions et remarques, si elles existent, ne sont jamais révélées, même en cas de succès. Le compteur d'essais est \(\infty\).
  • 'no_valid' : quels que soient les fichiers/sections disponibles, le bouton et les raccourcis de validations sont inactifs. Le compteur d'essais est \(\infty\).
  • 'revealed' : les solutions et remarques, si elles existent, sont révélées dès le chargement de la page. Le compteur d'essais est \(\infty\).

Une erreur est levée si une valeur est passée en argument alors qu'elle ne correspond à aucun MODE existant.

L'utilisation de l'argument MODEsupprime les routines de validation des données des IDEs

Si les profiles sont utilisés, toutes les vérifications faites habituellement par le thème lorsqu'il construit l'IDE sont supprimées.

Ceci concerne notamment les vérifications liées aux options suivantes du plugin :

MAX_SIZE#

Type Défaut Résumé
int 30 Impose la hauteur maximale possible pour un éditeur, en nombres de lignes.

Permet de définir le nombre maximal de lignes de la fenêtre de l'éditeur : si le code comporte plus de lignes, des glissières apparaîtront.

MIN_SIZE#

Type Défaut Résumé
int 3 Nombre de lignes minimal de l'éditeur.

Permet de définir le nombre minimal de lignes de la fenêtre de l'éditeur.

TERM_H#

Type Défaut Résumé
int 10 Nombre de lignes initiales utilisées pour la hauteur du terminal (approximatif).

Remarques :

  • Le réglage n'est pas très précis et peut devenir erroné selon les règles CSS surchargées par vos soins.
  • Cet argument est ignoré pour les macros IDEv.

TEST#

Type Défaut Résumé
str '' Définit la façon dont l'IDE doit être géré lors des tests dans la page générée automatiquement pour tester tous les IDEs de la documentation.
  • Depuis un fichier de configuration, un fichier.meta.pmt.yml ou l'entête d'une page markdown : "", "skip", "fail", "code", "human" et "no_clear".
  • Depuis un appel de macro: les mêmes, ou bien utiliser un object Case, défini dans l'environnement, pour plus de possibilités.

Voir la page dédiée pour plus d'information sur les tests automatiques des IDEs de la documentation.

TWO_COLS#

Type Défaut Résumé
bool False Si True, cet IDE passe automatiquement en mode "deux colonnes" au chargement de la page.

Comme tous les autres arguments de macros, il peut être défini au niveau des meta (fichiers, entêtes, mkdocs.yml).
À noter que si plusieurs IDEs ont ce réglage à True dans la même page, il n'y a aucune garantie sur celui qui sera effectivement en mode "deux colonnes" après chargement de la page.

STD_KEY#

Type Défaut Résumé
str '' Clef à passer en argument de terminal_message pour autoriser son utilisation lorsque la sortie standard est désactivée pendant les tests.

Lors de la construction du site, une erreur est levée si un IDE est rencontré avec toutes ces conditions réunies :

  • Des tests de validation
  • La sortie standard est désactivée durant les validations
  • Un argument STD_KEY qui est "falsy"
  • La chaîne "terminal_message(" est trouvée dans l'une des sections tests ou secrets.

Ne pas utiliser terminal_message dans les sections tests

Il est à noter que la fonction terminal_message ne devrait normalement jamais être utilisée dans la section tests, car son contenu apparaît dans l'éditeur et est également utilisé dans les tests de validation. Or, pour pouvoir l'utiliser lors d'une validation, la bonne valeur pour l'argument @key doit être utilisée, ce qui veut dire que cette valeur apparaîtra aussi dans l'éditeur...

EXPORT#

Type Défaut Résumé
bool False Définis si le contenu de l'éditeur de cet IDE doit être ajouté à l'archive zip récupérant les codes de tous les IDEs de la page.

Les IDEs marqués avec EXPORT=True se voient ajouté un bouton zip permettant de télécharger une archive .zip avec les contenus de tous les éditeurs marqués dans la page en cours.

Le but de cette fonctionnalité est multiple :

  • Permettre aux utilisateurs de télécharger en une fois tous les contenus des éditeurs (marqués) de la page, pour garder une trace de leurs codes.
  • Générer en un clic une archive que l'enseignant peut ensuite récupérer (voir plus bas pour ce qui concerne les noms de fichiers donnés aux archives)
  • Il est en fait possible de charger dans la page du site le contenu d'un fichier zip en faisant un glissé-déposé de l'archive sur le bouton de création du fichier zip de l'un des IDEs marqués de la page. Cela permet de tester rapidement les codes d'un élève ou groupe d'élèves. Cette fonctionnalité n'est pas décrite dans la documentation des utilisateurs.

Gestion des noms de fichiers des archives zip :

  • Par défaut, le nom de l'archive zip est créé à partir de l'adresse de la page sur le site construit (en excluant la racine du site).
  • Il est possible à l'auteur d'ajouter un préfixe de son choix aux noms des archives, en renseignant l'option ides.export_zip_prefix, dans les métadonnées de la page.
  • Si l'enseignant envisage de récupérer les archives zip des élèves, il est également possible de forcer les utilisateurs à renseigner leur nom au moment de créer l'archive zip (ou tout autre chose pouvant servir d'identifiant). Pour cela configurer dans les métadonnées l'option ides.export_zip_with_names: true.

Le nom complet des archives est généré selon le modèle suivant, selon les éléments activés via les options de configuration : PREFIX-NAMES-DEFAULT.

Écrire des tests #

La façon de rédiger les tests peut drastiquement changer le confort de l'utilisateur, lorsqu'il essaie de résoudre un problème.

En particulier, si la sortie standard est désactivée durant les tests secrets, il est critique que les assertions donnent bien le niveau d'information souhaité par le rédacteur, sans quoi l'utilisateur peut se retrouver sans aucune information utile, et donc très peu de chances de trouver comment modifier son code.

Stratégies #

De manière générale, les "stratégies" suivantes sont conseillées :

  1. Mettre tous les cas particuliers dans les tests publics.

  2. Mettre suffisamment de tests publics (section tests) pour couvrir au moins une fois tous les types de cas présents dans les tests de la section secrets.

  3. Les assertions sans message d'erreur sont à proscrire dans les tests secrets.
    Si vous comptez utiliser la fonctionnalité de génération automatique des messages d'erreurs des macros IDE (voir l'argument LOGS et sa configuration globale), il faut à minima que les valeurs des arguments soient définies dans le code de chaque assertion.

  4. On peut adapter le niveau de feedback des messages d'erreur selon le but recherché et le contexte de l'exercice.
    Typiquement :

    • Un message donnant uniquement le ou les arguments peut être considéré comme le strict minimum acceptable.

      assert func(arg) == attendu, f"func({arg})"
      
    • Un message donnant en plus la réponse de l'utilisateur est un confort ajouté certain. Cela ne change rien au niveau d'information accessible à l'utilisateur, mais cela lui évite d'avoir à créer lui-même le test public correspondant pour savoir ce que sa fonction a renvoyé.

      val = func(arg)
      assert val == attendu, f"func({arg}): a renvoyé {val}"
      
    • Un message présentant en plus la réponse attendue est la "Rolls" du message d'assertion (mais n'est pas toujours souhaitable pour des tests "secrets").

      val = func(arg)
      assert val == attendu, f"func({arg}): {val} devrait être {attendu}"
      
    Ne pas appeler plusieurs fois la fonction de l'utilisateur pour un même test

    Il est vivement déconseillé d'appeler plusieurs fois la fonction de l'utilisateur pour le même test. Cela peut rendre le code plus difficile à déboguer.

    assert func(arg) == attendu, f"func({arg}): {func(arg)} devrait être {attendu}"
    
    val = func(arg)
    assert val == attendu, f"func({arg}): {val} devrait être {attendu}"
    
  5. Si des tests aléatoires sont implantés, il est INDISPENSABLE de leur associer un niveau de feedback élevé (voir ci-dessus) :

    Gardez en tête que des tests aléatoires vont générer des cas particuliers pour les fonctions alambiquées de vos utilisateurs, que vous n'avez aucune chance de prévoir dans les tests publics. Les insuffisances du feedback peuvent alors transformer l'exercice que vous aurez passé des heures à peaufiner en véritable cauchemar pour vos élèves, ce qui serait tout de même dommage...

Aspects techniques #

Problématique :

Gardez en mémoire que l'environnement Pyodide est commun à toute la page, ou jusqu'à un éventuel rechargement de celle-ci. Cela implique que de nombreux effets de bords peuvent se présenter, dont les utilisateurs n'auront pas conscience.

En particulier toute variable ou fonction définie dans les tests est visible depuis la fonction de l'utilisateur. Ceci signifie que l'environnement de la seconde exécution n'est souvent pas le même que celui de la première exécution des tests.

Concrètement, cela peut amener à des erreurs très dures à tracer côté utilisateur, car les comportements ne sont pas toujours reproductibles. On a par exemple vu des codes lever ou pas NameError, selon l'ordre d'utilisation des boutons de l'IDE après un chargement de page...


Solution :

  • Il est vivement conseillé d'écrire les tests dans une fonction afin que leur contenu ne puisse pas "leaker" dans l'environnement de l'utilisateur.
  • Ne pas oublier d'appeler la fonction des tests après l'avoir écrite... 😉
  • Comme les fonctions restent définies dans l'environnement global, après avoir lancé une première validation, un utilisateur malin pourrait exécuter des fonctions définies dans la section secrets (1)
    1. Le nom des fonctions est visible dans les stacktraces des erreurs, ou encore, accessible via dir() dans le terminal.
    en ajoutant les appels aux tests publics dans l'éditeur de l'IDE. Ce qui permettrait de contourner le compteur d'essais.
    Dans le contexte du thème, cela n'est sans doute pas un problème (il n'y a rien à gagner, après tout... !), mais si vous voulez l'empêcher, il vous faudra supprimer la fonction de l'environnement en utilisant del comme montré dans le second exemple ci-dessous.

Le décorateur @auto_run proposé par le thème facilite tout ceci.


De bons tests secrets...
def tests():
    for n in range(100):
        # Il est conseillé d'utiliser une fonction pour éviter que des
        # variables des tests se retrouvent dans l'environnement global.
        val = est_pair(n)
        exp = n%2 == 0

        msg = f"est_pair({n})"                           # Minimum vital
        msg = f"est_pair({n}): valeur renvoyée {val}"    # Conseillé
        msg = f"est_pair({n}): {val} devrait être {exp}" # La totale

        assert val == exp, msg

tests()         # Ne pas oublier d'appeler la fonction de tests... ! x)

Voir l'outil @auto_run, plus bas dans la page

Le thème met à disposition un décorateur faisant exactement tout cela, et gérant également le nettoyage régulier de l'environnement.

Effacer les fonctions de tests peut devenir assez fastidieux, notamment de par le fait que si une erreur est levée, le code suivant la fonction de test n'est pas exécuté, sauf à utiliser des blocs try/except ou assimilés...

Il existe une solution très rapide qui permet :

  1. D'être certain de ne pas oublier d'appeler les fonctions de tests.
  2. D'être certain que la fonction de tests ne sera pas disponible du côté de l'utilisateur, et ce quelle que soit le déroulement des tests (succès/erreur).
  3. Ne fournit aucun élément exploitable à l'utilisateur pour investiguer le contenu des tests.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
test = lambda f: f()

@test
def passing():
    print('passed')        # Simule des tests réussis

print(passing)

@test
def failing():
    assert False, 'oups...'

# Entrer `failing` dans le terminal donne NameError

test repose sur la syntaxe des décorateurs, mais n'en est en fait pas vraiment un : test appelle directement la fonction f passée en argument, au lieu de renvoyer une fonction comme un décorateur normal.

Ceci fait que toute fonction décorée avec test est une fonction de test exécutée.


Par ailleurs, la variable du scope global correspondant au nom d'une fonction décorée se voit en fait associée ce que l'appel au décorateur renvoie. Ici, test renvoie le résultat de f(), qui est la fonction décorée, mais les fonctions de tests ne renvoient rien, et test renvoie donc None, si aucune erreur n'est levée.

Une fonction décorée avec test n'est donc en fait jamais assignée à la variable correspondant dans le scope globale, et ne sera donc jamais accessible à l'utilisateur.


résultat

Après les exécutions :

  • test est définie et accessible à l'utilisateur, mais n'a strictement aucun intérêt pour lui.
  • passing est également définie, mais est en fait None au lieu d'être la fonction passing.
  • failing n'est pas définie car la fonction a levé une erreur avant que le décorateur n'affecte la variable dans le scope global.

Il est possible de constater tout cela avec l'IDE ci-dessous :

###(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


Vous pouvez appliquer la même stratégie aux tests publics, mais cela surcharge notablement le code et n'est donc probablement pas souhaitable.
D'autant plus que pour ces tests, si les assertions sont écrites avec toutes les données "en dur", la construction automatique des messages d'erreur donne presque tout le feedback nécessaire à l'utilisateur, tout en faisant gagner du temps au rédacteur (le seul élément non affiché étant alors la valeur renvoyée par la fonction de l'utilisateur).

## Suffisant pour des tests publics, mais pas forcément pour des tests secrets !
assert est_pair(3) is False
assert est_pair(24) is True
...

Outil auto_run#

À partir de la version 2.0, le thème propose un décorateur qui permet d'exécuter automatiquement la fonction qu'il décore, et empêche de la réutiliser par la suite.

Ceci est particulièrement utile pour écrire des fonctions devant exécuter certaines tâches en gardant leur contenu isolé de l'environnement global, comme des fonctions de tests pour les sections secrets des IDEs, par exemple.


@auto_run
def test():
    """ tests dans un scope isolé """
    assert func() == 42
  • Le code executant la fonction (le décorateur) est écrit au plus proche de la déclaration de fonction, ce qui évite d'oublier l'appel de fonction dans le code, quand la fonction devient très longue.
  • La façon de fonctionner du décorateur fait que la fonction décorée n'est en fait jamais affectée dans l'environnement, et l'utilisateur ne pourra pas y accéder lors d'une exécution ultérieure.
def test():
    """ tests dans un scope isolé """
    assert func() == 42

try:
    test()
finally:
    test=None
  • Obtenir le même comportement sans le décorateur est très laborieux, car il faut garantir l'effacement de la fonction, si test() lève une erreur.
  • Si le code de la fonction est long, on ne voit pas de suite si la fonction est bien appelée ou pas.


Spécifications :

  • Le décorateur exécute automatiquement la fonction décorée.
  • Le décorateur renvoie None, donc la fonction d'origine n'est jamais disponible dans le scope de l'utilisateur après exécution.
  • Le décorateur est redéfini à chaque exécution lancée par l'utilisateur, de manière à fiabiliser son utilisation.
  • Une fonction func décorée va laisser une variable func=None dans l'environnement. Le thème supprime automatiquement ces variables, à différents moments des exécutions :

    • juste avant d'exécuter le code ou la commande de l'utilisateur,
    • à la toute fin d'une exécution, après post_term et post.
  • Il est possible de déclencher manuellement le nettoyage de l'environnement en appelant la méthode auto_run.clean(), si besoin.


Il est déconseillé d'utiliser le décorateur dans les tests publics

L'utilisateur n'a pas besoin de connaître l'existence du décorateur : il pourrait sinon interagir avec lui et potentiellement changer le comportement des exécutions, s'il sait comment faire.



Voici une mise en évidence du comportement des fonctions décorées, et des variables présentes dans l'environnement après exécutions :

Contenu de auto_run_ex.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# --- PYODIDE:env --- #
def show(section):
    print(f'Dans { section } :')
    for s in ('passing', 'failing'):
        print(s, globals().get(s, 'non définie'))

# --- PYODIDE:code --- #
show('code')

def user_func():
    raise KeyError()

# --- PYODIDE:secrets --- #
@auto_run
def passing():
    pass            # Simule succès

@auto_run
def failing():
    user_func()     # Lève une erreur

# --- PYODIDE:post --- #
show('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

Feedback : terminal_message#

Si l'option ides.deactivate_stdout_for_secrets est activée, la fonction print est désactivée durant les tests de validation.
Si cela permet de sécuriser un peu le contenu de la section secrets, cela présente un énorme désavantage : il devient impossible de donner un quelconque feedback à l'utilisateur, ni même de mettre en évidence un découpage des tests en différentes partie, s'ils sont nombreux.

À partir de la version 2.5.0, le thème met à disposition dans l'environnement pyodide une fonction terminal_message, qui permet d'afficher du contenu directement dans le terminal, potentiellement formaté, et donc sans passer par la sortie standard de python ni être impacté par la désactivation de la sortie standard.

Signature#

def terminal_message(key, msg:str, format:str="none", new_line=True):
    """
    Affiche @msg directement dans le terminal de l'IDE en cours, sans passer par la sortie
    standard de python/pyodide.
    Le message est formaté avec le réglage désigné par `@format`.

    @key:      Valeur autorisant l'utilisation de la fonction, lorsque la sortie standard est
               désactivée. La valeur attendue est définie par l'argument `STD_KEY` de la macro IDE.
    @msg:      Message à afficher. Potentiellement multilignes.
    @format:   Nom du formatage à appliqué au message.
    @new_line: Si False, ne rajoute pas `\n` à la fin de @msg.
    @throws:  `ValueError` si la sortie standard est désactivée et @key n'est pas la valeur attendue
    @returns:  None
    """

Contrats & comportements#

  1. L'argument @key

    Cet argument, bien qu'ennuyeux à l'usage, est nécessaire pour empêcher un utilisateur de se servir de la fonction terminal_message en lieu et place de print lorsque la sortie standard est désactivée.

    Il n'est en fait utilisé que lors des validations, et à condition que la sortie standard soit désactivée (option ides.deactivate_stdout_for_secrets: true). Dans les autres cas, il est possible d'utiliser n'importe quelle valeur, et la fonction terminal_message se comporte alors plus ou moins comme print.


  2. Définition de la valeur cible pour @key

    C'est l'argument STD_KEY des macros IDE qui permet de choisir quelle sera la valeur autorisant l'utilisation de la fonction terminal_message. Comme toutes les valeurs passées via les macros, cet argument peut également être défini via les fichiers de métadonnées ou les entêtes de pages markdown.


  3. Validation de l'argument {{ IDE(..., STD_KEY=...)}}

    Lors de la construction du site, une erreur est levée si un IDE est rencontré avec toutes ces conditions réunies :

    • Des tests de validation
    • La sortie standard est désactivée durant les validations
    • Un argument STD_KEY qui est "falsy"
    • La chaîne "terminal_message(" est trouvée dans l'une des sections tests ou secrets.

    Ne pas utiliser terminal_message dans les sections tests

    Il est à noter que la fonction terminal_message ne devrait normalement jamais être utilisée dans la section tests, car son contenu apparaît dans l'éditeur et est également utilisé dans les tests de validation. Or, pour pouvoir l'utiliser lors d'une validation, la bonne valeur pour l'argument @key doit être utilisée, ce qui veut dire que cette valeur apparaîtra aussi dans l'éditeur...


  4. L'argument @format

    Voici les différentes options de formatage disponibles.

    Remarques

    • La couleur par défaut des terminaux est celle définie par la palette de couleur du site (dans le fichier mkdocs.yml:theme.palette).
    • Les couleurs utilisées dans les exemples ci-contre ne sont pas exactement les mêmes que celles utilisées dans les terminaux, mais permettent d'avoir une bonne idée du rendu qui sera obtenu.
    Nom Rendu
    "error" Rouge + gras
    "info" Gris + italique
    "italic" Italique
    "none" Défaut
    "stress" Gras
    "success" Vert + gras + italique
    "warning" Orange + gras + italique

Tester l'utilisation de print#

Il est possible d'intercepter facilement le contenu de la sortie standard, pour tester l'utilisation de la fonction print par l'utilisateur.

Durant les exécutions, sys.stdout est en fait une instance de io.String, qui récupère tous les contenus affichés dans la console pour la section en cours. Cet objet n'est cependant pas utilisé par le thème pour afficher les messages dans les terminaux, et un rédacteur peut donc l'utiliser à sa guise pour réaliser des tests.

Accéder au contenu de la sortie standard
import sys

txt = sys.stdout.getvalue()

Le code ci-dessus renvoie l'intégralité de tout ce qui a été transmis à la sortie standard via des appels à la fonction print, depuis le début de la section en cours d'exécution.

Comme cet objet n'est en fait pas utilisé par le thème, il est possible de le modifier pour tester plus facilement les contenus transmis à la sortie standard :

Vider la sortie standard avant le prochain appel à print
sys.stdout.truncate(0)
sys.stdout.seek(0)

Le contenu de cet objet est également mis à jour lors de l'utilisation de print alors que la sortie standard dans le terminal est désactivée, ce qui permet de réaliser des tests même lors des validations.


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

Contenu de stdout_capture.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ---PYODIDE:code --- #
import sys

print('abcd')
print(42)
txt = sys.stdout.getvalue()
print('---')
print(repr(txt))

sys.stdout.truncate(0)
sys.stdout.seek(0)
print('reset?')
txt = sys.stdout.getvalue()
print('---')
print(repr(txt))


# ---PYODIDE:secrets --- #

print('rien dans le terminal')
print('mais visible dans sys.stdout')

assert "visible dans sys.stdout" in sys.stdout.getvalue()

Tester la correction et les tests #

Tester la correction

  • Durant le développement en local, via mkdocs serve, les IDEs disposant d'un contenu pour la section corr se voient automatiquement ajoutés un nouveau bouton qui permet d'exécuter une validation, mais avec le contenu de la correction en lieu et place du contenu de l'éditeur de code. Cela permet de :

    • Tester le code de la correction, dans le même contexte que le code de l'utilisateur.
    • Tester aussi le bon fonctionnement des tests publics et secrets, puisque les tests des sections tests et secrets sont tous deux exécutés lors des validations (voir les informations sur le déroulement des exécutions).

    Ce bouton n'est jamais présent dans le site construit avec la commande mkdocs build.


Tester tous les IDEs de la documentation

  • À partir de la version 2.3.0, le thème propose de générer automatiquement une page pour tester tous les IDEs du site, permettant notamment de vérifier que le code présent dans la section corr d'un IDE passe bien les tests de validations.
    Cette fonctionnalité vient avec diverses options permettant d'affiner la façon de tester un IDE (ne pas faire le test, le faire mais considérer une erreur comme un succès, ...).

    Ceci permet de tester les codes dans le même environnement que l'utilisateur, de manière semi-automatisée.



Tester la correction

Voir les contenus corr & REMs #

De manière similaire, et toujours durant le développement en local, via mkdocs serve, les IDEs disposant d'un contenu corr ou de fichiers {exo}_REM.md ou {exo}_VIS_REM.md se voient automatiquement ajoutés un autre bouton permettant de révéler le contenu caché sans exécuter les tests.

Ce bouton n'est jamais présent dans le site construit avec la commande mkdocs build.