Skip to content

Commit

Permalink
Forms and exceptions improvements (#259)
Browse files Browse the repository at this point in the history
- Removed some unnecessary `= ...` on class attributes ([see typing docs](https://typing.readthedocs.io/en/latest/guides/writing_stubs.html#classes))
- Added new exceptions to `django.core.exceptions`
- Added more specific types to `ValidationError`
- Gave `BaseForm` its parent class and made **`BaseForm.fields` more specific**
- Moved the `_meta` attribute from `BaseForm` to `ModelForm`
- Gave `Form` its metaclass and marked its attributes as class variables
- Improved type for `ModelFormOptions.formfield_callback`
- `forms.utils`: Added new classes, polished types on existing ones
  • Loading branch information
noelleleigh authored Aug 20, 2024
1 parent 8a8a805 commit 84064af
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 60 deletions.
27 changes: 16 additions & 11 deletions django-stubs/core/exceptions.pyi
Original file line number Diff line number Diff line change
@@ -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): ...

Expand All @@ -16,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): ...
Expand All @@ -28,14 +27,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: ...
Expand All @@ -44,9 +48,10 @@ 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 FullResultSet(Exception): ...
class SynchronousOnlyOperation(Exception): ...
47 changes: 21 additions & 26 deletions django-stubs/forms/forms.pyi
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
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
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:
_meta: Options[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, Any] = ...
renderer: BaseRenderer = ...
cleaned_data: dict[str, Any] = ...
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]
def __init__(
self,
data: Mapping[str, Any] | None = ...,
Expand All @@ -53,9 +51,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: ...
Expand All @@ -79,6 +74,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]]
31 changes: 19 additions & 12 deletions django-stubs/forms/models.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -42,12 +47,13 @@ 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 = ...,
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 = ...,
Expand All @@ -58,15 +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 = ...
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): ...
Expand All @@ -92,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],
Expand Down
49 changes: 38 additions & 11 deletions django-stubs/forms/utils.pyi
Original file line number Diff line number Diff line change
@@ -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: ...

0 comments on commit 84064af

Please sign in to comment.