Templates

The openviper.template package integrates Jinja2 into OpenViper, providing a cached environment factory and a plugin auto-loader for custom filters and global functions.

Overview

Template rendering is available directly from response classes — use HTMLResponse with template= and context= in any view handler and the framework resolves and renders the named template automatically.

The environment module maintains a functools.lru_cache-backed Environment keyed by the tuple of template search paths. Every subsequent render with the same paths returns the same environment at zero cost.

Autoescape is enabled for .html and .jinja2 extensions by default.

Key Classes & Functions

openviper.template.environment.get_jinja2_env(search_paths) jinja2.Environment

Return a cached Jinja2 Environment for search_paths (a tuple of directory strings). Calls load() on first construction so that all configured filters and globals are available immediately.

Raises ImportError if jinja2 is not installed.

openviper.template.plugin_loader

The plugin loader discovers and registers custom Jinja2 filters and globals from two locations on startup (once per process):

  1. <app_dir>/jinja_plugins/ for each app in INSTALLED_APPS.

  2. The project-level directory configured by settings.JINJA_PLUGINS["path"] (default: "jinja_plugins").

Expected directory layout:

jinja_plugins/
    filters/
        slugify.py      # def slugify(value): ...
        truncate.py     # def truncate(value, length=100): ...
    globals/
        now.py          # def now(): ...

Each callable in a discovered module is registered under its own name. Private names (starting with _) and unsafe built-ins (eval, exec, etc.) are always skipped.

openviper.template.plugin_loader.load(env) None

Discover and register all plugins into env. Idempotent — calling it multiple times is safe.

Example Usage

See also

Working projects that use templates:

HTMLResponse with a Template

from openviper.routing.router import Router
from openviper.http.request import Request
from openviper.http.response import HTMLResponse

router = Router()

@router.get("/")
async def home(request: Request) -> HTMLResponse:
    posts = await Post.objects.filter(is_published=True).limit(10).all()
    return HTMLResponse(template="home.html", context={
        "request": request,
        "posts": posts,
    })

Template Structure

Place templates in a templates/ directory at the project root or inside any installed app:

myproject/
    templates/
        base.html
        home.html
    blog/
        templates/
            blog/
                post_detail.html

templates/home.html:

{% extends "base.html" %}
{% block content %}
<ul>
  {% for post in posts %}
    <li><a href="/posts/{{ post.id }}">{{ post.title }}</a></li>
  {% endfor %}
</ul>
{% endblock %}

Custom Filter Plugin

Create jinja_plugins/filters/truncate_words.py:

def truncate_words(value: str, count: int = 20) -> str:
    """Truncate *value* to at most *count* words."""
    words = value.split()
    if len(words) <= count:
        return value
    return " ".join(words[:count]) + "…"

The filter is available in all templates automatically:

{{ post.body | truncate_words(15) }}

Configuration

@dataclasses.dataclass(frozen=True)
class MySettings(Settings):
    TEMPLATES_DIR: str = "templates"   # base template search path
    JINJA_PLUGINS: dict = dataclasses.field(default_factory=lambda: {
        "enable": 1,
        "path": "jinja_plugins",
    })