diff --git a/django-stubs/contrib/auth/views.pyi b/django-stubs/contrib/auth/views.pyi index 8b45cf9bb..40163cf16 100644 --- a/django-stubs/contrib/auth/views.pyi +++ b/django-stubs/contrib/auth/views.pyi @@ -25,7 +25,7 @@ class LoginView(RedirectURLMixin, FormView[AuthenticationForm]): extra_context: Any def get_redirect_url(self) -> str: ... -class LogoutView(RedirectURLMixin, TemplateView): +class LogoutView(RedirectURLMixin, TemplateView[HttpResponse]): next_page: str | None redirect_field_name: str extra_context: Any diff --git a/django-stubs/views/generic/base.pyi b/django-stubs/views/generic/base.pyi index 464bfb05b..6a0176838 100644 --- a/django-stubs/views/generic/base.pyi +++ b/django-stubs/views/generic/base.pyi @@ -1,9 +1,10 @@ import logging from collections.abc import Callable, Mapping, Sequence -from typing import Any +from typing import Any, Generic, TypeVar from django.http.request import HttpRequest from django.http.response import HttpResponse, HttpResponseBase +from django.template.response import TemplateResponse from django.utils.functional import _Getter logger: logging.Logger @@ -12,7 +13,9 @@ class ContextMixin: extra_context: Mapping[str, Any] | None def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ... -class View: +_ViewResponse = TypeVar("_ViewResponse", bound=HttpResponseBase, default=HttpResponseBase) + +class View(Generic[_ViewResponse]): http_method_names: Sequence[str] request: HttpRequest args: Any @@ -20,23 +23,25 @@ class View: def __init__(self, **kwargs: Any) -> None: ... view_is_async: _Getter[bool] | bool @classmethod - def as_view(cls: Any, **initkwargs: Any) -> Callable[..., HttpResponseBase]: ... + def as_view(cls: Any, **initkwargs: Any) -> Callable[..., _ViewResponse]: ... def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: ... - def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: ... + def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> _ViewResponse: ... def http_method_not_allowed(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: ... def options(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: ... -class TemplateResponseMixin: +_TemplateResponse = TypeVar("_TemplateResponse", bound=HttpResponse, default=TemplateResponse) + +class TemplateResponseMixin(Generic[_TemplateResponse]): template_name: str | None template_engine: str | None - response_class: type[HttpResponse] + response_class: type[_TemplateResponse] content_type: str | None request: HttpRequest - def render_to_response(self, context: dict[str, Any], **response_kwargs: Any) -> HttpResponse: ... + def render_to_response(self, context: dict[str, Any], **response_kwargs: Any) -> _TemplateResponse: ... def get_template_names(self) -> list[str]: ... -class TemplateView(TemplateResponseMixin, ContextMixin, View): - def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: ... +class TemplateView(TemplateResponseMixin[_TemplateResponse], ContextMixin, View[_TemplateResponse]): + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> _TemplateResponse: ... class RedirectView(View): permanent: bool diff --git a/ext/django_stubs_ext/patch.py b/ext/django_stubs_ext/patch.py index 7f2f482e8..1db452817 100644 --- a/ext/django_stubs_ext/patch.py +++ b/ext/django_stubs_ext/patch.py @@ -27,6 +27,8 @@ from django.forms.models import BaseModelForm, BaseModelFormSet, ModelChoiceField, ModelFormOptions from django.utils.connection import BaseConnectionHandler, ConnectionProxy from django.utils.functional import classproperty +from django.views import View +from django.views.generic.base import TemplateResponseMixin from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import DeletionMixin, FormMixin from django.views.generic.list import MultipleObjectMixin @@ -84,6 +86,8 @@ def __repr__(self) -> str: MPGeneric(ExpressionWrapper), MPGeneric(ReverseManyToOneDescriptor), MPGeneric(ModelIterable), + MPGeneric(View), + MPGeneric(TemplateResponseMixin), # These types do have native `__class_getitem__` method since django 3.1: MPGeneric(QuerySet, (3, 1)), MPGeneric(BaseManager, (3, 1)), diff --git a/tests/typecheck/views/generic/test_template.yml b/tests/typecheck/views/generic/test_template.yml index 41115f4e6..e9229f1b6 100644 --- a/tests/typecheck/views/generic/test_template.yml +++ b/tests/typecheck/views/generic/test_template.yml @@ -14,3 +14,23 @@ class MyTemplateView(TemplateView): template_name = "template.html" extra_context = MappingProxyType({}) + +- case: template_view_returns_template_response + main: | + from typing import reveal_type + + from django.views.generic import TemplateView + from django.test import RequestFactory + + + class MyView(TemplateView): + template_name = "template.html" + + + rf = RequestFactory() + request = rf.get("/") + view = MyView.as_view() + response = view(request) + + reveal_type(response) # N: Revealed type is "django.template.response.TemplateResponse" + reveal_type(response.rendered_content) # N: Revealed type is "builtins.str"