diff --git a/fixtures/stubs-for-mypy/rest_framework/serializers.pyi b/fixtures/stubs-for-mypy/rest_framework/serializers.pyi new file mode 100644 index 000000000000..dc50e9fbb3a7 --- /dev/null +++ b/fixtures/stubs-for-mypy/rest_framework/serializers.pyi @@ -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]): ... diff --git a/tests/tools/mypy_helpers/test_typed_validated_data.py b/tests/tools/mypy_helpers/test_typed_validated_data.py new file mode 100644 index 000000000000..d5ff640a5483 --- /dev/null +++ b/tests/tools/mypy_helpers/test_typed_validated_data.py @@ -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 == ""