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.
Syntaxe
Commençons par le point le plus simple. La syntaxe de Jinja comporte quelques différences avec celle de Django :
- L'appel aux méthodes n'est plus implicite, ainsi vous devrez écrire
{{ 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. - Les arguments des filtres sont à utiliser comme ceux des fonctions, ce qui permet d'en placer plusieurs ; on passe ainsi de
{{ liste|join:", " }}à{{ liste|join(", ") }}. - Point de tag {% include "gabarit.html" %} mais
rendertemplate("gabarit.html")
. - Plus de dérivées de
ifcommeifequal, 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ésandetor. - Plus non plus de
{% 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 %}
Fonctionnement global
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 !
Filtres
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 !
Tags
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.
Macros
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 !
Utiliser Jinja avec Django
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...
Conclusion
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...


