From eaf32b11cf41d53cb1cdc091a21a20786f84475b Mon Sep 17 00:00:00 2001 From: "alex.oleshkevich" Date: Thu, 6 Oct 2022 15:07:39 +0300 Subject: [PATCH 01/16] Add template context processors. --- docs/templates.md | 59 +++++++++++++++++++++++++++++++++++++++++ starlette/templating.py | 14 +++++++++- tests/test_templates.py | 29 ++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/templates.md b/docs/templates.md index 8f2714fe2..0f846650f 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -50,6 +50,65 @@ 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, +]) +``` + +### Asynchronous context processors + +Asynchronous context processors are not supported. You have several options to workaround it: +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 + +```python + +class MyTeamsMiddleware: + def __init__(self, app): + self.app = app + async def __call__(self, scope, receive, send): + scope.setdefault('state', {}) + scope['state']['teams'] = await fetch_teams() + await self.app(scope, receive, send) + + +def teams_context_processor(request): + return {'teams': request.state.teams} + +``` + + + ## 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..c3e7d5fc8 100644 --- a/starlette/templating.py +++ b/starlette/templating.py @@ -1,5 +1,7 @@ import typing from os import PathLike +from urllib import request +from urllib.request import Request from starlette.background import BackgroundTask from starlette.responses import Response @@ -59,10 +61,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 +102,10 @@ def TemplateResponse( ) -> _TemplateResponse: if "request" not in context: raise ValueError('context must include a "request" key') + + for context_processor in self.context_processors: + context.update(context_processor(typing.cast(Request, request))) + template = self.get_template(name) return _TemplateResponse( template, diff --git a/tests/test_templates.py b/tests/test_templates.py index ad42488de..0f788f29b 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -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("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=str(tmpdir), + 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"} From ac20226c9a90108a7939d37d6b7d156076268f43 Mon Sep 17 00:00:00 2001 From: "alex.oleshkevich" Date: Thu, 6 Oct 2022 15:08:47 +0300 Subject: [PATCH 02/16] fix code format in docs --- docs/templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/templates.md b/docs/templates.md index 0f846650f..ff2ff6225 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -73,8 +73,8 @@ Pass context processors to `context_processors` argument of the `Jinja2Templates ```python import typing -from starlette.requests import Request +from starlette.requests import Request from starlette.templating import Jinja2Templates def app_context(request: Request) -> typing.Dict[str, typing.Any]: From 3f6b13be3800ee447406f3644ea43a4d590256d4 Mon Sep 17 00:00:00 2001 From: "alex.oleshkevich" Date: Thu, 6 Oct 2022 15:09:23 +0300 Subject: [PATCH 03/16] format code in docs --- docs/templates.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/templates.md b/docs/templates.md index ff2ff6225..472366f05 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -92,7 +92,6 @@ Asynchronous context processors are not supported. You have several options to w 2. do IO operations in the middleware, set their results into `request.state` and then read it in the context processor ```python - class MyTeamsMiddleware: def __init__(self, app): self.app = app From 946db980d21322f864d8182657c2b1bd7ceb1f91 Mon Sep 17 00:00:00 2001 From: "alex.oleshkevich" Date: Thu, 6 Oct 2022 15:11:26 +0300 Subject: [PATCH 04/16] fix imports --- starlette/templating.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/starlette/templating.py b/starlette/templating.py index c3e7d5fc8..da340f569 100644 --- a/starlette/templating.py +++ b/starlette/templating.py @@ -1,9 +1,8 @@ import typing from os import PathLike -from urllib import request -from urllib.request import Request from starlette.background import BackgroundTask +from starlette.requests import Request from starlette.responses import Response from starlette.types import Receive, Scope, Send @@ -103,8 +102,9 @@ def 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(typing.cast(Request, request))) + context.update(context_processor(request)) template = self.get_template(name) return _TemplateResponse( From b0fdd07002768ee20fb039045ff1cd28d27db59e Mon Sep 17 00:00:00 2001 From: "alex.oleshkevich" Date: Thu, 6 Oct 2022 15:12:00 +0300 Subject: [PATCH 05/16] fix doc formatting --- docs/templates.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index 472366f05..cef4bacdc 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -103,11 +103,9 @@ class MyTeamsMiddleware: def teams_context_processor(request): return {'teams': request.state.teams} - ``` - ## Testing template responses When using the test client, template responses include `.template` and `.context` From d814af5fd2e95e5c4356dd57bbacb67819a2958f Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Sun, 25 Dec 2022 21:41:02 +0300 Subject: [PATCH 06/16] Update docs/templates.md Co-authored-by: Marcelo Trylesinski --- docs/templates.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index cef4bacdc..1c4762b7f 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -62,9 +62,7 @@ import typing from starlette.requests import Request def app_context(request: Request) -> typing.Dict[str, typing.Any]: - return { - 'app': request.app, - } + return {'app': request.app} ``` ### Registering context templates From 1a30dadcedbf06ff9695c8092938891856a4bf2f Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Sun, 25 Dec 2022 21:41:15 +0300 Subject: [PATCH 07/16] Update docs/templates.md Co-authored-by: Marcelo Trylesinski --- docs/templates.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index 1c4762b7f..2b839135d 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -78,9 +78,9 @@ 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, -]) +templates = Jinja2Templates( + directory='templates', context_processors=[app_context] +) ``` ### Asynchronous context processors From ff446dd6f8def40205ebfef350aca96ccc134aa2 Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Sun, 25 Dec 2022 21:41:34 +0300 Subject: [PATCH 08/16] Update docs/templates.md Co-authored-by: Marcelo Trylesinski --- docs/templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/templates.md b/docs/templates.md index 2b839135d..662bc33a8 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -85,7 +85,7 @@ templates = Jinja2Templates( ### Asynchronous context processors -Asynchronous context processors are not supported. You have several options to workaround it: +Asynchronous context processors are not supported. You have the following alternatives: 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 From b7cb00266acda4311afa44070ea00d726bd57a45 Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Sun, 25 Dec 2022 21:41:53 +0300 Subject: [PATCH 09/16] Update docs/templates.md Co-authored-by: Marcelo Trylesinski --- docs/templates.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/templates.md b/docs/templates.md index 662bc33a8..41fab6ef2 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -93,6 +93,7 @@ Asynchronous context processors are not supported. You have the following altern class MyTeamsMiddleware: def __init__(self, app): self.app = app + async def __call__(self, scope, receive, send): scope.setdefault('state', {}) scope['state']['teams'] = await fetch_teams() From aee1007e4888dc48ec55ed7c4e0ed00abc7af2e8 Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Sun, 25 Dec 2022 21:43:31 +0300 Subject: [PATCH 10/16] Update docs/templates.md Co-authored-by: Marcelo Trylesinski --- docs/templates.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/templates.md b/docs/templates.md index 41fab6ef2..44e090ef1 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -104,6 +104,8 @@ def teams_context_processor(request): return {'teams': request.state.teams} ``` +!!! tip + If the above middleware is not clear, check the [Pure ASGI Middleware](https://www.starlette.io/middleware/#pure-asgi-middleware) section on our documentation. ## Testing template responses From a7f323a0ab32f593a785c9a8ba8eb7460d8b536d Mon Sep 17 00:00:00 2001 From: "alex.oleshkevich" Date: Sun, 25 Dec 2022 21:55:10 +0300 Subject: [PATCH 11/16] remove async context processors docs --- docs/templates.md | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index 44e090ef1..7b5552389 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` @@ -85,27 +85,7 @@ templates = Jinja2Templates( ### Asynchronous context processors -Asynchronous context processors are not supported. You have the following alternatives: -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 - -```python -class MyTeamsMiddleware: - def __init__(self, app): - self.app = app - - async def __call__(self, scope, receive, send): - scope.setdefault('state', {}) - scope['state']['teams'] = await fetch_teams() - await self.app(scope, receive, send) - - -def teams_context_processor(request): - return {'teams': request.state.teams} -``` - -!!! tip - If the above middleware is not clear, check the [Pure ASGI Middleware](https://www.starlette.io/middleware/#pure-asgi-middleware) section on our documentation. +Asynchronous context processors currently are not supported. ## Testing template responses From c6a60d705c1fbeb8ef5509c7e79301fe6aa315ea Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Mon, 26 Dec 2022 00:00:07 +0300 Subject: [PATCH 12/16] Update docs/templates.md Co-authored-by: Marcelo Trylesinski --- docs/templates.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index 7b5552389..57d18d0d5 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -83,9 +83,8 @@ templates = Jinja2Templates( ) ``` -### Asynchronous context processors - -Asynchronous context processors currently are not supported. +!!! info + Asynchronous functions as context processors are not supported. ## Testing template responses From 03d6a99237b3a361c5bba768d84060b4e8c64daf Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Mon, 26 Dec 2022 00:00:26 +0300 Subject: [PATCH 13/16] Update tests/test_templates.py Co-authored-by: Marcelo Trylesinski --- tests/test_templates.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index 0f788f29b..c2fe6f4b3 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -34,10 +34,9 @@ def test_template_response_requires_request(tmpdir): 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("Hello {{ username }}") +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}) From 4345cacf9f111d274ef047ea617d928bd94be5e7 Mon Sep 17 00:00:00 2001 From: "alex.oleshkevich" Date: Tue, 27 Dec 2022 12:44:07 +0300 Subject: [PATCH 14/16] fix tests --- tests/test_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index c2fe6f4b3..4a7c77f3a 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -34,7 +34,7 @@ def test_template_response_requires_request(tmpdir): templates.TemplateResponse("", {}) -def test_calls_context_processors(tmp_path, test_client_factory): +def test_calls_context_processors(tmp_path, test_client_factory, tmpdir): path = tmp_path / "index.html" path.write_text("Hello {{ username }}") From e8678d3f6de58ebc246e23fdfce6158b0b9a80c2 Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Tue, 27 Dec 2022 12:55:43 +0300 Subject: [PATCH 15/16] Update tests/test_templates.py Co-authored-by: Marcelo Trylesinski --- tests/test_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index 4a7c77f3a..ce21b42c8 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -49,7 +49,7 @@ def hello_world_processor(request): routes=[Route("/", endpoint=homepage)], ) templates = Jinja2Templates( - directory=str(tmpdir), + directory=tmp_path, context_processors=[ hello_world_processor, ], From d97b3be38be91bc45bbf7dc1d8d0e3a13d463ac6 Mon Sep 17 00:00:00 2001 From: Alex Oleshkevich Date: Tue, 27 Dec 2022 14:33:28 +0300 Subject: [PATCH 16/16] Update tests/test_templates.py Co-authored-by: Marcelo Trylesinski --- tests/test_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index ce21b42c8..0bf4bce07 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -34,7 +34,7 @@ def test_template_response_requires_request(tmpdir): templates.TemplateResponse("", {}) -def test_calls_context_processors(tmp_path, test_client_factory, tmpdir): +def test_calls_context_processors(tmp_path, test_client_factory): path = tmp_path / "index.html" path.write_text("Hello {{ username }}")