Replace models.Model annotations with type variables (#603)

* Replace models.Model annotations with type variables

* Adds generic type args to generic views

* Adds more tests

* Revert "Adds generic type args to generic views"

This reverts commit 6522f30cdb9027483f46d77167394c84eb7b7f4b.

* Adds Generic support for DetailView and ListView

Co-authored-by: sobolevn <mail@sobolevn.me>
This commit is contained in:
Sondre Lillebø Gundersen
2021-05-01 12:21:19 +02:00
committed by GitHub
parent 5c3898d3b0
commit e4de8453cf
8 changed files with 133 additions and 16 deletions

View File

@@ -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 = ...

View File

@@ -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

View File

@@ -18,6 +18,7 @@ class SafeText(str, SafeData):
SafeString = SafeText
_C = TypeVar("_C", bound=Callable)
@overload
def mark_safe(s: _SD) -> _SD: ...
@overload

View File

@@ -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]): ...

View File

@@ -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]): ...

View File

@@ -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]")

View File

@@ -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"