Search code examples
pythontemplatesyamljinja2

Jinja YAML Templating: Get Value of Optional Nested Key or Use Default Value


I'm trying to create a YAML file from some inputs passed to a Jinja template. I want certain input chained keys be optional and used in the templating if present, but otherwise ignored. In the example below, override.source.property may or may not exist in the input file, and override is a top level key when it is present. Is there a way to optionally retrieve a.certain.key by its full chained path via Jinja templating?

# without_override.yaml
name: blah
# with_override.yaml
name: blah
overrides:
  source:
    property: something
# template.yaml.jinja
name: {{ name }}
source.property: {{ overrides.source.property or "property of " + name }}
source.property3: {{ overrides.source.property | default("property of " + name) }}

{# is there a way to provide a full path and return a value if it exists?
# top level document reference?
source.property2: {{ self.get("overrides.source.property") or "property of " + name }}
#}
# renderer.py
import yaml
import sys
from jinja2 import Environment, StrictUndefined, ChainableUndefined


def render_jinja(template, context):
    # jinja_env = Environment(extensions=["jinja2.ext.do"], undefined=StrictUndefined)
    jinja_env = Environment(extensions=["jinja2.ext.do"], undefined=ChainableUndefined)
    template_obj = jinja_env.from_string(template)
    return template_obj.render(**context).strip()


if __name__ == "__main__":
    with open(sys.argv[1]) as f:
        config = yaml.safe_load(f.read())

    with open("template.yaml.jinja") as f:
        template = f.read()

    print(render_jinja(template, config))
# python renderer.py with_override.yaml
# using StrictUndefined or ChainedUndefined RETURNS
name: blah
source.property: something
source.property3: something
# python renderer.py without_override.yaml
# with ChainedUndefined RETURNS
name: blah
source.property: property of blah
source.property3: property of blah

# with StrictUndefined ERRORS
jinja2.exceptions.UndefinedError: 'overrides' is undefined

Solution

  • You are doing what you are asking when you use the paramater undefined=ChainableUndefined. Without that parameter passed to the Jinja2 environment, the line below would simply raise an exception (jinja2.exceptions.UndefinedError: 'overrides' is undefined) when trying to access a variable that is undefined.

    source.property: {{ overrides.source.property | default("property of " + name) }}
    

    You can even chain several default() until a variable is defined or variables that evaluate to false.

    {{ overrides.source.property | default(defaults.source.property) | default("property of " + name) }}
    

    If you need to do something more complex you can always use Python itself and pass the processed dict to the template renderer. Like set default values and then check if there is an overridden value:

        default_values = {
            "source": {
                "property": config.get("overrides", {})
                .get("source", {})
                .get("property", "something")
            }
        }