Aller au contenu

Exécutions / Runtime#

Cette page contient des informations supplémentaires concernant la gestion des IDEs ou des terminaux durant le "runtime", ainsi que ce qui touche aux restrictions (de près ou de loin).

La structure et l'agencement des fichiers pythons annexes eux-mêmes sont discutés ici pour les IDEs et là pour les terminaux.

Pour des gestions de fichiers plus complexes (bibliothèques, extraction de fichiers distants, ...) voir la page dédiée.

Spécificités liées à Pyodide#

Sont décrites ici quelques différences observables entre le fonctionnement original d'un moteur Python et l'environnement Pyodide tel que disponible via Pyodide-Mkdocs-Theme.

Généralités sur les remplacements de builtins#

Certaines fonctions builtins sont remplacées à la volée durant les exécutions, pour faciliter l'articulation avec la partie JavaScript/le DOM du site construit.

Les fonctions builtins remplacées utilisent des objets "wrappers", afin de les ramener au plus proche des comportements des objets d'origine :

  • Appeler les fonctions str ou repr avec ces objets renverra le résultat de la fonction d'origine.
  • La fonction help affichera le docstring d'origine.
  • Il est cependant possible d'identifier les fonctions remplacées en demandant leur type.


Concrètement :

>>> help(input)
Python Library Documentation: built-in function input in module builtins

input(prompt='', /)
    Read a string from standard input.  The trailing newline is stripped.

    The prompt string, if given, is printed to standard output without a
    trailing newline before reading input.

    If the user hits EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return), raise EOFError.
    On *nix systems, readline is used if available.

>>> input
<built-in function input>
>>> type(input)
<class '__main__.BuiltinWrapperInput'>

print#

La fonction print disponible dans l'environnement utilise différents objets pour gérer le feedback donné à l'utilisateur via les terminaux.

Concernant l'objet sys.stdout, il est à noter que:

  • Il est renouvelé à chaque exécution de section.
  • Il est de type io.String, ce qui permet d'en récupérer le contenu après affichage dans le terminal, via sys.stdout.getvalue()
  • Cet objet n'est en fait pas utilisé par le thème lui-même et peut donc être muté par le rédacteur à volonté, notamment pour faciliter des tests vérifiant le contenu de la sortie standard en le vidant juste avant un test (explications plus détaillées ici).

input#

La fonction input est remplacée de manière à ouvrir une fenêtre window.prompt dans le navigateur.
Le message passé à la fonction input ainsi que la réponse de l'utilisateur sont ensuite affichés dans le terminal (même comportement que la fonction originale dans un terminal python).


L'affichage de ces messages dans le terminal n'est cependant visible qu'une fois le code de la section en cours exécuté, ce qui peut s'avérer ennuyeux si l'utilisateur doit faire plusieurs choix successifs (typiquement: deviner un nombre par dichotomie, pierre-feuille-ciseaux, ...).
Pour palier à cela, la fonction mise à disposition par le thème possède un second argument optionnel, non documenté (au sens de la fonction help) :

input(question:str="", beginning:str="") -> str

Cet argument permet d'ajouter du texte au début de la fenêtre de prompt, sans en afficher le contenu dans le terminal.

Un exemple valant mieux qu'un long discours...

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

help#

La fonction builtin ne marche pas de manière simple dans l'environnement de Pyodide car c'est une fonctionnalité interactive. Elle a donc été remplacée par une fonction affichant directement la totalité du docstring de l'argument dans le terminal (via la fonction print).

Exécutions des codes python#

Déroulements#

  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 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.

Connaître le type d'exécution en cours ?#

Il y a différentes façons de savoir depuis la couche python/pyodide quel est le type d'exécution qui a déclenché le code.


Le plus simple, pour différencier une exécution via le terminal d'une via l'éditeur de code, est de regarder laquelle des variables globales __USER_CODE__ ou __USER_CMD__ n'est pas vide (voir ci-dessous).
Ceci en supposant évidemment que l'utilisateur n'exécute pas une ligne de commande vide ou un éditeur vide...


S'il est nécessaire d'avoir un niveau d'information plus fin ou plus fiable, il est également possible d'accéder au profil des exécutions telles que vues dans la couche JS en procédant comme suit :

Savoir quel type d'exécution est en cours depuis la couche python

# --- PYODIDE:env --- #

@auto_run
def _hide_all_this():
    import js

    currently_running = js.config().running
    if "Validate" in currently_running:
        do_something()
    else:
        do_something_else()

Toujours utiliser "..." in running pour l'identification !

Il y a différentes façons de lancer les mêmes types d'opérations, notamment lors de l'utilisation de la page des tests de tous les IDEs du site.

Les noms des "profiles d'exécution" sont des compositions des chaînes de caractères appropriées, donc une validation peut par exemple être identifiée par :

  • "Validate" : validation normale, par action de l'utilisateur.
  • "ValidateCorr" : validation avec le bouton de test de la correction, lors d'un mkdocs serve.
  • "TestingValidate" : validation lancée depuis la page des tests des IDEs.
  • ...

Les valeurs pouvant être utiles sont les suivantes :

running Déclenché par...
"Command" ...une commande tapée dans un terminal
"Play" ...le bouton play d'un IDE
"Validate" ...le bouton check d'un IDE
"PyBtn" ...un py_btn ou la macro run


De manière similaire, il est également possible d'accéder à l'id html de l'IDE (ou terminal, py_btn, ...) en cours d'exécution, avec js.config().runningId.
Concernant les IDE, cet id est aussi l'identifiant de cet IDE dans le localStorage du navigateur.

Extraction codes et commandes utilisateur#

Il est possible d'accéder depuis le code python au code de l'utilisateur au moment où les exécutions sont lancées (contenu de l'éditeur quand il existe, commande exécutée depuis un terminal).

  • Le code de l'éditeur est contenu dans une variable cachée nommée __USER_CODE__ et est accessible depuis n'importe quelle étape des exécutions. S'il n'y a pas d'éditeur associé (ex: terminal isolé), la variable contient une chaîne vide.
  • Si les exécutions sont lancées depuis un terminal, la commande utilisée est stockée dans une variable cachée nommée __USER_CMD__. Lorsque le terminal n'est pas utilisé, la variable contient une chaîne vide.

Ces variables peuvent être utilisées pour faire des vérifications additionnelles, en plus des fonctionnalités proposées avec les restrictions classiques (voir argument {{ IDE(... SANS=...) }}).


À titre d'exemple, voici une utilisation de __USER_CMD__, pour empêcher l'utilisateur d'exécuter des commandes dans le terminal :

Contenu de user_cmd.py

1
2
3
4
5
6
7
8
# --- PYODIDE:env--- #
print("`env` says: 'this is not my responsibility...'")

# --- PYODIDE:env_term --- #
assert not __USER_CMD__, "Ne pas utiliser le terminal..."

# --- PYODIDE:code --- #
# Taper qqc dans le terminal...

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

Le but est cette fois d'avoir un fichier python qui puisse tourner aussi en local, en dehors de l'environnement de pyodide (à supposer que le reste du fichier le permette également).

Contenu de user_code.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# --- PYODIDE:env --- #
def env_test():
    triche = "joujous bijous caillous chous pous genous".split()

    for x in triche:
        assert x not in __USER_CODE__, "~~Code~~ Français non conforme"

try:
    __USER_CODE__
except:
    pass            # exécution en dehors de pyodide, la variable n'existe pas
else:
    env_test()      # c'est l'utilisateur qui joue avec l'IDE
finally:
    del env_test    # suppression de l'environnement si vous l'estimez nécessaire
                    # (pourrait aussi être dans la branche `else`)

# --- PYODIDE:code --- #
# joujous ?

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


Important
  • L'utilisateur a aussi accès à ces variables, s'il en connaît l'existence, donc un rédacteur devrait y faire appel uniquement depuis la section env pour __USER_CODE__ et depuis la section env_term pour __USER_CMD__, c'est-à-dire avant que le code de l'utilisateur ne puisse lui-même y accéder (et donc potentiellement les modifier).

  • Si le but est de connaître le nombre de caractères du code de l'utilisateur, ne pas oublier que les tests publics font partie du contenu de l'éditeur.

  • Si des restrictions sont appliquées sur le code de l'éditeur sans utiliser les arguments SANS des IDEs et terminaux, garder en tête que si les commandes du terminal ne sont pas également analysées, l'utilisateur pourrait mettre ce dernier à profit pour y définir différentes choses, et ainsi rendre des données disponibles dans l'environnement de l'éditeur sans taper de code dans l'éditeur lui-même (voir aussi la fonction clear_scope, dans l'admonition ci-dessous).

  • Il est très difficile d'écrire de bonnes restrictions ! (surtout en python...)

    De bonnes restrictions devraient :

    1. Être efficaces (au sens de contraignantes).
    2. Ne pas provoquer de faux positifs (ou alors le moins possible).
    3. Ne pas rendre l'expérience de l'utilisateur horrible, de par leur fonctionnement (mauvais feedback, erreurs incompréhensibles, comportements inattendus d'objets ou fonctions classiques sans avertissement ou feedback, ...).


    D'expérience, si vous vous lancez dans ce genre de choses, il y a fort à parier que vous n'arriviez pas à un résultat satisfaisant les trois points à la fois.
    Les restrictions déjà proposées via l'argument SANS des macros sont ce qui s'en rapproche le plus et je déconseille fortement d'en mettre d'autres en place. Par ailleurs, il est probable que vous ne puissiez pas mélanger facilement votre propre logique à celle issue de l'utilisation de l'argument SANS, sauf à explorer le code du thème pour voir comment vous y adapter...

clear_scope(keep=())

À but de complétude uniquement, mais encore une fois, l'utilisation de tout ceci est déconseillée.

Une fonction clear_scope, masquée mais accessible depuis n'importe où, permet de supprimer tout ce qui a été défini dans l'environnement de l'utilisateur depuis le démarrage de Pyodide.
Elle peut permettre de s'assurer que l'utilisateur ne définit pas des choses qu'il n'est pas sensé définir via un terminal, pour contourner certaines restrictions. Il est également possible de passer un itérable en argument, contenant les noms d'objets ou de variables à ne pas effacer de l'environnement.

Si cette fonction est utilisée, elle devrait l'être depuis la section env, pour les mêmes raisons que précédemment (et également pour que cela n'affecte pas les comportements liées aux restrictions de codes).

Fonctionnement des restrictions#

Voici quelques informations supplémentaires sur la façon dont les restrictions de code fonctionnent (argument SANS des macros IDE, IDEv et terminal).

Philosophie#

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.

Codes concernés#

Les restrictions ne sont appliquées qu'à certaines sections/situations :

Codes Méthodes
+
Mots clefs
Fonctions
+
Imports
Code de l'IDE
(code+tests)
Validations
(tests+secrets)
Commande terminal

Ordre et méthodes d'application#

Lorsqu'une section ("code cible") concernée par des restrictions est exécutée, le thème applique les opérations décrites ci-dessous. Si une erreur est levée à une de ces étapes, les étapes suivantes ne sont pas exécutées (sauf la dernière, si nécessaire).

  1. Si le code exécuté ne contient pas d'erreur de syntaxes, vérifie les exclusions de méthodes et de mot clefs.
    Cette vérification est faite via une analyse de l'AST représentant le "code cible", ce qui évite d'éventuels faux positifs avec le contenu des commentaires ou des docstrings.

  2. Le code à exécuter est analysé pour installer d'éventuels modules/bibliothèques manquants. Les modules/bibliothèques interdits d'utilisation ne sont normalement pas installés durant cette étape.

  3. Les restrictions pour les fonctions et les modules/bibliothèques sont mises en place dans l'environnement.
    Ces restrictions sont directement liées au code exécuté car elles reposent sur des redéfinitions de fonctions disponibles dans dans l'environnement de l'utilisateur. Le contenu des commentaires/docstrings ne peut donc pas déclencher de faux positifs.

  4. Le "code cible" est exécuté (contenu de l'IDE, tests de validation, ou commande du terminal).

  5. Si des restrictions sur les fonctions ou les imports ont été mises en place, le code vérifie qu'elles n'ont pas été altérées par l'utilisateur. Si c'est le cas, une erreur sera levée après avoir supprimé les restrictions.
    Des restrictions qui ont été mises en place seront systématiquement retirées, quoi qu'il se soit passé dans l'intervalle.

Méthodes & attributs#

Interdictions de méthodes ou attributs

Syntaxe SANS=".method1 .method2 .attribut"
Soit plus généralement : .identifiant
Mode d'application Recherche d'accès à des attributs dans l'AST du code.

Exemple :

Code python dans un IDE utilisant SANS='.sort'
meme_si_ce_n_est_pas_une_liste.sort()  # Lève ExclusionError

meme_si_pas appelée.sort               # Lève ExclusionError

# lst.sort() dans un commentaire n'a pas d'effet.

Fonctions#

Interdictions de fonctions

Syntaxe SANS="min sorted"
Soit plus généralement : identifiant
Mode d'application Ces fonctions lèvent une erreur lorsqu'elles sont appelées.

Les interdictions de fonctions sont mises en place en remplaçant les fonctions d'origine dans l'environnement de l'utilisateur par des versions qui lèvent une erreur lorsqu'elles sont appelées.

Ceci présente des avantages et des inconvénients :

  • Point positif: les éléments interdits mis dans des commentaires ne vont pas déclencher de faux positifs.
  • Contrainte : ces interdictions concernent tous les codes susceptibles d'utiliser la fonction de l'utilisateur, donc elles s'appliquent aux codes des sections code, tests et secrets. Il n'est donc "pas possible" d'utiliser les functions ou modules interdits depuis les tests pour valider les réponses de l'utilisateur...

Exemple :

SANS='sorted'
# sorted(arr)               # Ce commentaire ne lève pas d'erreur

def func1(arr:list):
    return sorted(arr)      # Lève une erreur si func1 est appelée

f = sorted                  # Pas d'erreur car pas d'appel !

def func2(arr:list):
    return f(arr)           # Lève une erreur si func2 est appelée !

Modules#

Interdictions de modules / bibliothèques

Syntaxe SANS="numpy heapq"
Soit plus généralement : nom_module
Mode d'application Ces modules/bibliothèques lèvent une erreur lorsqu'ils sont importés.

Les interdictions de modules/bibliothèques fonctionnent de manière similaire aux interdictions de fonctions : la fonction d'import originale est remplacée par une autre, qui se comporte normalement pour les modules autorisés, mais qui lève une erreur si l'utilisateur tente d'importer un module interdit.

Exemple :

SANS='numpy'
# Chacune de ces instructions lèverait une erreur :
import numpy
import numpy as np
from numpy import array


Ne pas importer de module interdits directement dans le scope global, depuis les sections d'environnement !

Les restrictions ne sont pas appliquées aux sections dites "d'environnement" (env, env_term, post_term, post), donc il est possible d'y importer des modules interdits à l'utilisateur. Il ne faut cependant pas oublier qu'une fois un import effectué, le module est ensuite disponible dans le scope dans lequel il a été importé.

Si des modules interdits à l'utilisateur doivent être utilisés depuis ces sections, il faut donc les importer depuis l'intérieur d'une fonction, afin éviter que l'utilisateur ne puisse ensuite directement y accéder.
En effet, bien que le module ait déjà été chargé dans l'environnement python, l'utilisateur devrait tout de même utiliser la fonction d'importation pour pouvoir utiliser le module, et c'est cette fonction qui lèvera une erreur pour un module interdit.


SANS='numpy'

ok

# --- PYODIDE:env --- #
@auto_run
def _prepare():
    import numpy
    # Utiliser numpy ici...

NON !

# --- PYODIDE:env --- #
import numpy

# numpy est ensuite accessible à
# l'utilisateur depuis l'IDE !

Mots clefs & opérateurs#

"Mot clef" est ici à prendre au sens large. Cela recouvre évidemment les mots clefs du langage, mais aussi quelques constantes (True, False et None) ainsi que l'utilisation de f-strings et des opérateurs mathématiques.

Ces restrictions doivent être indiquées après le séparateur AST: dans l'argument SANS :

Interdictions de mots clefs

Syntaxe SANS="... AST: mot_clef1 mot_clef2"
Soit plus généralement : ... AST: identifiants
Mode d'application Recherche des mots clefs ou combinaisons de mots clefs dans l'AST du code.

Exemple :

SANS='AST: for'
# Les boucles for sont interdites...
# Le commentaire précédent ne lève pas d'erreur !

for _ in (): pass        # Lève ExclusionError


Concernant les mots clefs, il y a quelques subtilités qu'il faut garder à l'esprit. Notamment :

Concerne... Particularité
in Affecte uniquement les instructions de vérification de contenu, pas les boucles for.
Restrictions larges
(ex: else)
Les mots clefs autres que in sont recherchés partout où il peuvent être trouvés.
Par exemple, dans le cas de else:
  • Conditions
  • Clauses terminales des boucles for ou while
  • Dans les blocs try ... except ... else
Restrictions ciblées
(ex: for_else)
Il est possible de trouver une version composée de certains mot clefs, qui permet de cibler des utilisations particulières afin d'obtenir un meilleur contrôle sur ce qui est interdit ou pas.
Par exemple :
  • for_else pour interdire uniquement le else des boucles for
  • while_else
  • try_else
  • ...
Boucles classiques
vs compréhensions
Il est possible d'interdire spécifiquement l'une ou l'autre, en utilisant :
  • for_comp pour interdire les compréhensions.
  • for_inline pour interdire les boucles classiques.
Constantes True, False et None sont considérés comme des mots clefs par le language lui-mème, donc ils ont été ajoutés à la liste des exclusions possibles.
f"{ ... }" Un "bonus" proposé par le thème, car il est difficile d'interdire les conversions en chaînes de caractères sans cette restriction.
Utiliser f_str ou f_string en tant que mot clef pour les interdire.
as Le mot clef as n'est pas couvert par le thème (intérêt trop limité).


Liste des mots clefs et opérateurs et leurs combinaisons (SANS='AST: ...')

Ci-dessous, la liste de tous les mot clefs ou opérateurs utilisable avec l'argument SANS des macros IDE, IDEv et terminal.
Les noms de classes entre parenthèses correspondent aux classes du module python ast qui sont concernés par le mot clef ou opérateur en question (liens vers la documentation python, si besoin de clarifications).


Exemples d'utilisation des règles d'exclusions

Si on va au plus simple, il suffit d'indiquer le nom de la méthode à interdire avec un point devant dans l'argument SANS :

{{ IDE(..., SANS=".count") }}

Ceci n'empêche cependant pas un utilisateur de passer par getattr. On peut alors ajouter cette fonction en tant que builtin interdit, et interdire également les méthodes .__getattr__ :

{{ IDE(..., SANS=".count .__getattr__ getattr") }}

S'il reste nécessaire d'utiliser getattr dans l'exercice et qu'on souhaite que la fonction laisse passer certains appels, il faut:

  • Ne pas renseigner la fonction dans l'argument SANS
  • En construire une version personnalisée dans la section env, levant ExclusionError aux moments appropriés, et qui est ensuite supprimée de l'environnement depuis la section post de l'exercice.
    La même logique est applicable aux terminaux, en travaillant cette fois avec les sections env_term et post_term.

Les interdictions de modules sont les plus simples à mettre en place : il suffit de donner le nom du module dans l'argument SANS.

{{ IDE(..., SANS="numpy") }}

Ceci couvre tous les cas/syntaxes d'imports envisageables, et fonctionne aussi bien sur des modules internes, des python_libs venant avec le site construit/PMT, ou des bibliothèques externes à installer durant les exécutions.

Il est à noter qu'interdire un opérateur et les fonctions faisant le même travail peut très vite se révéler très fastidieux et nécessite une bonne connaissance de python pour couvrir le maximum de cas.

Par exemple, pour interdire la multiplication, il faut :

  • Interdire *
  • Interdire le module operator
  • Interdire le module math (à cause de la fonction prod)
  • Interdire les méthodes .__mul__, .__rmul__ et .__imul__ (accessibles sur les objets int et float)
  • Si on va au bout de la logique, interdire getattr, .__getattribute__ et .__getattr__ à cause du point précédent.
  • Potentiellement interdire le module fractions, selon ce que l'on veut faire ou pas dans l'exercice
  • Potentiellement interdire l'opérateur / également, selon ce que l'on veut faire ou pas dans l'exercice (à cause des opérations du type a / (1/b), notamment avec des fractions).
    Noter que cette nouvelle interdiction implique également de bloquer de nouvelles méthodes concernant les soustractions...

Au final :

{{ IDE(...,
    SANS = "operator math getattr
            .__getattr__ .__getattribute__ .__mul__ .__rmul__ .__imul__
            AST: *
   ") }}


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

Work around...#

Si vous souhaitez tout de même utiliser des fonctions interdites depuis les tests secrets, il est en fait possible de récupérer les fonctions originales et de les utiliser.

Notez cependant qu'il est alors indispensable d'exécuter les tests secrets dans une fonction afin de ne pas définir la fonction d'origine dans le scope de l'utilisateur, ce qui, outre le fait de la rendre disponible aussi pour lui, lèverait une erreur lors de la vérification finale des restrictions (voir point précédent).


Le décorateur auto_run peut aider à simplifier l'écriture des tests, en évitant d'avoir à appeler les fonctions puis de les effacer à la main (onglet suivant) et en couvrant plus de cas.

Extraction des builtins d'origine, avec SANS='sorted'
@auto_run
def test_anti_leak_function():
    sorted = __move_forward__('sorted')
    arr = [1, 3, 7, 1, 25, 8, 5, 2]
    exp = sorted(arr)
    assert func(arr) == exp, "some message"

@auto_run
def test_anti_leak_import():
    __import__ = __move_forward__('__import__')
    module = __import__("forbidden_module")
    assert ...
Extraction des builtins d'origine, avec SANS='sorted'
def test_anti_leak_function():
    sorted = __move_forward__('sorted')
    arr = [1, 3, 7, 1, 25, 8, 5, 2]
    exp = sorted(arr)
    assert func(arr) == exp, "some message"

def test_anti_leak_import():
    __import__ = __move_forward__('__import__')
    module = __import__("forbidden_module")

    assert ...


test_anti_leak_function() ; del test_anti_leak_function
test_anti_leak_import() ; del test_anti_leak_import