From 600069fb72c91b314ffa258ed8853fa59a40b4d2 Mon Sep 17 00:00:00 2001 From: Andreas Pelme Date: Thu, 25 Jul 2024 20:03:06 +0200 Subject: [PATCH] Experimental Django template loader. --- conftest.py | 7 ++- docs/django.md | 43 +++++++++++++++++++ .../djangoproject/exampleproject/settings.py | 2 + examples/djangoproject/exampleproject/urls.py | 2 + examples/djangoproject/pizza/__init__.py | 0 examples/djangoproject/pizza/admin.py | 1 + examples/djangoproject/pizza/apps.py | 6 +++ examples/djangoproject/pizza/components.py | 5 +++ .../pizza/migrations/0001_initial.py | 27 ++++++++++++ .../pizza/migrations/__init__.py | 0 examples/djangoproject/pizza/models.py | 5 +++ examples/djangoproject/pizza/tests.py | 1 + examples/djangoproject/pizza/views.py | 8 ++++ htpy/django.py | 27 ++++++++++++ tests/test_django.py | 30 ++++++++++++- 15 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 examples/djangoproject/pizza/__init__.py create mode 100644 examples/djangoproject/pizza/admin.py create mode 100644 examples/djangoproject/pizza/apps.py create mode 100644 examples/djangoproject/pizza/components.py create mode 100644 examples/djangoproject/pizza/migrations/0001_initial.py create mode 100644 examples/djangoproject/pizza/migrations/__init__.py create mode 100644 examples/djangoproject/pizza/models.py create mode 100644 examples/djangoproject/pizza/tests.py create mode 100644 examples/djangoproject/pizza/views.py create mode 100644 htpy/django.py diff --git a/conftest.py b/conftest.py index 4b33e2a..02aa6eb 100644 --- a/conftest.py +++ b/conftest.py @@ -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() diff --git a/docs/django.md b/docs/django.md index ca9250f..f2e4b82 100644 --- a/docs/django.md +++ b/docs/django.md @@ -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"])] +``` diff --git a/examples/djangoproject/exampleproject/settings.py b/examples/djangoproject/exampleproject/settings.py index 338464e..3dc0bff 100644 --- a/examples/djangoproject/exampleproject/settings.py +++ b/examples/djangoproject/exampleproject/settings.py @@ -38,6 +38,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "index", + "pizza", ] MIDDLEWARE = [ @@ -66,6 +67,7 @@ ], }, }, + {"BACKEND": "htpy.django.HtpyTemplateBackend", "NAME": "htpy"}, ] WSGI_APPLICATION = "exampleproject.wsgi.application" diff --git a/examples/djangoproject/exampleproject/urls.py b/examples/djangoproject/exampleproject/urls.py index 0d7108a..84bbccd 100644 --- a/examples/djangoproject/exampleproject/urls.py +++ b/examples/djangoproject/exampleproject/urls.py @@ -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 @@ -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), ] diff --git a/examples/djangoproject/pizza/__init__.py b/examples/djangoproject/pizza/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/djangoproject/pizza/admin.py b/examples/djangoproject/pizza/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/examples/djangoproject/pizza/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/examples/djangoproject/pizza/apps.py b/examples/djangoproject/pizza/apps.py new file mode 100644 index 0000000..62aaf35 --- /dev/null +++ b/examples/djangoproject/pizza/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TemplatebackendConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "pizza" diff --git a/examples/djangoproject/pizza/components.py b/examples/djangoproject/pizza/components.py new file mode 100644 index 0000000..0418c50 --- /dev/null +++ b/examples/djangoproject/pizza/components.py @@ -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"])] diff --git a/examples/djangoproject/pizza/migrations/0001_initial.py b/examples/djangoproject/pizza/migrations/0001_initial.py new file mode 100644 index 0000000..d7d373c --- /dev/null +++ b/examples/djangoproject/pizza/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/examples/djangoproject/pizza/migrations/__init__.py b/examples/djangoproject/pizza/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/djangoproject/pizza/models.py b/examples/djangoproject/pizza/models.py new file mode 100644 index 0000000..2100389 --- /dev/null +++ b/examples/djangoproject/pizza/models.py @@ -0,0 +1,5 @@ +from django.db import models + + +class Pizza(models.Model): + name = models.CharField(max_length=100) diff --git a/examples/djangoproject/pizza/tests.py b/examples/djangoproject/pizza/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/examples/djangoproject/pizza/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/examples/djangoproject/pizza/views.py b/examples/djangoproject/pizza/views.py new file mode 100644 index 0000000..800d2d9 --- /dev/null +++ b/examples/djangoproject/pizza/views.py @@ -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" diff --git a/htpy/django.py b/htpy/django.py new file mode 100644 index 0000000..0446db6 --- /dev/null +++ b/htpy/django.py @@ -0,0 +1,27 @@ +from collections.abc import Callable +from typing import Any + +from django.http import HttpRequest +from django.template import Context, TemplateDoesNotExist +from django.utils.module_loading import import_string + +from . import Element, render_node + + +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: Any): + pass + + def get_template(self, name: str) -> _HtpyTemplate: + try: + return _HtpyTemplate(import_string(name)) + except ImportError: + raise TemplateDoesNotExist(name) diff --git a/tests/test_django.py b/tests/test_django.py index 93a2202..a464dc1 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -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") @@ -29,3 +33,25 @@ def test_explicit_escape() -> None: def test_errorlist() -> None: result = div[ErrorList(["my error"])] assert str(result) == """
""" + + +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 == "
hey andreas
" + + def test_render_fragment(self) -> None: + result = render_to_string(__name__ + ".my_template_fragment", {"name": "andreas"}) + assert result == "
hey andreas
" + + def test_template_does_not_exist(self) -> None: + with pytest.raises(TemplateDoesNotExist): + render_to_string(__name__ + ".does_not_exist", {})