await response(receive, send)
```
-### TemplateResponse
-
-The `TemplateResponse` class return plain text responses generated
-from a template instance, and a dictionary of context to render into the
-template.
-
-A `request` argument must always be included in the context. Responses default
-to `text/html` unless an alternative `media_type` is specified.
-
-```python
-from starlette.responses import TemplateResponse
-from starlette.requests import Request
-
-from jinja2 import Environment, FileSystemLoader
-
-
-env = Environment(loader=FileSystemLoader('templates'))
-
-
-class App:
- def __init__(self, scope):
- assert scope['type'] == 'http'
- self.scope = scope
-
- async def __call__(self, receive, send):
- template = env.get_template('index.html')
- context = {
- 'request': Request(self.scope),
- }
- response = TemplateResponse(template, context)
- await response(receive, send)
-```
-
-The advantage with using `TemplateResponse` over `HTMLResponse` is that
-it will make `template` and `context` properties available on response instances
-returned by the test client.
-
-```python
-def test_app():
- client = TestClient(App)
- response = client.get("/")
- assert response.status_code == 200
- assert response.template.name == "index.html"
- assert "request" in response.context
-```
-
### JSONResponse
Takes some data and returns an `application/json` encoded response.
Starlette is not *strictly* coupled to any particular templating engine, but
Jinja2 provides an excellent choice.
-The `Starlette` application class provides a simple way to get `jinja2`
-configured. This is probably what you want to use by default.
-
-```python
-app = Starlette(debug=True, template_directory='templates')
-app.mount('/static', StaticFiles(directory='statics'), name='static')
-
-
-@app.route('/')
-async def homepage(request):
- template = app.get_template('index.html')
- content = template.render(request=request)
- return HTMLResponse(content)
-```
-
-If you include `request` in the template context, then the `url_for` function
-will also be available within your template code.
-
-The Jinja2 `Environment` instance is available as `app.template_env`.
-
-## Handling templates explicitly
-
-If you don't want to use `jinja2`, or you don't want to rely on
-Starlette's default configuration you can configure a template renderer
-explicitly instead.
-
-Here we're going to take a look at an example of how you can explicitly
-configure a Jinja2 environment together with Starlette.
+Starlette provides a simple way to get `jinja2` configured. This is probably
+what you want to use by default.
```python
from starlette.applications import Starlette
-from starlette.staticfiles import StaticFiles
-from starlette.responses import HTMLResponse
-
+from starlette.templating import Jinja2Templates
-def setup_jinja2(template_dir):
- @jinja2.contextfunction
- def url_for(context, name, **path_params):
- request = context['request']
- return request.url_for(name, **path_params)
- loader = jinja2.FileSystemLoader(template_dir)
- env = jinja2.Environment(loader=loader, autoescape=True)
- env.globals['url_for'] = url_for
- return env
+templates = Jinja2Templates(directory='templates')
-
-env = setup_jinja2('templates')
app = Starlette(debug=True)
app.mount('/static', StaticFiles(directory='statics'), name='static')
@app.route('/')
async def homepage(request):
- template = env.get_template('index.html')
- content = template.render(request=request)
- return HTMLResponse(content)
+ return templates.TemplateResponse('index.html', {'request': request})
```
-This gives you the equivalent of the default `app.get_template()`, but we've
-got all the configuration explicitly out in the open now.
-
-The important parts to note from the above example are:
-
-* The StaticFiles app has been mounted with `name='static'`, meaning we can use `app.url_path_for('static', path=...)` or `request.url_for('static', path=...)`.
-* The Jinja2 environment has a global `url_for` included, which allows us to use `url_for`
-inside our templates. We always need to pass the incoming `request` instance
-in our context in order to be able to use the `url_for` function.
+The Jinja2 environment sets up a global `url_for` included, which allows us to
+use `url_for` inside our templates. We always need to pass the incoming `request`
+instance as part of the template context.
We can now link to static files from within our HTML templates. For example:
<link href="{{ url_for('static', path='/css/bootstrap.min.css') }}" rel="stylesheet">
```
+## Testing template responses
+
+When using the test client, template responses include `.template` and `.context`
+attributes.
+
+```python
+def test_homepage():
+ client = TestClient(app)
+ response = client.get("/")
+ assert response.status_code == 200
+ assert response.template.name == 'index.html'
+ assert "request" in response.context
+```
+
## Asynchronous template rendering
Jinja2 supports async template rendering, however as a general rule
)
self.lifespan_middleware = LifespanMiddleware(self.error_middleware)
self.schema_generator = None # type: typing.Optional[BaseSchemaGenerator]
- self.template_env = self.load_template_env(template_directory)
+ if template_directory is not None:
+ from starlette.templating import Jinja2Templates
+
+ self.templates = Jinja2Templates(template_directory)
@property
def routes(self) -> typing.List[BaseRoute]:
self.exception_middleware.debug = value
self.error_middleware.debug = value
- def load_template_env(self, template_directory: str = None) -> typing.Any:
- if template_directory is None:
- return None
-
- # Import jinja2 lazily.
- import jinja2
-
- @jinja2.contextfunction
- def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
- request = context["request"]
- return request.url_for(name, **path_params)
-
- loader = jinja2.FileSystemLoader(str(template_directory))
- env = jinja2.Environment(loader=loader, autoescape=True)
- env.globals["url_for"] = url_for
- return env
-
def get_template(self, name: str) -> typing.Any:
- return self.template_env.get_template(name)
+ return self.templates.get_template(name)
@property
def schema(self) -> dict:
import uuid
from types import TracebackType
-import aiomysql
from sqlalchemy.dialects.mysql import pymysql
from sqlalchemy.engine.interfaces import Dialect
from sqlalchemy.sql import ClauseElement
+import aiomysql
from starlette.database.core import (
DatabaseBackend,
DatabaseSession,
media_type = "text/plain"
-class TemplateResponse(Response):
- media_type = "text/html"
-
- def __init__(
- self,
- template: typing.Any,
- context: dict,
- status_code: int = 200,
- headers: dict = None,
- media_type: str = None,
- background: BackgroundTask = None,
- ):
- if "request" not in context:
- raise ValueError('context must include a "request" key')
- self.template = template
- self.context = context
- content = template.render(context)
- super().__init__(content, status_code, headers, media_type, background)
-
- async def __call__(self, receive: Receive, send: Send) -> None:
- request = self.context["request"]
- extensions = request.get("extensions", {})
- if "http.response.template" in extensions:
- await send(
- {
- "type": "http.response.template",
- "template": self.template,
- "context": self.context,
- }
- )
- await super().__call__(receive, send)
-
-
class JSONResponse(Response):
media_type = "application/json"
)
if self.background is not None:
await self.background()
+
+
+# For compat with earlier versions.
+# We'll drop this with the next median release.
+from starlette.templating import _TemplateResponse as TemplateResponse
--- /dev/null
+import typing
+
+from starlette.background import BackgroundTask
+from starlette.responses import Response
+from starlette.types import Receive, Send
+
+try:
+ import jinja2
+except ImportError: # pragma: nocover
+ jinja2 = None # type: ignore
+
+
+class _TemplateResponse(Response):
+ media_type = "text/html"
+
+ def __init__(
+ self,
+ template: typing.Any,
+ context: dict,
+ status_code: int = 200,
+ headers: dict = None,
+ media_type: str = None,
+ background: BackgroundTask = None,
+ ):
+ self.template = template
+ self.context = context
+ content = template.render(context)
+ super().__init__(content, status_code, headers, media_type, background)
+
+ async def __call__(self, receive: Receive, send: Send) -> None:
+ request = self.context.get("request", {})
+ extensions = request.get("extensions", {})
+ if "http.response.template" in extensions:
+ await send(
+ {
+ "type": "http.response.template",
+ "template": self.template,
+ "context": self.context,
+ }
+ )
+ await super().__call__(receive, send)
+
+
+class Jinja2Templates:
+ """
+ templates = Jinja2Templates("templates")
+
+ return templates.TemplateResponse("index.html", {"request": request})
+ """
+
+ def __init__(self, directory: str) -> None:
+ self.env = self.get_env(directory)
+
+ def get_env(self, directory: str) -> jinja2.Environment:
+ @jinja2.contextfunction
+ def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
+ request = context["request"]
+ return request.url_for(name, **path_params)
+
+ loader = jinja2.FileSystemLoader(directory)
+ env = jinja2.Environment(loader=loader, autoescape=True)
+ env.globals["url_for"] = url_for
+ return env
+
+ def get_template(self, name: str) -> jinja2.Template:
+ return self.env.get_template(name)
+
+ def TemplateResponse(
+ self,
+ name: str,
+ context: dict,
+ status_code: int = 200,
+ headers: dict = None,
+ media_type: str = None,
+ background: BackgroundTask = None,
+ ) -> _TemplateResponse:
+ if "request" not in context:
+ raise ValueError('context must include a "request" key')
+ template = self.get_template(name)
+ return _TemplateResponse(
+ template,
+ context,
+ status_code=status_code,
+ headers=headers,
+ media_type=media_type,
+ background=background,
+ )
-import databases
import pytest
import sqlalchemy
+import databases
from starlette.applications import Starlette
from starlette.database import transaction
from starlette.datastructures import CommaSeparatedStrings, DatabaseURL
assert not response.cookies.get("mycookie")
-def test_template_response():
- def app(scope):
- request = Request(scope)
-
- class Template:
- def __init__(self, name):
- self.name = name
-
- def render(self, context):
- return f"username: {context['username']}"
-
- async def asgi(receive, send):
- template = Template("index.html")
- context = {"username": "tomchristie", "request": request}
- response = TemplateResponse(template, context)
- await response(receive, send)
-
- return asgi
-
- client = TestClient(app)
- response = client.get("/")
- assert response.text == "username: tomchristie"
- assert response.template.name == "index.html"
- assert response.context["username"] == "tomchristie"
-
-
-def test_template_response_requires_request():
- with pytest.raises(ValueError):
- TemplateResponse(None, {})
-
-
def test_populate_headers():
def app(scope):
headers = {}
import os
+import pytest
from starlette.applications import Starlette
from starlette.responses import HTMLResponse
+from starlette.templating import Jinja2Templates
from starlette.testclient import TestClient
with open(path, "w") as file:
file.write("<html>Hello, <a href='{{ url_for('homepage') }}'>world</a></html>")
- app = Starlette(debug=True, template_directory=tmpdir)
+ app = Starlette(debug=True)
+ templates = Jinja2Templates(directory=str(tmpdir))
+
+ @app.route("/")
+ async def homepage(request):
+ return templates.TemplateResponse("index.html", {"request": request})
+
+ client = TestClient(app)
+ response = client.get("/")
+ assert response.text == "<html>Hello, <a href='http://testserver/'>world</a></html>"
+ assert response.template.name == "index.html"
+ assert set(response.context.keys()) == {"request"}
+
+
+def test_templates_legacy(tmpdir):
+ path = os.path.join(tmpdir, "index.html")
+ with open(path, "w") as file:
+ file.write("<html>Hello, <a href='{{ url_for('homepage') }}'>world</a></html>")
+
+ app = Starlette(debug=True, template_directory=str(tmpdir))
@app.route("/")
async def homepage(request):
client = TestClient(app)
response = client.get("/")
assert response.text == "<html>Hello, <a href='http://testserver/'>world</a></html>"
+
+
+def test_template_response_requires_request(tmpdir):
+ templates = Jinja2Templates(str(tmpdir))
+ with pytest.raises(ValueError):
+ templates.TemplateResponse(None, {})