diff --git a/README.md b/README.md index a293b332..bfdcce6c 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,11 @@ Starlette does not have any hard dependencies, but the following are optional: * [`requests`][requests] - Required if you want to use the `TestClient`. * [`aiofiles`][aiofiles] - Required if you want to use `FileResponse` or `StaticFiles`. +* [`jinja2`][jinja2] - Required if you want to use the default template configuration. * [`python-multipart`][python-multipart] - Required if you want to support form parsing, with `request.form()`. -* [`graphene`][graphene] - Required for `GraphQLApp` support. * [`itsdangerous`][itsdangerous] - Required for `SessionMiddleware` support. * [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support. +* [`graphene`][graphene] - Required for `GraphQLApp` support. * [`ujson`][ujson] - Required if you want to use `UJSONResponse`. You can install all of these with `pip3 install starlette[full]`. diff --git a/docs/index.md b/docs/index.md index 1cdb5df0..362f5b39 100644 --- a/docs/index.md +++ b/docs/index.md @@ -79,10 +79,11 @@ Starlette does not have any hard dependencies, but the following are optional: * [`requests`][requests] - Required if you want to use the `TestClient`. * [`aiofiles`][aiofiles] - Required if you want to use `FileResponse` or `StaticFiles`. +* [`jinja2`][jinja2] - Required if you want to use the default template configuration. * [`python-multipart`][python-multipart] - Required if you want to support form parsing, with `request.form()`. -* [`graphene`][graphene] - Required for `GraphQLApp` support. * [`itsdangerous`][itsdangerous] - Required for `SessionMiddleware` support. * [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support. +* [`graphene`][graphene] - Required for `GraphQLApp` support. * [`ujson`][ujson] - Required if you want to use `UJSONResponse`. You can install all of these with `pip3 install starlette[full]`. diff --git a/docs/release-notes.md b/docs/release-notes.md index 7c123974..cbedd51a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,3 +1,22 @@ +## 0.8.1 + +## Templating + +* Add a default templating configuration with Jinja2. + +Allows the following: + +```python +app = Starlette(template_directory="templates") + +@app.route('/') +async def homepage(request): + # `url_for` is available inside the template. + template = app.get_template('index.html') + content = template.render(request=request) + return HTMLResponse(content) +``` + ## 0.8.0 ### Exceptions diff --git a/docs/templates.md b/docs/templates.md index 9cb22f45..f4e59c44 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -1,8 +1,34 @@ -Starlette is not coupled to any particular templating engine, but Jinja2 -provides an excellent choice. +Starlette is not *strictly* coupled to any particular templating engine, but +Jinja2 provides an excellent choice. -Here we're going to take a look at a complete example of how you can configure -a Jinja2 environment together with Starlette. +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. ```python from starlette.applications import Starlette @@ -34,6 +60,9 @@ async def homepage(request): return HTMLResponse(content) ``` +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=...)`. diff --git a/requirements.txt b/requirements.txt index e135e521..10d91790 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ aiofiles graphene itsdangerous +jinja2 python-multipart pyyaml requests diff --git a/starlette/__init__.py b/starlette/__init__.py index 777f190d..8088f751 100644 --- a/starlette/__init__.py +++ b/starlette/__init__.py @@ -1 +1 @@ -__version__ = "0.8.0" +__version__ = "0.8.1" diff --git a/starlette/applications.py b/starlette/applications.py index db47ef62..560a60eb 100644 --- a/starlette/applications.py +++ b/starlette/applications.py @@ -11,7 +11,7 @@ from starlette.types import ASGIApp, ASGIInstance, Scope class Starlette: - def __init__(self, debug: bool = False) -> None: + def __init__(self, debug: bool = False, template_directory: str = None) -> None: self._debug = debug self.router = Router() self.lifespan_handler = LifespanHandler() @@ -20,6 +20,7 @@ class Starlette: self.exception_middleware, debug=debug ) self.schema_generator = None # type: typing.Optional[BaseSchemaGenerator] + self.template_env = self.load_template_env(template_directory) @property def routes(self) -> typing.List[BaseRoute]: @@ -35,6 +36,26 @@ class Starlette: 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) + @property def schema(self) -> dict: assert self.schema_generator is not None diff --git a/tests/test_templates.py b/tests/test_templates.py new file mode 100644 index 00000000..bc213a0b --- /dev/null +++ b/tests/test_templates.py @@ -0,0 +1,23 @@ +import os + +from starlette.applications import Starlette +from starlette.responses import HTMLResponse +from starlette.testclient import TestClient + + +def test_templates(tmpdir): + path = os.path.join(tmpdir, "index.html") + with open(path, "w") as file: + file.write("Hello, world") + + app = Starlette(debug=True, template_directory=tmpdir) + + @app.route("/") + async def homepage(request): + template = app.get_template("index.html") + content = template.render(request=request) + return HTMLResponse(content) + + client = TestClient(app) + response = client.get("/") + assert response.text == "Hello, world"