Commit 65cd02ec authored by Vincent Pelletier's avatar Vincent Pelletier

Add jinja2-based template recipe entry.

parent c582a98c
......@@ -5,6 +5,7 @@
# "setup.py test"-installed eggs
/zc.buildout-*.egg
/Jinja2-*.egg
# Editor backupfiles
.*.swp
......
2.3 (unreleased)
================
* No changes yet.
* Add jinja2 entry point with jinja2 template support. [Vincent Pelletier]
2.2 (2011-10-12)
================
......
include CHANGES.txt
include slapos/recipe/template/README.txt
include slapos/recipe/template/README.jinja2.txt
......@@ -6,3 +6,5 @@ Template recipe which supports remote resource.
Inspired by collective.recipe.template, with minimum set of features, but with
(hopefully) safer buildout-based templating.
"jinja2" entry point allows rendering jinja2 templates.
......@@ -24,11 +24,13 @@ setup(name=name,
install_requires=[
'setuptools', # namespaces
'zc.buildout', # plays with buildout
'jinja2',
],
zip_safe=True,
entry_points={
'zc.buildout': [
'default = slapos.recipe.template:Recipe',
'jinja2 = slapos.recipe.template.jinja2_template:Recipe',
]},
test_suite = "slapos.recipe.template.tests.test_suite",
)
Usage
=====
Getting started
---------------
Example buildout demonstrating some types::
>>> write('buildout.cfg',
... '''
... [buildout]
... parts = template
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = foo.in
... rendered = foo
... context =
... key bar section:key
... key recipe :recipe
... raw knight Ni !
... json words ["Peng", "Neee-wom"]
... jsonkey later section:later-words
... import json_module json
... section param_dict parameter-collection
...
... [parameter-collection]
... foo = 1
... bar = bar
...
... [section]
... key = value
... later-words = "Ekke Ekke Ekke Ekke Ptangya Ziiinnggggggg Ni!"
... ''')
And according Jinja2 template (kept simple, control structures are possible)::
>>> write('foo.in',
... '{{bar}}\n'
... 'Knights who say "{{knight}}" also protect {{ words | join(", ") }}.\n'
... 'They later say {{later}}\n'
... '${this:is_literal}\n'
... 'swallow: {{ json_module.dumps(("african", "european")) }}\n'
... 'parameters from section: {{ param_dict | dictsort }}\n'
... 'Rendered with {{recipe}}'
... )
We run buildout::
>>> print system(join('bin', 'buildout')),
Installing template.
And the template has been rendered::
>>> cat('foo')
value
Knights who say "Ni !" also protect Peng, Neee-wom.
They later say Ekke Ekke Ekke Ekke Ptangya Ziiinnggggggg Ni!
${this:is_literal}
swallow: ["african", "european"]
parameters from section: [('bar', 'bar'), ('foo', '1')]
Rendered with slapos.recipe.template:jinja2
Parameters
----------
Mandatory:
``template``
Template url/path, as accepted by zc.buildout.download.Download.__call__ .
``rendered``
Where rendered template should be stored.
Optional:
``context``
Jinja2 context specification, one variable per line, with 3
whitespace-separated parts: type, name and expression. Available types are
described below. "name" is the variable name to declare. Expression semantic
varies depending on the type.
Available types:
``raw``
Immediate literal string.
``json``
Immediate json-encoded string.
``key``
Indirect literal string.
``jsonkey``
Indirect json-encoded string.
``import``
Import a python module.
``section``
Make a whole buildout section available to template, as a dictionary.
Indirection targets are specified as: [section]:key .
It is possible to use buildout's buit-in variable replacement instead instead
of ``key`` or ``jsonkey`` types, but keep in mind that different lines are
different variables for this recipe. It might be what you want (factorising
context chunk declarations), otherwise you should use indirect types.
``md5sum``
Template's MD5, for file integrity checking. By default, no integrity check
is done.
``umask``
Umask, in octal notation (no need for 0-prefix), to create output file with.
Defaults to system's umask at the time recipe is instanciated.
``extensions``
Jinja2 extensions to enable when rendering the template,
whitespace-separated. By default, none is loaded.
Use jinja2 extensions
~~~~~~~~~~~~~~~~~~~~~
>>> write('foo.in',
... '''{% set foo = ['foo'] -%}
... {% do foo.append(bar) -%}
... {{ foo | join(', ') }}''')
>>> write('buildout.cfg',
... '''
... [buildout]
... parts = template
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = foo.in
... rendered = foo
... context = key bar buildout:parts
... # We don't actually use all those extensions in this minimal example.
... extensions = jinja2.ext.do jinja2.ext.loopcontrols
... jinja2.ext.with_
... ''')
>>> print system(join('bin', 'buildout')),
Uninstalling template.
Installing template.
>>> cat('foo')
foo, template
Check file integrity
~~~~~~~~~~~~~~~~~~~~
Compute template's MD5 sum::
>>> write('foo.in', '{{bar}}')
>>> import md5
>>> md5sum = md5.new(open('foo.in', 'r').read()).hexdigest()
>>> write('buildout.cfg',
... '''
... [buildout]
... parts = template
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = foo.in
... rendered = foo
... context = key bar buildout:parts
... md5sum = ''' + md5sum + '''
... ''')
>>> print system(join('bin', 'buildout')),
Uninstalling template.
Installing template.
>>> cat('foo')
template
If the md5sum doesn't match, the buildout fail::
>>> write('buildout.cfg',
... '''
... [buildout]
... parts = template
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = foo.in
... rendered = foo
... context = key bar buildout:parts
... md5sum = 0123456789abcdef0123456789abcdef
... ''')
>>> print system(join('bin', 'buildout')),
While:
Installing.
Getting section template.
Initializing part template.
Error: MD5 checksum mismatch for local resource at 'foo.in'.
Specify filesystem permissions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can specify the umask for rendered file::
>>> write('template.in', '{{bar}}')
>>> write('buildout.cfg',
... '''
... [buildout]
... parts = template
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = foo.in
... rendered = foo
... context = key bar buildout:parts
... umask = 570
... ''')
>>> print system(join('bin', 'buildout')),
Uninstalling template.
Installing template.
And the generated file with have the right permissions::
>>> import stat
>>> import os
>>> print oct(stat.S_IMODE(os.stat('foo').st_mode))
0206
Section dependency
------------------
You can use other part of buildout in the template. This way this parts
will be installed as dependency::
>>> write('foo.in', '{{bar}}')
>>> write('buildout.cfg', '''
... [buildout]
... parts = template
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = foo.in
... rendered = foo
... context = key bar dependency:foobar
...
... [dependency]
... foobar = dependency content
... recipe = zc.buildout:debug
... ''')
>>> print system(join('bin', 'buildout')),
Uninstalling template.
Installing dependency.
foobar='dependency content'
recipe='zc.buildout:debug'
Installing template.
This way you can get options which are computed in the ``__init__`` of
the dependent recipe.
Let's create a sample recipe modifying its option dict::
>>> write('setup.py',
... '''
... from setuptools import setup
...
... setup(name='samplerecipe',
... entry_points = {
... 'zc.buildout': [
... 'default = main:Recipe',
... ],
... }
... )
... ''')
>>> write('main.py',
... '''
... class Recipe(object):
...
... def __init__(self, buildout, name, options):
... options['data'] = 'foobar'
...
... def install(self):
... return []
... ''')
Let's just use ``buildout.cfg`` using this egg::
>>> write('foo.in', '{{bar}}')
>>> write('buildout.cfg',
... '''
... [buildout]
... develop = .
... parts = template
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = foo.in
... rendered = foo
... context = key bar sample:data
...
... [sample]
... recipe = samplerecipe
... ''')
>>> print system(join('bin', 'buildout')),
Develop: ...
Uninstalling template.
Uninstalling dependency.
Installing sample.
Installing template.
>>> cat('foo')
foobar
##############################################################################
#
# Copyright (c) 2012 Vifib SARL and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import errno
import os
import json
import zc.buildout
from jinja2 import Template, StrictUndefined
from contextlib import contextmanager
@contextmanager
def umask(mask):
original = os.umask(mask)
try:
yield original
finally:
os.umask(original)
def getKey(expression, buildout, _,options):
section, entry = expression.split(':')
if section:
return buildout[section][entry]
else:
return options[entry]
def getJsonKey(expression, buildout, _, __):
return json.loads(getKey(expression, buildout, _, __))
EXPRESSION_HANDLER = {
'raw': (lambda expression, _, __, ___: expression),
'key': getKey,
'json': (lambda expression, _, __, ___: json.loads(expression)),
'jsonkey': getJsonKey,
'import': (lambda expression, _, __, ___: __import__(expression)),
'section': (lambda expression, buildout, _, __: dict(buildout[expression])),
}
class Recipe(object):
def __init__(self, buildout, name, options):
self.template = zc.buildout.download.Download(
buildout['buildout'],
hash_name=True,
)(
options['template'],
md5sum=options.get('md5sum'),
)[0]
self.rendered = options['rendered']
self.extension_list = [x for x in (y.strip() for y in options.get('extensions', '').split()) if x]
self.context = context = {}
for line in options.get('context').splitlines(False):
if not line:
continue
expression_type, variable_name, expression = line.split(None, 2)
if variable_name in context:
raise ValueError('Duplicate context entry %r' % (
variable_name, ))
context[variable_name] = EXPRESSION_HANDLER[expression_type](
expression, buildout, name, options)
if 'umask' in options:
self.umask = int(options['umask'], 8)
else:
self.umask = os.umask(0)
os.umask(self.umask)
def install(self):
if os.path.lexists(self.rendered):
# Unlink any existing file, so umask is always applied.
os.unlink(self.rendered)
with umask(self.umask):
outdir = os.path.dirname(self.rendered)
if outdir and not os.path.exists(outdir):
os.makedirs(outdir)
with open(self.rendered, 'w') as out:
out.write(Template(
open(self.template).read(),
extensions=self.extension_list,
undefined=StrictUndefined,
).render(
**self.context
)
)
return self.rendered
update = install
......@@ -41,6 +41,7 @@ def test_suite():
optionflags=doctest.ELLIPSIS,
) for filename in [
'README.txt',
'README.jinja2.txt',
]
])
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment