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
ourepr
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, viasys.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...
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#
- Le message
"Script lancé..."
apparaît dans le terminal. - Exécution du code de la section
env
. - Exécution du contenu actuel de l'éditeur.
- Affichage de la sortie standard.
- 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.
- Si l'IDE n'a pas de bouton de validation,
- Exécution du code de la section
post
.
- Le message
"Script lancé..."
apparaît dans le terminal. - Exécution du code de la section
env
. - Exécution du contenu actuel de l'éditeur.
- Affichage de la sortie standard.
- 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
.
- Exécution de la version originale des tests publics (section
- 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).
- Si une erreur a été rencontrée, le compteur d'essais est diminué de 1 (pour modifier ce comportement, voir ici).
- 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'argumentMODE
des IDEs). - Exécution du code de la section
post
.
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").
Le terminal d'un IDE suit le même mode d'exécution que les terminaux isolés, dont voici le résumé :
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'unmkdocs 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 ![]() |
"Validate" |
...le bouton ![]() |
"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 |
|
# Tests
(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
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 |
|
# Tests
(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
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 sectionenv_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 fonctionclear_scope
, dans l'admonition ci-dessous). -
Il est très difficile d'écrire de bonnes restrictions ! (surtout en python...)
De bonnes restrictions devraient :
- Être efficaces (au sens de contraignantes).
- Ne pas provoquer de faux positifs (ou alors le moins possible).
- 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'argumentSANS
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'argumentSANS
, 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).
-
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. -
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.
-
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. -
Le "code cible" est exécuté (contenu de l'IDE, tests de validation, ou commande du terminal).
-
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 :
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
etsecrets
. 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 :
# 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 :
# 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 :
# 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 :
|
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 :
|
Boucles classiques vs compréhensions |
Il est possible d'interdire spécifiquement l'une ou l'autre, en utilisant :
|
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).
False
(Constant
)None
(Constant
)True
(Constant
)and
(And
)assert
(Assert
)async
(AsyncFor
,AsyncWhile
,AsyncWith
,AsyncFunctionDef
,comprehension
)async_def
(AsyncFunctionDef
)async_for
(AsyncFor
,comprehension
)async_with
(AsyncWith
)await
(Await
)break
(Break
)case
(match_case
)class
(ClassDef
)continue
(Continue
)def
(FunctionDef
,AsyncFunctionDef
)del
(Delete
)elif
(If
)else
(For
,AsyncFor
,While
,AsyncWhile
,If
,IfExp
,Try
)except
(ExceptHandler
)f_str
(JoinedStr
)f_string
(JoinedStr
)finally
(Try
,TryStar
)for
(For
,AsyncFor
,comprehension
)for_comp
(comprehension
)for_else
(For
,AsyncFor
)for_inline
(For
)from
(ImportFrom
,YieldFrom
,Raise
)from_import
(ImportFrom
)global
(Global
)if
(comprehension
,If
,IfExp
)import
(Import
,ImportFrom
)in
(In
,NotIn
)is
(Is
,IsNot
)is_not
(IsNot
)lambda
(Lambda
)match
(Match
)nonlocal
(Nonlocal
)not
(Not
,NotIn
,IsNot
)not_in
(NotIn
)or
(Or
)pass
(Pass
)raise
(Raise
)raise_from
(Raise
)return
(Return
)try
(Try
,TryStar
)try_else
(Try
,TryStar
)while
(While
)while_else
(While
)with
(With
,AsyncWith
)yield
(Yield
,YieldFrom
)yield_from
(YieldFrom
)!=
(NotEq
)%
(Mod
)&
(BitAnd
)*
(Mult
)**
(Pow
)+
(UAdd
,Add
)-
(USub
,Sub
)/
(Div
)//
(FloorDiv
):=
(NamedExpr
)<
(Lt
)<<
(LShift
)<=
(LtE
)=
(Assign
,AnnAssign
,AugAssign
)==
(Eq
)>
(Gt
)>=
(GtE
)>>
(RShift
)@
(MatMult
)^
(BitXor
)|
(BitOr
)~
(Invert
)
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
, levantExclusionError
aux moments appropriés, et qui est ensuite supprimée de l'environnement depuis la sectionpost
de l'exercice.
La même logique est applicable aux terminaux, en travaillant cette fois avec les sectionsenv_term
etpost_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 fonctionprod
) - Interdire les méthodes
.__mul__
,.__rmul__
et.__imul__
(accessibles sur les objetsint
etfloat
) - 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 typea / (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: *
") }}
# Tests
(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
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.
@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 ...
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
# Tests
(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)