Quick Solution
The existing answer works just fine, but since you asked for any improvements I will try.
from jinja2 import Template
records = [{'a':1,'b':1, 'c':1},{'a':1,'b':1, 'c':1}, {'a':2,'b':1, 'c':1}, {'a':3,'b':1, 'c':1}]
t = Template("""
{%- set record_info = dict() %}
{%- for item in records %}
{%- set key = item['a'] ~ ':' ~ item['b'] ~ ':' ~ item['c'] %}
{%- do record_info.update({key: 1 if not record_info[key] else record_info[key]+1}) %}
{%- endfor -%}
{{record_info}}""",
extensions=['jinja2.ext.do']
)
print(t.render(records=records))
Improvements
- Show the imports - some SO users coming here might be brand new to Python and need full context
- Use Python3 and the
print()
function (Not a criticism - I realise this is back in 2017, although from __future__
would have worked too )
- Allow use of the do "expression statement" extension
- Use
do
instead of setting the _dummy
variable
- Avoid all the extra rendered newlines/whitespace with
{%-
and -%}
adding the -
into those begin/end tags
Other changes
Whether or not these are improvements is subjective - is readability improved or hindered?
- use inline if (ternary) to enable removal of the
else
clause
Notes
Using default value
This line:
{%- do record_info.update({key: 1 if not record_info[key] else record_info[key]+1}) %}
Could also be written using default
:
{%- do record_info.update({key: (record_info[key] | default(0)) + 1 }) %}
Note that the check on the existing record_info[key]
pipes to the default value even though it is an error when the key does not yet exist.
For example, this does not work:
{%- do record_info.update({key: (record_info[key]+1 | default(1)) }) %}
...because when it tries to evaluate record_info[key]+1
the record_info[key]
error is not handled.
Environment and Loaders
The docs recommend using a Loader
e.g. PackageLoader
which enables template inheritance amongst other things. In your example it looks like a quick set-up so I'll ignore that requirement. Note using the Template constructor directly will create a shared environment anyway without the benefit of the Loader or inheritance.
If you use the PackageLoader
you would have a package name and a folder called templates
is used to load them. It's also nice to have your templates in separate files.
For a quick way to use a real loader without having those separate files, the FunctionLoader
is useful. You could add more template strings to the function and return them by name, e.g.
from jinja2 import Environment, FunctionLoader
records = [{'a':1,'b':1, 'c':1},{'a':1,'b':1, 'c':1}, {'a':2,'b':1, 'c':1}, {'a':3,'b':1, 'c':1}]
def my_template(name: str) -> str:
if name == 'first_one':
return """
{%- set record_info = dict() %}
{%- for item in records %}
{%- set key = item['a'] ~ ':' ~ item['b'] ~ ':' ~ item['c'] %}
{%- do record_info.update({key: (record_info[key] | default(0)) + 1}) %}
{%- endfor -%}
{{record_info}}"""
else:
return """
"""
jinja_env = Environment(
loader=FunctionLoader(my_template),
extensions=['jinja2.ext.do']
)
t = jinja_env.get_template('first_one')
print(t.render(records=records))
I know it's just setting up a quick & dirty example for experimenting, but I think this is a better boilerplate starting point because it explicitly uses the jinja Environment
and Loaders
so can be extended more easily, and when I inevitably come back to my own answer in a few years when I forget all this I will appreciate it :D