Skip to content

Commit f48e722

Browse files
flaeppesobolevn
andauthored
Check correct model on other side of many to many reverse filtering (#2283)
Co-authored-by: sobolevn <mail@sobolevn.me>
1 parent 8eeed9b commit f48e722

File tree

4 files changed

+35
-4
lines changed

4 files changed

+35
-4
lines changed

mypy_django_plugin/lib/helpers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class DjangoTypeMetadata(TypedDict, total=False):
5656
queryset_bases: Dict[str, int]
5757
m2m_throughs: Dict[str, str]
5858
m2m_managers: Dict[str, str]
59+
manager_to_model: str
5960

6061

6162
def get_django_metadata(model_info: TypeInfo) -> DjangoTypeMetadata:
@@ -94,6 +95,14 @@ def set_many_to_many_manager_info(to: TypeInfo, derived_from: str, manager_info:
9495
get_django_metadata(to).setdefault("m2m_managers", {})[derived_from] = manager_info.fullname
9596

9697

98+
def set_manager_to_model(manager: TypeInfo, to_model: TypeInfo) -> None:
99+
get_django_metadata(manager)["manager_to_model"] = to_model.fullname
100+
101+
102+
def get_manager_to_model(manager: TypeInfo) -> Optional[str]:
103+
return get_django_metadata(manager).get("manager_to_model")
104+
105+
97106
class IncompleteDefnException(Exception):
98107
pass
99108

mypy_django_plugin/transformers/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,7 @@ def create_many_related_manager(self, model: Instance) -> None:
934934
helpers.set_many_to_many_manager_info(
935935
to=model.type, derived_from="_default_manager", manager_info=related_manager_info
936936
)
937+
helpers.set_manager_to_model(related_manager_info, model.type)
937938

938939

939940
class MetaclassAdjustments(ModelClassInitializer):

mypy_django_plugin/transformers/orm_lookups.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext)
1818
if not isinstance(ctx.type, Instance) or not ctx.type.args or not isinstance(ctx.type.args[0], Instance):
1919
return ctx.default_return_type
2020

21-
model_cls_fullname = ctx.type.args[0].type.fullname
21+
manager_info = ctx.type.type
22+
model_cls_fullname = helpers.get_manager_to_model(manager_info) or ctx.type.args[0].type.fullname
2223
model_cls = django_context.get_model_class_by_fullname(model_cls_fullname)
2324
if model_cls is None:
2425
return ctx.default_return_type

tests/typecheck/fields/test_related.yml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,9 +1480,9 @@
14801480
MyModel.objects.get(xyz__isnull=False) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc]
14811481
MyModel.objects.exclude(xyz__isnull=False) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc]
14821482
other = Other()
1483-
other.mymodel_set.filter(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc]
1484-
other.mymodel_set.get(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc]
1485-
other.mymodel_set.exclude(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc]
1483+
other.mymodel_set.filter(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc]
1484+
other.mymodel_set.get(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc]
1485+
other.mymodel_set.exclude(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc]
14861486
MyModel.others.through.objects.filter(xyz__isnull=False) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc]
14871487
MyModel.others.through.objects.get(xyz__isnull=False) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc]
14881488
MyModel.others.through.objects.exclude(xyz__isnull=False) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc]
@@ -1498,3 +1498,23 @@
14981498
14991499
class MyModel(models.Model):
15001500
others = models.ManyToManyField(Other)
1501+
1502+
- case: test_reverse_m2m_relation_checks_other_model
1503+
main: |
1504+
from myapp.models import Author
1505+
Author().book_set.filter(featured=True)
1506+
Author().book_set.filter(xyz=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1507+
installed_apps:
1508+
- myapp
1509+
files:
1510+
- path: myapp/__init__.py
1511+
- path: myapp/models.py
1512+
content: |
1513+
from django.db import models
1514+
1515+
class Author(models.Model):
1516+
...
1517+
1518+
class Book(models.Model):
1519+
featured = models.BooleanField(default=False)
1520+
authors = models.ManyToManyField(Author)

0 commit comments

Comments
 (0)