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?
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()
You can add to global variables which can be accessed from Jinja templates. You can put your own function definitions in there, which do whatever you need.
Related
I want to pre-process C source code with jinja2 and I would like for some macros to be able to output #line lines:
#!/usr/bin/env python3
from jinja2 import *
#pass_context
def mymacro(ctx):
return '#line ?? "??"'
env = Environment()
env.globals["mymacro"] = mymacro
rr = env.from_string(
"""
// file.h
{{ mymacro() }}
"""
).render()
print(rr)
How do I get current line within mymacro global? I tried inspecting jinja2.runtime.Context, but I can't find anything helpful. Is this possible? Note that the line of macro invocation is visible when an exception is thrown - so it is stored somewhere.
This is the line that brought a solution:
template = tb.tb_frame.f_globals.get("__jinja_template__")
Source: debug.py#L55
The variable tb being a exception traceback, in this context.
And then, looking further, I realised that Jinja is using this line __jinja_template__ to frame where the template lines are in the stack of Python.
With that, and the function get_corresponding_lineno that they are using a few lines later in the debug.py file:
template = tb.tb_frame.f_globals.get("__jinja_template__")
if template is not None:
lineno = template.get_corresponding_lineno(tb.tb_lineno)
Source: debug.py#L58
It was now quite clear how to achieve it:
get the whole Python stack
loop over it until you find the template boundary
translate the current line of Python code in a line of the template with the help of get_corresponding_lineno
This gives:
#!/usr/bin/env python3
from jinja2 import *
from inspect import stack, currentframe
def mymacro():
for frameInfo in stack():
if frameInfo.frame.f_globals.get("__jinja_template__") is not None:
template = frameInfo.frame.f_globals.get("__jinja_template__")
break
return (
'#line '
f'{template.get_corresponding_lineno(currentframe().f_back.f_lineno)}'
)
env = Environment()
env.globals["mymacro"] = mymacro
rr = env.from_string(
"""
// file.h
{{ mymacro() }}
"""
).render()
print(rr)
Which prints us:
// file.h
#line 4
I am new in saltstack and i have some troubles creating a python function to make some regex checks.
i have this function
from re import sub, match, search
def app_instance_match(app):
instance_no = 0
m = search('^(.*)(-)(\d)$', app)
if m is not None:
app = m.group(1)
instance_no = int(m.group(3))
return app, instance_no
when i call it from console with
salt-ssh -i 'genesis-app-1' emod.app_instance_match test-14
i get
$ salt-ssh -i 'genesis-app-1' emod.app_instance_match test-14
genesis-app-1:
- test-14
- 0
When i try to use it inside a sls file like
{% set app = salt['emod.app_instance_match'](app) %}
i cannot use the app anymore. i tried
{% for x,y in app %}
test:
cmd.run:
- names:
- echo {{x} {{y}}
or like
cmd.run:
- names:
- echo {{app}}
I know that it return to me a dictionary but i am unable to access the values of it. The only thing that i need is the 2 returns from the python function: test-14 and 0.
when i echo for testing the X from the loop fox x,y in app i saw values like retcode, stdout, stderror.
Is there any other way to syntax the
{% set app = salt['emod.app_instance_match'](app) %}
something like that so will have 2 set variables in sls
{% set app,no = salt['emod.app_instance_match'](app) %}
i also tried like
{% set app = salt['emod.app_instance_match'](app).items() %}
I am missing something in the syntax but i cannot find anything in the internet to help me continue. I have the values that i want inside app, but i am not able to access them to take the part that i want.
First, You are not getting a dict back, you are getting a tuple back. there is a big difference. second {% set app,no = salt['emod.app_instance_match'](app) %} is exactly what you should be using. that will split the variables into two parts app and no. I should note sometimes using salt-ssh actually makes debugging things in salt harder. I would suggest installing a local minion to at least test these basic things.
Here is an example using your own code. I named it epp instead of emod.
[root#salt00 tests]# cat tests.sls
{% set x,y = salt['epp.app_instance_match']('test-14') %}
x: {{x}}
y: {{y}}
[root#salt00 tests]# salt-call slsutil.renderer salt://tests/tests.sls default_render=jinja
local:
----------
x:
test-14
y:
0
[root#salt00 tests]# cat ../_modules/epp.py
from re import sub, match, search
def app_instance_match(app):
instance_no = 0
m = search('^(.*)(-)(\d)$', app)
if m is not None:
app = m.group(1)
instance_no = int(m.group(3))
return app, instance_no
The second thing is you might want to look at https://docs.saltproject.io/en/latest/topics/jinja/index.html#regex-search which is already a regex search.
And third. Your regex looks off. ^ and $ don't really work well with single strings. which would explain why test-14 didn't come back as ('test',1) but instead came back as ('test-14',0)
I'm thinking you want '(.*)-(\d*)' as your real regex. which will return ('test',14) for test-14
# conf.py
language='en'
html_extra_path = ["customize.html"]
<!-- customize.html -->
{{ variables }} <!-- from the conf.py -->
{{ language }} <!-- expected output: en -->
How can I let the customize.html know the variables is from the config?
.. note:: assume the customize.html file is not in the documents of the theme.
I can do it by myself with Jinja, but this is not what I want.
I think sphinx already provides a way to do the things, does anyone know what is it?
Solution 1: modify sphinx-build.exe process
I hack the code (i.e. you could not build with sphinx-build.exe directly) to achieve it.
First, we observe sphinx-build.exe do what things.
# site-packages\Sphinx-x.x.x.dist-info\entry_points.txt
[console_scripts]
...
sphinx-build = sphinx.cmd.build:main
...
and then you know it actually calls sphinx.cmd.build:main to run,
you can reference it and make modifications to satisfying you.
For example:
import sphinx.cmd.build
from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.cmd.build import patch_docutils, docutils_namespace, handle_exception, Sphinx
def setup_extra_html(app):
html_builder = app.builder
ctx = {attr: app.config[attr] for attr in dir(app.config) if not attr.startswith('_')}
html_builder.globalcontext = ctx.copy()
# Please put your HTML to the ``templates_path`` that you define, since it concept about the BuiltinTemplateLoader.pathchain
pagename = 'disqus_statistic' # <-- your HTML, you can set it on the conf.py and then get it with ``ctx``
templatename = f'{pagename}.html'
html_builder.handle_page(pagename=pagename, addctx=dict(), templatename=templatename, outfilename=None)
def your_build_main(*args):
...
try:
with patch_docutils(source_dir)), docutils_namespace():
app = Sphinx(...)
if isinstance(app.builder, StandaloneHTMLBuilder):
setup_extra_html(app)
app.build(force_all=False, filenames)
return app.statuscode
except (Exception, KeyboardInterrupt) as exc:
...
cmd_list = [source_dir, output_dir, '-b', 'html', ...]
sphinx.cmd.build.build_main = your_build_main # override it.
sphinx.cmd.build.main(cmd_list) # it will call sphinx.cmd.build.build_main
And now, the following contents will work as you expected.
<!-- original disqus_statistic.html -->
{%- if html_favicon %}
Test Icon
{%- endif %}
{{ language }}
you should complete the detail by yourself since the code is too long.
or you can refer my script of sphinx_cmd_build.py
Solution 2: add the plugin (extension)
tl;dr
# your_extension.py
from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
import pathlib
def expand_init_builder(app):
Sphinx._init_builder(app)
do_something(app)
def setup(app: Sphinx):
app.add_config_value('config_value_define_by_you', default='', rebuild=True)
app._init_builder = lambda: expand_init_builder(app)
def do_something(app: Sphinx):
user_config = {attr: app.config[attr] for attr in dir(app.config) if not attr.startswith('_')} # all variable of conf.py
# user_config.update(...) # Hard coding is fine, but not recommend.
user_config.update(dict(Path=pathlib.Path)) # recommend you, it's useful.
html_builder: StandaloneHTMLBuilder = app.builder
html_builder.globalcontext = user_config
html_builder.handle_page(pagename=page_name, addctx=dict(), templatename=template_name, outfilename=None)
# conf.py
# sys.path.insert(...)
extensions.append('your_extension') # Make sure your scrips can found in sys.path.
# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-templates_path
templates_path = ['_templates/sphinx_rtd_theme'] # I hope you know what I mean... see the above link.
# I put my_html.html in ``_templates/sphinx_rtd_theme/my_html.html``
config_value_define_by_you = "https://github.com/CarsonSlovoka/typing-game"
<!-- my_html.html -->
{{ Path(config_value_define_by_you).name }} <!-- render result: typing-game -->
Long story (explain solutions2)
Sphinx-build.exe doing what things?
init # <-- and this one is not we cared.
create an instance of Sphinx(), the instance is app. i.e. app = Sphinx(...)
app.build
it calls the builder to start building, the builder format which defines by the user, in my case the build format is HTML, so its builder is StandaloneHTMLBuilder
And then, you know all files that are created by the builder.
The idea is: if we can get the builder, then we can do anything we want.
You will find the builder that is creat after the Sphinx(...),
so the solutions one, I tell you setup_extra_html after the app = Sphinx(...)
If you don't like to write these codes and think it's too complex.
The second way is to write the extensions,
the concept is the same as the above -- try to get the builder
you see the Sphinx(...) its constructor, and you find the code as below
class Sphinx:
def __init__(...):
...
# load all user-given extension modules
for extension in self.config.extensions:
self.setup_extension(extension) # <-- the extension you write
...
# create the builder
self.builder = self.create_builder(buildername) <-- this is we want
self._init_env(freshenv)
self._init_builder()
And then, you know the normal way to create the extensions which couldn't get the builder,
but you notice that if you do something after the self._init_builder() finished, then it's ok.
I provide my projects for your reference.
The real things that I want is, I want to create a page and I hope it can count the numbers of comments for each article, and show me.
You will understand, if I don't use the sphinx, but choose naming convention, then I must hard code a lot of things.
solution 1: https://github.com/CarsonSlovoka/typing-game/commit/376c9e20eac4a3c9b269b5bfbc8adb85ad9f6d36
solution 2: https://github.com/CarsonSlovoka/typing-game/commit/69411e15f1ace853edcafafc14759ba79b7ac288
demo: https://carsonslovoka.github.io/typing-game/en/doc.html#statistic -> and then, click counts of the Disqus.
original HTML
I hope you will get help and think it useful!
Here is the code snippet I have initialized jinja2 template
from jinja2 import Template
templ = Template("{{foo}} to {{bar}}")
And I am willing to extract the template string variable keys from the template obj as below.
templ.keys() == ["foo", "bar"]
Is there any API to make it work? I have searched for a long while but got nothing work.
using jinja2.meta you could:
from jinja2 import Environment, meta
templ_str = "{{foo}} to {{bar}}"
env = Environment()
ast = env.parse(templ_str)
print(meta.find_undeclared_variables(ast)) # {'bar', 'foo'}
which returns the set of the undeclared variables.
you could also use the regex module to find the variable names in your template string:
from jinja2 import Template
import re
rgx = re.compile('{{(?P<name>[^{}]+)}}')
templ_str = "{{foo}} to {{bar}}"
templ = Template(templ_str)
variable_names = {match.group('name') for match in rgx.finditer(templ_str)}
print(variable_names) # {'foo', 'bar'}
the regex (could probably be better...) matches {{ followed by anything that is not a curly bracket until }} is encountered.
I build simple webpage using CherryPy and Jinja2
The webserver python file:
import cherrypy
from jinja2 import Environment, FileSystemLoader
from soltyslib import listFiles
env = Environment(loader=FileSystemLoader('templates'))
class HelloWorld(object):
#cherrypy.expose
def index(self):
template = env.get_template('index.html')
result = template.render(name='Pawel',files=listFiles('templates'))
return result
cherrypy.quickstart(HelloWorld())
Template file:
Hello {{name}}!
<ul>
{% for file in files %}
<li>{{file}}</li>
{% endfor %}
</ul>
Ok, and then I run webserver, I go to 127.0.0.1:8080 and see result that is expected:
Hello Pawel!
templates\index.html
templates\list.html
But then I hit refresh in browser and this is a result:
Hello Pawel!
templates\index.html
templates\list.html
templates\index.html
templates\list.html
Why? for loop is evaluated again? How to prevent from doing so?
And in case somebody wondering how listFiles function looks:
import os,sys
from collections import deque
def listFiles(cdir, fileslist=[]):
basedir = cdir
queuedir = deque()
queuedir.append(basedir)
while len(queuedir) > 0:
currentbase = queuedir.popleft()
for f in os.listdir(currentbase):
f = os.path.join(currentbase,f)
if os.path.isdir(f):
queuedir.append(f)
else:
fileslist.append(f)
return fileslist
Your problem is
def listFiles(cdir, fileslist=[]):
You're reusing the same list on every call, because default arguments are evaluated only when the function is defined, not every time it's called. See "Least Astonishment" and the Mutable Default Argument for a long discussion if this behavior.
Do
def listFiles(cdir, fileslist=None):
if fileslist is None:
fileslist = []
Your problem is in fileslist=[] in the function declaration. Default values are only ever evaluated once, which means that the list is created the first call, but never rebuilt or cleared.
It's the default kwarg for fileslist in listFiles. That list is created once at module load time and keeps accruing items as you append.