import json import jinja2 # The Jinja2 environment __ENV = None def _env_init(force: bool = False): """ Initialize the Jinja2 environment. :param force: If true, force reload the environment. """ global __ENV if __ENV is None or force: # Use json.dumps as finalizer in order to preserve complex data structures __ENV = jinja2.Environment(finalize=json.dumps) return __ENV def render(template: str): """ Render the given string as a Jinja2 template. :param template: The template string to render. """ # Make sure the Jinja2 environment is initialized env = _env_init() # Create a Jinja2 template from the input string t = env.from_string(template) decoder = json.JSONDecoder() # Render the template and turn the JSON dump back into complex data structures # Only parse the first JSON object in the string, ignore the rest obj, i = decoder.raw_decode(t.render()) return obj def render_traverse(obj): """ Walk through a complex, JSON-serializable data structure, and pass string objects through the Jinja2 templating engine. :param obj: The object to traverse. """ if isinstance(obj, list): # list -> recurse into each item for i in range(len(obj)): obj[i] = render_traverse(obj[i]) return obj elif isinstance(obj, dict): # dict -> recurse into the value of each (key, value) for k, v in obj.items(): obj[k] = render_traverse(obj[k]) return obj elif isinstance(obj, str): # str -> template return render(obj) else: # anything else -> return as-is return obj