Commit 320f29b2 authored by Vincent Pelletier's avatar Vincent Pelletier

Add jinja2 "import" directive support.

Based largely on work by Timothée Lacroix <timothee.lacroix@tiolive.com>,
plus:
- reworked loader classes
- reduced unnecessary diffs
- added rawfile and rawfolder types
- documented & tested
parent 95897331
......@@ -120,6 +120,29 @@ Optional:
Jinja2 extensions to enable when rendering the template,
whitespace-separated. By default, none is loaded.
``import-delimiter``
Delimiter character for in-temlate imports.
Defaults to ``/``.
See also: import-list
``import-list``
Declares a list of import paths. Format is similar to ``context``.
"name" becomes import's base name.
Available types:
``rawfile``
Literal path of a file.
``file``
Indirect path of a file.
``rawfolder``
Literal path of a folder. Any file in such folder can be imported.
``folder``
Indirect path of a folder. Any file in such folder can be imported.
FAQ
---
......@@ -236,6 +259,142 @@ And the generated file with have the right permissions::
>>> print oct(stat.S_IMODE(os.stat('foo').st_mode))
0206
Template imports
~~~~~~~~~~~~~~~~
Here is a simple template importing an equaly-simple library:
>>> write('template.in', '''
... {%- import "library" as library -%}
... {{ library.foo() }}
... ''')
>>> write('library.in', '{% macro foo() %}FOO !{% endmacro %}')
To import a template from rendered template, you need to specify what can be
imported::
>>> write('buildout.cfg', '''
... [buildout]
... parts = template
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = template.in
... rendered = bar
... import-list = rawfile library library.in
... ''')
>>> print system(join('bin', 'buildout')),
Uninstalling template.
Installing template.
>>> cat('bar')
FOO !
Just like context definition, it also works with indirect values::
>>> write('buildout.cfg', '''
... [buildout]
... parts = template
...
... [template-library]
... path = library.in
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = template.in
... rendered = bar
... import-list = file library template-library:path
... ''')
>>> print system(join('bin', 'buildout')),
Uninstalling template.
Installing template.
>>> cat('bar')
FOO !
This also works to allow importing from identically-named files in different
directories::
>>> write('template.in', '''
... {%- import "dir_a/1.in" as a1 -%}
... {%- import "dir_a/2.in" as a2 -%}
... {%- import "dir_b/1.in" as b1 -%}
... {%- import "dir_b/c/1.in" as bc1 -%}
... {{ a1.foo() }}
... {{ a2.foo() }}
... {{ b1.foo() }}
... {{ bc1.foo() }}
... ''')
>>> mkdir('a')
>>> mkdir('b')
>>> mkdir(join('b', 'c'))
>>> write(join('a', '1.in'), '{% macro foo() %}a1foo{% endmacro %}')
>>> write(join('a', '2.in'), '{% macro foo() %}a2foo{% endmacro %}')
>>> write(join('b', '1.in'), '{% macro foo() %}b1foo{% endmacro %}')
>>> write(join('b', 'c', '1.in'), '{% macro foo() %}bc1foo{% endmacro %}')
All templates can be accessed inside both folders::
>>> write('buildout.cfg', '''
... [buildout]
... parts = template
...
... [template-library]
... path = library.in
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = template.in
... rendered = bar
... import-list =
... rawfolder dir_a a
... rawfolder dir_b b
... ''')
>>> print system(join('bin', 'buildout')),
Uninstalling template.
Installing template.
>>> cat('bar')
a1foo
a2foo
b1foo
bc1foo
It is possible to override default path delimiter (without any effect on final
path)::
>>> write('template.in', r'''
... {%- import "dir_a\\1.in" as a1 -%}
... {%- import "dir_a\\2.in" as a2 -%}
... {%- import "dir_b\\1.in" as b1 -%}
... {%- import "dir_b\\c\\1.in" as bc1 -%}
... {{ a1.foo() }}
... {{ a2.foo() }}
... {{ b1.foo() }}
... {{ bc1.foo() }}
... ''')
>>> write('buildout.cfg', r'''
... [buildout]
... parts = template
...
... [template-library]
... path = library.in
...
... [template]
... recipe = slapos.recipe.template:jinja2
... template = template.in
... rendered = bar
... import-delimiter = \
... import-list =
... rawfolder dir_a a
... rawfolder dir_b b
... ''')
>>> print system(join('bin', 'buildout')),
Uninstalling template.
Installing template.
>>> cat('bar')
a1foo
a2foo
b1foo
bc1foo
Section dependency
------------------
......
......@@ -27,11 +27,13 @@
import os
import json
import zc.buildout
from jinja2 import Template, StrictUndefined
from jinja2 import Environment, StrictUndefined, \
BaseLoader, TemplateNotFound, PrefixLoader
from contextlib import contextmanager
_buildout_safe_dumps = getattr(zc.buildout.buildout, 'dumps', None)
DUMPS_KEY = 'dumps'
DEFAULT_IMPORT_DELIMITER = '/'
@contextmanager
def umask(mask):
......@@ -61,8 +63,69 @@ EXPRESSION_HANDLER = {
buildout[expression])),
}
class RelaxedPrefixLoader(PrefixLoader):
"""
Same as PrefixLoader, but accepts imports lacking separator.
"""
def get_source(self, environment, template):
if self.delimiter not in template:
template += self.delimiter
return super(RelaxedPrefixLoader, self).get_source(environment,
template)
class RecipeBaseLoader(BaseLoader):
"""
Base class for import classes altering import path.
"""
def __init__(self, path, delimiter):
self.base = os.path.normpath(path)
self.delimiter = delimiter
def get_source(self, environment, template):
path = self._getPath(template)
# Code adapted from jinja2's doc on BaseLoader.
if path is None or not os.path.exists(path):
raise TemplateNotFound(template)
mtime = os.path.getmtime(path)
with file(path) as f:
source = f.read().decode('utf-8')
return source, path, lambda: mtime == os.path.getmtime(path)
def _getPath(self, template):
raise NotImplementedError
class FileLoader(RecipeBaseLoader):
"""
Single-path loader.
"""
def _getPath(self, template):
if template:
return None
return self.base
class FolderLoader(RecipeBaseLoader):
"""
Multi-path loader (to allow importing a folder's content).
"""
def _getPath(self, template):
path = os.path.normpath(os.path.join(
self.base,
*template.split(self.delimiter)
))
if path.startswith(self.base):
return path
return None
LOADER_TYPE_DICT = {
'rawfile': (FileLoader, EXPRESSION_HANDLER['raw']),
'file': (FileLoader, getKey),
'rawfolder': (FolderLoader, EXPRESSION_HANDLER['raw']),
'folder': (FolderLoader, getKey),
}
class Recipe(object):
mode = None
loader = None
def __init__(self, buildout, name, options):
self.template = zc.buildout.download.Download(
......@@ -72,6 +135,24 @@ class Recipe(object):
options['template'],
md5sum=options.get('md5sum'),
)[0]
import_delimiter = options.get('import-delimiter',
DEFAULT_IMPORT_DELIMITER)
import_dict = {}
for line in options.get('import-list', '').splitlines(False):
if not line:
continue
expression_type, alias, expression = line.split(None, 2)
if alias in import_dict:
raise ValueError('Duplicate import-list entry %r' % alias)
loader_type, expression_handler = LOADER_TYPE_DICT[
expression_type]
import_dict[alias] = loader_type(
expression_handler(expression, buildout, name, options),
import_delimiter,
)
if import_dict:
self.loader = RelaxedPrefixLoader(import_dict,
delimiter=import_delimiter)
self.rendered = options['rendered']
self.extension_list = [x for x in (y.strip()
for y in options.get('extensions', '').split()) if x]
......@@ -114,10 +195,13 @@ class Recipe(object):
if self.mode is not None:
os.fchmod(out_fd, self.mode)
with os.fdopen(out_fd, 'w') as out:
out.write(Template(
open(self.template).read(),
out.write(
Environment(
extensions=self.extension_list,
undefined=StrictUndefined,
loader=self.loader,
).from_string(
open(self.template).read(),
).render(
**self.context
)
......
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