Skip to content
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

Add type hints to builtin models' fields #2214

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
22 changes: 15 additions & 7 deletions django-stubs/contrib/admin/models.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from datetime import date, datetime
from typing import Any, ClassVar
from uuid import UUID

from django.contrib.auth.models import AbstractUser
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.base import Model
from django.db.models.expressions import Combinable

ADDITION: int
CHANGE: int
Expand All @@ -21,13 +25,17 @@ class LogEntryManager(models.Manager[LogEntry]):
) -> LogEntry: ...

class LogEntry(models.Model):
action_time: models.DateTimeField
user: models.ForeignKey
content_type: models.ForeignKey
object_id: models.TextField
object_repr: models.CharField
action_flag: models.PositiveSmallIntegerField
change_message: models.TextField
id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
action_time: models.DateTimeField[str | datetime | date | Combinable, datetime]
user: models.ForeignKey[AbstractUser | Combinable, AbstractUser]
user_id: Any
content_type: models.ForeignKey[ContentType | Combinable | None, ContentType | None]
content_type_id: int | None
object_id: models.TextField[str | int | Combinable | None, str | None]
object_repr: models.CharField[str | int | Combinable, str]
action_flag: models.PositiveSmallIntegerField[float | int | str | Combinable, int]
change_message: models.TextField[str | int | Combinable, str]
objects: ClassVar[LogEntryManager]
def is_addition(self) -> bool: ...
def is_change(self) -> bool: ...
Expand Down
8 changes: 4 additions & 4 deletions django-stubs/contrib/auth/base_user.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from datetime import date, datetime
from typing import Any, ClassVar, Literal, TypeVar, overload

from django.db import models
from django.db.models.base import Model
from django.db.models.expressions import Combinable
from django.db.models.fields import BooleanField

_T = TypeVar("_T", bound=Model)

Expand All @@ -16,9 +16,9 @@ class BaseUserManager(models.Manager[_T]):
class AbstractBaseUser(models.Model):
REQUIRED_FIELDS: ClassVar[list[str]]

password = models.CharField(max_length=128)
last_login = models.DateTimeField(blank=True, null=True)
is_active: bool | BooleanField[bool | Combinable, bool]
password: models.CharField[str | int | Combinable, str]
last_login: models.DateTimeField[str | datetime | date | Combinable, datetime | None]
is_active: bool | models.BooleanField[bool | Combinable, bool]

def get_username(self) -> str: ...
def natural_key(self) -> tuple[str]: ...
Expand Down
59 changes: 41 additions & 18 deletions django-stubs/contrib/auth/models.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections.abc import Iterable
from typing import Any, ClassVar, Literal, TypeVar
from datetime import date, datetime
from typing import Any, ClassVar, Literal, TypeVar, type_check_only

from django.contrib.auth.base_user import AbstractBaseUser as AbstractBaseUser
from django.contrib.auth.base_user import BaseUserManager as BaseUserManager
Expand All @@ -8,6 +9,8 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import QuerySet
from django.db.models.base import Model
from django.db.models.expressions import Combinable
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
from django.db.models.manager import EmptyManager
from django.utils.functional import _StrOrPromise
from typing_extensions import Self, TypeAlias
Expand All @@ -20,22 +23,40 @@ class PermissionManager(models.Manager[Permission]):
def get_by_natural_key(self, codename: str, app_label: str, model: str) -> Permission: ...

class Permission(models.Model):
content_type_id: int
objects: ClassVar[PermissionManager]

name = models.CharField(max_length=255)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
codename = models.CharField(max_length=100)
id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
name: models.CharField[str | int | Combinable, str]
content_type: models.ForeignKey[ContentType | Combinable, ContentType]
content_type_id: int
codename: models.CharField[str | int | Combinable, str]
group_set: ManyToManyDescriptor[Group, Group_permissions]
def natural_key(self) -> tuple[str, str, str]: ...

class GroupManager(models.Manager[Group]):
def get_by_natural_key(self, name: str) -> Group: ...

# This is a model that only exists in Django's model registry and doesn't have any
# class statement form. It's the through model between 'Group' and 'Permission'.
@type_check_only
class Group_permissions(models.Model):
objects: ClassVar[models.Manager[Self]]

id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
group: models.ForeignKey[Group | Combinable, Group]
group_id: int
permission: models.ForeignKey[Permission | Combinable, Permission]
permission_id: int

class Group(models.Model):
objects: ClassVar[GroupManager]

name = models.CharField(max_length=150)
permissions = models.ManyToManyField(Permission)
id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
name: models.CharField[str | int | Combinable, str]
permissions: models.ManyToManyField[Permission, Group_permissions]
def natural_key(self) -> tuple[str]: ...

_T = TypeVar("_T", bound=Model)
Expand All @@ -57,9 +78,9 @@ class UserManager(BaseUserManager[_T]):
) -> QuerySet[_T]: ...

class PermissionsMixin(models.Model):
is_superuser = models.BooleanField()
groups = models.ManyToManyField(Group)
user_permissions = models.ManyToManyField(Permission)
is_superuser: models.BooleanField[bool | Combinable, bool]
groups: models.ManyToManyField[Group, Any]
user_permissions: models.ManyToManyField[Permission, Any]

def get_user_permissions(self, obj: _AnyUser | None = ...) -> set[str]: ...
def get_group_permissions(self, obj: _AnyUser | None = ...) -> set[str]: ...
Expand All @@ -71,13 +92,13 @@ class PermissionsMixin(models.Model):
class AbstractUser(AbstractBaseUser, PermissionsMixin):
username_validator: UnicodeUsernameValidator

username = models.CharField(max_length=150)
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=150, blank=True)
email = models.EmailField(blank=True)
is_staff = models.BooleanField()
is_active = models.BooleanField()
date_joined = models.DateTimeField()
username: models.CharField[str | int | Combinable, str]
first_name: models.CharField[str | int | Combinable, str]
last_name: models.CharField[str | int | Combinable, str]
email: models.EmailField[str | Combinable, str]
is_staff: models.BooleanField[bool | Combinable, bool]
is_active: models.BooleanField[bool | Combinable, bool]
date_joined: models.DateTimeField[str | datetime | date | Combinable, datetime]

objects: ClassVar[UserManager[Self]]

Expand All @@ -90,7 +111,9 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
self, subject: _StrOrPromise, message: _StrOrPromise, from_email: str = ..., **kwargs: Any
) -> None: ...

class User(AbstractUser): ...
class User(AbstractUser):
id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]

class AnonymousUser:
id: Any
Expand Down
15 changes: 11 additions & 4 deletions django-stubs/contrib/contenttypes/models.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from typing import Any, ClassVar

from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import Permission
from django.db import models
from django.db.models.base import Model
from django.db.models.expressions import Combinable
from django.db.models.fields.related_descriptors import ReverseManyToOneDescriptor
from django.db.models.query import QuerySet

class ContentTypeManager(models.Manager[ContentType]):
Expand All @@ -12,13 +16,16 @@ class ContentTypeManager(models.Manager[ContentType]):
def clear_cache(self) -> None: ...

class ContentType(models.Model):
id: int
app_label: models.CharField
model: models.CharField
id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
app_label: models.CharField[str | int | Combinable, str]
model: models.CharField[str | int | Combinable, str]
logentry_set: ReverseManyToOneDescriptor[LogEntry]
permission_set: ReverseManyToOneDescriptor[Permission]
objects: ClassVar[ContentTypeManager]
@property
def name(self) -> str: ...
def model_class(self) -> type[Model] | None: ...
def get_object_for_this_type(self, **kwargs: Any) -> Model: ...
def get_all_objects_for_this_type(self, **kwargs: Any) -> QuerySet: ...
def get_all_objects_for_this_type(self, **kwargs: Any) -> QuerySet[Model]: ...
def natural_key(self) -> tuple[str, str]: ...
33 changes: 26 additions & 7 deletions django-stubs/contrib/flatpages/models.pyi
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
from typing import ClassVar, type_check_only

from django.contrib.sites.models import Site
from django.db import models
from django.db.models.expressions import Combinable
from typing_extensions import Self

# This is a model that only exists in Django's model registry and doesn't have any
# class statement form. It's the through model between 'FlatPage' and 'Site'.
@type_check_only
class FlatPage_sites(models.Model):
objects: ClassVar[models.Manager[Self]]

id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
site: models.ForeignKey[Site | Combinable, Site]
site_id: int
flatpage: models.ForeignKey[FlatPage | Combinable, FlatPage]
flatpage_id: int

class FlatPage(models.Model):
url: models.CharField
title: models.CharField
content: models.TextField
enable_comments: models.BooleanField
template_name: models.CharField
registration_required: models.BooleanField
sites: models.ManyToManyField[Site, Site]
id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
url: models.CharField[str | int | Combinable, str]
title: models.CharField[str | int | Combinable, str]
content: models.TextField[str | int | Combinable, str]
enable_comments: models.BooleanField[bool | Combinable, bool]
template_name: models.CharField[str | int | Combinable, str]
registration_required: models.BooleanField[bool | Combinable, bool]
sites: models.ManyToManyField[Site, FlatPage_sites]
def get_absolute_url(self) -> str: ...
11 changes: 8 additions & 3 deletions django-stubs/contrib/redirects/models.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from django.contrib.sites.models import Site
from django.db import models
from django.db.models.expressions import Combinable

class Redirect(models.Model):
site: models.ForeignKey
old_path: models.CharField
new_path: models.CharField
id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
site: models.ForeignKey[Site | Combinable, Site]
site_id: int
old_path: models.CharField[str | int | Combinable, str]
new_path: models.CharField[str | int | Combinable, str]
11 changes: 7 additions & 4 deletions django-stubs/contrib/sessions/base_session.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from datetime import datetime
from datetime import date, datetime
from typing import Any, ClassVar, TypeVar

from django.contrib.sessions.backends.base import SessionBase
from django.db import models
from django.db.models.expressions import Combinable
from typing_extensions import Self

_T = TypeVar("_T", bound=AbstractBaseSession)
Expand All @@ -12,9 +13,11 @@ class BaseSessionManager(models.Manager[_T]):
def save(self, session_key: str, session_dict: dict[str, Any], expire_date: datetime) -> _T: ...

class AbstractBaseSession(models.Model):
session_key = models.CharField(primary_key=True)
session_data = models.TextField()
expire_date = models.DateTimeField()
session_key: models.CharField[str | int | Combinable | None, str]
# 'session_key' is declared as primary key
pk: models.CharField[str | int | Combinable | None, str]
session_data: models.TextField[str | int | Combinable, str]
expire_date: models.DateTimeField[str | datetime | date | Combinable, datetime]
objects: ClassVar[BaseSessionManager[Self]]

@classmethod
Expand Down
7 changes: 5 additions & 2 deletions django-stubs/contrib/sessions/models.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing import TypeVar
from typing import ClassVar, TypeVar

from django.contrib.sessions.base_session import AbstractBaseSession, BaseSessionManager
from typing_extensions import Self

_T = TypeVar("_T", bound=Session)

class SessionManager(BaseSessionManager[_T]): ...
class Session(AbstractBaseSession): ...

class Session(AbstractBaseSession):
objects: ClassVar[SessionManager[Self]] # type: ignore[assignment]
12 changes: 10 additions & 2 deletions django-stubs/contrib/sites/models.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from typing import Any, ClassVar

from django.contrib.flatpages.models import FlatPage, FlatPage_sites
from django.contrib.redirects.models import Redirect
from django.db import models
from django.db.models.expressions import Combinable
from django.db.models.fields.related_descriptors import ManyToManyDescriptor, ReverseManyToOneDescriptor
from django.http.request import HttpRequest

SITE_CACHE: Any
Expand All @@ -13,8 +17,12 @@ class SiteManager(models.Manager[Site]):
class Site(models.Model):
objects: ClassVar[SiteManager]

domain = models.CharField(max_length=100)
name = models.CharField(max_length=50)
id: models.AutoField[str | int | Combinable | None, int]
pk: models.AutoField[str | int | Combinable | None, int]
domain: models.CharField[str | int | Combinable, str]
name: models.CharField[str | int | Combinable, str]
flatpage_set: ManyToManyDescriptor[FlatPage, FlatPage_sites]
redirect_set: ReverseManyToOneDescriptor[Redirect]
def natural_key(self) -> tuple[str]: ...

def clear_site_cache(sender: type[Site], **kwargs: Any) -> None: ...
8 changes: 4 additions & 4 deletions django-stubs/db/models/fields/related_descriptors.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class ReverseOneToOneDescriptor(Generic[_From, _To]):
def __set__(self, instance: _From, value: _To | None) -> None: ...
def __reduce__(self) -> tuple[Callable[..., Any], tuple[type[_To], str]]: ...

class ReverseManyToOneDescriptor:
class ReverseManyToOneDescriptor(Generic[_To]):
flaeppe marked this conversation as resolved.
Show resolved Hide resolved
"""
In the example::

Expand All @@ -84,14 +84,14 @@ class ReverseManyToOneDescriptor:
"""

rel: ManyToOneRel
field: ForeignKey
field: ForeignKey[_To, _To]
def __init__(self, rel: ManyToOneRel) -> None: ...
@cached_property
def related_manager_cls(self) -> type[RelatedManager[Any]]: ...
def related_manager_cls(self) -> type[RelatedManager[_To]]: ...
@overload
def __get__(self, instance: None, cls: Any = ...) -> Self: ...
@overload
def __get__(self, instance: Model, cls: Any = ...) -> RelatedManager[Any]: ...
def __get__(self, instance: Model, cls: Any = ...) -> RelatedManager[_To]: ...
def __set__(self, instance: Any, value: Any) -> NoReturn: ...

# Fake class, Django defines 'RelatedManager' inside a function body
Expand Down
5 changes: 4 additions & 1 deletion mypy_django_plugin/lib/fullnames.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
ABSTRACT_USER_MODEL_FULLNAME = "django.contrib.auth.models.AbstractUser"
USER_MODEL_FULLNAME = "django.contrib.auth.models.User"
PERMISSION_MODEL_FULLNAME = "django.contrib.auth.models.Permission"
GROUP_MODEL_FULLNAME = "django.contrib.auth.models.Group"
PERMISSION_MIXIN_CLASS_FULLNAME = "django.contrib.auth.models.PermissionsMixin"
MODEL_METACLASS_FULLNAME = "django.db.models.base.ModelBase"
MODEL_CLASS_FULLNAME = "django.db.models.base.Model"
Expand All @@ -12,7 +15,7 @@
ONETOONE_FIELD_FULLNAME = "django.db.models.fields.related.OneToOneField"
MANYTOMANY_FIELD_FULLNAME = "django.db.models.fields.related.ManyToManyField"
DUMMY_SETTINGS_BASE_CLASS = "django.conf._DjangoConfLazyObject"
AUTH_USER_MODEL_FULLNAME = "django.conf.settings.AUTH_USER_MODEL"
AUTH_USER_MODEL_SETTING_FULLNAME = "django.conf.settings.AUTH_USER_MODEL"

QUERYSET_CLASS_FULLNAME = "django.db.models.query.QuerySet"
BASE_MANAGER_CLASS_FULLNAME = "django.db.models.manager.BaseManager"
Expand Down
11 changes: 8 additions & 3 deletions mypy_django_plugin/transformers/fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, cast
from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Union, cast

from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields import AutoField, Field
Expand Down Expand Up @@ -114,12 +114,17 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context
)


class FieldDescriptorTypes(NamedTuple):
set: MypyType
get: MypyType


def get_field_descriptor_types(
field_info: TypeInfo, *, is_set_nullable: bool, is_get_nullable: bool
) -> Tuple[MypyType, MypyType]:
) -> FieldDescriptorTypes:
set_type = helpers.get_private_descriptor_type(field_info, "_pyi_private_set_type", is_nullable=is_set_nullable)
get_type = helpers.get_private_descriptor_type(field_info, "_pyi_private_get_type", is_nullable=is_get_nullable)
return set_type, get_type
return FieldDescriptorTypes(set=set_type, get=get_type)


def set_descriptor_types_for_field_callback(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
Expand Down
Loading
Loading