mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-11 06:21:58 +08:00
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:
committed by
GitHub
parent
5c3898d3b0
commit
e4de8453cf
@@ -1,6 +1,7 @@
|
|||||||
from typing import Callable, Optional, TypeVar, overload
|
from typing import Callable, Optional, TypeVar, overload
|
||||||
|
|
||||||
_C = TypeVar("_C", bound=Callable)
|
_C = TypeVar("_C", bound=Callable)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def staff_member_required(
|
def staff_member_required(
|
||||||
view_func: _C = ..., redirect_field_name: Optional[str] = ..., login_url: str = ...
|
view_func: _C = ..., redirect_field_name: Optional[str] = ..., login_url: str = ...
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class DjangoUnicodeDecodeError(UnicodeDecodeError):
|
|||||||
_P = TypeVar("_P", bound=Promise)
|
_P = TypeVar("_P", bound=Promise)
|
||||||
_S = TypeVar("_S", bound=str)
|
_S = TypeVar("_S", bound=str)
|
||||||
_PT = TypeVar("_PT", None, int, float, Decimal, datetime.datetime, datetime.date, datetime.time)
|
_PT = TypeVar("_PT", None, int, float, Decimal, datetime.datetime, datetime.date, datetime.time)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def smart_text(s: _P, encoding: str = ..., strings_only: bool = ..., errors: str = ...) -> _P: ...
|
def smart_text(s: _P, encoding: str = ..., strings_only: bool = ..., errors: str = ...) -> _P: ...
|
||||||
@overload
|
@overload
|
||||||
@@ -40,6 +41,7 @@ def force_bytes(s: Any, encoding: str = ..., strings_only: bool = ..., errors: s
|
|||||||
|
|
||||||
smart_str = smart_text
|
smart_str = smart_text
|
||||||
force_str = force_text
|
force_str = force_text
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def iri_to_uri(iri: None) -> None: ...
|
def iri_to_uri(iri: None) -> None: ...
|
||||||
@overload
|
@overload
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class SafeText(str, SafeData):
|
|||||||
SafeString = SafeText
|
SafeString = SafeText
|
||||||
|
|
||||||
_C = TypeVar("_C", bound=Callable)
|
_C = TypeVar("_C", bound=Callable)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def mark_safe(s: _SD) -> _SD: ...
|
def mark_safe(s: _SD) -> _SD: ...
|
||||||
@overload
|
@overload
|
||||||
|
|||||||
@@ -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.views.generic.base import ContextMixin, TemplateResponseMixin, View
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
class SingleObjectMixin(ContextMixin):
|
T = TypeVar("T", bound=models.Model)
|
||||||
model: Type[models.Model] = ...
|
|
||||||
queryset: models.query.QuerySet = ...
|
class SingleObjectMixin(Generic[T], ContextMixin):
|
||||||
|
model: Type[T] = ...
|
||||||
|
queryset: models.query.QuerySet[T] = ...
|
||||||
slug_field: str = ...
|
slug_field: str = ...
|
||||||
context_object_name: str = ...
|
context_object_name: str = ...
|
||||||
slug_url_kwarg: str = ...
|
slug_url_kwarg: str = ...
|
||||||
pk_url_kwarg: str = ...
|
pk_url_kwarg: str = ...
|
||||||
query_pk_and_slug: bool = ...
|
query_pk_and_slug: bool = ...
|
||||||
def get_object(self, queryset: Optional[models.query.QuerySet] = ...) -> models.Model: ...
|
def get_object(self, queryset: Optional[models.query.QuerySet] = ...) -> T: ...
|
||||||
def get_queryset(self) -> models.query.QuerySet: ...
|
def get_queryset(self) -> models.query.QuerySet[T]: ...
|
||||||
def get_slug_field(self) -> str: ...
|
def get_slug_field(self) -> str: ...
|
||||||
def get_context_object_name(self, obj: Any) -> Optional[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: ...
|
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: ...
|
||||||
|
|
||||||
class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
|
class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
|
||||||
template_name_field: Optional[str] = ...
|
template_name_field: Optional[str] = ...
|
||||||
template_name_suffix: str = ...
|
template_name_suffix: str = ...
|
||||||
|
|
||||||
class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView): ...
|
class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView[T]): ...
|
||||||
|
|||||||
@@ -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.core.paginator import Paginator
|
||||||
from django.db.models.query import QuerySet, _BaseQuerySet
|
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.db.models import Model
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
class MultipleObjectMixin(ContextMixin):
|
T = TypeVar("T", bound=Model)
|
||||||
|
|
||||||
|
class MultipleObjectMixin(Generic[T], ContextMixin):
|
||||||
allow_empty: bool = ...
|
allow_empty: bool = ...
|
||||||
queryset: Optional[QuerySet] = ...
|
queryset: Optional[QuerySet[T]] = ...
|
||||||
model: Optional[Type[Model]] = ...
|
model: Optional[Type[T]] = ...
|
||||||
paginate_by: int = ...
|
paginate_by: int = ...
|
||||||
paginate_orphans: int = ...
|
paginate_orphans: int = ...
|
||||||
context_object_name: Optional[str] = ...
|
context_object_name: Optional[str] = ...
|
||||||
paginator_class: Type[Paginator] = ...
|
paginator_class: Type[Paginator] = ...
|
||||||
page_kwarg: str = ...
|
page_kwarg: str = ...
|
||||||
ordering: Sequence[str] = ...
|
ordering: Sequence[str] = ...
|
||||||
def get_queryset(self) -> QuerySet: ...
|
def get_queryset(self) -> QuerySet[T]: ...
|
||||||
def get_ordering(self) -> Sequence[str]: ...
|
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_paginate_by(self, queryset: _BaseQuerySet) -> Optional[int]: ...
|
||||||
def get_paginator(
|
def get_paginator(
|
||||||
self, queryset: QuerySet, per_page: int, orphans: int = ..., allow_empty_first_page: bool = ..., **kwargs: Any
|
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_allow_empty(self) -> bool: ...
|
||||||
def get_context_object_name(self, object_list: _BaseQuerySet) -> Optional[str]: ...
|
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: ...
|
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: ...
|
||||||
|
|
||||||
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
|
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
|
||||||
template_name_suffix: str = ...
|
template_name_suffix: str = ...
|
||||||
|
|
||||||
class ListView(MultipleObjectTemplateResponseMixin, BaseListView): ...
|
class ListView(MultipleObjectTemplateResponseMixin, BaseListView[T]): ...
|
||||||
|
|||||||
55
tests/typecheck/views/generic/test_detail.yml
Normal file
55
tests/typecheck/views/generic/test_detail.yml
Normal 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]")
|
||||||
52
tests/typecheck/views/generic/test_list.yml
Normal file
52
tests/typecheck/views/generic/test_list.yml
Normal 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"
|
||||||
Reference in New Issue
Block a user