Page 1 sur 2

API Exercice

MessagePosté: Ven 31 Mai 2013, 12:39
de djinn
Salut à tous,

En continuation de cette discussion sur l'API exercice, je pense qu'il est pertinent de lui ouvrir un fil dédié, afin de faire l'état des lieux et d'évaluer ensemble les solutions possibles.

État des lieux

1. Génération du code TeX

Aujourd'hui, un exercice() est une simple fonction, sans arguments, retournant un tuple (enonce, correction). enonce et correction contiennent chacun le code TeX correspondant découpé en lignes, sous forme d'une liste de string.
def exercice():
enonce = [u"Code TeX de l'énoncé (1ère ligne)", u"2ième ligne", ... ]
correction = [u"Code TeX de la correction (1ère ligne)", u"2ième ligne", ... ]
return (enonce, correction)
En d'autres termes, à chaque appel exercice() génère le code TeX correspondant à l'énoncé et à sa correction, avec de nouvelles valeurs tirées au hasard.

Remarques:
  1. Le titre (ou tout autre attribut, comme la vignette correspondante) de l'exercice doit être défini ailleurs dans le code (voir plus bas).
  2. Énoncé et correction sont générés à chaque appel, même si l'un ou l'autre n'est pas nécessaire.
  3. Rien n'est prévu pour générer deux fois le même exercice avec les mêmes valeurs, ou bien générer énoncé et correction du même exercice en deux temps (use case).
  4. Enrichir le comportement des exercices (actimaths, quizz Moodle…) requiert d'ajouter un élément au tuple de retour, une solution ad-hoc qui rend ces versions enrichies incompatibles entre elles.

2. Référencement dans le générateur

Pour que pyromaths puisse générer cet exercice, la fonction exercice() doit aussi être référencée dans un module python. Celui-ci doit posséder une fonction main() exigeant trois arguments:
  • no: numéro de l'exercice,
  • f_enonce et f_correction: fichiers TeX dans lesquels seront inscrits respectivement l'énoncé et la correction.
def main(no, f_enonce, f_correction):
exercices = (
exercice, # fonction exercice() ci-dessus
... # autres exercice
)
write(f_enonce, f_correction, exercices[no]())
En d'autres termes, main() appelle l'exercice numéro no et écrit le code TeX ainsi généré dans les fichiers f_enonce et f_correction (grâce à la fonction write()).

En pratique, les exercices sont réunis en groupes ou packages d'exercices (aujourd'hui, ils sont regroupés par leur niveau dans une scolarité française). Chaque package possède un module avec une telle fonction main(), référençant les toutes les fonctions-exercices qu'il contient. Ce module doit lui-même être référencé dans System.py:creation().

Remarques:
  1. Les exercices sont référencés statiquement ("en dur") dans leur package.
  2. Les packages et leur module principal sont référencés statiquement dans System.py.
  3. write() est inutilement dupliquée dans chaque package d'exercices. Or, ce n'est pas leur rôle d'écrire dans des fichiers. write() devrait se trouver dans System.py.
  4. main() est également dupliquée dans chaque package d'exercices: la seule différence étant la liste de fonctions-exercices qu'il contient.

3. Référencement dans l'interface graphique

Enfin, pour apparaître dans l'interface graphique, il faut définir le titre de cet exercice dans Values.py:LESFICHES. Ce titre doit figurer, dans une sous-liste correspondant à ce package d'exercice, à la même position (no) que dans main().
LESFICHES = [
['Titre package', '',
['Titre exercice()',
... # autres titres d'exercices
]
],
... # autres packages
]
Remarques:
  1. Chaque exercice est référencé statiquement un nouvelle fois: double référencement dans package (fonction) et Values.py (titre).
  2. Obligation d'utiliser le même indice dans ces deux listes d'exercices.
  3. LESFICHES définit à la fois les titres des exercices et leur présentation dans l'interface graphique.
  4. Pour autant, LESFICHES dépend totalement de l'organisation des exercices en packages python (groupes et ordre).

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 02:45
de djinn
J'ai créé une branche exapi dans le dépôt git afin d'accompagner ce fil de discussion, et j'y ai fait quelques commits. J'ai notamment mis en place la découverte automatique des exercices au runtime (plus besoin de les référencer). :)

En attendant la spécification de l'API-Exercice, j'ai essayé de rendre l'architecture existante plus flexible, tout en changeant le moins de choses possibles au fonctionnement de pyromaths:
  1. Le titre de l'exercice est relocalisé: sa définition est déplacée de Values.py:LESFICHES dans la fonction exercice() correspondante [1a, 3a,3c].
    def exercice():
    ...
    return (enonce, correction)

    exercice.description = u'Titre'
  2. Du coup, les exercices peuvent être découverts dynamiquement au runtime. Plus besoin de référencement statique pour chaque exercice [2a, 3b].
    Est considéré comme exercice toute fonction, située dans un package d'exercices, ayant un attribut description de type chaîne Unicode (exemple ci-dessus). Bien entendu, pour se conformer à l'API actuelle, cette fonction doit également retourner un tuple TeX (enonce, correction).
  3. Les fonctions write() disparaissent, leur logique est intégrée à System.py:creation() [2c].
  4. Les fonctions main() disparaissent [2d].
  5. Les modules niveau/niveau.py de chaque package d'exercices disparaissent donc également[3a].
  6. Le titre de chaque package d'exercices est défini dans son module __init__.py [2b]:
    description = u'Titre du package (niveau)'
  7. Les packages d'exercices sont déplacés dans pyromaths/ex/. Ainsi, il sera trivial de les découvrir dynamiquement à l'avenir [2b].
Écrire un nouvel exercice revient donc à écrire une fonction, n'importe où dans un package d'exercices (pyromaths.ex.sixieme, pyromaths.ex.cinquiemes…), et de lui adjoindre un titre via un attribut description.
Au prochain lancement de pyromaths, il apparaîtra dans l'interface graphique et pourra être utilisé normalement.

Ces descriptions ne sont pas censés remplacer une vraie couche de présentation séparée. Quand celle-ci sera mise en place, elle pourra "renommer" les exercices et les regrouper comme elle l'entend (pour se conformer aux niveaux scolaires de tel ou tel pays par exemple). En attendant, la présentation par défaut se conforme à l'organisation en packages.
De toutes manières, nous sommes contraint d'organiser et de regrouper rationnellement le code des exercices pour nous y retrouver. Autant que ce travail puisse également être visualisé dans l'interface graphique.

Or, si nous désirons internationaliser pyromaths, il faudrait que cette organisation soit la plus universelle possible. L'organisation en niveaux dans une scolarité française ne convient pas, par exemple (elle conviendrait pour une couche de présentation).
L'organisation qui s"impose, me semble-t-il, est celle en domaines (géométrie, arithmétique…?). Voire, pour supporter un grand nombre d'exercices clairement, une arborescence de domaines, sous-domaines, etc:
Code: Tout sélectionner
arithmetique/
    exercice.py
    ....
geometrie/
    exercice.py
    ...
    euclidienne/
        exercice.py
        ...
    espace/
        exercice.py
        ...
Bien entendu, toute cette arborescence d'exercices est découverte dynamiquement au runtime, ce qui facilite sa réorganisation.

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 10:17
de Yves
Beau boulot ! :)

Sur OS X, ça fonctionne bien en lançant pyromaths.py depuis le terminal.

Par contre, la création de l'app avec py2app conduit à l'erreur suivante:
File "pyromaths/Values.pyc", line 9, in <module>
ImportError: No module named pkgutil
En rajoutant pkgutil dans l'option packages de setup.py,
packages = ['pkgutil'],
on obtient l'erreur suivante:
File "build/bdist.macosx-10.6-intel/egg/modulegraph/find_modules.py", line 199, in find_needed_modules
TypeError: 'NoneType' object has no attribute '__getitem__'
En rajoutant pkgutil dans l'option includes de setup.py,
includes = ['gzip', 'pkgutil'],
l'application ne crashe plus mais l'interface ne contient aucun exercice !

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 12:12
de djinn
Yves a écrit:Beau boulot ! :)
Merci. :-)
Je viens de commiter le déplacement des vignettes dans le sous-dossier img/ du package d'exercices correspondant. Ce qui signifie l'utilisation pour la première fois de package_data dans setup.py. J'anticipe quelques soucis, notamment avec py2app. :P

Yves a écrit:(avec py2app) l'application ne crashe plus mais l'interface ne contient aucun exercice !
Les exercices ne sont pas découverts: ça ressemble bien à un problème avec pkgutil
As-tu essayé en ajoutant ça:
includes = ['gzip', 'pkgutil'],
Mais en enlevant ça:
packages = ['pkgutil'],

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 12:34
de Yves
Yves a écrit:As-tu essayé en ajoutant ça:
includes = ['gzip', 'pkgutil'],
Mais en enlevant ça:
packages = ['pkgutil'],

Oui, j'ai bien veillé à enlever packages = ['pkgutil'] mais effectivement les exercices ne sont pas découverts.

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 12:37
de djinn
Aucun message d'erreur?
Est-ce que les packages d'exercices figurent bien dans Pyromaths.app?

Et tant que j'y suis: est-ce que les sous-dossier img/ et leur vignettes sont bien présents dans les packages d'exercices de Pyromaths.app?

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 15:45
de Yves
djinn a écrit:Aucun message d'erreur?

Non.

djinn a écrit:Est-ce que les packages d'exercices figurent bien dans Pyromaths.app?

Et tant que j'y suis: est-ce que les sous-dossier img/ et leur vignettes sont bien présents dans les packages d'exercices de Pyromaths.app?


Tu as trouvé l'origine du problème.

En dézippant Pyromaths.app/Contents/Resources/lib/python2.7/site-packages.zip, il y a bien un dossier pyromaths/ex mais sans les exercices (et encore moins les vignettes), uniquement le fichier __init__.pyc.

Le dossier classes est également manquant et le dossier outils est incomplet.

Avant le commit relatif au déplacement des vignettes, il y avait le fichier __init__.pyc, et les dossiers lycee, sixiemes, cinquiemes, quatriemes et troisiemes mais ces dossiers ne contenaient qu'un fichier __init__.pyc pas les exercices.

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 16:01
de djinn
D'accord. Je pense que c'est modulegraph qui fait des siennes: comme il n'y a plus de dépendance statique à ces packages/modules dans le code, modulegraph les omet. Et comme py2app ne respecte pas non plus Manifest.in, il va falloir le forcer à les intégrer… :(

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 16:32
de Yves
On peut forcer l'intégration avec
packages = ['src/pyromaths'],
Le dossier pyromaths n'est plus compressé au sein de site-packages.zip, il se retrouve à côté de l'archive dans Pyromaths.app/Contents/Resources/lib/python2.7. Par ailleurs, dans le dossier pyromaths, on trouve des fichiers py, pyc et pyo donc il faut prévoir un nettoyage supplémentaire dans le Makefile. Tout ça n'est pas très satisfaisant mais ça fonctionne.

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 16:36
de Yves
Depuis ton commit relatif au déplacement des vignettes, l'ordre des onglets dans l'interface graphique est modifié: il y a d'abord Cinquième puis Lycée, Quatrième, Sixième, Troisième et Options.

Re: API Exercice

MessagePosté: Sam 01 Juin 2013, 17:29
de djinn
D'accord.
Je te propose de continuer à avancer sur exapi tout en continuant à réfléchir à l'intégration dans py2app. Quitte à se contenter temporairement d'un hack (tu dois aussi pouvoir forcer l'inclusion d'un module par modulegraph en l'important quelque part dans le code). Quitte à se faire une grosse session py2app une fois qu'on est arrivé à une forme plus définitive.

Yves a écrit:Depuis ton commit relatif au déplacement des vignettes, l'ordre des onglets dans l'interface graphique est modifié: il y a d'abord Cinquième puis Lycée, Quatrième, Sixième, Troisième et Options.
En effet. :)
En l'occurence, c'est depuis le commit cab7809b [Découverte automatique des packages d'exercices situés dans pyromaths.ex]: les packages sont découverts automatiquement, du coup leur ordre dans l'UI n'est plus celui des niveaux, mais l'ordre alphabétique.
En passant, c'est également de ce commit dont tu voulais parler tout à l'heure, quand tu mentionnais la présence dans l'archive des packages ex.sixiemes, etc, (sans leurs modules): quand on enlève les références statiques dans le code, les modules/packages concernés (ainsi que certaines dépendances -- comme celles de classes) disparaissent de l'application py2app.

Je pars du principe qu'on va séparer code et présentation [2d], d'une manière ou d'une autre, comme on en a déjà discuté. Il me semble qu'il y a consensus sur cette idée.
Du coup, la couche présentation pourra présenter les exercices qu'elle désire, groupés et ordonnés comme elle le désire. J'imagine qu'en Espagne, par exemple, les niveaux sont différents et les exercices éventuellement abordés à différents moments? Dans ce cas, en version espagnole, pyromaths présentera ces exercices différemment dans l'UI.
En revanche, pour ce qui est de la couche logique, cet ordre n'a pas d'importance. Ce sont d'autres critères qui s'imposent, comme l'universalité (pour faciliter entre autres l'internationalisation) ou l'extensibilité (il faut que la base d'exercices puisse grandir sans que ce soit le bordel, ni qu'il faille la réorganiser dramatiquement). À mon avis, l'organisation en arborescence s'impose comme la plus simple et la plus extensible. Si on doit afficher une arborescence d'exercices, l'ordre alphabétique paraît tout à fait raisonnable; Notons que pour afficher une arborescence il faudra quelque chose comme un TreeWidget, pas une liste "plate" d'onglets…

En l'absence de couche de présentation, on peut retrouver l'ancien affichage par niveau dans l'UI en modifiant Values._packages() de manière à lister nommément les packages dans l'ordre que l'on souhaite (comme avant cab7809b):
def _packages():
''' List exercise packages from pyromaths.ex. '''
import ex.sixiemes, ex.cinquiemes, ex.quatriemes, ex.troisiemes, ex.lycee
return [ex.sixiemes, ex.cinquiemes, ex.quatriemes, ex.troisiemes, ex.lycee]

Je cherche à mettre en place une découverte récursive des packages d'exercices au runtime, qui puisse donc supporter une organisation arborescente des packages d'exercices. Cet automatisme vise à faciliter la (ré-)organisation des exercices:
  1. Isoler chaque fonction exercice() dans son propre fichier.
  2. Pouvoir créer des sous-packages (arborescence).
  3. Passer à une organisation plus universelle des exercices, comme une organisation par domaines, si un consensus se dégage.
  4. Ajouter de nouveaux exercices facilement.
La seule chose qui m'en empêche à ce stade c'est que l'UI dépend encore d'une représentation à "un seul étage" des exercices (les onglets)… :P
En d'autres termes, j'ai besoin d'une couche de présentation indépendante pour pouvoir avancer!

Re: API Exercice

MessagePosté: Mer 05 Juin 2013, 19:40
de djinn
Voici une première proposition d'API pour les exercices! :)


API Exercice

Le code est disponible dans la branche exapi. Il contient un exemple: sixieme/arrondi.py a été traduit au nouveau format d'exercice.

Dans ce nouveau format, un exercice n'est plus une fonction retournant un tuple (énoncé, correction), c'est une classe présentant deux méthodes:
  • tex_statement(): renvoit l'énoncé (au format TeX)
  • tex_answer(): renvoit la correction (au format TeX)
Cette classe présente également deux attributs:
  • description: la description de l'exercice (pour affichage dans l'UI, notamment)
  • level: le niveau académique auquel cet exercice est enseigné

Hello World!

Comme un exemple vaut mieux qu'un long discours, voici le sempiternel hello world!:
from pyromaths import ex

class HelloWorld(ex.TexExercise):

description = u'HelloWorld example exercise'
level = u'Example'

def tex_statement(self):
return ['\\exercice', 'Hello, world!']

def tex_answer(self):
return ['\\exercice*', 'Hello again, world!']
Il suffit de mettre ce bout de code dans un package d'exercice quelconque et il apparaîtra au prochain lancement de pyromaths, sous le nouvel onglet 'Example' (level), en tant que 'HelloWorld example exercise' (description). Aucun référencement statique n'est nécessaire.

Modules et packages d'exercices

Il est possible de mettre ce code dans un module d'exercice existant ou dans son propre module python (recommandé).

De même, on peut mettre un module d'exercice dans un package d'exercices existant ou dans un nouveau package: tout package python situé dans pyromaths.ex sera scanné au démarrage pour découvrir d'éventuels exercices.
On peut également créer un nouveau package dans un package existant: le scan est récursif de manière à encourager une organisation arborescente des packages, plus à même de supporter un grand nombre d'exercices à l'avenir.

Interface graphique

Dans l'interface utilisateur, les exercices ayant le même level sont rassemblés dans le même onglet. Créer un onglet revient simplement à définir un nouveau level dans un exercice.
De même, on peut déplacer l'exercice HelloWorld dans l'onglet 'Lycée' en changeant cette ligne dans le code ci-dessus:
level       = u'Lycée'
Désormais orphelin, l'onglet 'Example' disparaît.
Enfin, on peut également afficher cet exercice dans plusieurs onglets:
level       = [u'Lycée', u'Example']
Il est donc possible de déplacer un module d'exercice dans un autre package sans altérer son affichage dans l'interface graphique: il est très facile de réorganiser les exercices.

On peut définir un level par défaut au niveau du module et au niveau du package, qui seront utilisés (dans cet ordre) en cas d'absence du level d'exercice.

Rétro-compatibilité

Enfin il faut noter que les exercices à l'ancien format sont automatiquement encapsulés dans leur propre classe, sous-classe de TexExercise, de façon à être compatibles avec le nouveau format.
La traduction manuelle des anciens exercices au nouveau format peut donc se faire progressivement.

Impact sur le reste du code

Au démarrage de l'application, Values appelle ex.load(), qui scanne les packages d'exercices contenus dans pyromaths.ex et en extrait les TexExercices.
Ce faisant, ex.load() met à jour l'index par niveau ex.levels, un dictionnaire associant chaque niveau à une liste (de classes) d'exercices correspondants. C'est cet index que Values utilise ensuite pour construire LESFICHES.

Ces classes d'exercice sont (ou seront) utilisées aussi directement que possible dans le reste du code.
Par exemple, dans l'interface graphique, on associe directement au widget d'exercice la classe d'exercice correspondante. Pour générer une série d'exercice, il suffit de créer une instance de chaque classe d'exercice sélectionnée, et d'en passer la liste à creation(), qui appellera leurs méthodes tex_statement() et/ou tex_answer(). Plus besoin d'utiliser des indices de LESFICHES pour se référer à un exercice.

Classe et instances

Il est important de comprendre que ce sont des classes qui sont manipulées dans le code, jusqu'à la création de la liste d'exercices désirée par l'utilisateur. Par exemple, ex.levels ne référence que des classes d'exercices.
Une classe d'exercice est utilisée pour créer des instances (objets) de cet exercice. Par exemple, si l'utilisateur commande plusieurs fois le même exercice, la classe correspondante sera utilisée pour créer plusieurs instances de cet exercice -- chaque instance contenant des valeurs (aléatoires) différentes.
Ce sont ces différentes instances qui seront interrogées par creation() pour produire le code TeX final (méthodes tex_*()).

Précisions sur le nouveau format

Les productions de l'énoncé et de la correction étant découplées (respectivement dans tex_statement() et tex_answer()), il faut que ces méthodes partagent quelques variables, comme les valeurs tirées au hasard. On utilise pour cela une
propriété de l'instance (self).
Un TexExercise ne peut pas savoir quelle fonction tex_*() sera appelée en premier ou en second, ou pas du tout (ou plusieurs fois). Pour cette raison notamment, les valeurs partagées doivent être définies une fois pour toutes dans le constructeur (__init__()).

Voici en guise d'exemple un HelloWorld avec partage d'une valeur aléatoire:
import random
from pyromaths import ex

class RandomHelloWorld(ex.TexExercise):

description = u'RandomHelloWorld example exercise'
level = u'Example'

def __init__(self):
self.times = random.randint(2, 10)

def tex_statement(self):
return ['\\exercice', '%u times hello, world!' % self.times]

def tex_answer(self):
return ['\\exercice*', '%u times goodbye, world!' % self.times]
D'un point de vue général, il faut voir les méthodes tex_*() commes de simples "afficheurs TeX" de l'exercice. Demain, l'interface pourrait s'enrichir d'autres "afficheurs": moodle_statement(), odt_statement():)
En conséquence, toute la logique de l'exercice (tirage des paramètres aléatoires, calcul des solutions) doit se trouver dans __init__().

Extensibilité: Actimaths, Moodle, LibreOffice…

On peut enrichir l'interface et étendre les formats produits en définissant une nouvelle sous-classe de la classe mère: Exercise. Par exemple, pour produire un document au format LibreOffice:
class OdtExercise(Exercise):

def odt_statement(self):
return "Énoncé au format ODT"

def odt_answer(self):
return "Correction au format ODT"

Enfin, pour rendre notre HelloWorld ci-dessus compatible avec ce nouveau format:
from pyromaths import ex

class HelloWorld(ex.TexExercise, ex.OdtExercise):

description = u'HelloWorld example exercise'
level = u'Example'

def tex_statement(self):
return ['\\exercice', 'Hello, world!']

def tex_answer(self):
return ['\\exercice*', 'Hello again, world!']

def odt_statement(self):
return "Hello, ODT World!"

def odt_answer(self):
return 'Hello again, ODT World!'



Dans l'attente de vos commentaires… :)

Re: API Exercice

MessagePosté: Mer 05 Juin 2013, 19:52
de Jérôme
Premier commentaire : impressionnant ! :o
je crois que ça va me plaire... :)

Re: API Exercice

MessagePosté: Mer 05 Juin 2013, 21:02
de Yves
Bravo pour ces avancées ! :)

Re: API Exercice

MessagePosté: Mer 05 Juin 2013, 21:39
de Arnaud
djinn, je lis en diagonale tes améliorations du code, mais j'avoue ne pas pouvoir tout regarder en détail pour le moment.
D'ici quelques semaines probablement ;)