Custom filter in jinja2 using in bottle

I try to define custom filter in jinja2 using bottle (not flask). Suppose I want to define a pandoc custom filter. Regarding the jinja2 documentation, I have to define it that way:

from bottle import jinja2_view as view, jinja2_template as template
from jinja2 import environment, filters

def pandoc_convert(s):
    return pypandoc.convert_text(s, format='md', to='html5')

@get("/foo")
def foo():
    return template('foo.html')

environment = jinja2.Environment()
# Putting my custom filter.
environment.filters['pandoc'] = pandoc_convert
run(host='localhost', port=8080, debug=True)

In template foo.html I have

  {{ "This is *some* markdown" | pandoc }}

When I run bottle code, it gets me:

jinja2.exceptions.TemplateAssertionError: No filter named 'pandoc'

but if you print environment.filters, then 'pandoc': <function pandoc_convert at 0x7f827e233040>} appears in that print.

Answer

(Updated)

This answer provided generic instructions for working with Jinja, but it looks as if bottle has its own special way of doing things. You need to ignore the Jinja documentation about environments and filters, and instead delve into the bottle source; specifically, the Jinja2Template, which looks like this:

class Jinja2Template(BaseTemplate):
    def prepare(self, filters=None, tests=None, globals={}, **kwargs):
        from jinja2 import Environment, FunctionLoader
        self.env = Environment(loader=FunctionLoader(self.loader), **kwargs)
        if filters: self.env.filters.update(filters)
        ...

Note that the prepare method accepts a filters keyword argument for setting filters on the environment it creates.

The jinja2_template function, which is defined like this:

jinja2_template = functools.partial(template, template_adapter=Jinja2Template)

And lastly, the template function, which includes:

    if tplid not in TEMPLATES or DEBUG:
        settings = kwargs.pop('template_settings', {})
        if isinstance(tpl, adapter):
            TEMPLATES[tplid] = tpl
            if settings: TEMPLATES[tplid].prepare(**settings)
        elif "n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
            TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
        else:
            TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)

So, when you call jinja2_template("foo.html") (which is what you’re doing), this becomes:

template("foo.html", template_adapter=Jinja2Template)

And within the template function, template will call the prepare method of Jinja2Template with keyword arguments from settings, which comes from the template_settings argument to template. So, we can use a custom filter like this:

import bottle


def hellofilter(value):
    return value.replace("hello", "goodbye")


@bottle.get("/foo")
def foo():
    settings = {
        "filters": {
            "hello": hellofilter,
        }
    }
    return bottle.jinja2_template("foo.html", template_settings=settings)


if __name__ == "__main__":
    bottle.run(host="127.0.0.1", port=7070, debug=True)

If my template foo.html looks like this:

This is a test. {{ "hello world"|hello }}

Asking for 127.0.0.1:7070/foo will return:

This is a test. goodbye world

If we didn’t want to pass the template_settings argument every time we call jinja2_template, we could use functools.partial ourselves to create a wrapper for the jinja2_template function:

import bottle
import functools


def hellofilter(value):
    return value.replace("hello", "goodbye")


template_settings = {
    "filters": {
        "hello": hellofilter,
    },
}
template = functools.partial(
    bottle.jinja2_template, template_settings=template_settings
)


@bottle.get("/foo")
def foo():
    return template("foo.html")


if __name__ == "__main__":
    bottle.run(host="127.0.0.1", port=7070, debug=True)