summaryrefslogtreecommitdiffstats
path: root/jenkins_jobs/formatter.py
blob: 6159678030061a69519c6646a47bb1cb7f9ffd94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#!/usr/bin/env python
# Copyright (C) 2015 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

# Manage interpolation of JJB variables into template strings.

import logging
from pprint import pformat
import re
from string import Formatter

from jenkins_jobs.errors import JenkinsJobsException
from jenkins_jobs.local_yaml import CustomLoader

logger = logging.getLogger(__name__)


def deep_format(obj, paramdict, allow_empty=False):
    """Apply the paramdict via str.format() to all string objects found within
       the supplied obj. Lists and dicts are traversed recursively."""
    # YAML serialisation was originally used to achieve this, but that places
    # limitations on the values in paramdict - the post-format result must
    # still be valid YAML (so substituting-in a string containing quotes, for
    # example, is problematic).
    if hasattr(obj, "format"):
        try:
            ret = CustomFormatter(allow_empty).format(obj, **paramdict)
        except KeyError as exc:
            missing_key = exc.args[0]
            desc = "%s parameter missing to format %s\nGiven:\n%s" % (
                missing_key,
                obj,
                pformat(paramdict),
            )
            raise JenkinsJobsException(desc)
        except Exception:
            logging.error(
                "Problem formatting with args:\nallow_empty:"
                "%s\nobj: %s\nparamdict: %s" % (allow_empty, obj, paramdict)
            )
            raise

    elif isinstance(obj, list):
        ret = type(obj)()
        for item in obj:
            ret.append(deep_format(item, paramdict, allow_empty))
    elif isinstance(obj, dict):
        ret = type(obj)()
        for item in obj:
            try:
                ret[
                    CustomFormatter(allow_empty).format(item, **paramdict)
                ] = deep_format(obj[item], paramdict, allow_empty)
            except KeyError as exc:
                missing_key = exc.args[0]
                desc = "%s parameter missing to format %s\nGiven:\n%s" % (
                    missing_key,
                    obj,
                    pformat(paramdict),
                )
                raise JenkinsJobsException(desc)
            except Exception:
                logging.error(
                    "Problem formatting with args:\nallow_empty:"
                    "%s\nobj: %s\nparamdict: %s" % (allow_empty, obj, paramdict)
                )
                raise
    else:
        ret = obj
    if isinstance(ret, CustomLoader):
        # If we have a CustomLoader here, we've lazily-loaded a template;
        # attempt to format it.
        ret = deep_format(ret, paramdict, allow_empty=allow_empty)
    return ret


class CustomFormatter(Formatter):
    """
    Custom formatter to allow non-existing key references when formatting a
    string
    """

    _expr = r"""
        (?<!{){({{)*                # non-pair opening {
        (?:obj:)?                   # obj:
        (?P<key>\w+)                # key
        (?:\|(?P<default>[^}]*))?   # default fallback
        }(}})*(?!})                 # non-pair closing }
    """

    def __init__(self, allow_empty=False):
        super(CustomFormatter, self).__init__()
        self.allow_empty = allow_empty

    def vformat(self, format_string, args, kwargs):
        matcher = re.compile(self._expr, re.VERBOSE)

        # special case of returning the object if the entire string
        # matches a single parameter
        try:
            result = re.match("^%s$" % self._expr, format_string, re.VERBOSE)
        except TypeError:
            return format_string.format(**kwargs)
        if result is not None:
            try:
                return kwargs[result.group("key")]
            except KeyError:
                pass

        # handle multiple fields within string via a callback to re.sub()
        def re_replace(match):
            key = match.group("key")
            default = match.group("default")

            if default is not None:
                if key not in kwargs:
                    return default
                else:
                    return "{%s}" % key
            return match.group(0)

        format_string = matcher.sub(re_replace, format_string)

        return Formatter.vformat(self, format_string, args, kwargs)

    def get_value(self, key, args, kwargs):
        try:
            return Formatter.get_value(self, key, args, kwargs)
        except KeyError:
            if self.allow_empty:
                logger.debug(
                    "Found uninitialized key %s, replaced with empty string", key
                )
                return ""
            raise