Completed
Pull Request — master (#2849)
by Lakshmi
06:30
created

use_none()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
dl 0
loc 5
rs 9.4285
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import json
17
import six
18
19
import jinja2
20
21
from st2common.jinja.filters import regex
22
from st2common.jinja.filters import time
23
from st2common.jinja.filters import version
24
25
26
__all__ = [
27
    'get_jinja_environment',
28
    'render_values',
29
    'is_jinja_expression'
30
]
31
32
# Magic string to which None type is serialized when using use_none filter
33
NONE_MAGIC_VALUE = '%*****__%NONE%__*****%'
34
35
JINJA_EXPRESSIONS_START_MARKERS = [
36
    '{{',
37
    '{%'
38
]
39
40
41
def use_none(value):
42
    if value is None:
43
        return NONE_MAGIC_VALUE
44
45
    return value
46
47
48
def get_filters():
49
    return {
50
        'regex_match': regex.regex_match,
51
        'regex_replace': regex.regex_replace,
52
        'regex_search': regex.regex_search,
53
54
        'to_human_time': time.to_human_time,
55
56
        'version_compare': version.version_compare,
57
        'version_more_than': version.version_more_than,
58
        'version_less_than': version.version_less_than,
59
        'version_equal': version.version_equal,
60
        'version_match': version.version_match,
61
        'version_bump_major': version.version_bump_major,
62
        'version_bump_minor': version.version_bump_minor,
63
        'version_bump_patch': version.version_bump_patch,
64
        'version_strip_patch': version.version_strip_patch,
65
        'use_none': use_none
66
    }
67
68
69
def get_jinja_environment(allow_undefined=False, trim_blocks=True, lstrip_blocks=True):
70
    '''
71
    jinja2.Environment object that is setup with right behaviors and custom filters.
72
73
    :param strict_undefined: If should allow undefined variables in templates
74
    :type strict_undefined: ``bool``
75
76
    '''
77
    undefined = jinja2.Undefined if allow_undefined else jinja2.StrictUndefined
78
    env = jinja2.Environment(  # nosec
79
        undefined=undefined,
80
        trim_blocks=trim_blocks,
81
        lstrip_blocks=lstrip_blocks
82
    )
83
    env.filters.update(get_filters())
84
    env.tests['in'] = lambda item, list: item in list
85
    return env
86
87
88
def render_values(mapping=None, context=None, allow_undefined=False):
89
    """
90
    Render an incoming mapping using context provided in context using Jinja2. Returns a dict
91
    containing rendered mapping.
92
93
    :param mapping: Input as a dictionary of key value pairs.
94
    :type mapping: ``dict``
95
96
    :param context: Context to be used for dictionary.
97
    :type context: ``dict``
98
99
    :rtype: ``dict``
100
    """
101
102
    if not context or not mapping:
103
        return mapping
104
105
    # Add in special __context variable that provides an easy way to get access to entire context.
106
    # This mean __context is a reserve key word although backwards compat is preserved by making
107
    # sure that real context is updated later and therefore will override the __context value.
108
    super_context = {}
109
    super_context['__context'] = context
110
    super_context.update(context)
111
112
    env = get_jinja_environment(allow_undefined=allow_undefined)
113
    rendered_mapping = {}
114
    for k, v in six.iteritems(mapping):
115
        # jinja2 works with string so transform list and dict to strings.
116
        reverse_json_dumps = False
117
        if isinstance(v, dict) or isinstance(v, list):
118
            v = json.dumps(v)
119
            reverse_json_dumps = True
120
        else:
121
            v = str(v)
122
123
        try:
124
            rendered_v = env.from_string(v).render(super_context)
125
        except Exception as e:
126
            # Attach key and value which failed the rendering
127
            e.key = k
128
            e.value = v
129
            raise e
130
131
        # no change therefore no templatization so pick params from original to retain
132
        # original type
133
        if rendered_v == v:
134
            rendered_mapping[k] = mapping[k]
135
            continue
136
        if reverse_json_dumps:
137
            rendered_v = json.loads(rendered_v)
138
        rendered_mapping[k] = rendered_v
139
    return rendered_mapping
140
141
142
def is_jinja_expression(value):
143
    """
144
    Function which very simplisticly detect if the provided value contains or is a Jinja
145
    expression.
146
    """
147
    if not value or not isinstance(value, six.string_types):
148
        return False
149
150
    for marker in JINJA_EXPRESSIONS_START_MARKERS:
151
        if marker in value:
152
            return True
153
154
    return False
155