Any way to get variables keys from jinja2 template string? - python

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.

Related

Jinja2 find_undeclared_variables ignores globals?

I want to find out which variables of a Jinja2 template are not covered by globals. I load the template source, parse it and feed the result into meta.find_undeclared_variables. No matter what is in the global dictionary of the environment I get a full list of variables for the template. How do I make this operation recognize the globals in the environment and in the template and only return the list of variables not covered by them.
The sample below creates an environment, renders the template to show that global variables are indeed read and calls meta.find_undeclared_variables to show its result.
from jinja2 import Environment, meta, FunctionLoader, PrefixLoader
def load_mapping(name):
return 'Mapping %s {{version}} {{docid}}' % name
def load_link(name):
return 'Link %s {{version}} {{docid}}' % name
loader = PrefixLoader({
'link': FunctionLoader(load_link),
'map': FunctionLoader(load_mapping)
})
env = Environment(loader=loader)
globals = {'version': '1.0'}
env.globals.update(globals)
print env.get_template('map/test').render(docid='asdf')
tsrc = env.loader.get_source(env, 'link/test')
parsed = env.parse(tsrc)
print meta.find_undeclared_variables(parsed)
The code prints:
Mapping test 1.0 asdf
set(['version', 'docid'])
With version being a global I would like to change my code so that only docid is returned.
I use Python 2.7.6 and Jinja 2.7.3.
jinja_globals = env.globals.keys()
print meta.find_undeclared_variables(parsed) - set(jinja_globals)

How to make a filter to check if someone has tagged a user with #?

I am making a social networking site. I am wondering how to make a jinja filter in python to make any word that starts with # a link to their profile.
Here's a Jinja filter you can use:
from flask import Markup
def linkify(text):
return Markup(re.sub(r'#([a-zA-Z0-9_]+)', r'#\1', text))
It finds usernames starting with #, containing lowercase or uppercase letters, numbers and underscores. It replaces it a link to the profile (\1 represents their username without the #)
Here's how you'd add it to you're environment:
app.jinja_env.filters['linkify'] = linkify
And call it from a Jinja template:
{{ post|linkify }}
EDIT
Run this in a Python shell:
>>> import re
>>> text = 'This is a post mentioning #nathancahill and #jacob_bennet'
>>> re.sub(r'#([a-zA-Z0-9_]+)', r'#\1', text)
'This is a post mentioning #nathancahill and #jacob_bennet'
Do you get the same output?

Best way to display only URL domain using jinja2

I have URLs stored in a Django model that I would like to display on a template, but I would only like to display the domain like this:
original_url:
https://wikipedia.org/wiki/List_of_chemical_process_simulators
display:
wikipedia.org/...
Would it be better to handle this on entirely on the back-end, font-end, or a custom function on with jinja2?
If it is something you would later reuse in the templates throughout the project and, taking into account that there is a pretty simple logic involved, defining a custom template filter would be perfectly okay here.
Use urlparse.urlparse() to get the domain name:
>>> from urlparse import urlparse
>>> from jinja2 import Environment, Template
>>>
>>> def get_domain(url):
... return "%s/..." % urlparse(url).netloc
...
>>>
>>> env = Environment()
>>> env.filters['domain'] = get_domain
>>>
>>> template = env.from_string('{{ url|domain }}')
>>> template.render(url='https://wikipedia.org/wiki/List_of_chemical_process_simulators')
u'wikipedia.org/...'
This is a simple example, you should additionally provide an error-handling mechanism in case urlparse() would fail parsing the url passed in.
The best way would probably be a custom template filter as answered by #alecxe, but in case you can't, here is a non-fool-proof way for future reference.
{{ original_url.rpartition("//")[-1] }}
https://wikipedia.org/wiki/List_of_chemical_process_simulators → wikipedia.org/wiki/List_of_chemical_process_simulators
//example.net/path/to/file → example.net/path/to/file
ftp://example.net/pub → example.net/pub
Get just the domain name (hostname):
{{ original_url.rpartition("//")[-1].partition("/")[0] }}
https://wikipedia.org/wiki/List_of_chemical_process_simulators → wikipedia.org
//example.net/path/to/file → example.net
ftp://example.net/pub → example.net

How to reference multi-word dictionary key in mako?

I am passing a csv file to Mako via csv.DictReader, and the dictionary uses row headers for keys. Some of these have names like 'Entry Id'. Mako throws out errors when I try to reference these multi-word keys in a template. Specifically, the error message is mako.exceptions.SyntaxException: (SyntaxError) invalid syntax (<unknown>, line 1).
The following code demonstrates the issue I am experiencing:
from mako.template import Template
mydata = {'foo': 'bar', 'better foo': 'beach bar'}
working_template = Template("Let's go to the ${foo}")
fail_template = Template("Let's go to the ${better foo}")
# this works
print working_template.render(**mydata)
# this generates an Exception
print fail_template.render(**mydata)
Is there a way to escape spaces for multi-word keys?
I have found one way that works, but I'd rather send the dictionary as **kwargs if possible. Here is the working solution:
from mako.template import Template
mydata = {'foo': 'bar', 'better foo': 'beach bar'}
working_template = Template("Let's go to the ${foo}")
fail_template = Template("Let's go to the ${mydata['better foo']}")
print working_template.render(**mydata)
print fail_template.render(mydata=mydata)

Is inline code allowed in Jinja templates?

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.

Categories

Resources