Je n’ai pas mis à jour ce blog depuis pas mal de temps pour cause d’activité débordante et je ne reprends le flambeau que pour quelques jours étant en vacances à partir de la semaine prochaine, mais c’est toujours un article de pris.
Je continue d’utiliser Django pour des projets professionnels et privés, et c’est vraiment un plaisir de jouer avec. Cependant, les limitations du langage de gabarits ont eu un jour raison de mon enthousiasme, et j’y ai cherché une alternative. Ne souhaitant pas recommencer l’apprentissage d’une nouvelle syntaxe, mon choix s’est rapidement porté sur Jinja, qui reprend en majorité celle de Django. Dans l’absolu, vous pouvez utiliser n’importe quel moteur de gabarits. Dans la pratique, il est préférable d’en choisir un qui soit adapté à Django !
Je vais présenter mon coup de cœur (qui n’était pas un coup de tête, je suis toujours fidèle à à mon choix deux mois plus tard) de manière certes non-exhaustive mais, je l’espère, complète, en se penchant sur les divers aspects de son fonctionnement.
Commençons par le point le plus simple. La syntaxe de Jinja comporte quelques différences avec celle de Django :
{{ instance.get_absolute_url() }} au lieu de {{ instance.get_absolute_url }}. C’est un peu contraignant lors de la migration car vous devrez vérifier chaque variable, mais le gain vaut à mon avis le coup : une meilleure clarté, la gestion du cas de figure (bien qu’à éviter) méthode/attribut homonymes, et la possibilité de fournir des arguments.
{{ liste|join:", " }} à {{ liste|join(", ") }}.
if comme ifequal, au profit du vrai test, celui disponible en Python. C’est un avantage majeur qui permet d’éviter l’inévitable {% if %}{% else %} du langage de Django, qui autorise de vraies comparaisons comme >=, et qui souffre la combinaison des conditions avec les mots-clés and et or.
{% if %} les uns dans les autres pour simuler un banal {% elif %}… Car ce tag existe dans Jinja ! Certains le jugent dangereux car il complexifie des gabarits, je trouve au contraire qu’il les simplifie,
{% if var %}
{% elif var2 %}
{% elif var3 %}
{% endif %}
étant autrement plus élégant que :
{% if var %}
{% else %}
{% if var2 %}
{% else %}
{% if var3 %}
{% endif %}
{% endif %}
{% endif %}
La classe Environment est la base de tout le système : c’est le contexte dans lequel seront traduits vos gabarits ; il se charge de les trouver à partir de leur chemin puis de les interpréter. Les différents filtres doivent y être recensés pour exister dans l’espace de nom du gabarit, et il contient votre configuration. Dans cet article, la référence à l’objet principal Environment se fera à travers la variable env. Le schéma classique de fonctionnement est le suivant :
TEMPLATE_PATH = settings.TEMPLATE_DIRS[0]
env = Environment(loader=FileSystemLoader(TEMPLATE_PATH))
gabarit = env.get_template('/chemin/vers/gabarit.html')
gabarit.render(contexte)
Par défaut, pour instancier l’environnement, vous devez simplement indiquer le chemin d’un dossier qui contiendra tous vos gabarits. Il est possible de personnaliser ce comportement en créant votre propre Loader, mais pour ça, référez-vous à la documentation, ce billet va déjà être bien assez dense !
Comme avec le système de Django, vous pouvez écrire vos propres filtres très simplement. Pour reprendre l’exemple de la documentation, voici le filtre cut version Jinja :
def do_cut(arg=u''):
def wrapped(env, context, valeur):
return valeur.replace(arg, '')
return wrapped
En fait, cet exemple est la version complète du filtre, avec un accès à l’environnement env et au contexte à la variable éponyme. En procédant de cette manière, il est nécessaire d’enregistrer le filtre dans l’espace de nom de l’environnement avec env.filters['cut'] = do_cut. Mais il est en réalité possible de faire plus simple :
from jinja.filters import stringfilter
def do_cut(valeur, arg):
return valeur.replace(arg, '')
env.filters['cut'] = stringfilter(do_cut)
Oui, c’est la même chose que dans Django ! À une différence près : la fonction peut prendre autant d’arguments que vous le désirez. Par exemple, pour faire un filtre similaire à cut mais limitant la taille de la chaîne :
def do_cut(valeur, arg, taille):
return valeur.replace(arg, ''):taille
env.filters['cut'] = stringfilter(do_cut)
que l’on utilisera simplement en faisant {{ somevariable|cut(0, 15) }}. C’est aussi simple que dans Django mais bien plus puissant, le nombre d’arguments n’étant pas limité et une possibilité d’accès au contexte étant réservée !
Jinja ne permet pas de créer ses propres tags mais plutôt d’appeler des méthodes ayant éventuellement accès au contexte. Ce système fonctionne de deux façons. La première, basique, est de créer une fonction prenant ou non des arguments et retournant une chaîne Unicode, de l’enregistrer dans l’espace de nom avec env.globals['ma_fonction'] = ma_fonction, puis de l’appeler comme vous l’auriez fait en Python avec {{ ma_fonction(argument) }}.
Si vous souhaitez accéder au contexte (sous la forme d’un objet Context), la procédure est (très) légèrement plus compliquée car il faut faire appel à un décorateur, from jinja.datastructure import contextcallable. Les deux premiers paramètres de la fonction seront alors env et contexte. Ce qui donne par exemple :
@contextcallable
def ma_fonction(env, contexte, argument):
...
env.globals['ma_fonction'] = ma_fonction
Vous utiliserez votre fonction de la même façon, mais aurez à votre disposition ces deux nouveaux éléments. Pour que vous visualisiez mieux le principe, voici quelques filtres concrets reprenant ceux de la documentation !
La puissance de Jinja n’attend pas, l’écriture du premier, current_time, suffit à comprendre l’imparable simplicité de ce système :
import datetime
def do_current_time(format):
return datetime.datetime.now().strftime(format)
env.globals['current_time'] = do_current_time
La façon dont vous devrez l’utiliser ne doit plus être un mystère : {{ current_time("%Y-%m-%d %I:%M %p") }}.
3 lignes au lieu de 14 et une seule fonction. Plus besoin de vérifier l’existence des arguments et leur validité, tout se passe intuitivement comme vous l’auriez fait en Python !
format_time est aussi simple à écrire :
import datetime
def do_format_time(date, format):
return date.strftime(format)
env.globals['format_time'] = do_format_time
Jinja passe directement les variables du contexte utilisées comme arguments à la fonction, plus besoin de les y aller chercher. Ce filtre s’utilise bien sûr en écrivant {{ format_time(blog_entry.date_updated, "%Y-%m-%d %I:%M %p") }} et fait toujours 3 lignes au lieu de 18.
Simple, n’est-il pas ?
Pour comprendre la seconde façon d’utiliser le système il est d’abord nécessaire de visualiser comment vous appellerez votre fonction :
{% call ma_fonction() %}
Affichage du titre : « {{ titre }} ».
{% endcall %}
Jinja passera alors un argument caller à la ma_fonction, une méthode qui, appelée, renvoie le contenu du tag {% call %}, en l’occurrence ici Affichage du titre : « Variable titre ». Si caller n’est pas une simple chaîne, ce qui peut paraître étrange au premier abord, c’est en fait pour permettre la surcharge du contexte, car le contenu de {% call %} ne sera évalué que lorsque la fonction sera appelée. C’est plutôt obscur, alors voici une application concrète, si l’on reprend l’exemple précédent en ajoutant un argument facultatif à ma_fonction.
def ma_fonction(argument=None, caller=None):
rendu = u''
if caller and argument:
rendu = caller(titre=argument)
elif caller:
rendu = caller()
return rendu
Si argument est fourni à l’appel de la fonction, il sera utilisé en lieu et place de la variable titre du contexte, sinon, c’est le titre par défaut qui sera affiché. L’exemple avec {% call %} un peu plus haut fonctionne donc toujours de la même façon, mais :
{% call ma_fonction('Jinja est un meilleur système de gabarit que celui de Django') %}
Affichage du titre : « {{ titre }} ».
{% endcall %}
rendra "Affichage du titre : « Jinja est un meilleur système de gabarit que celui de Django »".
Si vous souhaitez accéder au contexte avec une fonction de ce type, les choses se passent de la même façon qu’au départ, en utilisant le décorateur @contextcallable, qui transformera les deux premiers arguments en env et contexte. La définition de notre méthode deviendrait simplement :
@contextcallable
def ma_fonction(env, contexte, argument=None, caller=None):
...
Toujours pour donner des applications concrètes, reprenons l’exemple de current_time, qui insère une variable dans le contexte. La première façon d’écrire les filtres pourrait suffire :
@contextcallable
def do_current_time(env, context, format):
context['current_time'] = datetime.datetime.now().strftime(format)
Mais ce ne serait sûrement pas nécessaire, Jinja intégrant un tag set. Ainsi, avec le même current_time que tout à l’heure, {% set current_time = current_time("%Y-%M-%d %I:%M %p") %} ferait exactement la même chose que notre nouvelle méthode !
get_current_time (cf. la documentation) pourrait se faire sans toucher au code, avec {% set my_current_time = current_time("%Y-%M-%d %I:%M %p") %}, mais ce serait profiter des faiblesses du langage de Django. Pour faire vraiment la même chose que dans l’exemple proposé, 3 lignes contre 19 suffisent :
@contextcallable
def do_set_current_time(env, context, format, nom_variable):
context[nom_variable] = datetime.datetime.now().strftime(format)
env.globals['set_current_time'] = do_set_current_time
Que l’on utilise avec {{ set_current_time("%Y-%M-%d %I:%M %p", "my_current_time") }}, et tout ça bien sûr sans la moindre expression régulière, contrairement au système proposé par la documentation. En plus d’être incroyablement plus simple, Jinja permet de faire des fonctions plus efficaces !
La réécriture de upper utilise la seconde façon d’écrire ces fonctions et se révèle être un jeu d’enfant :
def do_upper(caller=lambda: u''):
return caller().upper()
env.globals['upper'] = do_upper
L’astuce de caller=lambda: u'' permet de ne pas avoir à tester l’existence de caller qui renverra dans tous les cas une chaîne Unicode. Cette fonction fait 2 lignes, contre 10 pour celle de Django ! Mais la simplicité a un (petit) prix : l’appel à la méthode est plus laid que dans Django :
{% call upper() %}Ceci apparaîtra en majuscules, votre_nom .{% endcall %}
Ce qui au final ne représente qu’un inconvénient plutôt négligeable.
Pour finir cet aperçu de l’utilisation de Jinja, je vais parler rapidement des macros, qui sont une fonctionnalité souvent demandée et assurément manquante à Django. Elles permettent de répéter un morceau de code sans le réécrire, un peu comme une variable. Comme les fonctions, elles peuvent aussi prendre des arguments. Par exemple :
{% macro afficher_dictionnaire(dico) %}
{% if dico %}
<ul>
{% for cle, valeur in dico|dictsort %}
<li>{{ cle }} : {{ valeur }}</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}
Que vous utiliserez avec {{ afficher_dictionnaire(form.errors) }}. C’est un atout majeur, car la consigne fétiche de Django, DRY, est bien mise à mal lorsqu’on est obligé de recopier des bouts de code plusieurs fois pour simuler ce comportement !
L’intégration à notre framework préféré se fait sans douleur grâce à l’existence d’un render_to_response pour Jinja. Il fonctionne exactement de la même façon que l’existant, aucun souci de migration à prévoir. Je vous conseille personnellement de créer un fichier utils/jinja.py à la racine de votre projet, et d’y placer votre render_to_response. Dès que vous aurez besoin d’un nouveau filtre, il vous suffira de l’éditer pour avoir accès à l’objet env très simplement ! Enfin, gardez l’astuce permettant de transformer un filtre Django en filtre Jinja sous la main. Plus tard peut-être, un nouveau billet sur Jinja pour aborder tout ce qui ne l’a pas été aujourd’hui…
Cette présentation ne vaut assurément pas la documentation, mais permet de donner une idée de la puissance de Jinja qui surpasse en bien des points le langage de gabarits de Django. C’est un module qui m’est désormais indispensable, car je n’ai plus à bidouiller continuellement pour obtenir ce que je veux, ce qui était devenu obligatoire même en faisant attention à ne pas déporter ce qui pourrait être fait dans la vue dans le template. Si vous avez décidé de sauter la marche, le plus simple pour obtenir de l’aide (en anglais) reste IRC sur #pocoo@irc.freenode.net. Les développeurs principaux sont souvent présents et très réactifs en cas de souci ! Mais vous pouvez aussi poser votre question ici-même, peut-être pourrais-je y répondre…
Baptiste le 13 juillet 2007
Pas grave, faut bien que certains continuent d’utiliser le système de Django, qui va faire le support après ? ;-)
Je ne vais pas reprendre tout le billet, mais les avantages qui ont été pour moi décisifs sont :
Et je ne vois pas vraiment ça comme une singularisation, mais comme une migration vers un moyen d’être plus performant. Bien sûr, ça nous prive des évolutions du langage de Django, mais la peur de la non-compatibilité descendante nuit sérieusement à toute possibilité d’évolution positive. Si un jour un changement intéressant a lieu, rien ne m’empêche de retourner à ce que propose Django !
Mais cet argument est quand même à double sens, tu peux aussi dire à quelqu’un venant de RoR et souhaitant nous rejoindre qu’il se prive des améliorations futures du framework Ruby, qui va devenir plus rapide et encore plus puissant ! En effet, c’est un choix à faire, et il te répondrait sûrement que Django progressera aussi de son côté. C’est la même chose avec le langage de Django, il progressera, Jinja aussi. Et je préfère un système qui me propose ce que je trouve utile par défaut, plutôt qu’un autre qui en plus de ne pas convenir à mes besoins les désapprouve :-)
Ceci dit, si le langage de Django t’est suffisant et que tu ne t’es jamais senti limité par lui, tu as absolument pas besoin de changer, bien sûr… J’ai migré car je me sentais à l’étroit dès que je voulais faire des trucs un peu différents des exemples de la doc, si tu te débrouilles avec tes templatetags et django-template-utils, et que ça te convient, là encore, ce n’est pas toi que j’invite à passer à Jinja ;-)
NiCoS le 15 juillet 2007
Hello,
Joli billet qui montre des choses intéressantes (même si j’en ai pas encore besoin :-P ). Jusqu’à présent, le langage de template django a convenu à mes besoins mais sait-on jamais !
++ NiCoS
olivier le 26 juillet 2007
J’ai découvert Jinja grâce à toi (recherche Google : “Django elif”…). Excellent ! Je commençais à tourner en bourrique avec les limitations incompréhensibles du templating Django. L’intégration n’est bien sûr pas parfaite (le tag url me manque…) mais les gains compensent largement.
sncf le 22 août 2007
Merci pour l’info.. je ne connaissais pas non plus, mais j’adopte !
paramoteur le 29 octobre 2007
C’est effectivement tres simple à transposer.
ah le 02 décembre 2007
Jinja est effectivement sympatique, cependant il n’est pas le plus performant, Mako me semble une meilleur alternative au templating de Django
rockandroll le 27 juin 2008
Django m’a l’air d’être quand même suffisant pour la majorité des applications… Personnellement je n’ai pas d’intérêt à changer mais merci au moins je suis au courant de ce qu’il y a autour :)
David, biologeek le 13 juillet 2007
Tu n’as pas vraiment réussi à me convaincre :-).
Concrètement, quels sont les avantages réels à changer de langage ?
Parce qu’à part de vrai tests dans les conditions (ce qui peut-être en partie changé via des templatetags : http://code.google.com/p/django-template-utils/ ) et le manque de switch qui peut être compensé avec http://www.djangosnippets.org/snippets/300/ je ne vois pas vraiment d’avantage à se singulariser de la sorte (ce qui enlève toute possibilité de bénéficier des améliorations futures du langage de template de django). À moins de coder un convertisseur django <-> jinja ;-).