Skip to content

Commit faa5cc0

Browse files
flaeppeandersksobolevnpre-commit-ci[bot]
authored
Declare manager class attributes on models as ClassVars (#1672)
* Move ModelBase.objects declaration to Model.objects, for mypy 1.5.0 mypy 1.5.0 was fixed to understand that metaclass attributes take precedence over attributes in the regular class. So we need to declare `objects` in the regular class to allow it to be overridden in subclasses. Fixes #1648. Signed-off-by: Anders Kaseorg <[email protected]> * Declare manager class attributes on models as `ClassVar`s Inclusions: - Adjustments for the plugin to make generated managers `ClassVar`s - Changes the default 'objects' to 'ClassVar' and controls it via the plugin - Plugin ensures to only add the 'objects' manager to models it exists on during runtime * fixup! Declare manager class attributes on models as `ClassVar`s * Enforce appropriate keyword only arguments Co-authored-by: Nikita Sobolev <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks * Bump mypy * Remove a bunch of `-redefinition` lines from `allowlist_todo.txt` --------- Signed-off-by: Anders Kaseorg <[email protected]> Co-authored-by: Anders Kaseorg <[email protected]> Co-authored-by: Nikita Sobolev <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent ff2f4a1 commit faa5cc0

File tree

18 files changed

+93
-78
lines changed

18 files changed

+93
-78
lines changed

django-stubs/contrib/admin/models.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, ClassVar
22
from uuid import UUID
33

44
from django.db import models
@@ -28,7 +28,7 @@ class LogEntry(models.Model):
2828
object_repr: models.CharField
2929
action_flag: models.PositiveSmallIntegerField
3030
change_message: models.TextField
31-
objects: LogEntryManager
31+
objects: ClassVar[LogEntryManager]
3232
def is_addition(self) -> bool: ...
3333
def is_change(self) -> bool: ...
3434
def is_deletion(self) -> bool: ...

django-stubs/contrib/auth/models.pyi

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Iterable
2-
from typing import Any, Literal, TypeVar
2+
from typing import Any, ClassVar, Literal, TypeVar
33

44
from django.contrib.auth.base_user import AbstractBaseUser as AbstractBaseUser
55
from django.contrib.auth.base_user import BaseUserManager as BaseUserManager
@@ -10,7 +10,7 @@ from django.db.models import QuerySet
1010
from django.db.models.base import Model
1111
from django.db.models.manager import EmptyManager
1212
from django.utils.functional import _StrOrPromise
13-
from typing_extensions import TypeAlias
13+
from typing_extensions import Self, TypeAlias
1414

1515
_AnyUser: TypeAlias = Model | AnonymousUser
1616

@@ -21,7 +21,7 @@ class PermissionManager(models.Manager[Permission]):
2121

2222
class Permission(models.Model):
2323
content_type_id: int
24-
objects: PermissionManager
24+
objects: ClassVar[PermissionManager]
2525

2626
name = models.CharField(max_length=255)
2727
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
@@ -32,7 +32,7 @@ class GroupManager(models.Manager[Group]):
3232
def get_by_natural_key(self, name: str) -> Group: ...
3333

3434
class Group(models.Model):
35-
objects: GroupManager
35+
objects: ClassVar[GroupManager]
3636

3737
name = models.CharField(max_length=150)
3838
permissions = models.ManyToManyField(Permission)
@@ -81,6 +81,8 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
8181
is_active = models.BooleanField()
8282
date_joined = models.DateTimeField()
8383

84+
objects: ClassVar[UserManager[Self]]
85+
8486
EMAIL_FIELD: str
8587
USERNAME_FIELD: str
8688

django-stubs/contrib/contenttypes/models.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, ClassVar
22

33
from django.db import models
44
from django.db.models.base import Model
@@ -15,7 +15,7 @@ class ContentType(models.Model):
1515
id: int
1616
app_label: models.CharField
1717
model: models.CharField
18-
objects: ContentTypeManager
18+
objects: ClassVar[ContentTypeManager]
1919
@property
2020
def name(self) -> str: ...
2121
def model_class(self) -> type[Model] | None: ...

django-stubs/contrib/gis/db/backends/oracle/models.pyi

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
from typing import Any
1+
from typing import Any, ClassVar
22

33
from django.contrib.gis.db import models
44
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
5+
from django.db.models.manager import Manager
6+
from typing_extensions import Self
57

68
class OracleGeometryColumns(models.Model):
79
table_name: Any
810
column_name: Any
911
srid: Any
12+
objects: ClassVar[Manager[Self]]
1013

1114
class Meta:
1215
app_label: str
@@ -24,6 +27,7 @@ class OracleSpatialRefSys(models.Model, SpatialRefSysMixin):
2427
auth_name: Any
2528
wktext: Any
2629
cs_bounds: Any
30+
objects: ClassVar[Manager[Self]]
2731

2832
class Meta:
2933
app_label: str

django-stubs/contrib/gis/db/backends/postgis/models.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from typing import Any
1+
from typing import Any, ClassVar
22

33
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
44
from django.db import models
5+
from typing_extensions import Self
56

67
class PostGISGeometryColumns(models.Model):
78
f_table_catalog: Any
@@ -11,6 +12,7 @@ class PostGISGeometryColumns(models.Model):
1112
coord_dimension: Any
1213
srid: Any
1314
type: Any
15+
objects: ClassVar[models.Manager[Self]]
1416

1517
class Meta:
1618
app_label: str
@@ -27,6 +29,7 @@ class PostGISSpatialRefSys(models.Model, SpatialRefSysMixin):
2729
auth_srid: Any
2830
srtext: Any
2931
proj4text: Any
32+
objects: ClassVar[models.Manager[Self]]
3033

3134
class Meta:
3235
app_label: str

django-stubs/contrib/gis/db/backends/spatialite/models.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from typing import Any
1+
from typing import Any, ClassVar
22

33
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
44
from django.db import models
5+
from typing_extensions import Self
56

67
class SpatialiteGeometryColumns(models.Model):
78
f_table_name: Any
@@ -10,6 +11,7 @@ class SpatialiteGeometryColumns(models.Model):
1011
srid: Any
1112
spatial_index_enabled: Any
1213
type: Any
14+
objects: ClassVar[models.Manager[Self]]
1315

1416
class Meta:
1517
app_label: str
@@ -27,6 +29,7 @@ class SpatialiteSpatialRefSys(models.Model, SpatialRefSysMixin):
2729
ref_sys_name: Any
2830
proj4text: Any
2931
srtext: Any
32+
objects: ClassVar[models.Manager[Self]]
3033

3134
class Meta:
3235
app_label: str

django-stubs/contrib/sites/models.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, ClassVar
22

33
from django.db import models
44
from django.http.request import HttpRequest
@@ -11,7 +11,7 @@ class SiteManager(models.Manager[Site]):
1111
def get_by_natural_key(self, domain: str) -> Site: ...
1212

1313
class Site(models.Model):
14-
objects: SiteManager
14+
objects: ClassVar[SiteManager]
1515

1616
domain = models.CharField(max_length=100)
1717
name = models.CharField(max_length=50)

django-stubs/db/migrations/recorder.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
from typing import Any
1+
from typing import Any, ClassVar
22

33
from django.db.backends.base.base import BaseDatabaseWrapper
44
from django.db.models.base import Model
5+
from django.db.models.manager import Manager
56
from django.db.models.query import QuerySet
7+
from typing_extensions import Self
68

79
class MigrationRecorder:
810
class Migration(Model):
911
app: Any
1012
name: Any
1113
applied: Any
14+
objects: ClassVar[Manager[Self]]
1215
connection: BaseDatabaseWrapper
1316
def __init__(self, connection: BaseDatabaseWrapper) -> None: ...
1417
@property

django-stubs/db/models/base.pyi

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from collections.abc import Collection, Iterable, Sequence
2-
from typing import Any, Final, TypeVar
2+
from typing import Any, ClassVar, Final, TypeVar
33

44
from django.core.checks.messages import CheckMessage
55
from django.core.exceptions import MultipleObjectsReturned as BaseMultipleObjectsReturned
66
from django.core.exceptions import ObjectDoesNotExist, ValidationError
77
from django.db.models import BaseConstraint, Field
8-
from django.db.models.manager import BaseManager
8+
from django.db.models.manager import BaseManager, Manager
99
from django.db.models.options import Options
1010
from typing_extensions import Self
1111

@@ -19,8 +19,6 @@ class ModelState:
1919
fields_cache: ModelStateFieldsCacheDescriptor
2020

2121
class ModelBase(type):
22-
@property
23-
def objects(cls: type[_Self]) -> BaseManager[_Self]: ... # type: ignore[misc]
2422
@property
2523
def _default_manager(cls: type[_Self]) -> BaseManager[_Self]: ... # type: ignore[misc]
2624
@property
@@ -34,6 +32,9 @@ class Model(metaclass=ModelBase):
3432
# and re-add them to correct concrete subclasses of 'Model'
3533
DoesNotExist: Final[type[ObjectDoesNotExist]]
3634
MultipleObjectsReturned: Final[type[BaseMultipleObjectsReturned]]
35+
# This 'objects' attribute will be deleted, via the plugin, in favor of managing it
36+
# to only exist on subclasses it exists on during runtime.
37+
objects: ClassVar[Manager[Self]]
3738

3839
class Meta: ...
3940
_meta: Options[Any]

mypy_django_plugin/lib/helpers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,14 +379,17 @@ def check_types_compatible(
379379
api.check_subtype(actual_type, expected_type, ctx.context, error_message, "got", "expected")
380380

381381

382-
def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType, no_serialize: bool = False) -> None:
382+
def add_new_sym_for_info(
383+
info: TypeInfo, name: str, sym_type: MypyType, *, no_serialize: bool = False, is_classvar: bool = False
384+
) -> None:
383385
# type=: type of the variable itself
384386
var = Var(name=name, type=sym_type)
385387
# var.info: type of the object variable is bound to
386388
var.info = info
387389
var._fullname = info.fullname + "." + name
388390
var.is_initialized_in_class = True
389391
var.is_inferred = True
392+
var.is_classvar = is_classvar
390393
info.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True, no_serialize=no_serialize)
391394

392395

0 commit comments

Comments
 (0)