Metadata-Version: 2.4
Name: jinja-typed-template
Version: 0.2.0
Summary: Static type checking for Jinja templates
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jinja2>=3.1.6
Provides-Extra: flask
Requires-Dist: flask>=3.0; extra == "flask"
Dynamic: license-file

# jinja-typed-template

Type-safe Jinja2 templates with compile-time validation — keep your Python code and Jinja templates in sync, fail fast on mismatches, and never ship a broken template again.

## Installation

```bash
pip install jinja-typed-template
```

For Flask support:

```bash
pip install jinja-typed-template[flask]
```

## Quick Start

### 1. Annotate your template

Add a header comment declaring every variable the template uses, along with its type:

```jinja
{#
    name: str
    age: int
    items: list[str]
#}
<h1>Hello {{ name }}!</h1>
<p>You are {{ age }} years old.</p>
<ul>
  {% for item in items %}
    <li>{{ item }}</li>
  {% endfor %}
</ul>
```

### 2. Define a typed template class

Subclass `TypedTemplate`, set the template name, and declare fields with type hints:

```python
from jinja_typed_template import TypedTemplate, env_context
from jinja2 import Environment, FileSystemLoader

class ProfileTemplate(TypedTemplate):
    __template_name__ = "profile.html"
    name: str
    age: int
    items: list[str]
```

### 3. Render safely

Wrap usage in `env_context` to bind a Jinja2 environment, then instantiate and render:

```python
env = Environment(loader=FileSystemLoader("templates"))

with env_context(env):
    t = ProfileTemplate(name="Alice", age=30, items=["apples", "bananas"])
    print(t.render())
```

## What Gets Validated

Every `TypedTemplate` instance is validated at construction time (`__post_init__`). Three checks run automatically:

| Check | Catches |
|-------|---------|
| **Type checking** | `T(name=123)` when `name: str` — raises `TypeError` |
| **Variable matching** | Missing or extra fields vs. what the template actually uses — raises `ValueError` |
| **Header comment consistency** | Header declares `name: int` but class says `name: str` — raises `TypeError` |

This means you catch mismatches the moment you create the object, not when a user hits the page.

## API Reference

### `TypedTemplate`

Base class. Subclass it, set `__template_name__`, and declare typed fields.

| Member | Description |
|--------|-------------|
| `render()` | Render the template to a string |
| `context_dict` | Dict of all field names → values |
| `copy_updating(**kwargs)` | Return a new instance with some fields replaced (instances are frozen/immutable) |

### `env_context(env)`

Context manager that binds a Jinja2 `Environment` for the current thread/context. Required before creating or rendering any `TypedTemplate`.

```python
from jinja_typed_template import env_context

with env_context(env):
    t = MyTemplate(...)
```

### `TypedTemplateExtension` (Flask)

Flask extension that automatically pushes the app's Jinja2 environment into the context on every request. Context-processor variables are also discovered and excluded from validation, so they can appear in templates without being declared in the header.

```python
from flask import Flask
from jinja_typed_template.extension_flask import TypedTemplateExtension

app = Flask(__name__)
ext = TypedTemplateExtension()
ext.init_app(app)

# Context-processor variables are automatically recognised:
@app.context_processor
def inject_globals():
    return {"app_name": "MyApp"}
```

Now any `TypedTemplate` instantiated inside a request will just work — no manual `env_context` needed.

## Header Comment Format

Place a Jinja2 comment at the **very top** of the template:

```jinja
{#
    variable_name: TypeName
    another_var: list[str]
    optional_var: str = "default"
#}
```

- `variable_name` must match a field on your `TypedTemplate` subclass.
- `TypeName` uses a short-form representation (`str`, `int`, `list[str]`, `dict[str, int]`, etc.) and must match the Python type hint.
- Default values after `=` are stripped — they're documentation only.

## License

MIT
