Mypal/taskcluster/taskgraph/util/templates.py

156 lines
4.6 KiB
Python

# 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 os
import pystache
import yaml
import copy
# Key used in template inheritance...
INHERITS_KEY = '$inherits'
def merge_to(source, dest):
'''
Merge dict and arrays (override scalar values)
Keys from source override keys from dest, and elements from lists in source
are appended to lists in dest.
:param dict source: to copy from
:param dict dest: to copy to (modified in place)
'''
for key, value in source.items():
# Override mismatching or empty types
if type(value) != type(dest.get(key)): # noqa
dest[key] = source[key]
continue
# Merge dict
if isinstance(value, dict):
merge_to(value, dest[key])
continue
if isinstance(value, list):
dest[key] = dest[key] + source[key]
continue
dest[key] = source[key]
return dest
def merge(*objects):
'''
Merge the given objects, using the semantics described for merge_to, with
objects later in the list taking precedence. From an inheritance
perspective, "parents" should be listed before "children".
Returns the result without modifying any arguments.
'''
if len(objects) == 1:
return copy.deepcopy(objects[0])
return merge_to(objects[-1], merge(*objects[:-1]))
class TemplatesException(Exception):
pass
class Templates():
'''
The taskcluster integration makes heavy use of yaml to describe tasks this
class handles the loading/rendering.
'''
def __init__(self, root):
'''
Initialize the template render.
:param str root: Root path where to load yaml files.
'''
if not root:
raise TemplatesException('Root is required')
if not os.path.isdir(root):
raise TemplatesException('Root must be a directory')
self.root = root
def _inherits(self, path, obj, properties, seen):
blueprint = obj.pop(INHERITS_KEY)
seen.add(path)
# Resolve the path here so we can detect circular references.
template = self.resolve_path(blueprint.get('from'))
variables = blueprint.get('variables', {})
# Passed parameters override anything in the task itself.
for key in properties:
variables[key] = properties[key]
if not template:
msg = '"{}" inheritance template missing'.format(path)
raise TemplatesException(msg)
if template in seen:
msg = 'Error while handling "{}" in "{}" circular template' + \
'inheritance seen \n {}'
raise TemplatesException(msg.format(path, template, seen))
try:
out = self.load(template, variables, seen)
except TemplatesException as e:
msg = 'Error expanding parent ("{}") of "{}" original error {}'
raise TemplatesException(msg.format(template, path, str(e)))
# Anything left in obj is merged into final results (and overrides)
return merge_to(obj, out)
def render(self, path, content, parameters, seen):
'''
Renders a given yaml string.
:param str path: used to prevent infinite recursion in inheritance.
:param str content: Of yaml file.
:param dict parameters: For mustache templates.
:param set seen: Seen files (used for inheritance)
'''
content = pystache.render(content, parameters)
result = yaml.load(content)
# In addition to the usual template logic done by mustache we also
# handle special '$inherit' dict keys.
if isinstance(result, dict) and INHERITS_KEY in result:
return self._inherits(path, result, parameters, seen)
return result
def resolve_path(self, path):
return os.path.join(self.root, path)
def load(self, path, parameters=None, seen=None):
'''
Load an render the given yaml path.
:param str path: Location of yaml file to load (relative to root).
:param dict parameters: To template yaml file with.
'''
seen = seen or set()
if not path:
raise TemplatesException('path is required')
path = self.resolve_path(path)
if not os.path.isfile(path):
raise TemplatesException('"{}" is not a file'.format(path))
content = open(path).read()
return self.render(path, content, parameters, seen)