pythondictionaryrecursionjinja2

Jinja templating with recursive in dict doesn't works


I'm stuck in a Jinja implementation problem.

Here is my little python script:

path = Path(__file__).parent

env = Environment(
    loader=FileSystemLoader(path / "templates")
)
template = env.get_template("template1.rst")

rendered = template.render(sample={"a": {"b": "c"}})

and here is my template for jinja:


.. toctree::
   :maxdepth: 3

{% for k, v in sample.items() recursive %}
- {{ k }}:

  {%- if v is string %}
    {{ v }}

  {%- else %}
    {{ loop(v) }}
  
  {%- endif -%}

{%endfor%}

The execution returns this error:

File "/home/jaja/Bureau/coding/bac_a_sable/sphinx_test/templates/template1.rst", line 5, in top-level template code
    {% for k, v in sample.items() recursive %}
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jaja/Bureau/coding/bac_a_sable/sphinx_test/templates/template1.rst", line 21, in template
    {{ loop(v) }}
^^^^^^^^^^^^^^^^^^
  File "/home/jaja/Bureau/coding/bac_a_sable/sphinx_test/templates/template1.rst", line 5, in template
    {% for k, v in sample.items() recursive %}
    ^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 2, got 1)

As the v value of first loop is {"b": "c"}, it must work, but it doesn't. Is Jinja unable to recursively loop in dictionnaries ?


Solution

  • immediate fix

    When you start the loop, you use sample.items() iterator -

    {% for k, v in sample.items() recursive %}
                   ^^^^^^^^^^^^^^
    

    When you recur, you are passing the dict itself -

      {%- else %}
        {{ loop(v) }}
                ^
    

    Simply change this to -

      {%- else %}
        {{ loop(v.items()) }}
                ^^^^^^^^^
    

    naive test

    An improvement to the entire loop would be to change the test to mapping, instead of string.

    {% for k, v in sample.items() recursive %}
    - {{ k }}:
    
      {%- if v is mapping %}
        {{ loop(v.items()) }}
    
      {%- else %}
        {{ v }}
      
      {%- endif -%}
    
    {%endfor%}
    

    This ensures that you only recur on dicts. In the original code, the else will recur any non-string input. If the data included a number, you would've encountered a different error.


    nested input, nested output

    Preserving the levels of nesting in the output can be challenging. Consider using the loop.depth helper to create the correct whitespace -

    {% for k, v in sample.items() recursive %}
    {{ "  " * (loop.depth - 1) }}- {{ k }}: 
      {%- if v is mapping %} {{- loop(v.items()) }}
      {%- else %} {{ v }}
      {%- endif -%}
    {%endfor%}
    

    Given sample input -

    sample = {
      'a': {
        'b': 1,
        'c': { 'd': 2 },
        'e': 'f',
      },
    }
    

    Output

    - a: 
      - c: 
        - d: 2
      - b: 1
      - e: f