Il y a beaucoup de moteur de rendu disponible pour pyramid mais il peut arriver qu'on ait besoin de créer le sien ou d'en ajouter un dont le support n'a pas encore été fait.
Plutôt que donner des exemples abstraits, je vais illustrer avec un module relativement simple que j'ai écrit (lire dont je comprends le code) : pyramid_xslt
C'est quoi xsl ?
Dans le cadre de mon travail, je manipule beaucoup de fichiers XML et notamment je les transforme en fichiers HTML. XSLT est le langage XML qui transforme un contenu XML en XML ou texte.
Soit le XML suivant :
<document>
<title>Some title</title>
<section>
<section-title>First Section</section-title>
<content>foo bar</content>
</section>
<section>
<section-title>Second Section</section-title>
<content>baz baz</content>
</section>
</document>
La XSLT suivante le transforme en HTML :
<?xml version="1.0" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="document">
<html>
<head>
<xsl:apply-templates select='title' mode="meta" />
</head>
<body>
<article>
<xsl:apply-templates select='title' />
<xsl:apply-templates select="section" />
</article>
</body>
</html>
</xsl:template>
<xsl:template match="title" mode="meta">
<title><xsl:value-of select="." /></title>
</xsl:template>
<xsl:template match="title">
<h1><xsl:value-of select='.' /></h1>
</xsl:template>
<xsl:template match="section">
<div>
<xsl:apply-templates />
</div>
</xsl:template>
<xsl:template match="section-title">
<h2><xsl:value-of select="." /></h2>
</xsl:template>
<xsl:template match="content">
<p>
<xsl:value-of select="." />
</p>
</xsl:template>
</xsl:stylesheet>
On exécute en CLI :
$ xsltproc sample.xsl sample.xml > sample.html
ce qui donne comme HTML :
<?xml version="1.0"?>
<html>
<head>
<title>Some title</title>
</head>
<body>
<article>
<h1>Some title</h1>
<div>
<h2>First Section</h2>
<p>foo bar</p>
</div>
<div>
<h2>Second Section</h2>
<p>baz baz</p>
</div>
</article>
</body>
</html>
Dans python, on peut utiliser lxml :
from lxml import etree
transform = etree.XSLT(etree.parse('sample.xsl'))
print(etree.tostring(transform(etree.parse('sample.xml')))
Voila pour XSLT.
Scénario d'utilisation
Le but du jeu est de me simplifier la vie. Sans l'ajout de moteur de rendu, le code serait comme cela :
from lxml import etree
# la contruction de cet objet pique un peu autant le faire qu'une seule fois.
transform = etree.XSLT(etree.parse('sample.xsl'))
@view_config(name='some_route', renderer='string')
def some_view(request):
filename = request.POST['select_file']
return etree.tostring(transform(etree.parse(filename)))
La finalité du moteur de rendu est que le code devienne :
@view_config(name='some_route', renderer='template/sample.xsl')
def some_view(request):
filename = request.POST['selected_file']
return etree.parse(filename)
On peut également passer des variables à une XSLT. ce qui donnerait dans notre scénario :
@view_config(name='some_route', renderer='template/sample.xsl')
def some_view(request):
filename = request.POST['selected_file']
name = request.POST['name']
return etree.parse(filename), {'name': name}
L'implémentation
Pour ajouter un moteur de rendu, on a besoin d'une factory, d'un renderer et de rajouter ce rendu à pyramid.
Factory
La factory sert à construire le renderer qui effetura le rendu.
La signature de la factory est la suivante :
def factory(info):
def renderer(value, system):
pass
return renderer
ou la version orientée objet :
class Factory(object):
def __init__(self, info):
pass
def __call__(self, value, system):
pass
renderer et __call__ doivent retourner une chaine contenant le rendu.
info contient un pyramid.renderers.RendererHelper.
Dans le cas de notre moteur de rendu xsl :
class XsltRendererFactory(object):
def __init__(self, info):
"""
Factory constructor.
"""
self._info = info
def __call__(self, value, system):
"""
Call to renderer.
The renderer is cached for optimize access.
"""
xsl = XslRenderer(os.path.join(package_path(self._info.package),
system['renderer_name']))
return xsl(value, system)
XslRenderer est notre objet qui va effectuer la transformation.
Le Rendu
Le rendu doit implémenter l'interface IRenderer [1] de pyramid : juste la méthode __call__ avec la signature suivante :
[1] | IRenderer est une interface ZCA : zope.interface : voir l'épisode précédent |
def __call__(self, value, system):
pass
La méthode __call__ doit retourner une chaine avec le résulat du rendu.
value contient la donnée retournée par la méthode décorée par le view_config. system est un dictionnaire contenant :
- renderer_info : même RendererHelper que info passé à la factory,
- renderer_name : valeur de renderer du décorateur view_config ; typiquement le chemin vers le template,
- context : un object pyramid.traversal.DefaultRootFactory,
- req : l'objet request,
- request : le même objet request,
- view : la fonction décorée correspondant à la vue.
@implementer(IRenderer)
class XslRenderer(object):
def __init__(self, xslfilename):
"""
Constructor.
:param: xslfilename : path to the xsl.
"""
self._transform = etree.XSLT(etree.parse(xslfilename))
def __call__(self, value, system):
"""
Rendering.
"""
xslArgs = {}
try:
xslArgs = {key: str(value[1][key]) for key in value[1]}
except IndexError:
pass
return etree.tostring(self._transform(value[0], **xslArgs))
Le rendu est très simple à écrire, la version sur github est à peine plus compliquée pour gérer des fichiers, url ou arbre etree.
Utilisation du registre
Si le rendu est très simple, la factory mérite d'être complexifiée. Comme dit plus haut, la construction de la xsl est coûteuse à construire. On va utiliser le registre de pyramid pour contruire une seule fois la classe de rendu. Le registre est un registre ZCA ; le nom de fichier de la xsl servira de clef. La première requête construira la xsl, les suivantes n'auront qu'à utiliser l'objet construit.
class XsltRendererFactory(object):
def __init__(self, info):
"""
Factory constructor.
"""
self._info = info
def __call__(self, value, system):
"""
Call to renderer.
The renderer is cached for optimize access.
"""
registry = system['request'].registry
try:
xsl = registry.getUtility(IRenderer, system['renderer_name'])
except ComponentLookupError:
xsl = XslRenderer(os.path.join(package_path(self._info.package),
system['renderer_name']))
registry.registerUtility(xsl, IRenderer, system['renderer_name'])
return xsl(value, system)
Inclusion dans pyramid
Il reste encore à ajouter le support du rendu à pyramid. Dans la fonction main de pyramid, on rajoute où config est la configuration de pyramid.
config.add_renderer('.xsl', XsltRendererFactory)
le .xsl permet de reconnaître le moteur de rendu via @view_config.
Rendre le rendu utilisable par d'autres applications pyramid
L'intérêt de porter ou d'écrire un moteur de rendu est de le réutiliser. pyramid a un mécanisme très simple pour cela.
Dans le __init__.py de notre rendu, il suffit d'inclure le code suivant :
def includeme(config):
"""
Auto include pour pyramid
"""
config.add_renderer('.xsl', XsltRendererFactory)
Pour l'utiliser dans notre application, où on l'inclus le code suivant dans le main de pyramid :
config.include('pyramid_xslt')
ou dans fichier .ini
pyramid.includes =
pyramid_xslt
And that's all folk.