Issues (70)

ssg/jinja.py (1 issue)

1
from __future__ import absolute_import
2
from __future__ import print_function
3
4
import os.path
5
import jinja2
6
7
try:
8
    from urllib.parse import quote
9
except ImportError:
10
    from urllib import quote
11
12
try:
13
    from shlex import quote as shell_quote
14
except ImportError:
15
    from pipes import quote as shell_quote
16
17
from .constants import JINJA_MACROS_DIRECTORY
18
from .utils import (required_key,
19
                    prodtype_to_name,
20
                    name_to_platform,
21
                    prodtype_to_platform,
22
                    banner_regexify,
23
                    banner_anchor_wrap,
24
                    escape_id,
25
                    escape_regex,
26
                    escape_yaml_key,
27
                    sha256
28
                    )
29
30
31
class MacroError(RuntimeError):
32
    pass
33
34
35
class AbsolutePathFileSystemLoader(jinja2.BaseLoader):
36
    """Loads templates from the file system. This loader insists on absolute
37
    paths and fails if a relative path is provided.
38
39
    >>> loader = AbsolutePathFileSystemLoader()
40
41
    Per default the template encoding is ``'utf-8'`` which can be changed
42
    by setting the `encoding` parameter to something else.
43
    """
44
45
    def __init__(self, encoding='utf-8'):
46
        self.encoding = encoding
47
48
    def get_source(self, environment, template):
49
        if not os.path.isabs(template):
50
            raise jinja2.TemplateNotFound(template)
51
52
        template_file = jinja2.utils.open_if_exists(template)
53
        if template_file is None:
54
            raise jinja2.TemplateNotFound(template)
55
        try:
56
            contents = template_file.read().decode(self.encoding)
57
        except Exception as exc:
58
            msg = ("Error reading file {template}: {exc}"
59
                   .format(template=template, exc=str(exc)))
60
            raise RuntimeError(msg)
61
        finally:
62
            template_file.close()
63
64
        mtime = os.path.getmtime(template)
65
66
        def uptodate():
67
            try:
68
                return os.path.getmtime(template) == mtime
69
            except OSError:
70
                return False
71
        return contents, template, uptodate
72
73
74
def _get_jinja_environment(substitutions_dict):
75
    if _get_jinja_environment.env is None:
76
        bytecode_cache = None
77
        if substitutions_dict.get("jinja2_cache_enabled") == "true":
78
            bytecode_cache = jinja2.FileSystemBytecodeCache(
79
                required_key(substitutions_dict, "jinja2_cache_dir")
80
            )
81
82
        # TODO: Choose better syntax?
83
        _get_jinja_environment.env = jinja2.Environment(
84
            block_start_string="{{%",
85
            block_end_string="%}}",
86
            variable_start_string="{{{",
87
            variable_end_string="}}}",
88
            comment_start_string="{{#",
89
            comment_end_string="#}}",
90
            loader=AbsolutePathFileSystemLoader(),
91
            bytecode_cache=bytecode_cache
92
        )
93
        _get_jinja_environment.env.filters['banner_anchor_wrap'] = banner_anchor_wrap
94
        _get_jinja_environment.env.filters['banner_regexify'] = banner_regexify
95
        _get_jinja_environment.env.filters['escape_id'] = escape_id
96
        _get_jinja_environment.env.filters['escape_regex'] = escape_regex
97
        _get_jinja_environment.env.filters['escape_yaml_key'] = escape_yaml_key
98
        _get_jinja_environment.env.filters['quote'] = shell_quote
99
        _get_jinja_environment.env.filters['sha256'] = sha256
100
101
    return _get_jinja_environment.env
102
103
104
_get_jinja_environment.env = None
105
106
107
def raise_exception(message):
108
    raise MacroError(message)
109
110
111
def update_substitutions_dict(filename, substitutions_dict):
112
    """
113
    Treat the given filename as a jinja2 file containing macro definitions,
114
    and export definitions that don't start with _ into the substitutions_dict,
115
    a name->macro dictionary. During macro compilation, symbols already
116
    existing in substitutions_dict may be used by those definitions.
117
    """
118
    template = _get_jinja_environment(substitutions_dict).get_template(filename)
119
    all_symbols = template.make_module(substitutions_dict).__dict__
120
    for name, symbol in all_symbols.items():
121
        if name.startswith("_"):
122
            continue
123
        substitutions_dict[name] = symbol
124
125
126
def process_file(filepath, substitutions_dict):
127
    """
128
    Process the jinja file at the given path with the specified
129
    substitutions. Return the result as a string. Note that this will not
130
    load the project macros; use process_file_with_macros(...) for that.
131
    """
132
    filepath = os.path.abspath(filepath)
133
    template = _get_jinja_environment(substitutions_dict).get_template(filepath)
134
    return template.render(substitutions_dict)
135
136
137
def add_python_functions(substitutions_dict):
138
    substitutions_dict['prodtype_to_name'] = prodtype_to_name
139
    substitutions_dict['name_to_platform'] = name_to_platform
140
    substitutions_dict['prodtype_to_platform'] = prodtype_to_platform
141
    substitutions_dict['url_encode'] = url_encode
142
    substitutions_dict['raise'] = raise_exception
143
    substitutions_dict['expand_yaml_path'] = expand_yaml_path
144
145
146
def load_macros(substitutions_dict=None):
147
    """
148
    Augment the substitutions_dict dict with project Jinja macros in /shared/.
149
    """
150
    if substitutions_dict is None:
151
        substitutions_dict = dict()
152
153
    add_python_functions(substitutions_dict)
154
    try:
155
        for filename in sorted(os.listdir(JINJA_MACROS_DIRECTORY)):
156
            if filename.endswith(".jinja"):
157
                macros_file = os.path.join(JINJA_MACROS_DIRECTORY, filename)
158
                update_substitutions_dict(macros_file, substitutions_dict)
159
    except Exception as exc:
160
        msg = ("Error extracting macro definitions from '{1}': {0}"
161
               .format(str(exc), filename))
0 ignored issues
show
The variable filename does not seem to be defined in case the for loop on line 155 is not entered. Are you sure this can never be the case?
Loading history...
162
        raise RuntimeError(msg)
163
164
    return substitutions_dict
165
166
167
def process_file_with_macros(filepath, substitutions_dict):
168
    """
169
    Process the file with jinja macros at the given path with the specified
170
    substitutions. Return the result as a string.
171
172
    See also: process_file
173
    """
174
    substitutions_dict = load_macros(substitutions_dict)
175
    assert 'indent' not in substitutions_dict
176
    return process_file(filepath, substitutions_dict)
177
178
179
def url_encode(source):
180
    return quote(source)
181
182
183
def expand_yaml_path(path, parameter):
184
    out = ""
185
    i = 0
186
    for x in path.split("."):
187
        i += 1
188
        if i != len(path.split(".")):
189
            out += i * "  " + x + ":\n"
190
        elif parameter != "":
191
            out += i * "  " + x + ":\n"
192
            i += 1
193
            out += i * "  " + parameter
194
        else:
195
            out += i * "  " + x
196
    return out
197
198
199
def render_template(data, template_path, output_path, loader):
200
    env = _get_jinja_environment(dict())
201
    env.loader = loader
202
    result = process_file(template_path, data)
203
    with open(output_path, "wb") as f:
204
        f.write(result.encode('utf8', 'replace'))
205