#!/usr/bin/env python3
# Author(s): Toni Sissala
# Copyright 2020 Finnish Social Science Data Archive FSD / University of Tampere
# Licensed under the EUPL. See LICENSE.txt for full license.
"""Load genshi templates.
Usage::
from genshi_loader import add_template_folder, GenPlate
add_template_folder('/path/to/genshi/templates')
@GenPlate('identify.xml')
def handler_identify(genplate_instance):
return {}
"""
import logging
from genshi.template import (
TemplateLoader,
TemplateNotFound
)
from genshi.output import (
XMLSerializer,
PI
)
_logger = logging.getLogger(__name__)
#: Template folders
FOLDERS = []
[docs]def add_template_folders(*folders):
"""Add folder to lookup for templates.
:param folder: absolute path to folder containing genshi templates.
:type folder: str
"""
FOLDERS.extend(folders)
[docs]def get_template_folders():
"""Get template folders.
:returns: template folders.
:rtype: list
"""
return FOLDERS
class _KuhaXMLStylesheetFilter:
"""Add or discard stylesheet to XML documents that are rendered by Genshi.
:param str or None stylesheet_url: Stylesheet URL that replaces
'${stylesheet_url}' notation in templates. Set to falsey to filter
out stylesheets from templates.
"""
def __init__(self, stylesheet_url):
self.stylesheet_url = stylesheet_url
def __call__(self, stream):
for kind, data, pos in stream:
if kind is PI and '${stylesheet_url}' in data[1]:
if self.stylesheet_url:
newdata = (data[0],
data[1].replace('${stylesheet_url}', self.stylesheet_url))
yield kind, newdata, pos
continue
yield kind, data, pos
[docs]class KuhaXMLSerializer(XMLSerializer):
"""Subclass XMLSerializer to add a custom filter."""
stylesheet_url = None
def __init__(self, **kw):
super().__init__(**kw)
self.filters.append(_KuhaXMLStylesheetFilter(self.stylesheet_url))
[docs]class GenPlate:
"""Genshi template decorator.
Decorate functions that should write output to genshi-templates.
The decorated function must be an asynchronous function and it must return a dictionary.
Example::
from genshi_loader import GenPlate
class Handler:
@GenPlate('error.xml')
async def build_error_message(self, genplate_instance):
...
return {'msg': 'there was an error'}
:param template_file: filename of the template to use.
:type template_file: str
:param template_folder: optional parameter to use a different template folder
to lookup for given template_file.
:type template_folder: str
:raises: :exc:`ValueError` if decorated function returns invalid type.
"""
def __init__(self, template_file, **kw):
self._template_file = template_file
self.properties = kw
self._loader = None
def __call__(self, ctx_func):
"""Retuns asynchronous wrapper to serialize XML for output.
.. Note::
This is the wrapper-function's docstring. The wrapper
function's __doc__ is overwritten by ctx_func's __doc__ to
make docstring lookup display correct docs in Sphinx.
The wrapper calls :func:`ctx_func` with parameter :obj:`ctx` and gets
the returned value. Passes this value to the template and serializes
it as XML. Finally sends the XML-serialization to the template_writer
function.
:note: :func:`ctx_func` and :obj:`ctx` are transparent for this
function. The only requirement regarding these objets is
that :func:`ctx_func` MUST be asynchronous function and
it should return a dictionary.
:param ctx: Context that gets passed to the decorated function.
:raises: :exc:`ValueError` for invalid return type of :func:`ctx_func`
"""
async def wrapper(ctx, *args, **kwargs):
folders = get_template_folders()
if self._loader is None:
self._loader = TemplateLoader(folders)
try:
template = self._loader.load(self._template_file)
except TemplateNotFound:
_logger.error("Could not load template from folders: %s",
', '.join(folders))
raise
context = await ctx_func(ctx, *args, **kwargs)
if not isinstance(context, dict):
raise ValueError("'{}' returns invalid type '{}'. Expecting dict".format(
ctx_func, type(context)))
if 'genplate' in context:
raise ValueError("Found 'genplate' key in context. It is reserved for GenPlate properties.")
_logger.debug("Serializing template '%s'", self._template_file)
return template.generate(**{**context, **{'genplate': self.properties}})\
.serialize(method=KuhaXMLSerializer, strip_whitespace=True)
wrapper.__doc__ = ctx_func.__doc__
return wrapper
[docs] @staticmethod
def set_stylesheet_url(path):
"""Set stylesheet URL to KuhaXMLSerializer.
Call this to replace '${stylesheet_url}' notation in templates with 'path'.
:param path: Replaces stylesheet_url in templates.
"""
KuhaXMLSerializer.stylesheet_url = path