Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,62 @@ templates = Jinja2Templates(directory='templates')
templates.env.filters['marked'] = marked_filter
```

## Context processors

A context processor is a function that returns a dictionary to be merged into a template context.
Every function takes only one argument `request` and must return a dictionary to add to the context.

A common use case of template processors is to extend the template context with shared variables.

```python
import typing
from starlette.requests import Request

def app_context(request: Request) -> typing.Dict[str, typing.Any]:
return {
'app': request.app,
}
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated
```

### Registering context templates

Pass context processors to `context_processors` argument of the `Jinja2Templates` class.

```python
import typing

from starlette.requests import Request
from starlette.templating import Jinja2Templates

def app_context(request: Request) -> typing.Dict[str, typing.Any]:
return {'app': request.app}

templates = Jinja2Templates(directory='templates', context_processors=[
app_context,
])
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated
```

### Asynchronous context processors
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated

Asynchronous context processors are not supported. You have several options to workaround it:
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated
1. perform IO operations in the view and pass their results to the template context as usually
2. do IO operations in the middleware, set their results into `request.state` and then read it in the context processor
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated

```python
class MyTeamsMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated
scope.setdefault('state', {})
scope['state']['teams'] = await fetch_teams()
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated
await self.app(scope, receive, send)


def teams_context_processor(request):
return {'teams': request.state.teams}
```

Comment thread
alex-oleshkevich marked this conversation as resolved.

## Testing template responses

When using the test client, template responses include `.template` and `.context`
Expand Down
14 changes: 13 additions & 1 deletion starlette/templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from os import PathLike

from starlette.background import BackgroundTask
from starlette.requests import Request
from starlette.responses import Response
from starlette.types import Receive, Scope, Send

Expand Down Expand Up @@ -59,10 +60,16 @@ class Jinja2Templates:
"""

def __init__(
self, directory: typing.Union[str, PathLike], **env_options: typing.Any
self,
directory: typing.Union[str, PathLike],
context_processors: typing.Optional[
typing.List[typing.Callable[[Request], typing.Dict[str, typing.Any]]]
] = None,
**env_options: typing.Any
) -> None:
assert jinja2 is not None, "jinja2 must be installed to use Jinja2Templates"
self.env = self._create_env(directory, **env_options)
self.context_processors = context_processors or []

def _create_env(
self, directory: typing.Union[str, PathLike], **env_options: typing.Any
Expand Down Expand Up @@ -94,6 +101,11 @@ def TemplateResponse(
) -> _TemplateResponse:
if "request" not in context:
raise ValueError('context must include a "request" key')

request = typing.cast(Request, context["request"])
for context_processor in self.context_processors:
context.update(context_processor(request))

template = self.get_template(name)
return _TemplateResponse(
template,
Expand Down
29 changes: 29 additions & 0 deletions tests/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,32 @@ def test_template_response_requires_request(tmpdir):
templates = Jinja2Templates(str(tmpdir))
with pytest.raises(ValueError):
templates.TemplateResponse("", {})


def test_calls_context_processors(tmpdir, test_client_factory):
path = os.path.join(tmpdir, "index.html")
with open(path, "w") as file:
file.write("<html>Hello {{ username }}</html>")
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated

async def homepage(request):
return templates.TemplateResponse("index.html", {"request": request})

def hello_world_processor(request):
return {"username": "World"}

app = Starlette(
debug=True,
routes=[Route("/", endpoint=homepage)],
)
templates = Jinja2Templates(
directory=str(tmpdir),
Comment thread
alex-oleshkevich marked this conversation as resolved.
Outdated
context_processors=[
hello_world_processor,
],
)

client = test_client_factory(app)
response = client.get("/")
assert response.text == "<html>Hello World</html>"
assert response.template.name == "index.html"
assert set(response.context.keys()) == {"request", "username"}