Skip to content

Commit 2df5d4c

Browse files
committed
Don't remove objects attribute from Model in plugin
Partially reverts typeddjango#1672
1 parent ef501f2 commit 2df5d4c

File tree

6 files changed

+70
-69
lines changed

6 files changed

+70
-69
lines changed

django-stubs/db/models/base.pyi

+1-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ class Model(metaclass=ModelBase):
3636
# and re-add them to correct concrete subclasses of 'Model'
3737
DoesNotExist: Final[type[ObjectDoesNotExist]]
3838
MultipleObjectsReturned: Final[type[BaseMultipleObjectsReturned]]
39-
# This 'objects' attribute will be deleted, via the plugin, in favor of managing it
40-
# to only exist on subclasses it exists on during runtime.
39+
4140
objects: ClassVar[Manager[Self]]
4241

4342
_meta: ClassVar[Options[Self]]

mypy_django_plugin/transformers/models.py

+26-7
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,18 @@ def reparametrize_dynamically_created_manager(self, manager_name: str, manager_i
319319
assert manager_info is not None
320320
# Reparameterize dynamically created manager with model type
321321
manager_type = helpers.fill_manager(manager_info, Instance(self.model_classdef.info, []))
322+
manager_node = self.model_classdef.info.get(manager_name)
323+
if manager_node and isinstance(manager_node.node, Var):
324+
manager_node.node.type = manager_type
322325
self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True)
323326

324327
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
325328
manager_info: Optional[TypeInfo]
326329

330+
def cast_var_to_classvar(symbol: Optional[SymbolTableNode]) -> None:
331+
if symbol and isinstance(symbol.node, Var):
332+
symbol.node.is_classvar = True
333+
327334
incomplete_manager_defs = set()
328335
for manager_name, manager in model_cls._meta.managers_map.items():
329336
manager_node = self.model_classdef.info.get(manager_name)
@@ -345,7 +352,24 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
345352

346353
assert self.model_classdef.info.self_type is not None
347354
manager_type = helpers.fill_manager(manager_info, self.model_classdef.info.self_type)
348-
self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True)
355+
# It seems that the type checker fetches a Var from expressions, but looks
356+
# through the symbol table for the type(at some later stage?). Currently we
357+
# don't overwrite the reference mypy holds from an expression to a Var
358+
# instance when adding a new node, we only overwrite the reference to the
359+
# Var in the symbol table. That means there's a lingering Var instance
360+
# attached to expressions and if we don't flip that to a ClassVar, the
361+
# checker will emit an error for overriding a class variable with an
362+
# instance variable. As mypy seems to check that via the expression and not
363+
# the symbol table. Optimally we want to just set a type on the existing Var
364+
# like:
365+
# manager_node.node.type = manager_type
366+
# but for some reason that doesn't work. It only works replacing the
367+
# existing Var with a new one in the symbol table.
368+
cast_var_to_classvar(manager_node)
369+
if manager_fullname == manager_info.fullname and manager_node and isinstance(manager_node.node, Var):
370+
manager_node.node.type = manager_type
371+
else:
372+
self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True)
349373

350374
if incomplete_manager_defs:
351375
if not self.api.final_iteration:
@@ -360,6 +384,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
360384
# setting _some_ type
361385
fallback_manager_info = self.get_or_create_manager_with_any_fallback()
362386
if fallback_manager_info is not None:
387+
cast_var_to_classvar(self.model_classdef.info.get(manager_name))
363388
assert self.model_classdef.info.self_type is not None
364389
manager_type = helpers.fill_manager(fallback_manager_info, self.model_classdef.info.self_type)
365390
self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True)
@@ -958,12 +983,6 @@ def adjust_model_class(cls, ctx: ClassDefContext) -> None:
958983
):
959984
del ctx.cls.info.names["MultipleObjectsReturned"]
960985

961-
objects = ctx.cls.info.names.get("objects")
962-
if objects is not None and isinstance(objects.node, Var) and not objects.plugin_generated:
963-
del ctx.cls.info.names["objects"]
964-
965-
return
966-
967986
def get_exception_bases(self, name: str) -> List[Instance]:
968987
bases = []
969988
for model_base in self.model_classdef.info.direct_base_classes():

tests/typecheck/fields/test_related.yml

+14-20
Original file line numberDiff line numberDiff line change
@@ -727,10 +727,9 @@
727727
- path: myapp/models.py
728728
content: |
729729
from django.db import models
730-
from django.db.models.manager import BaseManager
731730
class TransactionQuerySet(models.QuerySet):
732731
pass
733-
TransactionManager = BaseManager.from_queryset(TransactionQuerySet)
732+
TransactionManager = models.Manager.from_queryset(TransactionQuerySet)
734733
class Transaction(models.Model):
735734
pk = 0
736735
objects = TransactionManager()
@@ -742,7 +741,7 @@
742741
Transaction().test()
743742
744743
745-
- case: foreign_key_relationship_for_models_with_custom_manager_unsolvable
744+
- case: foreign_key_relationship_for_models_with_custom_manager_solvable_via_as_manager_type
746745
main: |
747746
from myapp.models import Transaction
748747
installed_apps:
@@ -752,30 +751,27 @@
752751
- path: myapp/models.py
753752
content: |
754753
from django.db import models
755-
from django.db.models.manager import BaseManager
756754
class TransactionQuerySet(models.QuerySet):
757755
def custom(self) -> None:
758756
pass
759757
760-
def TransactionManager() -> BaseManager:
761-
return BaseManager.from_queryset(TransactionQuerySet)()
758+
def TransactionManager() -> models.Manager:
759+
return models.Manager.from_queryset(TransactionQuerySet)()
762760
763761
class Transaction(models.Model):
764762
objects = TransactionManager()
765763
def test(self) -> None:
766-
reveal_type(self.transactionlog_set)
767-
# We use a fallback Any type:
768-
reveal_type(Transaction.objects)
769-
reveal_type(Transaction.objects.custom())
764+
reveal_type(self.transactionlog_set) # N: Revealed type is "django.db.models.fields.related_descriptors.RelatedManager[myapp.models.TransactionLog]"
765+
# We get a lucky shot here as long as the plugin predeclares a
766+
# manager for `.as_manager` for every base class of QuerySet.
767+
# It's just lucky that the runtime's manager name is the same
768+
# name as for the predeclared manager. Resolving this wouldn't
769+
# be possible without inspection of the runtime
770+
reveal_type(Transaction.objects) # N: Revealed type is "myapp.models.ManagerFromTransactionQuerySet[myapp.models.Transaction]"
771+
reveal_type(Transaction.objects.custom()) # N: Revealed type is "None"
770772
771773
class TransactionLog(models.Model):
772774
transaction = models.ForeignKey(Transaction, on_delete=models.CASCADE)
773-
out: |
774-
myapp/models:11: error: Could not resolve manager type for "myapp.models.Transaction.objects" [django-manager-missing]
775-
myapp/models:13: note: Revealed type is "django.db.models.fields.related_descriptors.RelatedManager[myapp.models.TransactionLog]"
776-
myapp/models:15: note: Revealed type is "myapp.models.UnknownManager[myapp.models.Transaction]"
777-
myapp/models:16: note: Revealed type is "Any"
778-
779775
780776
- case: resolve_primary_keys_for_foreign_keys_with_abstract_self_model
781777
main: |
@@ -913,12 +909,11 @@
913909
- path: myapp/models/purchase.py
914910
content: |
915911
from django.db import models
916-
from django.db.models.manager import BaseManager
917912
from .querysets import PurchaseQuerySet
918913
from .store import Store
919914
from .user import User
920915
921-
PurchaseManager = BaseManager.from_queryset(PurchaseQuerySet)
916+
PurchaseManager = models.Manager.from_queryset(PurchaseQuerySet)
922917
class Purchase(models.Model):
923918
objects = PurchaseManager()
924919
store = models.ForeignKey(to=Store, on_delete=models.CASCADE, related_name='purchases')
@@ -936,7 +931,6 @@
936931
- path: myapp/models.py
937932
content: |
938933
from django.db import models
939-
from django.db.models.manager import BaseManager
940934
941935
class User(models.Model):
942936
purchases: int
@@ -945,7 +939,7 @@
945939
def queryset_method(self) -> "PurchaseQuerySet":
946940
return self.all()
947941
948-
PurchaseManager = BaseManager.from_queryset(PurchaseQuerySet)
942+
PurchaseManager = models.Manager.from_queryset(PurchaseQuerySet)
949943
class Purchase(models.Model):
950944
objects = PurchaseManager()
951945
user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='purchases')

0 commit comments

Comments
 (0)