From 965f4b7918612839f0724c3812db580da1f4cd63 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Sun, 28 Jul 2024 16:09:26 +0200 Subject: [PATCH 1/5] Check correct model on other side of many to many reverse filtering Depending on the manager type there could be different model arguments. For a `ManyRelatedManager` the argument is the `through` model, but we want to check the `to` model. As a simple starter we stash the `to` model in the manager's metadata and set a precedence on fetching the `to` model from metadata. --- mypy_django_plugin/lib/helpers.py | 8 +++++++ mypy_django_plugin/transformers/models.py | 1 + .../transformers/orm_lookups.py | 3 ++- tests/typecheck/fields/test_related.yml | 21 +++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 862717236..059ad6afe 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -94,6 +94,14 @@ def set_many_to_many_manager_info(to: TypeInfo, derived_from: str, manager_info: get_django_metadata(to).setdefault("m2m_managers", {})[derived_from] = manager_info.fullname +def set_manager_to_model(manager: TypeInfo, to_model: TypeInfo) -> None: + get_django_metadata(manager)["manager_to_model"] = to_model.fullname + + +def get_manager_to_model(manager: TypeInfo) -> str | None: + return get_django_metadata(manager).get("manager_to_model") + + class IncompleteDefnException(Exception): pass diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index c85c87bf1..b77dc7dec 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -934,6 +934,7 @@ def create_many_related_manager(self, model: Instance) -> None: helpers.set_many_to_many_manager_info( to=model.type, derived_from="_default_manager", manager_info=related_manager_info ) + helpers.set_manager_to_model(related_manager_info, model.type) class MetaclassAdjustments(ModelClassInitializer): diff --git a/mypy_django_plugin/transformers/orm_lookups.py b/mypy_django_plugin/transformers/orm_lookups.py index 7eed520fd..761fd5453 100644 --- a/mypy_django_plugin/transformers/orm_lookups.py +++ b/mypy_django_plugin/transformers/orm_lookups.py @@ -18,7 +18,8 @@ def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) if not isinstance(ctx.type, Instance) or not ctx.type.args or not isinstance(ctx.type.args[0], Instance): return ctx.default_return_type - model_cls_fullname = ctx.type.args[0].type.fullname + manager_info = ctx.type.type + model_cls_fullname = helpers.get_manager_to_model(manager_info) or ctx.type.args[0].type.fullname model_cls = django_context.get_model_class_by_fullname(model_cls_fullname) if model_cls is None: return ctx.default_return_type diff --git a/tests/typecheck/fields/test_related.yml b/tests/typecheck/fields/test_related.yml index 983c3b241..c5fe0f067 100644 --- a/tests/typecheck/fields/test_related.yml +++ b/tests/typecheck/fields/test_related.yml @@ -1498,3 +1498,24 @@ class MyModel(models.Model): others = models.ManyToManyField(Other) + +- case: test_reverse_m2m_relation_checks_other_model + main: | + from myapp.models import Author + Author().book_set.filter(featured=True) + Author().book_set.filter(xyz=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc] + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class Author(models.Model): + ... + + + class Book(models.Model): + featured = models.BooleanField(default=False) + authors = models.ManyToManyField(Author) From cebdede68664da0854bb80b9520cc66a63256aee Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Sun, 28 Jul 2024 16:26:09 +0200 Subject: [PATCH 2/5] fixup! Check correct model on other side of many to many reverse filtering --- mypy_django_plugin/lib/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 059ad6afe..3a5d4ba28 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -98,7 +98,7 @@ def set_manager_to_model(manager: TypeInfo, to_model: TypeInfo) -> None: get_django_metadata(manager)["manager_to_model"] = to_model.fullname -def get_manager_to_model(manager: TypeInfo) -> str | None: +def get_manager_to_model(manager: TypeInfo) -> Optional[str]: return get_django_metadata(manager).get("manager_to_model") From 6adc91bbb01f0a9dd588735ebc9091a6982e1093 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Sun, 28 Jul 2024 16:28:10 +0200 Subject: [PATCH 3/5] fixup! fixup! Check correct model on other side of many to many reverse filtering --- mypy_django_plugin/lib/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 3a5d4ba28..19f0a28a4 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -56,6 +56,7 @@ class DjangoTypeMetadata(TypedDict, total=False): queryset_bases: Dict[str, int] m2m_throughs: Dict[str, str] m2m_managers: Dict[str, str] + manager_to_model: str def get_django_metadata(model_info: TypeInfo) -> DjangoTypeMetadata: From be478956c95886048c8f8605e476be08089c37b5 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Sun, 28 Jul 2024 17:04:31 +0200 Subject: [PATCH 4/5] fixup! fixup! fixup! Check correct model on other side of many to many reverse filtering --- tests/typecheck/fields/test_related.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/typecheck/fields/test_related.yml b/tests/typecheck/fields/test_related.yml index c5fe0f067..f91e08d55 100644 --- a/tests/typecheck/fields/test_related.yml +++ b/tests/typecheck/fields/test_related.yml @@ -1480,9 +1480,9 @@ MyModel.objects.get(xyz__isnull=False) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc] MyModel.objects.exclude(xyz__isnull=False) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc] other = Other() - other.mymodel_set.filter(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc] - other.mymodel_set.get(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc] - other.mymodel_set.exclude(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, mymodel, mymodel_id, other, other_id [misc] + other.mymodel_set.filter(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc] + other.mymodel_set.get(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc] + other.mymodel_set.exclude(xyz__isnull=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: id, others [misc] 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] 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] 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] From 69b63a84bb5e1126f56c1fcf9528e3b76f5bc109 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Sun, 28 Jul 2024 19:15:09 +0200 Subject: [PATCH 5/5] Update tests/typecheck/fields/test_related.yml Co-authored-by: sobolevn --- tests/typecheck/fields/test_related.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/typecheck/fields/test_related.yml b/tests/typecheck/fields/test_related.yml index f91e08d55..46bcff261 100644 --- a/tests/typecheck/fields/test_related.yml +++ b/tests/typecheck/fields/test_related.yml @@ -1515,7 +1515,6 @@ class Author(models.Model): ... - class Book(models.Model): featured = models.BooleanField(default=False) authors = models.ManyToManyField(Author)