Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add template context processors. #1904

Merged
merged 20 commits into from Dec 29, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/templates.md
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,
}
alex-oleshkevich marked this conversation as resolved.
Show resolved Hide resolved
```

### 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,
])
alex-oleshkevich marked this conversation as resolved.
Show resolved Hide resolved
```

### Asynchronous context processors
alex-oleshkevich marked this conversation as resolved.
Show resolved Hide resolved

Asynchronous context processors are not supported. You have several options to workaround it:
alex-oleshkevich marked this conversation as resolved.
Show resolved Hide resolved
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
alex-oleshkevich marked this conversation as resolved.
Show resolved Hide resolved

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


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

alex-oleshkevich marked this conversation as resolved.
Show resolved Hide 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
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
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>")
alex-oleshkevich marked this conversation as resolved.
Show resolved Hide resolved

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),
alex-oleshkevich marked this conversation as resolved.
Show resolved Hide resolved
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"}