From eb702384a8c77dcdaa57d5e56a1bd21272e04eb8 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 16 Jun 2021 11:25:57 +0300 Subject: [PATCH] Improves edit.py and its forms (#648) * Improves edit.py and its forms * Adds tests --- dev-requirements.txt | 3 +- django-stubs/contrib/postgres/indexes.pyi | 1 - django-stubs/forms/models.pyi | 11 +++---- django-stubs/views/generic/edit.pyi | 25 +++++++++------- tests/typecheck/views/generic/test_edit.yml | 33 +++++++++++++++++++++ 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 2824eec..194a790 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,10 +1,11 @@ wheel +black==21.6b0 requests==2.24.0 coreapi==2.3.3 gitpython==3.1.9 pre-commit==2.7.1 pytest==6.1.1 -pytest-mypy-plugins==1.6.1 +pytest-mypy-plugins==1.7.0 psycopg2-binary types-toml==0.1.1 -e ./django_stubs_ext diff --git a/django-stubs/contrib/postgres/indexes.pyi b/django-stubs/contrib/postgres/indexes.pyi index 94e968d..135990d 100644 --- a/django-stubs/contrib/postgres/indexes.pyi +++ b/django-stubs/contrib/postgres/indexes.pyi @@ -5,7 +5,6 @@ from django.db.models.query_utils import Q from django.db.models import Index, Func from django.db.models.expressions import BaseExpression, Combinable - class PostgresIndex(Index): ... class BrinIndex(PostgresIndex): diff --git a/django-stubs/forms/models.pyi b/django-stubs/forms/models.pyi index 2e471de..1886578 100644 --- a/django-stubs/forms/models.pyi +++ b/django-stubs/forms/models.pyi @@ -5,6 +5,7 @@ from typing import ( Collection, Dict, Iterator, + Generic, List, Mapping, MutableMapping, @@ -78,8 +79,8 @@ class ModelFormOptions: class ModelFormMetaclass(DeclarativeFieldsMetaclass): ... -class BaseModelForm(BaseForm): - instance: Any = ... +class BaseModelForm(Generic[_M], BaseForm): + instance: _M def __init__( self, data: Optional[Mapping[str, Any]] = ..., @@ -95,10 +96,10 @@ class BaseModelForm(BaseForm): renderer: Any = ..., ) -> None: ... def validate_unique(self) -> None: ... - save_m2m: Any = ... - def save(self, commit: bool = ...) -> Any: ... + def save(self, commit: bool = ...) -> _M: ... + def save_m2m(self) -> None: ... -class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass): +class ModelForm(BaseModelForm[_M], metaclass=ModelFormMetaclass): base_fields: ClassVar[Dict[str, Field]] = ... def modelform_factory( diff --git a/django-stubs/views/generic/edit.pyi b/django-stubs/views/generic/edit.pyi index 79e4149..eb33341 100644 --- a/django-stubs/views/generic/edit.pyi +++ b/django-stubs/views/generic/edit.pyi @@ -5,14 +5,17 @@ from django.forms.models import BaseModelForm from django.views.generic.base import ContextMixin, TemplateResponseMixin, View from django.views.generic.detail import BaseDetailView, SingleObjectMixin, SingleObjectTemplateResponseMixin from typing_extensions import Literal +from django.db import models from django.http import HttpRequest, HttpResponse _FormT = TypeVar("_FormT", bound=BaseForm) +_ModelFormT = TypeVar("_ModelFormT", bound=BaseModelForm) +_T = TypeVar("_T", bound=models.Model) -class AbstractFormMixin(ContextMixin): +class AbstractFormMixin(Generic[_FormT], ContextMixin): initial: Dict[str, Any] = ... - form_class: Optional[Type[BaseForm]] = ... + form_class: Optional[Type[_FormT]] = ... success_url: Optional[Union[str, Callable[..., Any]]] = ... prefix: Optional[str] = ... def get_initial(self) -> Dict[str, Any]: ... @@ -26,12 +29,12 @@ class FormMixin(Generic[_FormT], AbstractFormMixin): def form_valid(self, form: _FormT) -> HttpResponse: ... def form_invalid(self, form: _FormT) -> HttpResponse: ... -class ModelFormMixin(AbstractFormMixin, SingleObjectMixin): +class ModelFormMixin(Generic[_T, _ModelFormT], AbstractFormMixin, SingleObjectMixin[_T]): fields: Optional[Union[Sequence[str], Literal["__all__"]]] = ... - def get_form_class(self) -> Type[BaseModelForm]: ... - def get_form(self, form_class: Optional[Type[BaseModelForm]] = ...) -> BaseModelForm: ... - def form_valid(self, form: BaseModelForm) -> HttpResponse: ... - def form_invalid(self, form: BaseModelForm) -> HttpResponse: ... + def get_form_class(self) -> Type[_ModelFormT]: ... + def get_form(self, form_class: Optional[Type[_ModelFormT]] = ...) -> BaseModelForm: ... + def form_valid(self, form: _ModelFormT) -> HttpResponse: ... + def form_invalid(self, form: _ModelFormT) -> HttpResponse: ... class ProcessFormView(View): def get(self, request: HttpRequest, *args: str, **kwargs: Any) -> HttpResponse: ... @@ -40,10 +43,10 @@ class ProcessFormView(View): class BaseFormView(FormMixin[_FormT], ProcessFormView): ... class FormView(TemplateResponseMixin, BaseFormView[_FormT]): ... -class BaseCreateView(ModelFormMixin, ProcessFormView): ... -class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView): ... -class BaseUpdateView(ModelFormMixin, ProcessFormView): ... -class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView): ... +class BaseCreateView(ModelFormMixin[_T, _ModelFormT], ProcessFormView): ... +class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView[_T, _ModelFormT]): ... +class BaseUpdateView(ModelFormMixin[_T, _ModelFormT], ProcessFormView): ... +class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView[_T, _ModelFormT]): ... class DeletionMixin: success_url: Optional[str] = ... diff --git a/tests/typecheck/views/generic/test_edit.yml b/tests/typecheck/views/generic/test_edit.yml index 3cd62ec..e82ed88 100644 --- a/tests/typecheck/views/generic/test_edit.yml +++ b/tests/typecheck/views/generic/test_edit.yml @@ -10,6 +10,8 @@ """Ensure that form can have type AuthenticationForm.""" form.get_user() return HttpResponseRedirect(self.get_success_url()) + + - case: dispatch_http_response main: | from django.http import HttpResponse @@ -19,6 +21,8 @@ def dispatch(self, request, *args, **kwargs) -> HttpResponse: response: HttpResponse return response + + - case: dispatch_streaming_http_response main: | from django.http import StreamingHttpResponse @@ -28,3 +32,32 @@ def dispatch(self, request, *args, **kwargs) -> StreamingHttpResponse: response: StreamingHttpResponse return response + + +- case: generic_form_views + main: | + from django.views.generic.edit import CreateView, UpdateView + from django import forms + from myapp.models import Article + + class ArticleModelForm(forms.ModelForm[Article]): + class Meta: + model = Article + + class MyCreateView(CreateView[Article, ArticleModelForm]): + def some(self) -> None: + reveal_type(self.get_form_class()) # N: Revealed type is "Type[main.ArticleModelForm*]" + + class MyUpdateView(UpdateView[Article, ArticleModelForm]): + def some(self) -> None: + reveal_type(self.get_form_class()) # N: Revealed type is "Type[main.ArticleModelForm*]" + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class Article(models.Model): + pass