From 5340c60bb5c59b790d6ac33fe0610869721c8c37 Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:06:59 -0400 Subject: [PATCH 1/8] exceptions: Polish ValidationError --- django-stubs/core/exceptions.pyi | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/django-stubs/core/exceptions.pyi b/django-stubs/core/exceptions.pyi index 6be748258..45e24b0b6 100644 --- a/django-stubs/core/exceptions.pyi +++ b/django-stubs/core/exceptions.pyi @@ -1,8 +1,6 @@ from collections.abc import Iterator, Mapping from typing import Any -from django.forms.utils import ErrorDict - class FieldDoesNotExist(Exception): ... class AppRegistryNotReady(Exception): ... @@ -28,14 +26,19 @@ class FieldError(Exception): ... NON_FIELD_ERRORS: str class ValidationError(Exception): - error_dict: Any = ... - error_list: Any = ... - message: Any = ... - code: Any = ... - params: Any = ... + error_dict: dict[str, list[ValidationError]] | None + error_list: list[ValidationError] | None + message: str | None + code: str | None + params: Mapping[str, Any] | None def __init__( self, - message: Any, + message: ( + ValidationError + | dict[str, ValidationError | list[str]] + | list[ValidationError | str] + | str + ), code: str | None = ..., params: Mapping[str, Any] | None = ..., ) -> None: ... @@ -44,9 +47,9 @@ class ValidationError(Exception): @property def messages(self) -> list[str]: ... def update_error_dict( - self, error_dict: Mapping[str, Any] - ) -> dict[str, list[ValidationError]] | ErrorDict: ... - def __iter__(self) -> Iterator[tuple[str, list[str]] | str]: ... + self, error_dict: Mapping[str, list[ValidationError]] + ) -> Mapping[str, list[ValidationError]]: ... + def __iter__(self) -> Iterator[tuple[str, list[ValidationError]] | str]: ... class EmptyResultSet(Exception): ... class SynchronousOnlyOperation(Exception): ... From 565646d6e8cb21d670cb87ab117006687d0e0549 Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:07:15 -0400 Subject: [PATCH 2/8] exceptions: Add TooManyFilesSent, FullResultSet --- django-stubs/core/exceptions.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django-stubs/core/exceptions.pyi b/django-stubs/core/exceptions.pyi index 45e24b0b6..42d815712 100644 --- a/django-stubs/core/exceptions.pyi +++ b/django-stubs/core/exceptions.pyi @@ -14,6 +14,7 @@ class SuspiciousFileOperation(SuspiciousOperation): ... class DisallowedHost(SuspiciousOperation): ... class DisallowedRedirect(SuspiciousOperation): ... class TooManyFieldsSent(SuspiciousOperation): ... +class TooManyFilesSent(SuspiciousOperation): ... class RequestDataTooBig(SuspiciousOperation): ... class RequestAborted(Exception): ... class BadRequest(Exception): ... @@ -52,4 +53,5 @@ class ValidationError(Exception): def __iter__(self) -> Iterator[tuple[str, list[ValidationError]] | str]: ... class EmptyResultSet(Exception): ... +class FullResultSet(Exception): ... class SynchronousOnlyOperation(Exception): ... From 563ca7bec550e9f4b4a69d702fbadf59125c81d0 Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:08:33 -0400 Subject: [PATCH 3/8] forms.utils: Add missing classes, polish existing ones --- django-stubs/forms/utils.pyi | 49 ++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/django-stubs/forms/utils.pyi b/django-stubs/forms/utils.pyi index d9b06c8e4..187fb0153 100644 --- a/django-stubs/forms/utils.pyi +++ b/django-stubs/forms/utils.pyi @@ -1,34 +1,61 @@ from collections import UserList -from collections.abc import Sequence +from collections.abc import Mapping, Sequence from datetime import datetime from typing import Any from django.core.exceptions import ValidationError +from django.forms.renderers import BaseRenderer from django.utils.safestring import SafeText def pretty_name(name: str) -> str: ... def flatatt(attrs: dict[str, Any]) -> SafeText: ... -class ErrorDict(dict[str, Any]): +class RenderableMixin: + def get_context(self) -> Mapping[str, Any]: ... + def render( + self, + template_name: str | None = ..., + context: Mapping[str, Any] | None = ..., + renderer: BaseRenderer | None = ..., + ) -> SafeText: ... + +class RenderableFormMixin(RenderableMixin): + def as_p(self) -> SafeText: ... + def as_table(self) -> SafeText: ... + def as_ul(self) -> SafeText: ... + def as_div(self) -> SafeText: ... + +class RenderableErrorMixin(RenderableMixin): + def as_json(self, escape_html: bool = ...) -> str: ... + def as_text(self) -> SafeText: ... + def as_ul(self) -> SafeText: ... + +class ErrorDict(dict[str, ErrorList], RenderableErrorMixin): + template_name: str + template_name_text: str + template_name_ul: str + renderer: BaseRenderer + def __init__( + self, *args: Any, renderer: BaseRenderer | None = ..., **kwargs: Any + ): ... def as_data(self) -> dict[str, list[ValidationError]]: ... def get_json_data(self, escape_html: bool = ...) -> dict[str, Any]: ... - def as_json(self, escape_html: bool = ...) -> str: ... - def as_ul(self) -> str: ... - def as_text(self) -> str: ... -class ErrorList(UserList[Any]): +class ErrorList(UserList[ValidationError | str], RenderableErrorMixin): + template_name: str + template_name_text: str + template_name_ul: str data: list[ValidationError | str] - error_class: str = ... + error_class: str + renderer: BaseRenderer def __init__( self, - initlist: ErrorList | Sequence[str | Exception] | None = ..., + initlist: Sequence[str | Exception] | None = ..., error_class: str | None = ..., + renderer: BaseRenderer | None = None, ) -> None: ... def as_data(self) -> list[ValidationError]: ... def get_json_data(self, escape_html: bool = ...) -> list[dict[str, str]]: ... - def as_json(self, escape_html: bool = ...) -> str: ... - def as_ul(self) -> str: ... - def as_text(self) -> str: ... def from_current_timezone(value: datetime) -> datetime: ... def to_current_timezone(value: datetime) -> datetime: ... From 05147a90f0ccd5aa811b074cd6a14b37667d82f1 Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:09:50 -0400 Subject: [PATCH 4/8] forms.forms: Give BaseForm a parent class, make fields more precise Also mark the attributes of Form as class variables --- django-stubs/forms/forms.pyi | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/django-stubs/forms/forms.pyi b/django-stubs/forms/forms.pyi index ca387f908..2f953c491 100644 --- a/django-stubs/forms/forms.pyi +++ b/django-stubs/forms/forms.pyi @@ -1,5 +1,5 @@ from collections.abc import Iterator, Mapping -from typing import Any +from typing import Any, ClassVar from django.core.exceptions import ValidationError as ValidationError from django.core.files import uploadedfile @@ -7,14 +7,14 @@ from django.db.models.options import Options from django.forms.boundfield import BoundField from django.forms.fields import Field from django.forms.renderers import BaseRenderer -from django.forms.utils import ErrorDict, ErrorList +from django.forms.utils import ErrorDict, ErrorList, RenderableFormMixin from django.forms.widgets import Media, MediaDefiningClass from django.utils.datastructures import MultiValueDict from django.utils.safestring import SafeText class DeclarativeFieldsMetaclass(MediaDefiningClass): ... -class BaseForm: +class BaseForm(RenderableFormMixin): _meta: Options[Any] default_renderer: type[BaseRenderer] = ... field_order: list[str] | None = ... @@ -28,7 +28,7 @@ class BaseForm: prefix: str | None = ... label_suffix: str = ... empty_permitted: bool = ... - fields: dict[str, Any] = ... + fields: dict[str, Field] = ... renderer: BaseRenderer = ... cleaned_data: dict[str, Any] = ... def __init__( @@ -53,9 +53,6 @@ class BaseForm: def is_valid(self) -> bool: ... def add_prefix(self, field_name: str) -> str: ... def add_initial_prefix(self, field_name: str) -> str: ... - def as_table(self) -> SafeText: ... - def as_ul(self) -> SafeText: ... - def as_p(self) -> SafeText: ... def non_field_errors(self) -> ErrorList: ... def add_error(self, field: str | None, error: ValidationError | str) -> None: ... def has_error(self, field: str, code: str | None = ...) -> bool: ... @@ -79,6 +76,6 @@ class BaseForm: errors_on_separate_row: bool, ) -> SafeText: ... -class Form(BaseForm): - base_fields: dict[str, Field] - declared_fields: dict[str, Field] +class Form(BaseForm, metaclass=DeclarativeFieldsMetaclass): + base_fields: ClassVar[dict[str, Field]] + declared_fields: ClassVar[dict[str, Field]] From c815c96e4c00d447eeeb1bb9975db138a60fd170 Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:29:13 -0400 Subject: [PATCH 5/8] forms.models: Define formfield_callback --- django-stubs/forms/models.pyi | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/django-stubs/forms/models.pyi b/django-stubs/forms/models.pyi index 2fe8863a5..6a675bc58 100644 --- a/django-stubs/forms/models.pyi +++ b/django-stubs/forms/models.pyi @@ -7,7 +7,7 @@ from collections.abc import ( Sequence, ) from datetime import datetime -from typing import Any, ClassVar, TypeVar +from typing import Any, ClassVar, Protocol, TypeVar from typing_extensions import Literal from unittest.mock import MagicMock from uuid import UUID @@ -33,6 +33,11 @@ _ErrorMessages = dict[str, dict[str, str]] _M = TypeVar("_M", bound=Model) +# Modeled from example: +# https://docs.djangoproject.com/en/4.2/topics/forms/modelforms/#overriding-the-default-fields +class FormFieldCallback(Protocol): + def __call__(self, db_field: models.Field[Any, Any], **kwargs: Any) -> Field: ... + def construct_instance( form: BaseForm, instance: _M, @@ -47,7 +52,7 @@ def fields_for_model( fields: _Fields | None = ..., exclude: _Fields | None = ..., widgets: dict[str, type[Input]] | dict[str, Widget] | None = ..., - formfield_callback: Callable[..., Any] | str | None = ..., + formfield_callback: FormFieldCallback | None = ..., localized_fields: tuple[str] | str | None = ..., labels: _Labels | None = ..., help_texts: dict[str, str] | None = ..., @@ -67,6 +72,7 @@ class ModelFormOptions: help_texts: dict[str, str] | None = ... error_messages: _ErrorMessages | None = ... field_classes: dict[str, type[Field]] | None = ... + formfield_callback: FormFieldCallback | None = ... def __init__(self, options: type | None = ...) -> None: ... class ModelFormMetaclass(DeclarativeFieldsMetaclass): ... From 8fbec493b0416593ea7de0d90f3b21b91ab051c1 Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:29:34 -0400 Subject: [PATCH 6/8] forms.models: Add apply_limit_choices_to_to_formfield --- django-stubs/forms/models.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/django-stubs/forms/models.pyi b/django-stubs/forms/models.pyi index 6a675bc58..11702d0cf 100644 --- a/django-stubs/forms/models.pyi +++ b/django-stubs/forms/models.pyi @@ -47,6 +47,7 @@ def construct_instance( def model_to_dict( instance: Model, fields: _Fields | None = ..., exclude: _Fields | None = ... ) -> dict[str, Any]: ... +def apply_limit_choices_to_to_formfield(formfield: Field) -> None: ... def fields_for_model( model: type[Model], fields: _Fields | None = ..., From d68b6228f2a07aea17fd22143b188f34b72b8ecc Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:31:02 -0400 Subject: [PATCH 7/8] Move _meta from BaseForm to ModelForm --- django-stubs/forms/forms.pyi | 2 -- django-stubs/forms/models.pyi | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/django-stubs/forms/forms.pyi b/django-stubs/forms/forms.pyi index 2f953c491..c1d93d725 100644 --- a/django-stubs/forms/forms.pyi +++ b/django-stubs/forms/forms.pyi @@ -3,7 +3,6 @@ from typing import Any, ClassVar from django.core.exceptions import ValidationError as ValidationError from django.core.files import uploadedfile -from django.db.models.options import Options from django.forms.boundfield import BoundField from django.forms.fields import Field from django.forms.renderers import BaseRenderer @@ -15,7 +14,6 @@ from django.utils.safestring import SafeText class DeclarativeFieldsMetaclass(MediaDefiningClass): ... class BaseForm(RenderableFormMixin): - _meta: Options[Any] default_renderer: type[BaseRenderer] = ... field_order: list[str] | None = ... use_required_attribute: bool = ... diff --git a/django-stubs/forms/models.pyi b/django-stubs/forms/models.pyi index 11702d0cf..02df0a294 100644 --- a/django-stubs/forms/models.pyi +++ b/django-stubs/forms/models.pyi @@ -99,7 +99,7 @@ class BaseModelForm(BaseForm): def save(self, commit: bool = ...) -> Any: ... class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass): - base_fields: ClassVar[dict[str, Field]] = ... + _meta: ClassVar[ModelFormOptions] def modelform_factory( model: type[Model], From 87c8a0f634dbd81220b26f04f2e0eac83ad02fc1 Mon Sep 17 00:00:00 2001 From: Noelle Leigh <5957867+noelleleigh@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:37:44 -0400 Subject: [PATCH 8/8] Remove unnecessary assignment from class attributes https://typing.readthedocs.io/en/latest/guides/writing_stubs.html#classes --- django-stubs/forms/forms.pyi | 30 +++++++++++++++--------------- django-stubs/forms/models.pyi | 20 ++++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/django-stubs/forms/forms.pyi b/django-stubs/forms/forms.pyi index c1d93d725..d04395069 100644 --- a/django-stubs/forms/forms.pyi +++ b/django-stubs/forms/forms.pyi @@ -14,21 +14,21 @@ from django.utils.safestring import SafeText class DeclarativeFieldsMetaclass(MediaDefiningClass): ... class BaseForm(RenderableFormMixin): - default_renderer: type[BaseRenderer] = ... - field_order: list[str] | None = ... - use_required_attribute: bool = ... - is_bound: bool = ... - data: dict[str, Any] = ... - files: MultiValueDict[str, uploadedfile.UploadedFile] = ... - auto_id: bool | str = ... - initial: dict[str, Any] = ... - error_class: type[ErrorList] = ... - prefix: str | None = ... - label_suffix: str = ... - empty_permitted: bool = ... - fields: dict[str, Field] = ... - renderer: BaseRenderer = ... - cleaned_data: dict[str, Any] = ... + default_renderer: type[BaseRenderer] + field_order: list[str] | None + use_required_attribute: bool + is_bound: bool + data: dict[str, Any] + files: MultiValueDict[str, uploadedfile.UploadedFile] + auto_id: bool | str + initial: dict[str, Any] + error_class: type[ErrorList] + prefix: str | None + label_suffix: str + empty_permitted: bool + fields: dict[str, Field] + renderer: BaseRenderer + cleaned_data: dict[str, Any] def __init__( self, data: Mapping[str, Any] | None = ..., diff --git a/django-stubs/forms/models.pyi b/django-stubs/forms/models.pyi index 02df0a294..18d0ab110 100644 --- a/django-stubs/forms/models.pyi +++ b/django-stubs/forms/models.pyi @@ -64,16 +64,16 @@ def fields_for_model( ) -> dict[str, Any]: ... class ModelFormOptions: - model: type[Model] | None = ... - fields: _Fields | None = ... - exclude: _Fields | None = ... - widgets: dict[str, Widget | Input] | None = ... - localized_fields: tuple[str] | str | None = ... - labels: _Labels | None = ... - help_texts: dict[str, str] | None = ... - error_messages: _ErrorMessages | None = ... - field_classes: dict[str, type[Field]] | None = ... - formfield_callback: FormFieldCallback | None = ... + model: type[Model] | None + fields: _Fields | None + exclude: _Fields | None + widgets: dict[str, Widget | Input] | None + localized_fields: tuple[str] | str | None + labels: _Labels | None + help_texts: dict[str, str] | None + error_messages: _ErrorMessages | None + field_classes: dict[str, type[Field]] | None + formfield_callback: FormFieldCallback | None def __init__(self, options: type | None = ...) -> None: ... class ModelFormMetaclass(DeclarativeFieldsMetaclass): ...