summaryrefslogtreecommitdiffstats
path: root/taskcluster/taskgraph/transforms/base.py
blob: aab1392521995a4c9ec633436efffb677a69e5a8 (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
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from __future__ import absolute_import, print_function, unicode_literals

import re
import pprint
import voluptuous


class TransformConfig(object):
    """A container for configuration affecting transforms.  The `config`
    argument to transforms is an instance of this class, possibly with
    additional kind-specific attributes beyond those set here."""
    def __init__(self, kind, path, config, params):
        # the name of the current kind
        self.kind = kind

        # the path to the kind configuration directory
        self.path = path

        # the parsed contents of kind.yml
        self.config = config

        # the parameters for this task-graph generation run
        self.params = params


class TransformSequence(object):
    """
    Container for a sequence of transforms.  Each transform is represented as a
    callable taking (config, items) and returning a generator which will yield
    transformed items.  The resulting sequence has the same interface.

    This is convenient to use in a file full of transforms, as it provides a
    decorator, @transforms.add, that will add the decorated function to the
    sequence.
    """

    def __init__(self, transforms=None):
        self.transforms = transforms or []

    def __call__(self, config, items):
        for xform in self.transforms:
            items = xform(config, items)
            if items is None:
                raise Exception("Transform {} is not a generator".format(xform))
        return items

    def __repr__(self):
        return '\n'.join(
            ['TransformSequence(['] +
            [repr(x) for x in self.transforms] +
            ['])'])

    def add(self, func):
        self.transforms.append(func)
        return func


def validate_schema(schema, obj, msg_prefix):
    """
    Validate that object satisfies schema.  If not, generate a useful exception
    beginning with msg_prefix.
    """
    try:
        return schema(obj)
    except voluptuous.MultipleInvalid as exc:
        msg = [msg_prefix]
        for error in exc.errors:
            msg.append(str(error))
        raise Exception('\n'.join(msg) + '\n' + pprint.pformat(obj))


def get_keyed_by(item, field, item_name, subfield=None):
    """
    For values which can either accept a literal value, or be keyed by some
    other attribute of the item, perform that lookup.  For example, this supports

        chunks:
            by-test-platform:
                macosx-10.11/debug: 13
                win.*: 6
                default: 12

    The `item_name` parameter is used to generate useful error messages.
    The `subfield` parameter, if specified, allows access to a second level
    of the item dictionary: item[field][subfield]. For example, this supports

        mozharness:
            config:
                by-test-platform:
                    default: ...
    """
    value = item[field]
    if not isinstance(value, dict):
        return value
    if subfield:
        value = item[field][subfield]
        if not isinstance(value, dict):
            return value

    assert len(value) == 1, "Invalid attribute {} in {}".format(field, item_name)
    keyed_by = value.keys()[0]
    values = value[keyed_by]
    if keyed_by.startswith('by-'):
        keyed_by = keyed_by[3:]  # extract just the keyed-by field name
        if item[keyed_by] in values:
            return values[item[keyed_by]]
        for k in values.keys():
            if re.match(k, item[keyed_by]):
                return values[k]
        if 'default' in values:
            return values['default']
        for k in item[keyed_by], 'default':
            if k in values:
                return values[k]
        else:
            raise Exception(
                "Neither {} {} nor 'default' found while determining item {} in {}".format(
                    keyed_by, item[keyed_by], field, item_name))
    else:
        raise Exception(
            "Invalid attribute {} keyed-by value {} in {}".format(
                field, keyed_by, item_name))