From b36b99fb5ee3ecc36e887eaf2e2daeaf1edda716 Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:51:47 -0400 Subject: [PATCH] Improve types around template rendering (#258) Big improvements here are: - `template.Template.render` now returns `SafeText` instead of `Any`. - [`DjangoDivFormRenderer`](https://docs.djangoproject.com/en/4.2/ref/forms/renderers/#django.forms.renderers.DjangoDivFormRenderer) and [`Jinja2DivFormRenderer`](https://docs.djangoproject.com/en/4.2/ref/forms/renderers/#django.forms.renderers.Jinja2DivFormRenderer) in `forms.renderers`. - A `template.backends.base._BaseTemplate` [protocol class](https://docs.python.org/3/library/typing.html#typing.Protocol) that imitates the abstract [`BaseTemplate` class from DEP 182](https://github.com/django/deps/blob/main/final/0182-multiple-template-engines.rst#backends-api). This is needed because there is no real parent class from which each template backend inherits from, they implement their own `Template` classes from scratch. So, `BaseEngine` needed a return type for its abstract methods, which is what `_BaseTemplate` provides. Other changes include reducing repetition, adding more specific Template types for the different backends, using more generic `Mapping` type instead of `dict` for template contexts. --- django-stubs/forms/renderers.pyi | 39 +++++++++++++++-------- django-stubs/template/backends/base.pyi | 19 ++++++++--- django-stubs/template/backends/django.pyi | 24 ++++++++++---- django-stubs/template/backends/dummy.pyi | 13 ++++---- django-stubs/template/backends/jinja2.pyi | 22 +++++++++++-- django-stubs/template/backends/utils.pyi | 6 ++-- django-stubs/template/base.pyi | 2 +- django-stubs/template/loader.pyi | 9 +++--- 8 files changed, 94 insertions(+), 40 deletions(-) diff --git a/django-stubs/forms/renderers.pyi b/django-stubs/forms/renderers.pyi index d86022aec..32e6c096d 100644 --- a/django-stubs/forms/renderers.pyi +++ b/django-stubs/forms/renderers.pyi @@ -1,27 +1,40 @@ +from collections.abc import Mapping from typing import Any +from typing_extensions import override -from django.template import Template -from django.template.backends.base import BaseEngine +from django.http.request import HttpRequest +from django.template.backends.base import BaseEngine, _BaseTemplate +from django.template.backends.django import Template as DjangoTemplate +from django.template.backends.jinja2 import Template as Jinja2Template +from django.utils.safestring import SafeText -ROOT: Any - -def get_default_renderer() -> DjangoTemplates: ... +def get_default_renderer() -> BaseRenderer: ... class BaseRenderer: - def get_template(self, template_name: str) -> Any: ... + form_template_name: str + formset_template_name: str + def get_template(self, template_name: str) -> _BaseTemplate: ... def render( - self, template_name: str, context: dict[str, Any], request: None = ... - ) -> str: ... + self, + template_name: str, + context: Mapping[str, Any], + request: HttpRequest | None = ..., + ) -> SafeText: ... class EngineMixin: - def get_template(self, template_name: str) -> Any: ... + backend: BaseEngine + def get_template(self, template_name: str) -> _BaseTemplate: ... + @property def engine(self) -> BaseEngine: ... class DjangoTemplates(EngineMixin, BaseRenderer): - backend: Any = ... + @override + def get_template(self, template_name: str) -> DjangoTemplate: ... class Jinja2(EngineMixin, BaseRenderer): - backend: Any = ... + @override + def get_template(self, template_name: str) -> Jinja2Template: ... -class TemplatesSetting(BaseRenderer): - def get_template(self, template_name: str) -> Template: ... +class DjangoDivFormRenderer(DjangoTemplates): ... +class Jinja2DivFormRenderer(Jinja2): ... +class TemplatesSetting(BaseRenderer): ... diff --git a/django-stubs/template/backends/base.pyi b/django-stubs/template/backends/base.pyi index 3f83f2972..95dd07b04 100644 --- a/django-stubs/template/backends/base.pyi +++ b/django-stubs/template/backends/base.pyi @@ -1,7 +1,16 @@ from collections.abc import Iterator, Mapping -from typing import Any +from typing import Any, Protocol -from django.template.base import Template +from django.http.request import HttpRequest +from django.utils.safestring import SafeText + +# Source: https://github.com/django/deps/blob/main/final/0182-multiple-template-engines.rst#backends-api +class _BaseTemplate(Protocol): + def render( + self, + context: Mapping[str, Any] | None = ..., + request: HttpRequest | None = ..., + ) -> SafeText | str: ... class BaseEngine: name: str = ... @@ -9,9 +18,9 @@ class BaseEngine: app_dirs: bool = ... def __init__(self, params: Mapping[str, Any]) -> None: ... @property - def app_dirname(self) -> str | None: ... - def from_string(self, template_code: str) -> Template: ... - def get_template(self, template_name: str) -> Template | None: ... + def app_dirname(self) -> str: ... + def from_string(self, template_code: str) -> _BaseTemplate: ... + def get_template(self, template_name: str) -> _BaseTemplate: ... @property def template_dirs(self) -> tuple[str]: ... def iter_template_filenames(self, template_name: str) -> Iterator[str]: ... diff --git a/django-stubs/template/backends/django.pyi b/django-stubs/template/backends/django.pyi index 24bdd115d..bbce64c6e 100644 --- a/django-stubs/template/backends/django.pyi +++ b/django-stubs/template/backends/django.pyi @@ -1,9 +1,12 @@ -from collections.abc import Iterator -from typing import Any +from collections.abc import Iterator, Mapping +from typing import Any, NoReturn +from typing_extensions import override +from django.http.request import HttpRequest from django.template import base from django.template.engine import Engine from django.template.exceptions import TemplateDoesNotExist +from django.utils.safestring import SafeText from .base import BaseEngine @@ -13,18 +16,27 @@ class DjangoTemplates(BaseEngine): def get_templatetag_libraries( self, custom_libraries: dict[str, str] ) -> dict[str, str]: ... + @override + def from_string(self, template_code: str) -> Template: ... + @override + def get_template(self, template_name: str) -> Template: ... class Template: template: base.Template - backend: BaseEngine - def __init__(self, template: base.Template, backend: BaseEngine) -> None: ... + backend: DjangoTemplates + def __init__(self, template: base.Template, backend: DjangoTemplates) -> None: ... @property def origin(self) -> base.Origin: ... - def render(self, context: Any = ..., request: Any = ...) -> str: ... + def render( + self, + context: Mapping[str, Any] | None = ..., + request: HttpRequest | None = ..., + ) -> SafeText: ... def copy_exception( exc: TemplateDoesNotExist, backend: DjangoTemplates | None = ... ) -> TemplateDoesNotExist: ... -def reraise(exc: TemplateDoesNotExist, backend: DjangoTemplates) -> Any: ... +def reraise(exc: TemplateDoesNotExist, backend: DjangoTemplates) -> NoReturn: ... +def get_template_tag_modules() -> Iterator[tuple[str, str]]: ... def get_installed_libraries() -> dict[str, str]: ... def get_package_libraries(pkg: Any) -> Iterator[str]: ... diff --git a/django-stubs/template/backends/dummy.pyi b/django-stubs/template/backends/dummy.pyi index 573498eb0..e1d63e5e0 100644 --- a/django-stubs/template/backends/dummy.pyi +++ b/django-stubs/template/backends/dummy.pyi @@ -1,20 +1,21 @@ import string +from collections.abc import Mapping from typing import Any +from typing_extensions import override from django.http.request import HttpRequest from .base import BaseEngine class TemplateStrings(BaseEngine): - template_dirs: tuple[str] - def __init__( - self, params: dict[str, dict[Any, Any] | list[Any] | bool | str] - ) -> None: ... + @override + def from_string(self, template_code: str) -> Template: ... + @override + def get_template(self, template_name: str) -> Template: ... class Template(string.Template): - template: str def render( self, - context: dict[str, str] | None = ..., + context: Mapping[str, Any] | None = ..., request: HttpRequest | None = ..., ) -> str: ... diff --git a/django-stubs/template/backends/jinja2.pyi b/django-stubs/template/backends/jinja2.pyi index 709d4a92a..c51bb8bc7 100644 --- a/django-stubs/template/backends/jinja2.pyi +++ b/django-stubs/template/backends/jinja2.pyi @@ -1,16 +1,34 @@ -from collections.abc import Callable +from collections.abc import Callable, Mapping from typing import Any +from typing_extensions import override +from django.http.request import HttpRequest +from django.template import base from django.template.exceptions import TemplateSyntaxError +from django.utils.safestring import SafeText from .base import BaseEngine class Jinja2(BaseEngine): context_processors: list[str] = ... - def __init__(self, params: dict[str, Any]) -> None: ... + @override + def from_string(self, template_code: str) -> Template: ... + @override + def get_template(self, template_name: str) -> Template: ... @property def template_context_processors(self) -> list[Callable[..., Any]]: ... +class Template: + template: base.Template + backend: Jinja2 + origin: Origin + def __init__(self, template: base.Template, backend: Jinja2) -> None: ... + def render( + self, + context: Mapping[str, Any] | None = ..., + request: HttpRequest | None = ..., + ) -> SafeText: ... + class Origin: name: str = ... template_name: str | None = ... diff --git a/django-stubs/template/backends/utils.pyi b/django-stubs/template/backends/utils.pyi index c71527866..2b54c2ba9 100644 --- a/django-stubs/template/backends/utils.pyi +++ b/django-stubs/template/backends/utils.pyi @@ -1,9 +1,9 @@ -from typing import Any +from collections.abc import Callable from django.http.request import HttpRequest from django.utils.safestring import SafeText def csrf_input(request: HttpRequest) -> SafeText: ... -csrf_input_lazy: Any -csrf_token_lazy: Any +csrf_input_lazy: Callable[[HttpRequest], SafeText] +csrf_token_lazy: Callable[[HttpRequest], SafeText] diff --git a/django-stubs/template/base.pyi b/django-stubs/template/base.pyi index ab3747fc6..d1c69114c 100644 --- a/django-stubs/template/base.pyi +++ b/django-stubs/template/base.pyi @@ -67,7 +67,7 @@ class Template: self, context: Context | dict[str, Any] | None = ..., request: HttpRequest | None = ..., - ) -> Any: ... + ) -> SafeText: ... def compile_nodelist(self) -> NodeList: ... def get_exception_info( self, exception: Exception, token: Token diff --git a/django-stubs/template/loader.pyi b/django-stubs/template/loader.pyi index fc20d0413..44b92268d 100644 --- a/django-stubs/template/loader.pyi +++ b/django-stubs/template/loader.pyi @@ -1,20 +1,21 @@ from typing import Any from django.http.request import HttpRequest -from django.template.backends.django import Template +from django.template.backends.base import _BaseTemplate from django.template.exceptions import ( # noqa: F401 TemplateDoesNotExist as TemplateDoesNotExist, ) +from django.utils.safestring import SafeText from . import engines as engines # noqa: F401 -def get_template(template_name: str, using: str | None = ...) -> Template: ... +def get_template(template_name: str, using: str | None = ...) -> _BaseTemplate: ... def select_template( template_name_list: list[str] | str, using: str | None = ... -) -> Template: ... +) -> _BaseTemplate: ... def render_to_string( template_name: list[str] | str, context: dict[str, Any] | None = ..., request: HttpRequest | None = ..., using: str | None = ..., -) -> str: ... +) -> SafeText: ...