Skip to content

Commit 3a2a9a3

Browse files
authored
Check correct model on m2m reverse values/values_list (#2288)
1 parent a59861f commit 3a2a9a3

File tree

2 files changed

+46
-4
lines changed

2 files changed

+46
-4
lines changed

mypy_django_plugin/transformers/querysets.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from django.db.models.base import Model
66
from django.db.models.fields.related import RelatedField
77
from django.db.models.fields.reverse_related import ForeignObjectRel
8+
from mypy.checker import TypeChecker
89
from mypy.nodes import ARG_NAMED, ARG_NAMED_OPT, Expression
910
from mypy.plugin import FunctionContext, MethodContext
1011
from mypy.types import AnyType, Instance, TupleType, TypedDictType, TypeOfAny, get_proper_type
1112
from mypy.types import Type as MypyType
13+
from mypy.typevars import fill_typevars
1214

1315
from mypy_django_plugin.django.context import DjangoContext, LookupsAreUnsupported
1416
from mypy_django_plugin.lib import fullnames, helpers
@@ -17,7 +19,16 @@
1719
from mypy_django_plugin.transformers.models import get_or_create_annotated_type
1820

1921

20-
def _extract_model_type_from_queryset(queryset_type: Instance) -> Optional[Instance]:
22+
def _extract_model_type_from_queryset(queryset_type: Instance, api: TypeChecker) -> Optional[Instance]:
23+
if queryset_type.type.has_base(fullnames.MANAGER_CLASS_FULLNAME):
24+
to_model_fullname = helpers.get_manager_to_model(queryset_type.type)
25+
if to_model_fullname is not None:
26+
to_model = helpers.lookup_fully_qualified_typeinfo(api, to_model_fullname)
27+
if to_model is not None:
28+
to_model_instance = fill_typevars(to_model)
29+
assert isinstance(to_model_instance, Instance)
30+
return to_model_instance
31+
2132
for base_type in [queryset_type, *queryset_type.type.bases]:
2233
if (
2334
len(base_type.args)
@@ -161,7 +172,7 @@ def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context:
161172
if not isinstance(default_return_type, Instance):
162173
return ctx.default_return_type
163174

164-
model_type = _extract_model_type_from_queryset(ctx.type)
175+
model_type = _extract_model_type_from_queryset(ctx.type, helpers.get_typechecker_api(ctx))
165176
if model_type is None:
166177
return AnyType(TypeOfAny.from_omitted_generics)
167178

@@ -221,7 +232,7 @@ def extract_proper_type_queryset_annotate(ctx: MethodContext, django_context: Dj
221232
if not isinstance(default_return_type, Instance):
222233
return ctx.default_return_type
223234

224-
model_type = _extract_model_type_from_queryset(ctx.type)
235+
model_type = _extract_model_type_from_queryset(ctx.type, helpers.get_typechecker_api(ctx))
225236
if model_type is None:
226237
return AnyType(TypeOfAny.from_omitted_generics)
227238

@@ -288,7 +299,7 @@ def extract_proper_type_queryset_values(ctx: MethodContext, django_context: Djan
288299
if not isinstance(default_return_type, Instance):
289300
return ctx.default_return_type
290301

291-
model_type = _extract_model_type_from_queryset(ctx.type)
302+
model_type = _extract_model_type_from_queryset(ctx.type, helpers.get_typechecker_api(ctx))
292303
if model_type is None:
293304
return AnyType(TypeOfAny.from_omitted_generics)
294305

tests/typecheck/fields/test_related.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,14 +1502,36 @@
15021502
- case: test_reverse_m2m_relation_checks_other_model
15031503
main: |
15041504
from myapp.models import Author
1505+
# With builtin manager/queryset
15051506
Author().book_set.filter(featured=True)
15061507
Author().book_set.filter(xyz=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1508+
reveal_type(Author().book_set.values_list("featured")) # N: Revealed type is "django.db.models.query.QuerySet[myapp.models.Book, Tuple[builtins.bool]]"
1509+
Author().book_set.values_list("xyz") # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1510+
reveal_type(Author().book_set.values("featured")) # N: Revealed type is "django.db.models.query.QuerySet[myapp.models.Book, TypedDict({'featured': builtins.bool})]"
1511+
Author().book_set.values("xyz") # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1512+
reveal_type(Author().book_set.filter(featured=True).values_list("featured")) # N: Revealed type is "django.db.models.query.QuerySet[myapp.models.Book, Tuple[builtins.bool]]"
1513+
Author().book_set.filter(featured=True).values_list("xyz") # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1514+
reveal_type(Author().book_set.filter(featured=True).values("featured")) # N: Revealed type is "django.db.models.query.QuerySet[myapp.models.Book, TypedDict({'featured': builtins.bool})]"
1515+
Author().book_set.filter(featured=True).values("xyz") # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1516+
1517+
# With a custom manager/queryset
1518+
Author().other_set.filter(featured=True)
1519+
Author().other_set.filter(xyz=True) # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1520+
reveal_type(Author().other_set.values_list("featured")) # N: Revealed type is "django.db.models.query.QuerySet[myapp.models.Other, Tuple[builtins.bool]]"
1521+
Author().other_set.values_list("xyz") # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1522+
reveal_type(Author().other_set.values("featured")) # N: Revealed type is "django.db.models.query.QuerySet[myapp.models.Other, TypedDict({'featured': builtins.bool})]"
1523+
Author().other_set.values("xyz") # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1524+
reveal_type(Author().other_set.filter(featured=True).values_list("featured")) # N: Revealed type is "django.db.models.query.QuerySet[myapp.models.Other, Tuple[builtins.bool]]"
1525+
Author().other_set.filter(featured=True).values_list("xyz") # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
1526+
reveal_type(Author().other_set.filter(featured=True).values("featured")) # N: Revealed type is "django.db.models.query.QuerySet[myapp.models.Other, TypedDict({'featured': builtins.bool})]"
1527+
Author().other_set.filter(featured=True).values("xyz") # E: Cannot resolve keyword 'xyz' into field. Choices are: authors, featured, id [misc]
15071528
installed_apps:
15081529
- myapp
15091530
files:
15101531
- path: myapp/__init__.py
15111532
- path: myapp/models.py
15121533
content: |
1534+
from typing_extensions import Self
15131535
from django.db import models
15141536
15151537
class Author(models.Model):
@@ -1518,3 +1540,12 @@
15181540
class Book(models.Model):
15191541
featured = models.BooleanField(default=False)
15201542
authors = models.ManyToManyField(Author)
1543+
1544+
class OtherQuerySet(models.QuerySet["Other"]):
1545+
def custom(self) -> Self: ...
1546+
1547+
OtherManager = models.Manager.from_queryset(OtherQuerySet)
1548+
class Other(models.Model):
1549+
featured = models.BooleanField()
1550+
authors = models.ManyToManyField(Author)
1551+
objects = OtherManager()

0 commit comments

Comments
 (0)