ssg/jinja.py
from __future__ import absolute_import
from __future__ import print_function
import os.path
import jinja2
try:
from urllib.parse import quote
except ImportError:
from urllib import quote
try:
from shlex import quote as shell_quote
except ImportError:
from pipes import quote as shell_quote
from .constants import JINJA_MACROS_DIRECTORY
from .utils import (required_key,
product_to_name,
name_to_platform,
product_to_platform,
banner_regexify,
banner_anchor_wrap,
escape_id,
escape_regex,
escape_yaml_key,
sha256
)
class MacroError(RuntimeError):
pass
class AbsolutePathFileSystemLoader(jinja2.BaseLoader):
"""Loads templates from the file system. This loader insists on absolute
paths and fails if a relative path is provided.
>>> loader = AbsolutePathFileSystemLoader()
Per default the template encoding is ``'utf-8'`` which can be changed
by setting the `encoding` parameter to something else.
"""
def __init__(self, encoding='utf-8'):
self.encoding = encoding
def get_source(self, environment, template):
if not os.path.isabs(template):
raise jinja2.TemplateNotFound(template)
template_file = jinja2.utils.open_if_exists(template)
if template_file is None:
raise jinja2.TemplateNotFound(template)
try:
contents = template_file.read().decode(self.encoding)
except Exception as exc:
msg = ("Error reading file {template}: {exc}"
.format(template=template, exc=str(exc)))
raise RuntimeError(msg)
finally:
template_file.close()
mtime = os.path.getmtime(template)
def uptodate():
try:
return os.path.getmtime(template) == mtime
except OSError:
return False
return contents, template, uptodate
def _get_jinja_environment(substitutions_dict):
if _get_jinja_environment.env is None:
bytecode_cache = None
if substitutions_dict.get("jinja2_cache_enabled") == "true":
bytecode_cache = jinja2.FileSystemBytecodeCache(
required_key(substitutions_dict, "jinja2_cache_dir")
)
# TODO: Choose better syntax?
_get_jinja_environment.env = jinja2.Environment(
block_start_string="{{%",
block_end_string="%}}",
variable_start_string="{{{",
variable_end_string="}}}",
comment_start_string="{{#",
comment_end_string="#}}",
loader=AbsolutePathFileSystemLoader(),
bytecode_cache=bytecode_cache
)
_get_jinja_environment.env.filters['banner_anchor_wrap'] = banner_anchor_wrap
_get_jinja_environment.env.filters['banner_regexify'] = banner_regexify
_get_jinja_environment.env.filters['escape_id'] = escape_id
_get_jinja_environment.env.filters['escape_regex'] = escape_regex
_get_jinja_environment.env.filters['escape_yaml_key'] = escape_yaml_key
_get_jinja_environment.env.filters['quote'] = shell_quote
_get_jinja_environment.env.filters['sha256'] = sha256
return _get_jinja_environment.env
_get_jinja_environment.env = None
def raise_exception(message):
raise MacroError(message)
def update_substitutions_dict(filename, substitutions_dict):
"""
Treat the given filename as a jinja2 file containing macro definitions,
and export definitions that don't start with _ into the substitutions_dict,
a name->macro dictionary. During macro compilation, symbols already
existing in substitutions_dict may be used by those definitions.
"""
template = _get_jinja_environment(substitutions_dict).get_template(filename)
all_symbols = template.make_module(substitutions_dict).__dict__
for name, symbol in all_symbols.items():
if name.startswith("_"):
continue
substitutions_dict[name] = symbol
def process_file(filepath, substitutions_dict):
"""
Process the jinja file at the given path with the specified
substitutions. Return the result as a string. Note that this will not
load the project macros; use process_file_with_macros(...) for that.
"""
filepath = os.path.abspath(filepath)
template = _get_jinja_environment(substitutions_dict).get_template(filepath)
return template.render(substitutions_dict)
def add_python_functions(substitutions_dict):
substitutions_dict['product_to_name'] = product_to_name
substitutions_dict['name_to_platform'] = name_to_platform
substitutions_dict['product_to_platform'] = product_to_platform
substitutions_dict['url_encode'] = url_encode
substitutions_dict['raise'] = raise_exception
substitutions_dict['expand_yaml_path'] = expand_yaml_path
def load_macros(substitutions_dict=None):
"""
Augment the substitutions_dict dict with project Jinja macros in /shared/.
"""
if substitutions_dict is None:
substitutions_dict = dict()
add_python_functions(substitutions_dict)
try:
for filename in sorted(os.listdir(JINJA_MACROS_DIRECTORY)):
if filename.endswith(".jinja"):
macros_file = os.path.join(JINJA_MACROS_DIRECTORY, filename)
update_substitutions_dict(macros_file, substitutions_dict)
except Exception as exc:
msg = ("Error extracting macro definitions from '{1}': {0}"
.format(str(exc), filename))
raise RuntimeError(msg)
return substitutions_dict
def process_file_with_macros(filepath, substitutions_dict):
"""
Process the file with jinja macros at the given path with the specified
substitutions. Return the result as a string.
See also: process_file
"""
substitutions_dict = load_macros(substitutions_dict)
assert 'indent' not in substitutions_dict
return process_file(filepath, substitutions_dict)
def url_encode(source):
return quote(source)
def expand_yaml_path(path, parameter):
out = ""
i = 0
for x in path.split("."):
i += 1
if i != len(path.split(".")):
out += i * " " + x + ":\n"
elif parameter != "":
out += i * " " + x + ":\n"
i += 1
out += i * " " + parameter
else:
out += i * " " + x
return out
def render_template(data, template_path, output_path, loader):
env = _get_jinja_environment(dict())
env.loader = loader
result = process_file(template_path, data)
with open(output_path, "wb") as f:
f.write(result.encode('utf8', 'replace'))