I'm using Jinja on my site and I like it.
I've come across a simple need. How to display today's date? Is there a way to inline some Python code in a Jinja template?
import datetime
now = datetime.datetime.utcnow()
print now.strftime("%Y-%m-%d %H:%M")
This article says no, but suggests using a macro or a filter?
Really? Must we resort to all that? OK, what would that look like in this case?
Jinja2 is a full-featured template engine for Python. It has full unicode support, an optional integrated sandboxed execution environment, widely used and BSD licensed.
Jinja works with Python 2.7. x and >= 3.5. If you are using Python 3.2 you can use an older release of Jinja (2.6) as support for Python 3.2 was dropped in Jinja version 2.7. The last release which supported Python 2.6 and 3.3 was Jinja 2.10.
No, there is no way to inline Python into Jinja. However, you can add to the constructs that Jinja knows by extending the Environment of the template engine or the global namespace available to all templates. Alternately, you can add a filter that let's you format datetime objects.
Flask stores the Jinja2 Environment on app.jinja_env
. You can inject new context into the environment by either adding to this dictionary directly, or by using the @app.context_processor
decorator.
Whatever path you choose, this should be done while you are setting up the application, before you have served any requests. (See the snippets section of the website for some good examples of how to set up filters - the docs contain a good example of adding to the global variables).
The current answers are correct for pretty much every situation. However there are some very rare cases where you would want to have python code inside the template. In my case I want to use it to preprocess some latex files and I would prefer to keep the python code generating table values, plots, etc, inside the latex file it self.
So I made a Jinja2 extension that adds a new "py" block allowing python code to be written inside the template. Please keep in mind that I had to do some questionable work-arounds to get this to work, so I'm not 100% sure in which situations it fails or behaves unexpectedly.
This is an example template.
Foo was given to the template
foo: {{ foo }}
Bar was not, so it is missing
bar is missing: {{ bar == missing }}
{% py %}
# Normal python code in here
# Excess indentation will be removed.
# All template variables are accessible and can be modified.
import numpy as np
a = np.array([1, 2])
m = np.array([[3, 4], [5, 6]])
bar = m @ a * foo
# It's also possible to template the python code.
{% if change_foo %}
foo = 'new foo value'
{% endif %}
print("Stdio is redirected to the output.")
{% endpy %}
Foo will have the new value if you set change_foo to True
foo: {{ foo }}
Bar will now have a value.
bar: {{ bar }}
{% py %}
# The locals from previous blocks are accessible.
m = m**2
{% endpy %}
m:
{{ m }}
The output if we set the template parameters to foo=10, change_foo=True
is:
Foo was given to the template
foo: 10
Bar was not, so it is missing
bar is missing: True
Stdio is redirected to the output.
Foo will have the new value if you set change_foo to True
foo: new foo value
Bar will now have a value.
bar: [110 170]
m:
[[ 9 16]
[25 36]]
The extension with a main function to run the example.
from jinja2 import Environment, PackageLoader, nodes
from jinja2.ext import Extension
from textwrap import dedent
from io import StringIO
import sys
import re
import ctypes
def main():
env = Environment(
loader=PackageLoader('python_spike', 'templates'),
extensions=[PythonExtension]
)
template = env.get_template('emb_py2.txt')
print(template.render(foo=10, change_foo=True))
var_name_regex = re.compile(r"l_(\d+)_(.+)")
class PythonExtension(Extension):
# a set of names that trigger the extension.
tags = {'py'}
def __init__(self, environment: Environment):
super().__init__(environment)
def parse(self, parser):
lineno = next(parser.stream).lineno
body = parser.parse_statements(['name:endpy'], drop_needle=True)
return nodes.CallBlock(self.call_method('_exec_python',
[nodes.ContextReference(), nodes.Const(lineno), nodes.Const(parser.filename)]),
[], [], body).set_lineno(lineno)
def _exec_python(self, ctx, lineno, filename, caller):
# Remove access indentation
code = dedent(caller())
# Compile the code.
compiled_code = compile("\n"*(lineno-1) + code, filename, "exec")
# Create string io to capture stdio and replace it.
sout = StringIO()
stdout = sys.stdout
sys.stdout = sout
try:
# Execute the code with the context parents as global and context vars and locals.
exec(compiled_code, ctx.parent, ctx.vars)
except Exception:
raise
finally:
# Restore stdout whether the code crashed or not.
sys.stdout = stdout
# Get a set of all names in the code.
code_names = set(compiled_code.co_names)
# The the frame in the jinja generated python code.
caller_frame = sys._getframe(2)
# Loop through all the locals.
for local_var_name in caller_frame.f_locals:
# Look for variables matching the template variable regex.
match = re.match(var_name_regex, local_var_name)
if match:
# Get the variable name.
var_name = match.group(2)
# If the variable's name appears in the code and is in the locals.
if (var_name in code_names) and (var_name in ctx.vars):
# Copy the value to the frame's locals.
caller_frame.f_locals[local_var_name] = ctx.vars[var_name]
# Do some ctypes vodo to make sure the frame locals are actually updated.
ctx.exported_vars.add(var_name)
ctypes.pythonapi.PyFrame_LocalsToFast(
ctypes.py_object(caller_frame),
ctypes.c_int(1))
# Return the captured text.
return sout.getvalue()
if __name__ == "__main__":
main()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With