diff --git a/django-stubs/contrib/admin/views/decorators.pyi b/django-stubs/contrib/admin/views/decorators.pyi index ce7516d..55bbeb3 100644 --- a/django-stubs/contrib/admin/views/decorators.pyi +++ b/django-stubs/contrib/admin/views/decorators.pyi @@ -1,6 +1,7 @@ from typing import Callable, Optional, TypeVar, overload _C = TypeVar("_C", bound=Callable) + @overload def staff_member_required( view_func: _C = ..., redirect_field_name: Optional[str] = ..., login_url: str = ... diff --git a/django-stubs/utils/encoding.pyi b/django-stubs/utils/encoding.pyi index 11fd059..75579f8 100644 --- a/django-stubs/utils/encoding.pyi +++ b/django-stubs/utils/encoding.pyi @@ -12,6 +12,7 @@ class DjangoUnicodeDecodeError(UnicodeDecodeError): _P = TypeVar("_P", bound=Promise) _S = TypeVar("_S", bound=str) _PT = TypeVar("_PT", None, int, float, Decimal, datetime.datetime, datetime.date, datetime.time) + @overload def smart_text(s: _P, encoding: str = ..., strings_only: bool = ..., errors: str = ...) -> _P: ... @overload @@ -40,6 +41,7 @@ def force_bytes(s: Any, encoding: str = ..., strings_only: bool = ..., errors: s smart_str = smart_text force_str = force_text + @overload def iri_to_uri(iri: None) -> None: ... @overload diff --git a/django-stubs/utils/safestring.pyi b/django-stubs/utils/safestring.pyi index ac52284..fe4cd8f 100644 --- a/django-stubs/utils/safestring.pyi +++ b/django-stubs/utils/safestring.pyi @@ -18,6 +18,7 @@ class SafeText(str, SafeData): SafeString = SafeText _C = TypeVar("_C", bound=Callable) + @overload def mark_safe(s: _SD) -> _SD: ... @overload diff --git a/django-stubs/views/generic/detail.pyi b/django-stubs/views/generic/detail.pyi index 70229ea..c706d22 100644 --- a/django-stubs/views/generic/detail.pyi +++ b/django-stubs/views/generic/detail.pyi @@ -1,28 +1,30 @@ -from typing import Any, Optional, Type +from typing import Any, Generic, Optional, Type, TypeVar from django.views.generic.base import ContextMixin, TemplateResponseMixin, View from django.db import models from django.http import HttpRequest, HttpResponse -class SingleObjectMixin(ContextMixin): - model: Type[models.Model] = ... - queryset: models.query.QuerySet = ... +T = TypeVar("T", bound=models.Model) + +class SingleObjectMixin(Generic[T], ContextMixin): + model: Type[T] = ... + queryset: models.query.QuerySet[T] = ... slug_field: str = ... context_object_name: str = ... slug_url_kwarg: str = ... pk_url_kwarg: str = ... query_pk_and_slug: bool = ... - def get_object(self, queryset: Optional[models.query.QuerySet] = ...) -> models.Model: ... - def get_queryset(self) -> models.query.QuerySet: ... + def get_object(self, queryset: Optional[models.query.QuerySet] = ...) -> T: ... + def get_queryset(self) -> models.query.QuerySet[T]: ... def get_slug_field(self) -> str: ... def get_context_object_name(self, obj: Any) -> Optional[str]: ... -class BaseDetailView(SingleObjectMixin, View): +class BaseDetailView(SingleObjectMixin[T], View): def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: ... class SingleObjectTemplateResponseMixin(TemplateResponseMixin): template_name_field: Optional[str] = ... template_name_suffix: str = ... -class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView): ... +class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView[T]): ... diff --git a/django-stubs/views/generic/list.pyi b/django-stubs/views/generic/list.pyi index c933797..83a3e98 100644 --- a/django-stubs/views/generic/list.pyi +++ b/django-stubs/views/generic/list.pyi @@ -1,4 +1,4 @@ -from typing import Any, Optional, Sequence, Tuple, Type +from typing import Any, Generic, Optional, Sequence, Tuple, Type, TypeVar from django.core.paginator import Paginator from django.db.models.query import QuerySet, _BaseQuerySet @@ -7,19 +7,23 @@ from django.views.generic.base import ContextMixin, TemplateResponseMixin, View from django.db.models import Model from django.http import HttpRequest, HttpResponse -class MultipleObjectMixin(ContextMixin): +T = TypeVar("T", bound=Model) + +class MultipleObjectMixin(Generic[T], ContextMixin): allow_empty: bool = ... - queryset: Optional[QuerySet] = ... - model: Optional[Type[Model]] = ... + queryset: Optional[QuerySet[T]] = ... + model: Optional[Type[T]] = ... paginate_by: int = ... paginate_orphans: int = ... context_object_name: Optional[str] = ... paginator_class: Type[Paginator] = ... page_kwarg: str = ... ordering: Sequence[str] = ... - def get_queryset(self) -> QuerySet: ... + def get_queryset(self) -> QuerySet[T]: ... def get_ordering(self) -> Sequence[str]: ... - def paginate_queryset(self, queryset: _BaseQuerySet, page_size: int) -> Tuple[Paginator, int, QuerySet, bool]: ... + def paginate_queryset( + self, queryset: _BaseQuerySet, page_size: int + ) -> Tuple[Paginator, int, QuerySet[T], bool]: ... def get_paginate_by(self, queryset: _BaseQuerySet) -> Optional[int]: ... def get_paginator( self, queryset: QuerySet, per_page: int, orphans: int = ..., allow_empty_first_page: bool = ..., **kwargs: Any @@ -28,10 +32,10 @@ class MultipleObjectMixin(ContextMixin): def get_allow_empty(self) -> bool: ... def get_context_object_name(self, object_list: _BaseQuerySet) -> Optional[str]: ... -class BaseListView(MultipleObjectMixin, View): +class BaseListView(MultipleObjectMixin[T], View): def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: ... class MultipleObjectTemplateResponseMixin(TemplateResponseMixin): template_name_suffix: str = ... -class ListView(MultipleObjectTemplateResponseMixin, BaseListView): ... +class ListView(MultipleObjectTemplateResponseMixin, BaseListView[T]): ... diff --git a/tests/typecheck/views/generic/test_detail.yml b/tests/typecheck/views/generic/test_detail.yml new file mode 100644 index 0000000..8d65e4a --- /dev/null +++ b/tests/typecheck/views/generic/test_detail.yml @@ -0,0 +1,55 @@ +- case: generic_detail_view + main: | + from django.views.generic import DetailView + from django.db.models import QuerySet + + from myapp.models import MyModel + + class MyDetailView(DetailView[MyModel]): + model = MyModel + queryset = MyModel.objects.all() + + def get_queryset(self) -> QuerySet[MyModel]: + self.get_object(super().get_queryset()) + return super().get_queryset() + custom_settings: | + INSTALLED_APPS = ('myapp',) + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + ... + + +- case: generic_detail_view_wrong + main: | + from django.views.generic import DetailView + from django.db.models import QuerySet + + from myapp.models import MyModel, Other + + class MyDetailView(DetailView[Other]): + model = MyModel + queryset = MyModel.objects.all() + + def get_queryset(self) -> QuerySet[MyModel]: + self.get_object(super().get_queryset()) + return super().get_queryset() + custom_settings: | + INSTALLED_APPS = ('myapp',) + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + ... + class Other(models.Model): + ... + out: | + main:7: error: Incompatible types in assignment (expression has type "Type[MyModel]", base class "SingleObjectMixin" defined the type as "Type[Other]") + main:8: error: Incompatible types in assignment (expression has type "Manager[MyModel]", base class "SingleObjectMixin" defined the type as "QuerySet[Other]") + main:10: error: Return type "QuerySet[MyModel]" of "get_queryset" incompatible with return type "QuerySet[Other]" in supertype "SingleObjectMixin" + main:12: error: Incompatible return value type (got "QuerySet[Other]", expected "QuerySet[MyModel]") diff --git a/tests/typecheck/test_views.yml b/tests/typecheck/views/generic/test_edit.yml similarity index 100% rename from tests/typecheck/test_views.yml rename to tests/typecheck/views/generic/test_edit.yml diff --git a/tests/typecheck/views/generic/test_list.yml b/tests/typecheck/views/generic/test_list.yml new file mode 100644 index 0000000..b32afef --- /dev/null +++ b/tests/typecheck/views/generic/test_list.yml @@ -0,0 +1,52 @@ +- case: generic_list_view + main: | + from django.views.generic import ListView + from django.db.models import QuerySet + + from myapp.models import MyModel + + class MyListView(ListView[MyModel]): + model = MyModel + queryset = MyModel.objects.all() + + def get_queryset(self) -> QuerySet[MyModel]: + ... + custom_settings: | + INSTALLED_APPS = ('myapp',) + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + ... + + +- case: generic_list_view_wrong + main: | + from django.views.generic import ListView + from django.db.models import QuerySet + + from myapp.models import MyModel, Other + + class MyListView(ListView[Other]): + model = MyModel + queryset = MyModel.objects.all() + + def get_queryset(self) -> QuerySet[MyModel]: + ... + custom_settings: | + INSTALLED_APPS = ('myapp',) + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + ... + class Other(models.Model): + ... + out: | + main:7: error: Incompatible types in assignment (expression has type "Type[MyModel]", base class "MultipleObjectMixin" defined the type as "Optional[Type[Other]]") + main:8: error: Incompatible types in assignment (expression has type "Manager[MyModel]", base class "MultipleObjectMixin" defined the type as "Optional[QuerySet[Other]]") + main:10: error: Return type "QuerySet[MyModel]" of "get_queryset" incompatible with return type "QuerySet[Other]" in supertype "MultipleObjectMixin"