Ajouter un moteur de rendu à Pyramid

Le dans «fr python» par Cyprien Le Pannérer
Mots-clés:

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.