Skip to content

Commit 937435f

Browse files
committed
feat: Add tests and support for gis models
1 parent 74eae06 commit 937435f

File tree

15 files changed

+279
-51
lines changed

15 files changed

+279
-51
lines changed

django-stubs/contrib/admin/models.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class LogEntryManager(models.Manager[LogEntry]):
3131
queryset: QuerySet[Model],
3232
action_flag: int,
3333
change_message: str | list[Any] = "",
34+
*,
35+
single_object: bool = False,
3436
) -> list[LogEntry] | LogEntry: ...
3537

3638
class LogEntry(models.Model):

django-stubs/contrib/auth/models.pyi

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ from django.db import models
99
from django.db.models import QuerySet
1010
from django.db.models.base import Model
1111
from django.db.models.expressions import Combinable
12+
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
1213
from django.db.models.manager import EmptyManager
1314
from django.utils.functional import _StrOrPromise
1415
from typing_extensions import Self, TypeAlias
@@ -29,7 +30,8 @@ class Permission(models.Model):
2930
content_type = models.ForeignKey[ContentType | Combinable, ContentType](ContentType, on_delete=models.CASCADE)
3031
content_type_id: int
3132
codename = models.CharField(max_length=100)
32-
group_set: models.ManyToManyField[Group, Group_permissions]
33+
group_set: ManyToManyDescriptor[Group, Group_permissions]
34+
user_set: ManyToManyDescriptor[User, User_permissions]
3335
def natural_key(self) -> tuple[str, str, str]: ...
3436

3537
class GroupManager(models.Manager[Group]):
@@ -48,13 +50,40 @@ class Group_permissions(models.Model):
4850
permission: models.ForeignKey[Permission | Combinable, Permission]
4951
permission_id: int
5052

53+
# This is a model that only exists in Django's model registry and doesn't have any
54+
# class statement form. It's the through model between 'User' and 'Group'.
55+
@type_check_only
56+
class User_groups(models.Model):
57+
objects: ClassVar[models.Manager[Self]]
58+
59+
id: models.AutoField
60+
pk: models.AutoField
61+
user: models.ForeignKey[User | Combinable, User]
62+
user_id: int
63+
group: models.ForeignKey[Group | Combinable, Group]
64+
group_id: int
65+
66+
# This is a model that only exists in Django's model registry and doesn't have any
67+
# class statement form. It's the through model between 'User' and 'Permission'.
68+
@type_check_only
69+
class User_permissions(models.Model):
70+
objects: ClassVar[models.Manager[Self]]
71+
72+
id: models.AutoField
73+
pk: models.AutoField
74+
user: models.ForeignKey[User | Combinable, User]
75+
user_id: int
76+
permission: models.ForeignKey[Permission | Combinable, Permission]
77+
permission_id: int
78+
5179
class Group(models.Model):
5280
objects: ClassVar[GroupManager]
5381

5482
id: models.AutoField
5583
pk: models.AutoField
5684
name = models.CharField(max_length=150)
5785
permissions = models.ManyToManyField[Permission, Group_permissions](Permission)
86+
user_set: ManyToManyDescriptor[User, User_groups]
5887
def natural_key(self) -> tuple[str]: ...
5988

6089
_T = TypeVar("_T", bound=Model)
@@ -77,8 +106,8 @@ class UserManager(BaseUserManager[_T]):
77106

78107
class PermissionsMixin(models.Model):
79108
is_superuser = models.BooleanField()
80-
groups = models.ManyToManyField(Group)
81-
user_permissions = models.ManyToManyField(Permission)
109+
groups = models.ManyToManyField[Group, User_groups](Group)
110+
user_permissions = models.ManyToManyField[Permission, User_permissions](Permission)
82111

83112
def get_user_permissions(self, obj: _AnyUser | None = ...) -> set[str]: ...
84113
def get_group_permissions(self, obj: _AnyUser | None = ...) -> set[str]: ...

django-stubs/contrib/gis/db/models/fields.pyi

Lines changed: 79 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Iterable
2-
from typing import Any, NamedTuple, TypeVar
2+
from typing import Any, Generic, NamedTuple, TypeVar, type_check_only
33

44
from django.contrib.gis import forms
55
from django.contrib.gis.geos import (
@@ -23,18 +23,66 @@ _ST = TypeVar("_ST")
2323
# __get__ return type
2424
_GT = TypeVar("_GT")
2525

26+
_ST_PointField = TypeVar("_ST_PointField", default=Point | Combinable)
27+
_GT_PointField = TypeVar("_GT_PointField", default=Point)
28+
_Form_PointField = TypeVar("_Form_PointField", bound=forms.PointField, default=forms.PointField)
29+
_Geom_PointField = TypeVar("_Geom_PointField", bound=Point, default=Point)
30+
31+
_ST_LineStringField = TypeVar("_ST_LineStringField", default=LineString | Combinable)
32+
_GT_LineStringField = TypeVar("_GT_LineStringField", default=LineString)
33+
_Form_LineStringField = TypeVar("_Form_LineStringField", bound=forms.LineStringField, default=forms.LineStringField)
34+
_Geom_LineStringField = TypeVar("_Geom_LineStringField", bound=LineString, default=LineString)
35+
36+
_ST_PolygonField = TypeVar("_ST_PolygonField", default=Polygon | Combinable)
37+
_GT_PolygonField = TypeVar("_GT_PolygonField", default=Polygon)
38+
_Form_PolygonField = TypeVar("_Form_PolygonField", bound=forms.PolygonField, default=forms.PolygonField)
39+
_Geom_PolygonField = TypeVar("_Geom_PolygonField", bound=Polygon, default=Polygon)
40+
41+
_ST_MultiPointField = TypeVar("_ST_MultiPointField", default=MultiPoint | Combinable)
42+
_GT_MultiPointField = TypeVar("_GT_MultiPointField", default=MultiPoint)
43+
_Form_MultiPointField = TypeVar("_Form_MultiPointField", bound=forms.MultiPointField, default=forms.MultiPointField)
44+
_Geom_MultiPointField = TypeVar("_Geom_MultiPointField", bound=MultiPoint, default=MultiPoint)
45+
46+
_ST_MultiLineStringField = TypeVar("_ST_MultiLineStringField", default=MultiLineString | Combinable)
47+
_GT_MultiLineStringField = TypeVar("_GT_MultiLineStringField", default=MultiLineString)
48+
_Form_MultiLineStringField = TypeVar(
49+
"_Form_MultiLineStringField", bound=forms.MultiLineStringField, default=forms.MultiLineStringField
50+
)
51+
_Geom_MultiLineStringField = TypeVar("_Geom_MultiLineStringField", bound=MultiLineString, default=MultiLineString)
52+
53+
_ST_MultiPolygonField = TypeVar("_ST_MultiPolygonField", default=MultiPolygon | Combinable)
54+
_GT_MultiPolygonField = TypeVar("_GT_MultiPolygonField", default=MultiPolygon)
55+
_Form_MultiPolygonField = TypeVar(
56+
"_Form_MultiPolygonField", bound=forms.MultiPolygonField, default=forms.MultiPolygonField
57+
)
58+
_Geom_MultiPolygonField = TypeVar("_Geom_MultiPolygonField", bound=MultiPolygon, default=MultiPolygon)
59+
60+
_ST_GeometryCollectionField = TypeVar("_ST_GeometryCollectionField", default=GeometryCollection | Combinable)
61+
_GT_GeometryCollectionField = TypeVar("_GT_GeometryCollectionField", default=GeometryCollection)
62+
_Form_GeometryCollectionField = TypeVar(
63+
"_Form_GeometryCollectionField", bound=forms.GeometryCollectionField, default=forms.GeometryCollectionField
64+
)
65+
_Geom_GeometryCollectionField = TypeVar(
66+
"_Geom_GeometryCollectionField", bound=GeometryCollection, default=GeometryCollection
67+
)
68+
69+
_Form_ClassT = TypeVar("_Form_ClassT", bound=forms.GeometryField)
70+
_GEOM_ClassT = TypeVar("_GEOM_ClassT", bound=GEOSGeometry)
71+
2672
class SRIDCacheEntry(NamedTuple):
2773
units: Any
2874
units_name: str
2975
spheroid: str
3076
geodetic: bool
3177

3278
def get_srid_info(srid: int, connection: Any) -> SRIDCacheEntry: ...
79+
@type_check_only
80+
class SpatialClassField(Generic[_Form_ClassT, _GEOM_ClassT]):
81+
form_class: type[_Form_ClassT]
82+
geom_class: type[_GEOM_ClassT] | None
3383

34-
class BaseSpatialField(Field[_ST, _GT]):
35-
form_class: type[forms.GeometryField]
84+
class BaseSpatialField(Field[_ST, _GT], SpatialClassField[_Form_ClassT, _GEOM_ClassT]):
3685
geom_type: str
37-
geom_class: type[GEOSGeometry] | None
3886
geography: bool
3987
spatial_index: bool
4088
srid: int
@@ -52,7 +100,7 @@ class BaseSpatialField(Field[_ST, _GT]):
52100
null: bool = ...,
53101
db_index: bool = ...,
54102
default: Any = ...,
55-
db_default: type[NOT_PROVIDED] | Expression | _ST = ...,
103+
db_default: type[NOT_PROVIDED] | Expression | _ST = ..., # pyright: ignore[reportInvalidTypeVarUse]
56104
editable: bool = ...,
57105
auto_created: bool = ...,
58106
serialize: bool = ...,
@@ -78,7 +126,7 @@ class BaseSpatialField(Field[_ST, _GT]):
78126
def get_raster_prep_value(self, value: Any, is_candidate: Any) -> Any: ...
79127
def get_prep_value(self, value: Any) -> Any: ...
80128

81-
class GeometryField(BaseSpatialField[_ST, _GT]):
129+
class GeometryField(BaseSpatialField[_ST, _GT, _Form_ClassT, _GEOM_ClassT]):
82130
dim: int
83131
def __init__(
84132
self,
@@ -98,7 +146,7 @@ class GeometryField(BaseSpatialField[_ST, _GT]):
98146
null: bool = ...,
99147
db_index: bool = ...,
100148
default: Any = ...,
101-
db_default: type[NOT_PROVIDED] | Expression | _ST = ...,
149+
db_default: type[NOT_PROVIDED] | Expression | _ST = ..., # pyright: ignore[reportInvalidTypeVarUse]
102150
editable: bool = ...,
103151
auto_created: bool = ...,
104152
serialize: bool = ...,
@@ -122,62 +170,58 @@ class GeometryField(BaseSpatialField[_ST, _GT]):
122170
**kwargs: Any,
123171
) -> forms.GeometryField: ...
124172

125-
class PointField(GeometryField[_ST, _GT]):
173+
class PointField(GeometryField[_ST_PointField, _GT_PointField, _Form_PointField, _Geom_PointField]):
126174
_pyi_private_set_type: Point | Combinable
127175
_pyi_private_get_type: Point
128176
_pyi_lookup_exact_type: Point
129177

130-
geom_class: type[Point]
131-
form_class: type[forms.PointField]
132-
133-
class LineStringField(GeometryField[_ST, _GT]):
178+
class LineStringField(
179+
GeometryField[_ST_LineStringField, _GT_LineStringField, _Form_LineStringField, _Geom_LineStringField]
180+
):
134181
_pyi_private_set_type: LineString | Combinable
135182
_pyi_private_get_type: LineString
136183
_pyi_lookup_exact_type: LineString
137184

138-
geom_class: type[LineString]
139-
form_class: type[forms.LineStringField]
140-
141-
class PolygonField(GeometryField[_ST, _GT]):
185+
class PolygonField(GeometryField[_ST_PolygonField, _GT_PolygonField, _Form_PolygonField, _Geom_PolygonField]):
142186
_pyi_private_set_type: Polygon | Combinable
143187
_pyi_private_get_type: Polygon
144188
_pyi_lookup_exact_type: Polygon
145189

146-
geom_class: type[Polygon]
147-
form_class: type[forms.PolygonField]
148-
149-
class MultiPointField(GeometryField[_ST, _GT]):
190+
class MultiPointField(
191+
GeometryField[_ST_MultiPointField, _GT_MultiPointField, _Form_MultiPointField, _Geom_MultiPointField]
192+
):
150193
_pyi_private_set_type: MultiPoint | Combinable
151194
_pyi_private_get_type: MultiPoint
152195
_pyi_lookup_exact_type: MultiPoint
153196

154-
geom_class: type[MultiPoint]
155-
form_class: type[forms.MultiPointField]
156-
157-
class MultiLineStringField(GeometryField[_ST, _GT]):
197+
class MultiLineStringField(
198+
GeometryField[
199+
_ST_MultiLineStringField, _GT_MultiLineStringField, _Form_MultiLineStringField, _Geom_MultiLineStringField
200+
]
201+
):
158202
_pyi_private_set_type: MultiLineString | Combinable
159203
_pyi_private_get_type: MultiLineString
160204
_pyi_lookup_exact_type: MultiLineString
161205

162-
geom_class: type[MultiLineString]
163-
form_class: type[forms.MultiLineStringField]
164-
165-
class MultiPolygonField(GeometryField[_ST, _GT]):
206+
class MultiPolygonField(
207+
GeometryField[_ST_MultiPolygonField, _GT_MultiPolygonField, _Form_MultiPolygonField, _Geom_MultiPolygonField]
208+
):
166209
_pyi_private_set_type: MultiPolygon | Combinable
167210
_pyi_private_get_type: MultiPolygon
168211
_pyi_lookup_exact_type: MultiPolygon
169212

170-
geom_class: type[MultiPolygon]
171-
form_class: type[forms.MultiPolygonField]
172-
173-
class GeometryCollectionField(GeometryField[_ST, _GT]):
213+
class GeometryCollectionField(
214+
GeometryField[
215+
_ST_GeometryCollectionField,
216+
_GT_GeometryCollectionField,
217+
_Form_GeometryCollectionField,
218+
_Geom_GeometryCollectionField,
219+
]
220+
):
174221
_pyi_private_set_type: GeometryCollection | Combinable
175222
_pyi_private_get_type: GeometryCollection
176223
_pyi_lookup_exact_type: GeometryCollection
177224

178-
geom_class: type[GeometryCollection]
179-
form_class: type[forms.GeometryCollectionField]
180-
181225
class ExtentField(Field):
182226
def get_internal_type(self) -> Any: ...
183227

django-stubs/db/models/fields/__init__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
230230
validators: Iterable[validators._ValidatorCallable] = (),
231231
error_messages: _ErrorMessagesMapping | None = None,
232232
db_comment: str | None = None,
233-
db_default: type[NOT_PROVIDED] | Expression | _ST = ...,
233+
db_default: type[NOT_PROVIDED] | Expression | _ST = ..., # pyright: ignore[reportInvalidTypeVarUse]
234234
) -> None: ...
235235
def __set__(self, instance: Any, value: _ST) -> None: ...
236236
# class access

django-stubs/utils/ipv6.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ MAX_IPV6_ADDRESS_LENGTH: int
66
def clean_ipv6_address(
77
ip_str: Any, unpack_ipv4: bool = False, error_message: str = ..., max_length: int = 39
88
) -> str: ...
9-
def is_valid_ipv6_address(ip_addr: str | IPv6Address) -> bool: ...
9+
def is_valid_ipv6_address(ip_str: str | IPv6Address) -> bool: ...

scripts/stubtest/allowlist.txt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,6 @@ django.core.files.storage.default_storage
2727
django.contrib.admin.models.LogEntry_RelatedManager
2828
django.contrib.auth.models.Permission_RelatedManager
2929

30-
# '<Model>_ManyRelatedManager' entries are plugin generated and these subclasses only exist
31-
# _locally/dynamically_ runtime -- Created via
32-
# 'django.db.models.fields.related_descriptors.create_forward_many_to_many_manager'
33-
django.contrib.auth.models.Group_ManyRelatedManager
34-
django.contrib.auth.models.Permission_ManyRelatedManager
35-
django.contrib.auth.models.User_ManyRelatedManager
36-
3730
# BaseArchive abstract methods that take no argument, but typed with arguments to match the Archive and TarArchive Implementations
3831
django.utils.archive.BaseArchive.list
3932
django.utils.archive.BaseArchive.extract
@@ -458,8 +451,12 @@ django.contrib.auth.models.Group@AnnotatedWith
458451
django.contrib.auth.models.Permission@AnnotatedWith
459452
django.contrib.auth.models.PermissionsMixin@AnnotatedWith
460453
django.contrib.auth.models.User@AnnotatedWith
454+
django.contrib.auth.models.Group_permissions@AnnotatedWith
455+
django.contrib.auth.models.User_groups@AnnotatedWith
456+
django.contrib.auth.models.User_permissions@AnnotatedWith
461457
django.contrib.contenttypes.models.ContentType@AnnotatedWith
462458
django.contrib.flatpages.models.FlatPage@AnnotatedWith
459+
django.contrib.flatpages.models.FlatPage_sites@AnnotatedWith
463460
django.contrib.gis.db.backends.oracle.models.OracleGeometryColumns@AnnotatedWith
464461
django.contrib.gis.db.backends.oracle.models.OracleSpatialRefSys@AnnotatedWith
465462
django.contrib.gis.db.backends.postgis.models.PostGISGeometryColumns@AnnotatedWith

scripts/stubtest/allowlist_todo.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,6 @@ django.contrib.sessions.models.SessionManager.__slotnames__
472472
django.contrib.sitemaps.views.SitemapIndexItem
473473
django.contrib.sites.admin.SiteAdmin
474474
django.contrib.sites.models.Site.domain
475-
django.contrib.sites.models.Site.flatpage_set
476475
django.contrib.sites.models.Site.id
477476
django.contrib.sites.models.Site.name
478477
django.contrib.sites.models.SiteManager.__slotnames__
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from datetime import datetime
2+
from typing import Optional
3+
4+
from django.contrib.admin.models import LogEntry, LogEntryManager
5+
from django.contrib.auth.models import AbstractUser
6+
from django.contrib.contenttypes.models import ContentType
7+
from typing_extensions import assert_type
8+
9+
log_entry = LogEntry()
10+
assert_type(log_entry.id, int)
11+
assert_type(log_entry.pk, int)
12+
assert_type(log_entry.action_time, datetime)
13+
assert_type(log_entry.user, AbstractUser)
14+
assert_type(log_entry.content_type, Optional[ContentType])
15+
assert_type(log_entry.content_type_id, Optional[int])
16+
assert_type(log_entry.object_id, Optional[str])
17+
assert_type(log_entry.object_repr, str)
18+
assert_type(log_entry.action_flag, int)
19+
assert_type(log_entry.change_message, str)
20+
assert_type(LogEntry.objects, LogEntryManager)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from datetime import datetime
2+
from typing import Optional
3+
4+
from django.contrib.auth.models import Group, Group_permissions, Permission, User, User_groups, User_permissions
5+
from django.contrib.contenttypes.models import ContentType
6+
from django.db.models import Manager
7+
from typing_extensions import assert_type
8+
9+
user = User()
10+
assert_type(user.id, int)
11+
assert_type(user.pk, int)
12+
assert_type(user.password, str)
13+
assert_type(user.last_login, Optional[datetime])
14+
assert_type(user.is_active, bool)
15+
assert_type(user.username, str)
16+
assert_type(user.first_name, str)
17+
assert_type(user.last_name, str)
18+
assert_type(user.email, str)
19+
assert_type(user.is_staff, bool)
20+
assert_type(user.is_active, bool)
21+
assert_type(user.date_joined, datetime)
22+
assert_type(user.groups.get(), Group)
23+
assert_type(user.groups.through, type[User_groups])
24+
assert_type(user.user_permissions.get(), Permission)
25+
assert_type(user.user_permissions.through, type[User_permissions])
26+
27+
group = Group()
28+
assert_type(group.id, int)
29+
assert_type(group.pk, int)
30+
assert_type(group.name, str)
31+
assert_type(group.permissions.get(), Permission)
32+
assert_type(group.permissions.through, type[Group_permissions])
33+
assert_type(Group.permissions.through, type[Group_permissions])
34+
assert_type(Group.permissions.through.objects, Manager[Group_permissions])
35+
36+
group_permissions = Group.permissions.through.objects.get()
37+
assert_type(group_permissions.id, int)
38+
assert_type(group_permissions.pk, int)
39+
assert_type(group_permissions.group, Group)
40+
assert_type(group_permissions.group_id, int)
41+
assert_type(group_permissions.permission, Permission)
42+
assert_type(group_permissions.permission_id, int)
43+
44+
permission = Permission()
45+
assert_type(permission.id, int)
46+
assert_type(permission.pk, int)
47+
assert_type(permission.name, str)
48+
assert_type(permission.content_type, ContentType)
49+
assert_type(permission.content_type_id, int)
50+
assert_type(permission.group_set.get(), Group)
51+
assert_type(permission.group_set.through.objects.get(), Group_permissions)

0 commit comments

Comments
 (0)