Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions django-stubs/contrib/gis/db/models/fields.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class BaseSpatialField(Field[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[_ValidatorCallable] = ...,
validators: Iterable[_ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down Expand Up @@ -115,7 +115,7 @@ class GeometryField(BaseSpatialField[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[_ValidatorCallable] = ...,
validators: Iterable[_ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down
4 changes: 2 additions & 2 deletions django-stubs/contrib/postgres/fields/array.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ArrayField(CheckPostgresInstalledMixin, CheckFieldDefaultMixin, Field[_ST,
default_error_messages: ClassVar[_ErrorMessagesDict]
base_field: Field
size: int | None
default_validators: Sequence[_ValidatorCallable]
default_validators: Sequence[_ValidatorCallable[_GT]]
from_db_value: Any
def __init__(
self,
Expand Down Expand Up @@ -56,7 +56,7 @@ class ArrayField(CheckPostgresInstalledMixin, CheckFieldDefaultMixin, Field[_ST,
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[_ValidatorCallable] = ...,
validators: Iterable[_ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down
6 changes: 4 additions & 2 deletions django-stubs/core/validators.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from collections.abc import Callable, Collection, Sequence, Sized
from decimal import Decimal
from re import Pattern, RegexFlag
from typing import Any, TypeAlias
from typing import Any, TypeAlias, TypeVar

from django.core.files.base import File
from django.utils.deconstruct import _Deconstructible
Expand All @@ -12,7 +12,9 @@ EMPTY_VALUES: Any

_Regex: TypeAlias = str | Pattern[str]

_ValidatorCallable: TypeAlias = Callable[[Any], None] # noqa: PYI047
_VT = TypeVar("_VT", contravariant=True)

_ValidatorCallable: TypeAlias = Callable[[_VT], None] # noqa: PYI047

class RegexValidator(_Deconstructible):
regex: _Regex # Pattern[str] on instance, but may be str on class definition
Expand Down
26 changes: 13 additions & 13 deletions django-stubs/db/models/fields/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
empty_values: Sequence[Any]
creation_counter: int
auto_creation_counter: int
default_validators: Sequence[validators._ValidatorCallable]
default_validators: Sequence[validators._ValidatorCallable[_GT]]
default_error_messages: ClassVar[_ErrorMessagesDict]
hidden: bool
system_check_removed_details: Any | None
Expand Down Expand Up @@ -172,7 +172,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
db_column: str | None = None,
db_tablespace: str | None = None,
auto_created: bool = False,
validators: Iterable[validators._ValidatorCallable] = (),
validators: Iterable[validators._ValidatorCallable[_GT]] = (),
error_messages: _ErrorMessagesMapping | None = None,
db_comment: str | None = None,
db_default: type[NOT_PROVIDED] | Expression | _ST = ...,
Expand Down Expand Up @@ -205,7 +205,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
@cached_property
def error_messages(self) -> _ErrorMessagesDict: ...
@cached_property
def validators(self) -> list[validators._ValidatorCallable]: ...
def validators(self) -> list[validators._ValidatorCallable[_GT]]: ...
def run_validators(self, value: Any) -> None: ...
def validate(self, value: Any, model_instance: Model | None) -> None: ...
def clean(self, value: Any, model_instance: Model | None) -> Any: ...
Expand Down Expand Up @@ -325,7 +325,7 @@ class DecimalField(Field[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@cached_property
Expand Down Expand Up @@ -361,7 +361,7 @@ class CharField(Field[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
*,
db_collation: str | None = None,
Expand Down Expand Up @@ -393,7 +393,7 @@ class SlugField(CharField[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
*,
max_length: int | None = 50,
Expand Down Expand Up @@ -434,7 +434,7 @@ class URLField(CharField[_ST, _GT]):
db_comment: str | None = ...,
db_tablespace: str | None = ...,
auto_created: bool = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down Expand Up @@ -468,7 +468,7 @@ class TextField(Field[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
*,
db_collation: str | None = None,
Expand Down Expand Up @@ -520,7 +520,7 @@ class GenericIPAddressField(Field[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down Expand Up @@ -558,7 +558,7 @@ class DateField(DateTimeCheckMixin, Field[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down Expand Up @@ -593,7 +593,7 @@ class TimeField(DateTimeCheckMixin, Field[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down Expand Up @@ -635,7 +635,7 @@ class UUIDField(Field[_ST, _GT]):
db_comment: str | None = ...,
db_tablespace: str | None = ...,
auto_created: bool = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down Expand Up @@ -673,7 +673,7 @@ class FilePathField(Field[_ST, _GT]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
@override
Expand Down
2 changes: 1 addition & 1 deletion django-stubs/db/models/fields/composite.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class CompositePrimaryKey(Field[Any, Any]):
db_column: None = None,
db_tablespace: str | None = None,
auto_created: bool = False,
validators: Iterable[validators._ValidatorCallable] = (),
validators: Iterable[validators._ValidatorCallable[Any]] = (),
error_messages: Mapping[str, _StrOrPromise] | None = None,
db_comment: str | None = None,
db_default: type[NOT_PROVIDED] = ...,
Expand Down
2 changes: 1 addition & 1 deletion django-stubs/db/models/fields/files.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class FileField(Field[Any, Any]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[Any]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
) -> None: ...
# class access
Expand Down
2 changes: 1 addition & 1 deletion django-stubs/db/models/fields/generated.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class GeneratedField(models.Field[Any, Any]):
db_column: str | None = ...,
db_comment: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[_ValidatorCallable] = ...,
validators: Iterable[_ValidatorCallable[Any]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
**kwargs: Any,
) -> None: ...
Expand Down
8 changes: 4 additions & 4 deletions django-stubs/db/models/fields/related.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class RelatedField(FieldCacheMixin, Field[_ST, _GT]):
db_column: str | None = ...,
db_tablespace: str | None = ...,
auto_created: bool = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
db_comment: str | None = ...,
) -> None: ...
Expand Down Expand Up @@ -129,7 +129,7 @@ class ForeignObject(RelatedField[_ST, _GT]):
help_text: _StrOrPromise = ...,
db_column: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
db_comment: str | None = ...,
) -> None: ...
Expand Down Expand Up @@ -212,7 +212,7 @@ class ForeignKey(ForeignObject[_ST, _GT]):
help_text: _StrOrPromise = ...,
db_column: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
db_comment: str | None = ...,
) -> None: ...
Expand Down Expand Up @@ -264,7 +264,7 @@ class OneToOneField(ForeignKey[_ST, _GT]):
help_text: _StrOrPromise = ...,
db_column: str | None = ...,
db_tablespace: str | None = ...,
validators: Iterable[validators._ValidatorCallable] = ...,
validators: Iterable[validators._ValidatorCallable[_GT]] = ...,
error_messages: _ErrorMessagesMapping | None = ...,
db_comment: str | None = ...,
) -> None: ...
Expand Down
35 changes: 35 additions & 0 deletions tests/assert_type/db/models/fields/test_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from collections.abc import Callable
from datetime import date
from decimal import Decimal

from django.db import models
from django.db.models.expressions import Combinable
from typing_extensions import assert_type


def validate_title(value: str) -> None: ...


def validate_price(value: Decimal) -> None: ...


def validate_published_on(value: date) -> None: ...


title_field: models.CharField[str | int | Combinable, str] = models.CharField(
max_length=200,
validators=[validate_title],
)
price_field: models.DecimalField[str | float | Decimal | Combinable, Decimal] = models.DecimalField(
max_digits=8,
decimal_places=2,
validators=[validate_price],
)
published_on_field: models.DateField[str | date | Combinable, date] = models.DateField(
validators=[validate_published_on],
)


assert_type(title_field.validators, list[Callable[[str], None]]) # ty: ignore[type-assertion-failure]
assert_type(price_field.validators, list[Callable[[Decimal], None]]) # ty: ignore[type-assertion-failure]
assert_type(published_on_field.validators, list[Callable[[date], None]]) # ty: ignore[type-assertion-failure]
46 changes: 46 additions & 0 deletions tests/typecheck/fields/test_validators.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
- case: model_field_validators_follow_get_type_and_support_subclassing
main: |
from datetime import date
from decimal import Decimal
from typing import NewType
from uuid import UUID

from django.db import models
from django.db.models.expressions import Combinable
from typing_extensions import reveal_type

SlugValue = NewType("SlugValue", str)

class CustomSlugField(models.SlugField[SlugValue | str | int | Combinable, SlugValue]):
...

def validate_slug(value: str) -> None: ...
def validate_price(value: Decimal) -> None: ...
def validate_published_on(value: date) -> None: ...
def validate_identifier(value: UUID) -> None: ...
def validate_ip(value: str) -> None: ...
def validate_custom_slug(value: SlugValue) -> None: ...
def validate_int(value: int) -> None: ...
def validate_date(value: date) -> None: ...
def validate_uuid(value: UUID) -> None: ...

class Article(models.Model):
slug = models.SlugField(validators=[validate_slug])
price = models.DecimalField(max_digits=8, decimal_places=2, validators=[validate_price])
published_on = models.DateField(validators=[validate_published_on])
identifier = models.UUIDField(validators=[validate_identifier])
ip_address = models.GenericIPAddressField(validators=[validate_ip])
custom_slug = CustomSlugField(validators=[validate_custom_slug])

bad_slug = models.SlugField(validators=[validate_int]) # E: List item 0 has incompatible type "Callable[[int], None]"; expected "Callable[[str], None]" [list-item]
bad_published_on = models.DateField(validators=[validate_int]) # E: List item 0 has incompatible type "Callable[[int], None]"; expected "Callable[[date], None]" [list-item]
bad_identifier = models.UUIDField(validators=[validate_date]) # E: List item 0 has incompatible type "Callable[[date], None]"; expected "Callable[[UUID], None]" [list-item]
bad_ip_address = models.GenericIPAddressField(validators=[validate_uuid]) # E: List item 0 has incompatible type "Callable[[UUID], None]"; expected "Callable[[str], None]" [list-item]
bad_price = models.DecimalField(max_digits=8, decimal_places=2, validators=[validate_uuid]) # E: List item 0 has incompatible type "Callable[[UUID], None]"; expected "Callable[[Decimal], None]" [list-item]

reveal_type(Article().slug) # N: Revealed type is "builtins.str"
reveal_type(Article().price) # N: Revealed type is "decimal.Decimal"
reveal_type(Article().published_on) # N: Revealed type is "datetime.date"
reveal_type(Article().identifier) # N: Revealed type is "uuid.UUID"
reveal_type(Article().ip_address) # N: Revealed type is "builtins.str"
reveal_type(Article().custom_slug) # N: Revealed type is "main.SlugValue"
Loading