-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat(apidocs): Let serializers declare the type of validated_data #123923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+390
−0
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,287 @@ | ||
| # mypy: disable-error-code=type-arg | ||
| # Vendored from rest_framework-stubs. One change: BaseSerializer takes a second | ||
| # parameter for the shape of validated_data. Both default to Any, so opting in | ||
| # is per-serializer. Declare keys NotRequired -- partial=True omits absent ones. | ||
| from _typeshed import Incomplete | ||
| from collections.abc import Iterable, Iterator, Mapping, MutableMapping, Sequence | ||
| from typing import Any, ClassVar, Generic, Literal, NoReturn, TypeVar | ||
|
|
||
| from django.db import models | ||
| from django.db.models import Manager, Model, QuerySet | ||
| from django.utils.functional import cached_property | ||
| from django_stubs_ext import StrOrPromise | ||
| from rest_framework.exceptions import APIException as APIException | ||
| from rest_framework.exceptions import AuthenticationFailed as AuthenticationFailed | ||
| from rest_framework.exceptions import ErrorDetail as ErrorDetail | ||
| from rest_framework.exceptions import MethodNotAllowed as MethodNotAllowed | ||
| from rest_framework.exceptions import NotAcceptable as NotAcceptable | ||
| from rest_framework.exceptions import NotAuthenticated as NotAuthenticated | ||
| from rest_framework.exceptions import NotFound as NotFound | ||
| from rest_framework.exceptions import ParseError as ParseError | ||
| from rest_framework.exceptions import PermissionDenied as PermissionDenied | ||
| from rest_framework.exceptions import Throttled as Throttled | ||
| from rest_framework.exceptions import UnsupportedMediaType as UnsupportedMediaType | ||
| from rest_framework.exceptions import ValidationError as ValidationError | ||
| from rest_framework.fields import BooleanField as BooleanField | ||
| from rest_framework.fields import CharField as CharField | ||
| from rest_framework.fields import ChoiceField as ChoiceField | ||
| from rest_framework.fields import CreateOnlyDefault as CreateOnlyDefault | ||
| from rest_framework.fields import CurrentUserDefault as CurrentUserDefault | ||
| from rest_framework.fields import DateField as DateField | ||
| from rest_framework.fields import DateTimeField as DateTimeField | ||
| from rest_framework.fields import DecimalField as DecimalField | ||
| from rest_framework.fields import DictField as DictField | ||
| from rest_framework.fields import DurationField as DurationField | ||
| from rest_framework.fields import EmailField as EmailField | ||
| from rest_framework.fields import Field as Field | ||
| from rest_framework.fields import FileField as FileField | ||
| from rest_framework.fields import FilePathField as FilePathField | ||
| from rest_framework.fields import FloatField as FloatField | ||
| from rest_framework.fields import HiddenField as HiddenField | ||
| from rest_framework.fields import HStoreField as HStoreField | ||
| from rest_framework.fields import ImageField as ImageField | ||
| from rest_framework.fields import IntegerField as IntegerField | ||
| from rest_framework.fields import IPAddressField as IPAddressField | ||
| from rest_framework.fields import JSONField as JSONField | ||
| from rest_framework.fields import ListField as ListField | ||
| from rest_framework.fields import ModelField as ModelField | ||
| from rest_framework.fields import MultipleChoiceField as MultipleChoiceField | ||
| from rest_framework.fields import NullBooleanField as NullBooleanField | ||
| from rest_framework.fields import ReadOnlyField as ReadOnlyField | ||
| from rest_framework.fields import RegexField as RegexField | ||
| from rest_framework.fields import SerializerMethodField as SerializerMethodField | ||
| from rest_framework.fields import SkipField as SkipField | ||
| from rest_framework.fields import SlugField as SlugField | ||
| from rest_framework.fields import TimeField as TimeField | ||
| from rest_framework.fields import URLField as URLField | ||
| from rest_framework.fields import UUIDField as UUIDField | ||
| from rest_framework.fields import _DefaultInitial | ||
| from rest_framework.fields import empty as empty | ||
| from rest_framework.relations import Hyperlink as Hyperlink | ||
| from rest_framework.relations import HyperlinkedIdentityField as HyperlinkedIdentityField | ||
| from rest_framework.relations import HyperlinkedRelatedField as HyperlinkedRelatedField | ||
| from rest_framework.relations import ManyRelatedField as ManyRelatedField | ||
| from rest_framework.relations import PrimaryKeyRelatedField as PrimaryKeyRelatedField | ||
| from rest_framework.relations import RelatedField as RelatedField | ||
| from rest_framework.relations import SlugRelatedField as SlugRelatedField | ||
| from rest_framework.relations import StringRelatedField as StringRelatedField | ||
| from rest_framework.utils.model_meta import FieldInfo, RelationInfo | ||
| from rest_framework.utils.serializer_helpers import BindingDict, BoundField, ReturnDict, ReturnList | ||
| from rest_framework.validators import BaseUniqueForValidator, UniqueTogetherValidator, Validator | ||
| from typing_extensions import Self | ||
|
|
||
| LIST_SERIALIZER_KWARGS: Sequence[str] | ||
| LIST_SERIALIZER_KWARGS_REMOVE: Sequence[str] | ||
| ALL_FIELDS: str | ||
|
|
||
| _MT = TypeVar("_MT", bound=Model) # Model Type | ||
| _IN = TypeVar("_IN", default=Any) # Instance Type | ||
| _TVal = TypeVar("_TVal", default=Any) # shape of validated_data | ||
|
|
||
| class BaseSerializer(Field[Any, Any, Any, _IN], Generic[_IN, _TVal]): | ||
| partial: bool | ||
| many: bool | ||
| instance: _IN | None | ||
| initial_data: Any | ||
| _context: dict[str, Any] | ||
| def __new__(cls, *args: Any, **kwargs: Any) -> Self: ... | ||
| def __class_getitem__(cls, *args: Incomplete, **kwargs: Incomplete) -> Incomplete: ... | ||
| def __init__( | ||
| self, | ||
| instance: _IN | None = ..., | ||
| data: Any = ..., | ||
| *, | ||
| partial: bool = ..., | ||
| many: bool = ..., | ||
| allow_empty: bool = ..., | ||
| context: dict[str, Any] = ..., | ||
| read_only: bool = ..., | ||
| write_only: bool = ..., | ||
| required: bool | None = None, | ||
| default: Any = ..., | ||
| initial: Any = ..., | ||
| source: str | None = None, | ||
| label: StrOrPromise | None = None, | ||
| help_text: StrOrPromise | None = None, | ||
| style: dict[str, Any] | None = None, | ||
| error_messages: dict[str, StrOrPromise] | None = None, | ||
| validators: Sequence[Validator[Any]] | None = ..., | ||
| allow_null: bool = ..., | ||
| ) -> None: ... | ||
| @classmethod | ||
| def many_init(cls, *args: Any, **kwargs: Any) -> BaseSerializer: ... | ||
| def is_valid(self, *, raise_exception: bool = ...) -> bool: ... | ||
| @property | ||
| def data(self) -> Any: ... | ||
| @property | ||
| def errors(self) -> Iterable[Any]: ... | ||
| @property | ||
| def validated_data(self) -> _TVal: ... | ||
| def update(self, instance: _IN, validated_data: Any) -> _IN: ... | ||
| def create(self, validated_data: Any) -> _IN: ... | ||
| def save(self, **kwargs: Any) -> _IN: ... | ||
| def to_representation(self, instance: _IN) -> Any: ... | ||
|
|
||
| class SerializerMetaclass(type): | ||
| def __new__(cls, name: Any, bases: Any, attrs: Any) -> Incomplete: ... | ||
| @classmethod | ||
| def _get_declared_fields( | ||
| cls, bases: Sequence[type], attrs: dict[str, Any] | ||
| ) -> dict[str, Field]: ... | ||
|
|
||
| def as_serializer_error(exc: Exception) -> dict[str, list[ErrorDetail]]: ... | ||
|
|
||
| class Serializer(BaseSerializer[_IN, _TVal], metaclass=SerializerMetaclass): | ||
| _declared_fields: dict[str, Field] | ||
| default_error_messages: ClassVar[dict[str, StrOrPromise]] | ||
| def get_initial(self) -> Any: ... | ||
| def set_value( | ||
| self, dictionary: MutableMapping[str, Any], keys: Sequence[str], value: Any | ||
| ) -> None: ... | ||
| @cached_property | ||
| def fields(self) -> BindingDict: ... | ||
| def get_fields(self) -> dict[str, Field]: ... | ||
| def to_representation(self, instance: _IN) -> dict[str, Any]: ... | ||
| def validate(self, attrs: Any) -> Any: ... | ||
| def __iter__(self) -> Iterator[BoundField]: ... | ||
| def __getitem__(self, key: str) -> BoundField: ... | ||
| def _read_only_defaults(self) -> dict[str, Any]: ... | ||
| @property | ||
| def _writable_fields(self) -> list[Field]: ... | ||
| @property | ||
| def _readable_fields(self) -> list[Field]: ... | ||
| @property | ||
| def data(self) -> ReturnDict: ... | ||
| @property | ||
| def errors(self) -> ReturnDict: ... | ||
|
|
||
| class ListSerializer(BaseSerializer[_IN, _TVal]): | ||
| child: Field | BaseSerializer | None | ||
| many: bool | ||
| default_error_messages: ClassVar[dict[str, StrOrPromise]] | ||
| allow_empty: bool | None | ||
| def __init__( | ||
| self, | ||
| instance: _IN | None = ..., | ||
| data: Any = ..., | ||
| partial: bool = ..., | ||
| context: dict[str, Any] = ..., | ||
| allow_empty: bool = ..., | ||
| child: Field | BaseSerializer | None = ..., | ||
| read_only: bool = ..., | ||
| write_only: bool = ..., | ||
| required: bool | None = None, | ||
| default: Any = ..., | ||
| initial: Any = ..., | ||
| source: str | None = None, | ||
| label: StrOrPromise | None = None, | ||
| help_text: StrOrPromise | None = None, | ||
| style: dict[str, Any] | None = None, | ||
| error_messages: dict[str, StrOrPromise] | None = None, | ||
| validators: Sequence[Validator[list[Any]]] | None = ..., | ||
| allow_null: bool = ..., | ||
| min_length: int | None = ..., | ||
| max_length: int | None = ..., | ||
| ) -> None: ... | ||
| def run_child_validation(self, data: Any) -> Any: ... | ||
| def to_representation(self, data: Manager[Any] | Iterable[Any]) -> list[Any]: ... # type: ignore[override] | ||
| def get_initial(self) -> list[Mapping[Any, Any]]: ... | ||
| def validate(self, attrs: Any) -> Any: ... | ||
| @property | ||
| def data(self) -> ReturnList: ... | ||
| @property | ||
| def errors(self) -> ReturnList: ... | ||
|
|
||
| def raise_errors_on_nested_writes( | ||
| method_name: str, serializer: BaseSerializer, validated_data: Any | ||
| ) -> None: ... | ||
|
|
||
| class ModelSerializer(Serializer[_MT]): | ||
| serializer_field_mapping: ClassVar[dict[type[models.Field], type[Field]]] | ||
| serializer_related_field: ClassVar[type[RelatedField]] | ||
| serializer_related_to_field: ClassVar[type[RelatedField]] | ||
| serializer_url_field: ClassVar[type[RelatedField]] | ||
| serializer_choice_field: ClassVar[type[Field]] | ||
| url_field_name: ClassVar[str | None] | ||
|
|
||
| class Meta: | ||
| model: ClassVar[type[_MT]] # type: ignore[valid-type] | ||
| fields: ClassVar[Sequence[str] | Literal["__all__"]] | ||
| read_only_fields: ClassVar[Sequence[str] | None] | ||
| exclude: ClassVar[Sequence[str] | None] | ||
| depth: ClassVar[int | None] | ||
| extra_kwargs: ClassVar[dict[str, dict[str, Any]]] | ||
|
|
||
| def __init__( | ||
| self, | ||
| instance: None | _MT | Sequence[_MT] | QuerySet[_MT] | Manager[_MT] = ..., | ||
| data: Any = ..., | ||
| *, | ||
| partial: bool = ..., | ||
| many: bool = ..., | ||
| context: dict[str, Any] = ..., | ||
| read_only: bool = ..., | ||
| write_only: bool = ..., | ||
| required: bool | None = None, | ||
| default: _DefaultInitial[_MT | Sequence[_MT]] = ..., | ||
| initial: _DefaultInitial[_MT | Sequence[_MT]] = ..., | ||
| source: str | None = None, | ||
| label: StrOrPromise | None = None, | ||
| help_text: StrOrPromise | None = None, | ||
| style: dict[str, Any] | None = None, | ||
| error_messages: dict[str, StrOrPromise] | None = None, | ||
| validators: Sequence[Validator[_MT]] | None = ..., | ||
| allow_null: bool = ..., | ||
| allow_empty: bool = ..., | ||
| ) -> None: ... | ||
| def update(self, instance: _MT, validated_data: Any) -> _MT: ... | ||
| def create(self, validated_data: Any) -> _MT: ... | ||
| def save(self, **kwargs: Any) -> _MT: ... | ||
| def get_field_names( | ||
| self, declared_fields: Mapping[str, Field], info: FieldInfo | ||
| ) -> list[str]: ... | ||
| def get_default_field_names( | ||
| self, declared_fields: Mapping[str, Field], model_info: FieldInfo | ||
| ) -> list[str]: ... | ||
| def build_field( | ||
| self, field_name: str, info: FieldInfo, model_class: type[_MT], nested_depth: int | ||
| ) -> tuple[type[Field], dict[str, Any]]: ... | ||
| def build_standard_field( | ||
| self, field_name: str, model_field: models.Field | ||
| ) -> tuple[type[Field], dict[str, Any]]: ... | ||
| def build_relational_field( | ||
| self, field_name: str, relation_info: RelationInfo | ||
| ) -> tuple[type[Field], dict[str, Any]]: ... | ||
| def build_nested_field( | ||
| self, field_name: str, relation_info: RelationInfo, nested_depth: int | ||
| ) -> tuple[type[Field], dict[str, Any]]: ... | ||
| def build_property_field( | ||
| self, field_name: str, model_class: type[_MT] | ||
| ) -> tuple[type[Field], dict[str, Any]]: ... | ||
| def build_url_field( | ||
| self, field_name: str, model_class: type[_MT] | ||
| ) -> tuple[type[Field], dict[str, Any]]: ... | ||
| def build_unknown_field(self, field_name: str, model_class: type[_MT]) -> NoReturn: ... | ||
| def include_extra_kwargs( | ||
| self, kwargs: MutableMapping[str, Any], extra_kwargs: MutableMapping[str, Any] | ||
| ) -> MutableMapping[str, Any]: ... | ||
| def get_extra_kwargs(self) -> dict[str, Any]: ... | ||
| def get_unique_together_constraints( | ||
| self, model: _MT | ||
| ) -> Iterator[tuple[set[tuple[str, ...]], Manager[_MT]]]: ... | ||
| def get_uniqueness_extra_kwargs( | ||
| self, | ||
| field_names: Iterable[str], | ||
| declared_fields: Mapping[str, Field], | ||
| extra_kwargs: dict[str, Any], | ||
| ) -> tuple[dict[str, Any], dict[str, HiddenField]]: ... | ||
| def _get_model_fields( | ||
| self, | ||
| field_names: Iterable[str], | ||
| declared_fields: Mapping[str, Field], | ||
| extra_kwargs: MutableMapping[str, Any], | ||
| ) -> dict[str, models.Field]: ... | ||
| def get_unique_together_validators(self) -> list[UniqueTogetherValidator]: ... | ||
| def get_unique_for_date_validators(self) -> list[BaseUniqueForValidator]: ... | ||
|
|
||
| class HyperlinkedModelSerializer(ModelSerializer[_MT]): ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| """The serializer stub gives `validated_data` a declared shape. | ||
|
|
||
| Exercised through mypy under the repo's own config, because the mechanism is a | ||
| stub with nothing to import and CI resolves it the same way. If the stub ever | ||
| stops being found, `validated_data` falls back to `Any` and these cases would | ||
| pass without checking anything, so `test_stub_is_in_effect` guards that. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os.path | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
|
|
||
| REPO = os.path.join(os.path.dirname(__file__), "..", "..", "..") | ||
|
|
||
| PRELUDE = """\ | ||
| from typing import Any, NotRequired, TypedDict | ||
|
|
||
| from rest_framework import serializers | ||
|
|
||
|
|
||
| class MonitorData(TypedDict): | ||
| name: NotRequired[str] | ||
| threshold: NotRequired[int] | ||
|
|
||
|
|
||
| class MonitorValidator(serializers.Serializer[Any, MonitorData]): | ||
| name = serializers.CharField() | ||
| threshold = serializers.IntegerField() | ||
|
|
||
|
|
||
| class BareValidator(serializers.Serializer): | ||
| anything = serializers.CharField() | ||
|
|
||
|
|
||
| class InstanceOnlyValidator(serializers.Serializer[MonitorData]): | ||
| name = serializers.CharField() | ||
|
|
||
|
|
||
| def create_monitor(*, name: str = "", threshold: int = 0) -> None: ... | ||
| """ | ||
|
|
||
|
|
||
| def _check(body: str) -> str: | ||
| """Type-check the prelude plus `body`, returning mypy's diagnostics.""" | ||
| with tempfile.TemporaryDirectory() as tmpdir: | ||
| source = os.path.join(tmpdir, "case.py") | ||
| with open(source, "w") as fh: | ||
| fh.write(PRELUDE + body) | ||
| proc = subprocess.run( | ||
| (sys.executable, "-m", "mypy", "--no-incremental", source), | ||
| capture_output=True, | ||
| cwd=os.path.abspath(REPO), | ||
| ) | ||
| out = proc.stdout.decode() | ||
| return "\n".join(line for line in out.splitlines() if "case.py" in line) | ||
|
|
||
|
|
||
| def test_stub_is_in_effect() -> None: | ||
| # Without the stub `validated_data` is Any, so returning it as int is fine | ||
| # and every other case here would pass vacuously. | ||
| out = _check("def f(v: MonitorValidator) -> int:\n return v.validated_data['name']\n") | ||
| assert "Incompatible return value type" in out | ||
|
|
||
|
|
||
| def test_declared_key_resolves_to_its_type() -> None: | ||
| assert _check("def f(v: MonitorValidator) -> str:\n return v.validated_data['name']\n") == "" | ||
|
|
||
|
|
||
| def test_misspelled_key_is_an_error() -> None: | ||
| out = _check("def f(v: MonitorValidator) -> str:\n return v.validated_data['nmae']\n") | ||
| assert 'has no key "nmae"' in out | ||
|
|
||
|
|
||
| def test_value_type_is_checked_at_the_use_site() -> None: | ||
| out = _check("def f(v: MonitorValidator) -> str:\n return v.validated_data['threshold']\n") | ||
| assert "Incompatible return value type" in out | ||
|
|
||
|
|
||
| def test_splat_into_a_typed_callable_is_checked() -> None: | ||
| good = _check("def f(v: MonitorValidator) -> None:\n create_monitor(**v.validated_data)\n") | ||
| assert good == "" | ||
| bad = _check( | ||
| "class Other(TypedDict):\n" | ||
| " nope: NotRequired[bool]\n" | ||
| "class OtherV(serializers.Serializer[Any, Other]):\n" | ||
| " nope = serializers.BooleanField()\n" | ||
| "def f(v: OtherV) -> None:\n" | ||
| " create_monitor(**v.validated_data)\n" | ||
| ) | ||
| assert "nope" in bad | ||
|
|
||
|
|
||
| def test_bare_subclass_is_unaffected() -> None: | ||
| out = _check("def f(v: BareValidator) -> Any:\n return v.validated_data['whatever']\n") | ||
| assert out == "" | ||
|
|
||
|
|
||
| def test_existing_single_argument_subclass_still_resolves() -> None: | ||
| out = _check("def f(v: InstanceOnlyValidator) -> Any:\n return v.validated_data['any']\n") | ||
| assert out == "" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.