ComplianceAsCode/content

View on GitHub
ssg/jinja.py

Summary

Maintainability
A
35 mins
Test Coverage
D
69%
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'))