Skip to content

Commit ca604e9

Browse files
committed
Declare manager class attributes on models as ClassVars
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
1 parent fc3ef75 commit ca604e9

File tree

11 files changed

+73
-37
lines changed

11 files changed

+73
-37
lines changed

django-stubs/contrib/admin/models.pyi

+2-2
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

+6-4
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

+2-2
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/sites/models.pyi

+2-2
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/models/base.pyi

+5-4
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

@@ -25,15 +25,16 @@ class ModelBase(type):
2525
def _base_manager(cls: type[_Self]) -> BaseManager[_Self]: ... # type: ignore[misc]
2626

2727
class Model(metaclass=ModelBase):
28-
objects: BaseManager[Self]
29-
3028
# Note: these two metaclass generated attributes don't really exist on the 'Model'
3129
# class, runtime they are only added on concrete subclasses of 'Model'. The
3230
# metaclass also sets up correct inheritance from concrete parent models exceptions.
3331
# Our mypy plugin aligns with this behaviour and will remove the 2 attributes below
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

+4-1
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

mypy_django_plugin/transformers/models.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,13 @@ def create_new_var(self, name: str, typ: MypyType) -> Var:
7070
var.is_inferred = True
7171
return var
7272

73-
def add_new_node_to_model_class(self, name: str, typ: MypyType, no_serialize: bool = False) -> None:
74-
helpers.add_new_sym_for_info(self.model_classdef.info, name=name, sym_type=typ, no_serialize=no_serialize)
73+
def add_new_node_to_model_class(
74+
self, name: str, typ: MypyType, no_serialize: bool = False, is_classvar: bool = False
75+
) -> None:
76+
# TODO: Rename to signal that it is a `Var` that is added..
77+
helpers.add_new_sym_for_info(
78+
self.model_classdef.info, name=name, sym_type=typ, no_serialize=no_serialize, is_classvar=is_classvar
79+
)
7580

7681
def add_new_class_for_current_module(self, name: str, bases: List[Instance]) -> TypeInfo:
7782
current_module = self.api.modules[self.model_classdef.info.module_name]
@@ -311,7 +316,7 @@ def reparametrize_dynamically_created_manager(self, manager_name: str, manager_i
311316
assert manager_info is not None
312317
# Reparameterize dynamically created manager with model type
313318
manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])])
314-
self.add_new_node_to_model_class(manager_name, manager_type)
319+
self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True)
315320

316321
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
317322
manager_info: Optional[TypeInfo]
@@ -336,7 +341,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
336341
continue
337342

338343
manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])])
339-
self.add_new_node_to_model_class(manager_name, manager_type)
344+
self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True)
340345

341346
if incomplete_manager_defs:
342347
if not self.api.final_iteration:
@@ -351,7 +356,9 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
351356
# setting _some_ type
352357
fallback_manager_info = self.get_or_create_manager_with_any_fallback()
353358
self.add_new_node_to_model_class(
354-
manager_name, Instance(fallback_manager_info, [Instance(self.model_classdef.info, [])])
359+
manager_name,
360+
Instance(fallback_manager_info, [Instance(self.model_classdef.info, [])]),
361+
is_classvar=True,
355362
)
356363

357364
# Find expression for e.g. `objects = SomeManager()`
@@ -623,6 +630,10 @@ def adjust_model_class(cls, ctx: ClassDefContext) -> None:
623630
):
624631
del ctx.cls.info.names["MultipleObjectsReturned"]
625632

633+
objects = ctx.cls.info.names.get("objects")
634+
if objects is not None and isinstance(objects.node, Var) and not objects.plugin_generated:
635+
del ctx.cls.info.names["objects"]
636+
626637
return
627638

628639
def get_exception_bases(self, name: str) -> List[Instance]:

tests/typecheck/managers/test_managers.yml

+17-7
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,16 @@
194194
195195
- case: managers_inherited_from_abstract_classes_multiple_inheritance
196196
main: |
197-
from myapp.models import Child
197+
from myapp.models import AbstractBase1, AbstractBase2, Child
198+
reveal_type(Child.manager1)
199+
reveal_type(Child.restricted)
200+
reveal_type(AbstractBase1.manager1)
201+
reveal_type(AbstractBase2.restricted)
202+
out: |
203+
main:2: note: Revealed type is "myapp.models.CustomManager1[myapp.models.Child]"
204+
main:3: note: Revealed type is "myapp.models.CustomManager2[myapp.models.Child]"
205+
main:4: note: Revealed type is "myapp.models.CustomManager1[myapp.models.AbstractBase1]"
206+
main:5: note: Revealed type is "myapp.models.CustomManager2[myapp.models.AbstractBase2]"
198207
installed_apps:
199208
- myapp
200209
files:
@@ -297,12 +306,13 @@
297306
- path: myapp/__init__.py
298307
- path: myapp/models.py
299308
content: |
309+
from typing import ClassVar
300310
from django.db import models
301311
class ParentOfMyModel4(models.Model):
302312
objects = models.Manager()
303313
304314
class MyModel4(ParentOfMyModel4):
305-
objects = models.Manager['MyModel4']()
315+
objects: ClassVar[models.Manager["MyModel4"]] = models.Manager["MyModel4"]()
306316
307317
# TODO: make it work someday
308318
#- case: inheritance_of_two_models_with_custom_objects_manager
@@ -570,7 +580,7 @@
570580
- path: myapp/__init__.py
571581
- path: myapp/models.py
572582
content: |
573-
from typing import TypeVar
583+
from typing import ClassVar, TypeVar
574584
from django.db import models
575585
576586
T = TypeVar("T", bound="MyModel")
@@ -585,7 +595,7 @@
585595
pass
586596
587597
class MySubModel(MyModel):
588-
objects = MySubManager()
598+
objects: ClassVar[MySubManager["MySubModel"]] = MySubManager()
589599
590600
- case: subclass_manager_without_type_parameters_disallow_any_generics
591601
main: |
@@ -598,14 +608,14 @@
598608
[mypy-myapp.models]
599609
disallow_any_generics = true
600610
out: |
601-
main:2: note: Revealed type is "myapp.models.MySubManager[myapp.models.MySubModel]"
611+
main:2: note: Revealed type is "myapp.models.MySubManager"
602612
main:3: note: Revealed type is "Any"
603613
myapp/models:9: error: Missing type parameters for generic type "MyManager"
604614
files:
605615
- path: myapp/__init__.py
606616
- path: myapp/models.py
607617
content: |
608-
from typing import TypeVar
618+
from typing import ClassVar, TypeVar
609619
from django.db import models
610620
611621
T = TypeVar("T", bound="MyModel")
@@ -620,7 +630,7 @@
620630
pass
621631
622632
class MySubModel(MyModel):
623-
objects = MySubManager()
633+
objects: ClassVar[MySubManager] = MySubManager()
624634
625635
- case: nested_manager_class_definition
626636
main: |

tests/typecheck/models/test_abstract.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@
5151
Recursive(parent=Recursive(parent=None))
5252
Concrete(parent=Concrete(parent=None))
5353
out: |
54-
main:4: error: Access to generic instance variables via class is ambiguous
55-
main:4: error: Unexpected attribute "parent" for model "Recursive"
54+
main:4: error: "Type[Recursive]" has no attribute "objects"
5655
main:5: error: Cannot instantiate abstract class "Recursive" with abstract attributes "DoesNotExist" and "MultipleObjectsReturned"
5756
main:5: error: Unexpected attribute "parent" for model "Recursive"
5857
installed_apps:
@@ -210,4 +209,4 @@
210209
211210
def create_animal_generic(klass: Type[T], name: str) -> T:
212211
reveal_type(klass) # N: Revealed type is "Type[T`-1]"
213-
return klass.objects.create(name=name) # E: Incompatible return value type (got "Animal", expected "T")
212+
return klass._default_manager.create(name=name)

tests/typecheck/models/test_contrib_models.yml

+15-6
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
3232
- case: can_override_abstract_user_manager
3333
main: |
34-
from myapp.models import User
35-
reveal_type(User.objects) # N: Revealed type is "myapp.models.UserManager[myapp.models.User]"
36-
reveal_type(User.objects.all()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.User, myapp.models.User]"
34+
from myapp.models import MyBaseUser, MyUser
35+
reveal_type(MyBaseUser.objects) # N: Revealed type is "myapp.models.MyBaseUserManager[myapp.models.MyBaseUser]"
36+
reveal_type(MyBaseUser.objects.all()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.MyBaseUser, myapp.models.MyBaseUser]"
37+
reveal_type(MyUser.objects) # N: Revealed type is "myapp.models.MyUserManager"
38+
reveal_type(MyUser.objects.all()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.MyUser, myapp.models.MyUser]"
3739
installed_apps:
3840
- django.contrib.auth
3941
- myapp
@@ -42,8 +44,15 @@
4244
- path: myapp/models.py
4345
content: |
4446
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
45-
class UserManager(BaseUserManager["User"]):
47+
from django.contrib.auth.models import AbstractUser, UserManager
48+
from typing import ClassVar
49+
class MyBaseUserManager(BaseUserManager["MyBaseUser"]):
4650
...
4751
48-
class User(AbstractBaseUser):
49-
objects = UserManager()
52+
class MyBaseUser(AbstractBaseUser):
53+
objects = MyBaseUserManager()
54+
55+
class MyUserManager(UserManager["MyUser"]):
56+
...
57+
class MyUser(AbstractUser):
58+
objects: ClassVar[MyUserManager] = MyUserManager()

tests/typecheck/models/test_meta_options.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@
7777
# Should not raise:
7878
MyModel(field=1)
7979
MyModel.objects.create(field=2)
80+
AbstractModel._default_manager.create()
8081
8182
# Errors:
8283
AbstractModel() # E: Cannot instantiate abstract class "AbstractModel" with abstract attributes "DoesNotExist" and "MultipleObjectsReturned"
83-
AbstractModel.objects.create() # E: Access to generic instance variables via class is ambiguous
84+
AbstractModel.objects.create() # E: "Type[AbstractModel]" has no attribute "objects"
8485
installed_apps:
8586
- myapp
8687
files:

0 commit comments

Comments
 (0)