Skip to content

Commit 2969651

Browse files
authored
Allow any reverse relation using ForeignObjectRel to be type checked (#1451)
* Allow any reverse relation using ForeignObjectRel to be type checked Currently, reverse relations created using just `ForeignObjectRel` aren't checked. A real-world example of this is `django-composite-foreignkey`. Reverse relations created by it aren't discovered by the mypy plugin. * Add test demonstrating support for multi-column ForeignObject * Allow foreign keys using `ForeignObject` to be type checked * Freeze `RELATED_FIELDS_CLASSES` to prevent accidental mutation
1 parent 687e67e commit 2969651

File tree

3 files changed

+47
-3
lines changed

3 files changed

+47
-3
lines changed

mypy_django_plugin/lib/fullnames.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
ARRAY_FIELD_FULLNAME = "django.contrib.postgres.fields.array.ArrayField"
77
AUTO_FIELD_FULLNAME = "django.db.models.fields.AutoField"
88
GENERIC_FOREIGN_KEY_FULLNAME = "django.contrib.contenttypes.fields.GenericForeignKey"
9+
FOREIGN_OBJECT_FULLNAME = "django.db.models.fields.related.ForeignObject"
910
FOREIGN_KEY_FULLNAME = "django.db.models.fields.related.ForeignKey"
1011
ONETOONE_FIELD_FULLNAME = "django.db.models.fields.related.OneToOneField"
1112
MANYTOMANY_FIELD_FULLNAME = "django.db.models.fields.related.ManyToManyField"
@@ -30,7 +31,14 @@
3031
BASE_MANAGER_CLASS_FULLNAME,
3132
}
3233

33-
RELATED_FIELDS_CLASSES = {FOREIGN_KEY_FULLNAME, ONETOONE_FIELD_FULLNAME, MANYTOMANY_FIELD_FULLNAME}
34+
RELATED_FIELDS_CLASSES = frozenset(
35+
(
36+
FOREIGN_OBJECT_FULLNAME,
37+
FOREIGN_KEY_FULLNAME,
38+
ONETOONE_FIELD_FULLNAME,
39+
MANYTOMANY_FIELD_FULLNAME,
40+
)
41+
)
3442

3543
MIGRATION_CLASS_FULLNAME = "django.db.migrations.migration.Migration"
3644
OPTIONS_CLASS_FULLNAME = "django.db.models.options.Options"

mypy_django_plugin/transformers/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.db.models import Manager, Model
44
from django.db.models.fields import DateField, DateTimeField, Field
55
from django.db.models.fields.related import ForeignKey
6-
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
6+
from django.db.models.fields.reverse_related import ForeignObjectRel, OneToOneRel
77
from mypy.checker import TypeChecker
88
from mypy.nodes import ARG_STAR2, Argument, AssignmentStmt, CallExpr, Context, NameExpr, TypeInfo, Var
99
from mypy.plugin import AnalyzeTypeContext, AttributeContext, CheckerPluginInterface, ClassDefContext
@@ -464,7 +464,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
464464
self.add_new_node_to_model_class(attname, Instance(related_model_info, []))
465465
continue
466466

467-
if isinstance(relation, (ManyToOneRel, ManyToManyRel)):
467+
if isinstance(relation, ForeignObjectRel):
468468
related_manager_info = None
469469
try:
470470
related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(

tests/typecheck/models/test_related_fields.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,39 @@
8787
b = models.ForeignKey(Model2, related_name="test2", on_delete=models.CASCADE)
8888
8989
objects = Model4Manager()
90+
91+
- case: test_related_name_foreign_object_multi_column
92+
main: |
93+
from app1.models import Model1, Model2
94+
95+
reveal_type(Model2.model_1) # N: Revealed type is "django.db.models.fields.related.ForeignObject[app1.models.Model1, app1.models.Model1]"
96+
reveal_type(Model2().model_1) # N: Revealed type is "app1.models.Model1"
97+
reveal_type(Model1.model_2s) # N: Revealed type is "django.db.models.manager.RelatedManager[app1.models.Model2]"
98+
reveal_type(Model1().model_2s) # N: Revealed type is "django.db.models.manager.RelatedManager[app1.models.Model2]"
99+
100+
installed_apps:
101+
- app1
102+
files:
103+
- path: app1/__init__.py
104+
- path: app1/models.py
105+
content: |
106+
from django.db import models
107+
from django.db.models.fields.related import ForeignObject
108+
109+
class Model1(models.Model):
110+
type = models.TextField()
111+
ref = models.TextField()
112+
113+
class Model2(models.Model):
114+
name = models.TextField()
115+
116+
model_1_type = models.TextField()
117+
model_2_ref = models.TextField()
118+
119+
model_1 = ForeignObject(
120+
Model1,
121+
to_fields=["type", "ref"],
122+
from_fields=["model_1_type", "model_2_ref"],
123+
on_delete=models.CASCADE,
124+
related_name="model_2s",
125+
)

0 commit comments

Comments
 (0)