diff --git a/docs/templates.md b/docs/templates.md index 39f989d2e..749c7a8c2 100644 --- a/docs/templates.md +++ b/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 @@ -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 - + ``` If you want to use [custom filters][jinja2], you will need to update the `env` @@ -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` diff --git a/starlette/templating.py b/starlette/templating.py index a36c264ed..da340f569 100644 --- a/starlette/templating.py +++ b/starlette/templating.py @@ -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 @@ -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 @@ -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, diff --git a/tests/test_templates.py b/tests/test_templates.py index ad42488de..0bf4bce07 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -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("Hello {{ username }}") + + 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 == "Hello World" + assert response.template.name == "index.html" + assert set(response.context.keys()) == {"request", "username"}