Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
15 changes: 2 additions & 13 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, Generic, TypeAlias, TypeVar, overload, type_check_only
from typing import Any, TypeAlias

from django.core.files.base import File
from django.utils.deconstruct import _Deconstructible
Expand All @@ -13,19 +13,8 @@ _Regex: TypeAlias = str | Pattern[str]

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

_ClassT = TypeVar("_ClassT")
_InstanceT = TypeVar("_InstanceT")

@type_check_only
class _ClassOrInstanceAttribute(Generic[_ClassT, _InstanceT]):
@overload
def __get__(self, instance: None, owner: type[object]) -> _ClassT: ...
@overload
def __get__(self, instance: object, owner: type[object]) -> _InstanceT: ...
def __set__(self, instance: object, value: _InstanceT) -> None: ...

class RegexValidator(_Deconstructible):
regex: _ClassOrInstanceAttribute[_Regex, Pattern[str]]
regex: Pattern[str]
message: _StrOrPromise
code: str
inverse_match: bool
Expand Down
5 changes: 3 additions & 2 deletions scripts/stubtest/allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,9 @@ django.contrib.gis.forms.BaseModelFormSet.save_m2m
# Dynamically generated in https://github.com/django/django/blob/0ee06c04e0256094270db3ffe8b5dafa6a8457a3/django/core/mail/backends/locmem.py#L24
django.core.mail.outbox

# We use a trick using a descriptor class to represent an attribute with different type at the class and instance level.
# Here to narrow `str | Pattern[str]` at the class level to `Pattern[str]` at the instance level.
# The type system does not allow to represent an attribute with different type at the class level (`str | Pattern[str]`)
# and instance (`Pattern[str]`) level. But in order to have more precise type at the instance level, we restrict the types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in order to have more precise type at the instance level, we restrict the types

Sorry, we can't do that either. We can allow something that does not work to happen (false negative), but this will block valid useges (false positive). That's something we try to avoid, unless we are 100% sure that it is worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I guess we cannot do more currently. At least I did not find a way. I tried another approach using __new__ but could not get it to work properly for both case either.

I'll close the PR, we might be able to revisit once the type system allows to represent this.

Thanks for the review nonetheless!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe #2615 will find a way

# allowed at the class level to a subset. In an ideal world, these should probably have different attribute names.
django.core.validators.RegexValidator.regex
django.contrib.auth.validators.ASCIIUsernameValidator.regex
django.contrib.auth.validators.UnicodeUsernameValidator.regex
20 changes: 15 additions & 5 deletions tests/assert_type/core/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@
from django.core.validators import RegexValidator
from typing_extensions import assert_type

assert_type(RegexValidator.regex, str | Pattern[str])
assert_type(RegexValidator().regex, Pattern[str])
RegexValidator().regex = re.compile("")

assert_type(UnicodeUsernameValidator.regex, str | Pattern[str])
assert_type(UnicodeUsernameValidator().regex, Pattern[str])
UnicodeUsernameValidator().regex = re.compile("")

# expect "Pattern[str]"
RegexValidator().regex = "" # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue]
UnicodeUsernameValidator().regex = "" # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue]

# expect "_ClassOrInstanceAttribute[Union[str, Pattern[str]], Pattern[str]]"
RegexValidator.regex = "anything fails here" # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue]
UnicodeUsernameValidator.regex = "anything fails here" # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue]

class RegexSubtype(RegexValidator):
regex = re.compile("abc")


# We would like to improve on these, it should allow "str | Pattern[str]":
assert_type(RegexValidator.regex, Pattern[str])
assert_type(UnicodeUsernameValidator.regex, Pattern[str])

RegexValidator.regex = "" # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue]
UnicodeUsernameValidator.regex = "" # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue]


class StrSubtype(RegexValidator):
regex = "abc" # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue]

Check failure on line 32 in tests/assert_type/core/test_validators.py

View workflow job for this annotation

GitHub Actions / run-pyright (3.12)

Unnecessary "# pyright: ignore" rule: "reportAttributeAccessIssue" (reportUnnecessaryTypeIgnoreComment)

Check failure on line 32 in tests/assert_type/core/test_validators.py

View workflow job for this annotation

GitHub Actions / run-pyright (3.12)

Type "Literal['abc']" is not assignable to declared type "Pattern[str]"   "Literal['abc']" is not assignable to "Pattern[str]" (reportAssignmentType)
Loading