Skip to content

Commit 8703696

Browse files
authored
Make plugin handle explicitly declared reverse descriptors for FKs (#2230)
* Make plugin handle explicitly declared reverse descriptors for FKs If a reverse descriptor was explicitly declared on a model, the plugin skipped to create a more specific subclass for its related manager. * fixup! Make plugin handle explicitly declared reverse descriptors for FKs
1 parent 590f033 commit 8703696

File tree

3 files changed

+75
-25
lines changed

3 files changed

+75
-25
lines changed

mypy_django_plugin/transformers/models.py

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -463,34 +463,43 @@ def many_to_many_descriptor(self) -> TypeInfo:
463463

464464
def process_relation(self, relation: ForeignObjectRel) -> None:
465465
attname = relation.get_accessor_name()
466-
if attname is None or attname in self.model_classdef.info.names:
467-
# No reverse accessor or already declared. Note that this would also leave any
468-
# explicitly declared(i.e. non-inferred) reverse accessors alone
466+
if attname is None:
467+
# No reverse accessor.
469468
return
470469

471470
to_model_cls = self.django_context.get_field_related_model_cls(relation)
472471
to_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(to_model_cls)
473472

473+
reverse_lookup_declared = attname in self.model_classdef.info.names
474474
if isinstance(relation, OneToOneRel):
475-
self.add_new_node_to_model_class(
476-
attname,
477-
Instance(
478-
self.reverse_one_to_one_descriptor,
479-
[Instance(self.model_classdef.info, []), Instance(to_model_info, [])],
480-
),
481-
)
475+
if not reverse_lookup_declared:
476+
self.add_new_node_to_model_class(
477+
attname,
478+
Instance(
479+
self.reverse_one_to_one_descriptor,
480+
[Instance(self.model_classdef.info, []), Instance(to_model_info, [])],
481+
),
482+
)
482483
return
483-
484484
elif isinstance(relation, ManyToManyRel):
485-
# TODO: 'relation' should be based on `TypeInfo` instead of Django runtime.
486-
assert relation.through is not None
487-
through_fullname = helpers.get_class_fullname(relation.through)
488-
through_model_info = self.lookup_typeinfo_or_incomplete_defn_error(through_fullname)
485+
if not reverse_lookup_declared:
486+
# TODO: 'relation' should be based on `TypeInfo` instead of Django runtime.
487+
assert relation.through is not None
488+
through_fullname = helpers.get_class_fullname(relation.through)
489+
through_model_info = self.lookup_typeinfo_or_incomplete_defn_error(through_fullname)
490+
self.add_new_node_to_model_class(
491+
attname,
492+
Instance(
493+
self.many_to_many_descriptor, [Instance(to_model_info, []), Instance(through_model_info, [])]
494+
),
495+
is_classvar=True,
496+
)
497+
return
498+
elif not reverse_lookup_declared:
499+
# ManyToOneRel
489500
self.add_new_node_to_model_class(
490-
attname,
491-
Instance(self.many_to_many_descriptor, [Instance(to_model_info, []), Instance(through_model_info, [])]),
501+
attname, Instance(self.reverse_many_to_one_descriptor, [Instance(to_model_info, [])]), is_classvar=True
492502
)
493-
return
494503

495504
related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS)
496505
# TODO: Support other reverse managers than `_default_manager`
@@ -525,10 +534,6 @@ def process_relation(self, relation: ForeignObjectRel) -> None:
525534
self.ctx.cls,
526535
code=MANAGER_MISSING,
527536
)
528-
529-
self.add_new_node_to_model_class(
530-
attname, Instance(self.reverse_many_to_one_descriptor, [Instance(to_model_info, [])])
531-
)
532537
return
533538

534539
# Create a reverse manager subclassed from the default manager of the related
@@ -548,9 +553,6 @@ def process_relation(self, relation: ForeignObjectRel) -> None:
548553
derived_from="_default_manager",
549554
fullname=new_related_manager_info.fullname,
550555
)
551-
self.add_new_node_to_model_class(
552-
attname, Instance(self.reverse_many_to_one_descriptor, [Instance(to_model_info, [])])
553-
)
554556

555557
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
556558
# add related managers etc.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import ClassVar
2+
3+
from django.db import models
4+
from django.db.models.fields.related_descriptors import RelatedManager, ReverseManyToOneDescriptor
5+
from typing_extensions import assert_type
6+
7+
8+
class Other(models.Model):
9+
explicit_descriptor: ClassVar[ReverseManyToOneDescriptor["MyModel"]]
10+
11+
12+
class MyModel(models.Model):
13+
rel = models.ForeignKey[Other, Other](Other, on_delete=models.CASCADE, related_name="explicit_descriptor")
14+
15+
16+
# Pyright doesn't allow "runtime" usage of @type_check_only 'RelatedManager' but we're
17+
# only type checking these files so it should be fine.
18+
assert_type(Other().explicit_descriptor, RelatedManager[MyModel]) # pyright: ignore[reportGeneralTypeIssues]

tests/typecheck/fields/test_related.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,36 @@
10351035
class Book(PrintedGood):
10361036
name = models.CharField()
10371037
1038+
- case: test_explicit_reverse_many_to_one_descriptor
1039+
main: |
1040+
from myapp.models import Other
1041+
reveal_type(Other.explicit_descriptor) # N: Revealed type is "django.db.models.fields.related_descriptors.ReverseManyToOneDescriptor[myapp.models.MyModel]"
1042+
reveal_type(Other().explicit_descriptor) # N: Revealed type is "myapp.models.MyModel_RelatedManager"
1043+
reveal_type(Other().explicit_descriptor.custom_method()) # N: Revealed type is "builtins.int"
1044+
installed_apps:
1045+
- myapp
1046+
monkeypatch: true
1047+
files:
1048+
- path: myapp/__init__.py
1049+
- path: myapp/models/__init__.py
1050+
content: |
1051+
from typing import ClassVar
1052+
from django.db import models
1053+
from django.db.models.fields.related_descriptors import ReverseManyToOneDescriptor
1054+
1055+
class Other(models.Model):
1056+
explicit_descriptor: ClassVar[ReverseManyToOneDescriptor["MyModel"]]
1057+
1058+
class CustomManager(models.Manager["MyModel"]):
1059+
def custom_method(self) -> int: ...
1060+
1061+
class MyModel(models.Model):
1062+
rel = models.ForeignKey(
1063+
Other, on_delete=models.CASCADE, related_name="explicit_descriptor"
1064+
)
1065+
1066+
objects = CustomManager()
1067+
10381068
- case: test_reverse_one_to_one_descriptor
10391069
main: |
10401070
from myapp.models import MyModel, Other

0 commit comments

Comments
 (0)