mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-10 14:01:56 +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
|
||||
|
||||
_C = TypeVar("_C", bound=Callable)
|
||||
|
||||
@overload
|
||||
def staff_member_required(
|
||||
view_func: _C = ..., redirect_field_name: Optional[str] = ..., login_url: str = ...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@ class SafeText(str, SafeData):
|
||||
SafeString = SafeText
|
||||
|
||||
_C = TypeVar("_C", bound=Callable)
|
||||
|
||||
@overload
|
||||
def mark_safe(s: _SD) -> _SD: ...
|
||||
@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.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]): ...
|
||||
|
||||
@@ -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]): ...
|
||||
|
||||
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