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 18 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
40 changes: 38 additions & 2 deletions docs/templates.md
@@ -1,4 +1,4 @@
Starlette is not *strictly* coupled to any particular templating engine, but
Starlette is not _strictly_ coupled to any particular templating engine, but
Jinja2 provides an excellent choice.

Starlette provides a simple way to get `jinja2` configured. This is probably
Expand Down Expand Up @@ -33,7 +33,7 @@ so we can correctly hyperlink to other pages within the application.
For example, we can link to static files from within our HTML templates:

```html
<link href="{{ url_for('static', path='/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', path='/css/bootstrap.min.css') }}" rel="stylesheet" />
```

If you want to use [custom filters][jinja2], you will need to update the `env`
Expand All @@ -50,6 +50,42 @@ 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}
```

### 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
!!! info
Asynchronous functions as context processors are not supported.

## 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
28 changes: 28 additions & 0 deletions tests/test_templates.py
Expand Up @@ -32,3 +32,31 @@ def test_template_response_requires_request(tmpdir):
templates = Jinja2Templates(str(tmpdir))
with pytest.raises(ValueError):
templates.TemplateResponse("", {})


def test_calls_context_processors(tmp_path, test_client_factory):
path = tmp_path / "index.html"
path.write_text("<html>Hello {{ username }}</html>")

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=tmp_path,
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"}