Skip to content

Commit

Permalink
Add template context processors. (#1904)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
  • Loading branch information
alex-oleshkevich and Kludex committed Dec 29, 2022
1 parent fabceb8 commit 008e3a5
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 3 deletions.
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]
)
```

!!! 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"}

0 comments on commit 008e3a5

Please sign in to comment.