Jinja2 go-to-column - jinja2

I am using Jinja2 to perform code generation.
Besides the trivial problem to generate correctly indented code I also would like to perform certain in-line alignments; example use-cases would be:
start inline comments at a certain column
align operators in assignment series
A small excerpt of (1) would be:
Sound_Chime_t chime_array[] = {
{%- for k, cmd in commands.items() %}
{
"{{ cmd['Sound Command'] }}", // command
"{{ cmd['tag'] }}", // tag
{{ cmd['Priority'] }}, // priority
{{ cmd['Mix'] }}, // mix
{{ cmd['Loop'] }}, // loop
{{ cmd['region'] }}, // region
"{{ cmd['Sound File']}}" // filename
}{{ ',' if not loop.last else '' }}
{%- endfor %}
};
Of course //... is nicely aligned in template, but it won't be in generated code.
Is there some (not too convoluted) way to obtain this?

You can align the comments using two .format() calls. The second one is required by the occasional quotation marks and commas after a value. This will pad the values to 20:
Sound_Chime_t chime_array[] = {
{%- for k, cmd in commands.items() %}
{
{{ '{:<20} // command'.format('"{}",'.format(cmd['Sound Command'])) }}
{{ '{:<20} // tag'.format('"{},"'.format(cmd['tag'])) }}
{{ '{:<20} // priority'.format('{},'.format(cmd['Priority'])) }}
{{ '{:<20} // mix'.format('{},'.format(cmd['Mix'])) }}
{{ '{:<20} // loop'.format('{},'.format(cmd['Loop'])) }}
{{ '{:<20} // region'.format('{},'.format(cmd['region'])) }}
{{ '{:<20} // filename'.format('"{}"'.format(cmd['Sound File'])) }}
}{{ ',' if not loop.last else '' }}
{%- endfor %}
};

Related

String manipulation in DBT - Jinja

I want to develop a macro that will loop rows from a seeding query's result and create a dynamic query for another task.
Let's assume my row would be similar to
<agate.Row: ('product_available', Decimal('0.6'), 'Positive')>
<agate.Row: ('product_quality', Decimal('0.5'), 'Negative')>
I intend to generate an array selection for downstream queries which have my_udf that will take arguments from my seeding query's result. For example,
my_udf("product_available", 0.6, 'Positive')
my_udf("product_quality", 0.5, 'Negative')
The problem that I have is some arguments are actual column names, while others are values. Hence, column names should have double quotes, while value must have single quote. For instance, "product_available" vs 'Positive'
My code is
{% macro generate_list_select_from(seeding_query) %}
{# ... other code to execute my seeding query ... #}
{# loop query goes here #}
{% for i in results_list %}
{% set item = "my_udf( {{ i[0] }} , {{ i[1] }}, '{{ i[2] }}' )" %}
{{items.append(item)}}
{% endfor %}
{{ return(items) }}
{% endmacro %}
Below is output when I use my macro
select foo_column,
my_udf( {{ i[0] }} , {{ i[1] }}, '{{ i[2] }}' ),
my_udf( {{ i[0] }} , {{ i[1] }}, '{{ i[2] }}' )
from foo_table
My question is how to create a such string?
Update:
Tried other way, {% set item = "my_udf(" + {{i[0]}} + ")" %}, I end up with a compilation error expected token ':', got '}'
Don't nest your curlies.
~ is the string concatenation operator in jinja. You could use that to build up your function args, including the single quotes:
{% set item = "my_udf(" ~ i[0] ~ ", " ~ i[1] ~ ", '" ~ i[2] ~ "' )" %}
This is pretty hard to read, though. I'd probably add a macro called quoted:
{% macro quoted(s) %}
'{{ s }}'
{% endmacro %}
And then use the join filter to concatenate the items of a list:
{% set args = [i[0], i[1], quoted(i[2])] %}
{% set item = "my_udf(" ~ args | join(", ") ~ ")" %}
Lastly, if the purpose of this macro is to template SQL, then you don't need to use return(items). You should just have the body of the macro template the string you want (see the quoted macro above). That dramatically simplifies things:
{% macro quoted(s) %}
'{{ s }}'
{% endmacro %}
{% macro generate_list_select_from(seeding_query) %}
{# ... other code to execute my seeding query ... #}
{# loop query goes here #}
{% for i in results_list %}
{% set args = [i[0], i[1], quoted(i[2])] %}
my_udf({{ args | join(", "}}){% if not loop.last %},{% endif %}
{% endfor %}
{% endmacro %}

Concatenate optional variables in Jinja without duplicating whitespace

Question
I want to concatenate variables in Jinja by separating them with exactly one space (' ') where some of the variables might be undefined (not known to the template which ones).
import jinja2
template = jinja2.Template('{{ title }} {{ first }} {{ middle }} {{ last }}')
result = template.render(first='John', last='Doe') # result == ' John Doe'
I've been searching for a template string alternative that returns 'John Doe' instead of ' John Doe' in this case.
First attempt
Use
{{ [title, first, middle, last]|reject("undefined")|join(" ") }}
as template (which actually works), however
it's confusing
once I want to use a macro on one of the arguments (that e.g. puts middle in brackets if it is not undefined) and therefore return an empty string instead of Undefined this stops working.
2nd attempt
Replace multiple spaces with one space
{% filter replace(" ", " ") -%}
{{ title }} {{ first }} {{ middle }} {{ last }}
{%- endfilter %}
which actually only would work in all cases with a regular expression search/replace like
{% filter regex_replace(" +", " ") -%}
{{ title }} {{ first }} {{ middle }} {{ last }}
{%- endfilter %}
but regex_replace is not built-in (but could be provided by a custom regular expression filter).
Still I would not consider this to be optimal as double spaces in the content of variables would be replaced as well.
you dont use the power of jinja2/python, use the if else and test if a variable is defined:
import jinja2
template = "{{ title ~ ' ' if title is defined else '' }}"
template += "{{first ~ ' ' if first is defined else '' }}"
template += "{{ middle ~ ' ' if middle is defined else '' }}"
template += "{{ last if last is defined else '' }}"
template = jinja2.Template(template)
result = template.render(first='John', last='Doe')
print("----------")
print(result)
print("----------")
result:
----------
John Doe
----------
if you want to use regex, use :
import re
:
:
template = jinja2.Template("{{ title }} {{ first }} {{ middle }} {{ last }}")
result = template.render(first='John', last='Doe')
result = re.sub(" +", " ", result.strip())
strip() deletes any leading and trailing whitespaces including tabs (\t)
or a mixed of both solutions to avoid to suppress spaces not wanted
import jinja2
template = "{{ title ~ ' ' if title is defined else '' }}"
template += "{{first ~ ' ' if first is defined else '' }}"
template += "{{ middle ~ ' ' if middle is defined else '' }}"
template += "{{ last if last is defined else '' }}"
template = jinja2.Template(template)
result = template.render(first='John', last='Doe').strip()
A variant of the accepted answer is to use a joiner in the Jinja template:
{%- set space = joiner(" ") %}
{%- if title %}{{ space() }}{{ title }}{% endif %}
{%- if first %}{{ space() }}{{ first }}{% endif %}
{%- if middle %}{{ space() }}{{ middle }}{% endif %}
{%- if last %}{{ space() }}{{ last }}{% endif %}
This works without any extensions, custom filters or post-processing but is relatively verbose.

Nesting variables in Jekyll/Liquid

Assumption - From what I understand, Liquid works in a way that the variable page.my_key can be compared to a PHP array with name page and key my_key: $page['my_key']. Following this same logic we could compare {{ page.my_key }} with echo $page['my_key'].
And we could compare this front matter:
----
my_key: my_value
----
to this PHP code:
$page['my_key'] = "my_value";
Question - I would like to do something like this:
$page['my_key'] = "my_value";
$page['my_key2'] = "my_value2";
$key = "my_key";
echo $page[$key];
All I can think of is:
----
my_key: my_value
my_key2: my_value2
----
{% assign key = 'my_key' %}
{{ page.{{ key }} }}
However, that does not work... Is something like this possible, though?
Beware : array and hash are two different animals.
Just create a array-hash.md (note I wrote it in markdown for brevity) page in your jekyll. Paste this code. And you will understand how they are different and how to access their items.
---
layout: default
title: array-hash
myArray:
- item 1
- item 2
- one more
# or
myArray2: [ item 1, item 2, one more item ]
myHash:
item1: toto
"item 2": titi
item 3: yoyo
---
{% comment %} +++ Shortcuts
a = page.myArray
h = page.MyHash
h2 = page.myArray2
{% endcomment %}
{% assign a = page.myArray %}
{% assign a2 = page.myArray2 %}
{% assign h = page.myHash %}
## Arrays
page.myArray : {{ a }}
page.myArray with inspect : {{ a | inspect }}
page.myArray with join : {{ a | join:", " }}
page.myArray2 : {{ a2 | inspect }}
### Looping the array
<ul>
{% for item in a %}
<li>{{ item | inspect }}</li>
{% endfor %}
</ul>
### Targeting a specific item in the array
{% comment %} arrays start at zero {% endcomment %}
second element in the array = {{ a[1] }}
Note that {% raw %}{{ a["1"] }}{% endraw %} will not work. You need to pass
an integer and not a string.
Test (not working) : { a["1"] }
## Hashes
page.myHash : {{ h }}
#### looping the hash
{% for item in h %}
{{ item | inspect }}
{% endfor %}
You note that in the loop we get arrays that returns **key as item[0]**
and **value as item[1]**
The loop can then look like :
<ul>
{% for item in h %}
<li>{{ item[0] }} : {{ item[1] }}</li>
{% endfor %}
</ul>
### Targeting a specific item in the hash
**Item1** due to the absence of space in the key name, can both me accessed
by dot notation (h.item1) or bracket notation (h["item1"]).
hash.item1 : {{ h.item1 }}
hash["item1"] : {{ h.["item1"] }}
Item 2 and 3, containing a space in their key string can only be accessed with
bracket notation :
hash.item 2 (not working) : {{ h.item 2 }}
hash["item 2"] : {{ h.["item 2"] }}
hash.item 3 (not working) : {{ h.item 3 }}
hash["item 3"] : {{ h.["item 3"] }}
I think I found the solution:
----
my_key: my_value
my_key2: my_value2
----
{% assign key = 'my_key' %}
{{ page[key] }}
Found it here.

ansible jinja2 concatenate IP addresses

I would like to cocatenate a group of ips into a string.
example ip1:2181,ip2:2181,ip3:2181,etc
{% for host in groups['zookeeper'] %}
{{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}
{% endfor %}
I have the above code, but can't seem to quite figure out how to concatenate into a string.
searching for "Jinja2 concatenate" doesn't give me the info I need.
Updated this answer, because I think I misunderstood your question.
If you want to concatenate the IP's of each host with some string, you can work with the loop controls, to check if you're in the last iteration:
{% for host in groups['zookeeper'] -%}
{{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}
{%- if not loop.last %}, {% endif -%}
{%- endfor %}
Old answer:
The word you're looking for is join:
{{ hostvars[host]['ansible_eth0']['ipv4']['address'] | join(", ") }}
You can use the 'extract' filter for this (provided you use ansible>=2.1):
{{ groups['zookeeper'] | map('extract', hostvars, ['ansible_eth0', 'ipv4', 'address']) | join(',') }}
More info:
http://docs.ansible.com/ansible/playbooks_filters.html#extracting-values-from-containers
Found a similar solution at https://adamj.eu/tech/2014/10/02/merging-groups-and-hostvars-in-ansible-variables/ .
I did a set_fact using a groups variable as suggested in the post:
- hosts: all
connection: local
tasks:
- set_fact:
fqdn_list: |
{% set comma = joiner(",") %}
{% for item in play_hosts -%}
{{ comma() }}{{ hostvars[item].ansible_default_ipv4.address }}
{%- endfor %}
This relies on joiner, which has the advantage of not having to worry about the last loop conditional. Then with set_fact I can make use of the new string in later tasks.

How to access sub level JSON trough Django

So, I get this JSON from Django:
{'something' :
{'value':'somethingName','editable':'false'}
},
{'somethingElse':
{'value':'somethingElseName','editable':'true'}
}
And show it like this:
{% for key, value in obj.items %}
{{ key }} : {{ value }}
{% endfor %}
The problem is {{ value }} returns {'value':'somethingName','editable':'false'}, and I can't access value or editable trough {{ value.value }} or {{ value.editable }}.
I'd like to show {{ value.value }} as somethingName instead of the entire JSON.
Is there a way to access 'sub-level' JSON trough Django itself?
You cannot use template variable name as a dictionary key using the . notation. The second value in value.value is not interpreted as a string value because you have a variable name value in the loop.
Just rename key and value to obj_key and obj_value respectively:
{% for obj_key, obj_value in obj.items %}
{{ obj_key }} : {{ obj_value.value }}
{% endfor %}
Demo:
>>> from django.template import Context, Template
>>> t = Template("""
... {% for obj_key, obj_value in obj.items %}
... {{ obj_key }} : {{ obj_value.value }}
... {% endfor %}""")
>>> obj = {'something' : {'value':'somethingName','editable':'false'}}
>>> t.render(Context({'obj': obj}))
u'something : somethingName'
Hope that helps.