Keep undefined variables - python

I am interested in rendering a template in multiple steps or keeping the tags for the undefined variables in Jinja2. I believe this would mean not only creating the 'UndefinedSilent" class (so the template won't crash on missing data) but also keeping the tags with the appropriate variable names if they are missing.
Example:
Let's say we have the name = "Test" included in the context, but quantity is missing.
Givent the following template:
<p>{{name}} has {{quantity}}</p>
After rendering, I need the template to become:
<p>test has {{quantity}}</p>
Does anyone know if this is achievable?

Providing DebugUndefined to named parameter undefined in the env, apparently does the trick. The rendered template preserves the {{<undefined variable}}.
Like here:
from jinja2 import Environment, BaseLoader, DebugUndefined
rtemplate = Environment(loader=BaseLoader,undefined=DebugUndefined).from_string("{{ a }} is defined, but {{ b}} is undefined")
print(rtemplate.render({"a":"a"}))
The result is:
a is defined, but {{ b }} is undefined

It is achievable using the default built-in filter.
<p>{{name|default('{{name}}')}} has {{quantity|default('{{quantity}}')}}</p>
The drawback is that the code becomes uglier and the variable names are duplicated, thus reducing maintainability.

Here is another approach that preserves undefined double curly expressions after rendering, including those that contain "multilevel" (dot-notated) references as well as any others.
The answer provided by Willy Pregliasco does not support preservation of undefined list types, eg {{ some_list[4] }} which is something I required. The below solution addresses this, as well as all possible schema types.
The idea is to parse the input template and try to resolve each expression with the provided context. Any that can not be resolved, we replace with a double curly expression that simply resolves to the original expression as a string.
Pass your template and context through the below preserve_undefineds_in_template function before calling render:
from jinja2 import Template, StrictUndefined, UndefinedError
import re
def preserve_undefineds_in_template(template, context):
patt = re.compile(r'(\{\{[^\{]*\}\})')
j2_expressions = patt.findall(template)
for j2_expression in set(j2_expressions):
try:
Template(j2_expression, undefined=StrictUndefined).render(context)
except UndefinedError:
template = template.replace(j2_expression, f"{{% raw %}}{j2_expression}{{% endraw %}}")
return template
Example:
template = """hello {{ name }}, {{ preserve_me }} {{ preserve.me[2] }}"""
context = { "name": "Alice" }
# pass it through the preserver function
template = preserve_undefineds_in_template(template, context)
# template is now:
# hello {{ name }}, {% raw %}{{ preserve.me }}{% endraw %} {% raw %}{{ preserve.me.too[0] }}{% endraw %}
# render the new template as normal
result = Template(template).render(context)
print(result)
The output is:
hello Alice, {{ preserve_me }} {{ preserve.me[2] }}

I also wated the same behaviour. The library jinja2schema provides the schema of variables needed for your template.
The steps for my solution are:
have a template
obtain the schema structure
give some data to fill
complete de data filling the missing items with the original string
from jinja2 import Template
import jinja2schema
def assign(schema, data, root=''):
'''Returns a corrected data with untouched missing fields
'''
out = {}
for key in schema.keys():
if isinstance(schema[key], (str, jinja2schema.model.Scalar)):
try:
out[key] = data[key]
except:
out[key] = f'{{{{ {root+key} }}}}'
elif isinstance(schema[key], (dict, jinja2schema.model.Dictionary)):
out[key]={}
try:
data[key]
except:
data[key] = {}
out[key] = assign(schema[key], data[key], root+key+'.')
return out
# original template
template_str = '<p>{{name}} has {{quantity}}</p>'
# read schema
schema = jinja2schema.infer(template_str)
# available data
data = {'name':'test'}
# data autocompleted
data_corrected = assign(schema, data)
# render
template = Template(template_str)
print(template.render(data_corrected))
The output is
<p>test has {{ quantity }}</p>
That was the intended result.
Hope it will help. This solution doesn't work with lists, but I think it is possible to extend the solution. You can also obtain the list of missing fields, if you need them.

This is the version with Template rather than Environment:
from jinja2 import Template, DebugUndefined
template = Template("<p>{{name}} has {{quantity}}</p>", undefined=DebugUndefined)
new_template = Template(template.render(name="Test"))
Thanks #giwyni
Another little hack if you just have few variables:
<p>{{name}} has {{ "{{quantity}}" }}</p>
The second replacement will replace to {{quantity}} so all is good ;)

Related

In a Django template, how do I print the value in a dictionary when the key is stored in a variable?

I'm using Django and Python 3.7. In my template, I can see the output of this
{{ articlestat.article.website.id }}
which is "4". I have a dictionary, passed to my context, whose keys are numbers. If I do the following
{{ website_stats.4 }}
I see a value printed. However if I do this
{{ website_stats[articlestat.article.website.id] }}
I get the error
Could not parse the remainder: '[articlestat.article.website.id]' from 'website_stats[articlestat.article.website.id]'
So my question is, how do I access the value of a dictionary on my template when the key is stored in a variable? I'm not in a position to hard-code the keys.
You cannot access the dict keys with this syntax in django templates. Usually, we access the key with {{ my_dict.my_key }}.
But of course this will not work if you pass a variable like articlestat.article.website, all the dots would messing things up. Even without the dots, the name following the dot cannot be a variable, the template engine uses this name as a string to lookup for a key or a property of this object. (check https://code.djangoproject.com/ticket/12486)
If you really cannot match the values from the view, where it is easier to process, this will require a simple custom template filter.
in a folder templatetags in your app, add a file named custom_filters.py with this code :
from django.template import Library
register = Library()
#register.filter
def lookup(d, key):
return d.get(key) # simple access here, you can also raise exception in case key is not found
in the templates where you need such dict access, add {% load custom_filters %} on top, and use this syntax to call your values :
{{ website_stats|lookup:articlestat.article.website.id }}

How can I restructure my code for rendering in jinja 2 template?

I'm trying to display a values from mongo database in web application. I'm using flask framework where I'm calling function which will return lists to print in a html template. My code is as bellow
dic = mycol.find(myquery)
This will contents dictionary object querying from mongodatabase
When I loop over this object I get dictionary set
for x in dic:
like each value of x contains
{'name':'john','age':'15','weight':'50'}
{'name':'ash','age':'18','weight':'60'}
{'name':'tim','age':'20','weight':'80'}
Code which I'm using for rendering these values as below
person_name = []
person_age = []
person_weight = []
for x in dic:
person_name.append(x["person_name"])
person_age.append(x["person_age"])
person_weight.append(x["person_weight"])
later I'm returning these lists for printing in html
jinja 2 code is as below
{{person_name[loop.index0]}} {{person_age[loop.index0]}} {{person_weight[loop.index0]}}
I feel this is not better approach to do this way. So is there any other better approach to do this in a very few line of code? Thanks for reading
If you're using flask you can simply pass a list or dictionary to the HTML template using the render_template() flask function.
In your case you can simply do (using this as an example). Note, where dictionary=dic is written, the first dictionary can be any name / variable, which you will then reference in the Jinja2 syntax.
#app.route('/')
def index():
# Code to get dictionary from db, assigned to dic like yours
return render_template('[name of template].html', dictionary=dic)
Then inside your template you can use Jinja2 syntax to do a for loop to list through the dic dictionary / list.
// Somewhere inside HTML template
{% for x in dictionary %}
<p>{{ x.name }}</p>
<p>{{ x.age }}</p>
<p>{{ x.weight }}</p>
{% endfor %}

Jinja2 - Use Set (Assignments) to call customer_function

I want to call a python function from a jinja template. At the doucmentation (http://jinja.pocoo.org/docs/2.10/templates/ see Assignments) the following example is offered:
{% set key, value = call_something() %}
But the following source does not work.
import jinja2
#jinja2.contextfunction
def hw():
return "World"
template = jinja2.Template('{% set test = hw() %} Hello {{ test }}!')
template.render(name='John Doe')
Error: ... UndefinedError: 'hw' is undefined
Does any one knows how to solve the problem?!
Thank's to all!
You may simply supply it as a key=value pair in the template.render function, just as you have supplied the name variable.
import jinja2
def hw():
return "World"
template = jinja2.Template('{% set test = hw() %} Hello {{ test }}!')
print(template.render(name='John Doe', hw=hw))
Alternatively, if and when you plan to use a Jinja environment, you may add it to the globals dictionary before any templates are created, should you want the function to be accessible from every template within said environment.
import jinja2
def hw():
return "World"
template = """
{% set test = hw() %} Hello {{ test }}!
"""
env = jinja2.Environment()
env.globals['hw'] = hw
template = env.from_string(template)
print(template.render(name='John Doe'))
I've used the from_string method here to load your template, but there is a whole loaders system for environments which you may want to investigate.

How can I set a (non-global) variable in a jinja context function?

I want to set variable values, but only when they are not already assigned and within a local context.
So there is a solution to this:
{% with x=( x | default(1)) %}
{{ x }}
{% endwith %}
{% with x=2 %}
{% with x=( x | default(1)) %}
{{ x }}
{% endwith %}
{% endwith %}
This works nicely but it is a lot of text. I have many situations where I don't have just one but up to 20 variables that are being set and then a macro is called, or a template is included with those values.
Writing all those default conditions is just a mess and provokes mistakes. So I would love to be able to set a value on the current context in e.g. in a context function. But if I try the following:
#contextfunction
def defaults(ctx, **vals):
for k,v in vals.iteritems():
if k not in ctx:
ctx[k] = v
I get the an exception:
TypeError: 'Context' object does not support item assignment
And trying to set a value on ctx.vars would not help either:
vars
The template local variables. This list contains environment and context functions from the parent scope as well as local
modifications and exported variables from the template. The template
will modify this dict during template evaluation but filters and
context functions are not allowed to modify it.
http://jinja.pocoo.org/docs/2.9/api/#jinja2.Context.vars
I tried with
#contextfunction
def defaults(ctx, **vals):
for k,v in vals.iteritems():
if k not in ctx.vars:
ctx.vars[k] = v
And it gives no exception but just seems to not assign the value to the context.
I know I could write to the global context but that's not what I would like to do as it would produce side effects.
Is there a possibility to get just the current context and set a value on it? I didn't find any instructions on it and how this could be done and I did not really grasp that from reading the jinja source.
I found a solution, somehow I keep solving my own problems. It is not exactly an answer to the question of "How can I set a (non-global) variable in a jinja context function?", but it solves the problem.
I have written a jinja extension, that allows for a simple "default" tag:
from jinja2 import nodes
from jinja2.ext import Extension
"""
DefaultExtension
~~~~~~~~~~~~~~~~
Very simple jinja extension that allows for the following
{% set x=(x | default(1)) %}
{% set y=(y | default(2)) %}
to be written as
{% default x=1, y=2 %}
:copyright: (c) 2017 by the Roman Seidl
:license: BSD
"""
class DefaultExtension(Extension):
# a set of names that trigger the extension.
tags = set(['default'])
def parse(self, parser):
#just dump the tag
lineno = next(parser.stream).lineno
#parse through assignments (similar to parser.parse_with)
assignments = []
while parser.stream.current.type != 'block_end':
lineno = parser.stream.current.lineno
if assignments:
parser.stream.expect('comma')
target = parser.parse_assign_target()
parser.stream.expect('assign')
expr = (parser.parse_expression())
#consruct a 'default' filter
filter = nodes.Filter(nodes.Name(target.name, 'load'), 'default', [expr], [], None, None, lineno=lineno)
#produce an assignment with this filter as value
assignment = nodes.Assign(target, filter, lineno=lineno)
assignments.append(assignment)
return assignments
I just had to add it to my app:
app.jinja_env.add_extension(DefaultExtension)
and it works quite nicely, though I must confess I haven't yet tested it very thoroughly.
Anyone think I should submit this to jinja?

How to pluralize a name in a template with jinja2?

If I have a template variable called num_countries, to pluralize with Django I could just write something like this:
countr{{ num_countries|pluralize:"y,ies" }}
Is there a way to do something like this with jinja2? (I do know this doesn't work in jinja2) What's the jinja2 alternative to this?
Thanks for any tip!
Guy Adini's reply is definitely the way to go, though I think (or maybe I misused it) it is not exactly the same as pluralize filter in Django.
Hence this was my implementation (using decorator to register)
#app.template_filter('pluralize')
def pluralize(number, singular = '', plural = 's'):
if number == 1:
return singular
else:
return plural
This way, it is used exactly the same way (well, with parameters being passed in a slightly different way):
countr{{ num_countries|pluralize:("y","ies") }}
Current Jinja versions have the i18n extension which adds decent translation and pluralization tags:
{% trans count=list|length %}
There is {{ count }} {{ name }} object.
{% pluralize %}
There are {{ count }} {{ name }} objects.
{% endtrans %}
You can use this even if you don't actually have multiple language versions - and if you ever add other languages you'll have a decent base which requires no changes (not all languages pluralize by adding an 's' and some even have multiple plural forms).
According to Jinja's documentation, there is no built in filter which does what you want. You can easily design a custom filter to do that, however:
def my_plural(str, end_ptr = None, rep_ptr = ""):
if end_ptr and str.endswith(end_ptr):
return str[:-1*len(end_ptr)]+rep_ptr
else:
return str+'s'
and then register it in your environment:
environment.filters['myplural'] = my_plural
You can now use my_plural as a Jinja template.
You also want to check if the word is already plural. Here is my solution:
def pluralize(text):
if text[-1:] !='s':
return text+'s'
else:
return text
Then register the tag to your environment (this can be applied to the Django templating engine too).

Categories

Resources