Skip to content

Commit

Permalink
Experimental Django template loader.
Browse files Browse the repository at this point in the history
  • Loading branch information
pelme committed Aug 23, 2024
1 parent fb2787b commit 26187d0
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 3 deletions.
7 changes: 6 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ def django_env() -> None:
import django
from django.conf import settings

settings.configure(TEMPLATES=[{"BACKEND": "django.template.backends.django.DjangoTemplates"}])
settings.configure(
TEMPLATES=[
{"BACKEND": "django.template.backends.django.DjangoTemplates"},
{"BACKEND": "htpy.django.HtpyTemplateBackend", "NAME": "htpy"},
]
)
django.setup()
43 changes: 43 additions & 0 deletions docs/django.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,46 @@ class ShoelaceInput(widgets.Widget):
def render(self, name, value, attrs=None, renderer=None):
return str(sl_input(attrs, name=name, value=value))
```

## The htpy Template Backend

htpy includes a custom template backend. It makes it possible to use htpy
instead of Django templates in places where a template name is required. This
can be used with generic views or third party applications built to be used with
Django templates.

To enable the htpy template backend, add `htpy.django.HtpyTemplateBackend` to
the `TEMPLATES` setting:

```py
TEMPLATES = [
... # Regular Django template configuration goes here
{"BACKEND": "htpy.django.HtpyTemplateBackend", "NAME": "htpy"}
]
```

In places that expect template names, such as generic views, specify the import
path as a string to a htpy component function:


```python title="pizza/views.py"
from django.views.generic import ListView
from pizza.models import Pizza


class PizzaListView(ListView):
model = Pizza
template_name = "pizza.components.pizza_list"
```

In `pizza/components.py`, create a function that accepts two arguments: the
template `Context` (a dictionary with the template variables) and a
`HttpRequest`. It should return the htpy response:

```python title="pizza/components.py"
from htpy import li, ul


def pizza_list(context, request):
return ul[(li[pizza.name] for pizza in context["object_list"])]
```
2 changes: 2 additions & 0 deletions examples/djangoproject/exampleproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"django.contrib.messages",
"django.contrib.staticfiles",
"index",
"pizza",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -66,6 +67,7 @@
],
},
},
{"BACKEND": "htpy.django.HtpyTemplateBackend", "NAME": "htpy"},
]

WSGI_APPLICATION = "exampleproject.wsgi.application"
Expand Down
2 changes: 2 additions & 0 deletions examples/djangoproject/exampleproject/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from django.urls import path
from form.views import my_form
from index.views import index
from pizza.views import PizzaListView
from stream.views import stream
from widget.views import widget_view

Expand All @@ -27,5 +28,6 @@
path("form/", my_form),
path("widget/", widget_view),
path("stream/", stream),
path("pizza/", PizzaListView.as_view()),
path("admin/", admin.site.urls),
]
Empty file.
1 change: 1 addition & 0 deletions examples/djangoproject/pizza/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Register your models here.
6 changes: 6 additions & 0 deletions examples/djangoproject/pizza/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class TemplatebackendConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pizza"
5 changes: 5 additions & 0 deletions examples/djangoproject/pizza/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from htpy import li, ul


def pizza_list(context, request):
return ul[(li[pizza.name] for pizza in context["object_list"])]
27 changes: 27 additions & 0 deletions examples/djangoproject/pizza/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.0.7 on 2024-08-13 20:04

from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Pizza",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
],
),
]
Empty file.
5 changes: 5 additions & 0 deletions examples/djangoproject/pizza/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.db import models


class Pizza(models.Model):
name = models.CharField(max_length=100)
1 change: 1 addition & 0 deletions examples/djangoproject/pizza/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Create your tests here.
8 changes: 8 additions & 0 deletions examples/djangoproject/pizza/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.views.generic import ListView

from pizza.models import Pizza


class PizzaListView(ListView):
model = Pizza
template_name = "pizza.components.pizza_list"
32 changes: 32 additions & 0 deletions htpy/django.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

import typing as t

from django.template import Context, TemplateDoesNotExist
from django.utils.module_loading import import_string

from . import Element, render_node

if t.TYPE_CHECKING:
from collections.abc import Callable

from django.http import HttpRequest


class _HtpyTemplate:
def __init__(self, func: Callable[[Context | None, HttpRequest | None], Element]) -> None:
self.func = func

def render(self, context: Context | None, request: HttpRequest | None) -> str:
return render_node(self.func(context, request))


class HtpyTemplateBackend:
def __init__(self, config: t.Any):
pass

def get_template(self, name: str) -> _HtpyTemplate:
try:
return _HtpyTemplate(import_string(name))
except ImportError:
raise TemplateDoesNotExist(name)
30 changes: 28 additions & 2 deletions tests/test_django.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from typing import Any

import pytest
from django.forms.utils import ErrorList
from django.template import Context, Template
from django.http import HttpRequest
from django.template import Context, Template, TemplateDoesNotExist
from django.template.loader import render_to_string
from django.utils.html import escape
from django.utils.safestring import SafeString

from htpy import div, li, ul
from htpy import Element, Node, div, li, ul

pytestmark = pytest.mark.usefixtures("django_env")

Expand All @@ -29,3 +33,25 @@ def test_explicit_escape() -> None:
def test_errorlist() -> None:
result = div[ErrorList(["my error"])]
assert str(result) == """<div><ul class="errorlist"><li>my error</li></ul></div>"""


def my_template(context: dict[str, Any], request: HttpRequest | None) -> Element:
return div[f"hey {context['name']}"]


def my_template_fragment(context: dict[str, Any], request: HttpRequest | None) -> Node:
return [div[f"hey {context['name']}"]]


class Test_template_loader:
def test_render_element(self) -> None:
result = render_to_string(__name__ + ".my_template", {"name": "andreas"})
assert result == "<div>hey andreas</div>"

def test_render_fragment(self) -> None:
result = render_to_string(__name__ + ".my_template_fragment", {"name": "andreas"})
assert result == "<div>hey andreas</div>"

def test_template_does_not_exist(self) -> None:
with pytest.raises(TemplateDoesNotExist):
render_to_string(__name__ + ".does_not_exist", {})

0 comments on commit 26187d0

Please sign in to comment.