Skip to content

Commit 044739a

Browse files
authored
Determine the type of queryset methods on unions (#2027)
Prior to this if a type was a union of multiple django model managers then it wouldn't be able to determine the type of the queryset methods on them. This change will mean that in the case of a union, the type of the queryset method will be a union of the types of all the queryset methods on each item in the union
1 parent d9ef288 commit 044739a

File tree

5 files changed

+111
-2
lines changed

5 files changed

+111
-2
lines changed

mypy_django_plugin/transformers/managers.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,18 @@
1919
from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext
2020
from mypy.semanal import SemanticAnalyzer
2121
from mypy.semanal_shared import has_placeholder
22-
from mypy.types import AnyType, CallableType, FunctionLike, Instance, Overloaded, ProperType, TypeOfAny, TypeVarType
22+
from mypy.types import (
23+
AnyType,
24+
CallableType,
25+
FunctionLike,
26+
Instance,
27+
Overloaded,
28+
ProperType,
29+
TypeOfAny,
30+
TypeVarType,
31+
UnionType,
32+
get_proper_type,
33+
)
2334
from mypy.types import Type as MypyType
2435
from mypy.typevars import fill_typevars
2536

@@ -274,6 +285,13 @@ def resolve_manager_method(ctx: AttributeContext) -> MypyType:
274285

275286
if isinstance(ctx.type, Instance):
276287
return resolve_manager_method_from_instance(instance=ctx.type, method_name=method_name, ctx=ctx)
288+
elif isinstance(ctx.type, UnionType) and all(isinstance(instance, Instance) for instance in ctx.type.items):
289+
resolved = tuple(
290+
resolve_manager_method_from_instance(instance=instance, method_name=method_name, ctx=ctx)
291+
for instance in ctx.type.items
292+
if isinstance(instance, Instance)
293+
)
294+
return get_proper_type(UnionType(resolved))
277295
else:
278296
ctx.api.fail(f'Unable to resolve return type of queryset/manager method "{method_name}"', ctx.context)
279297
return AnyType(TypeOfAny.from_error)

tests/typecheck/managers/querysets/test_as_manager.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,44 @@
140140
class MyModel(models.Model):
141141
objects = ModelQuerySet.as_manager()
142142
143+
- case: includes_custom_queryset_methods_on_unions
144+
main: |
145+
from myapp.models import MyModel1, MyModel2
146+
import typing
147+
kls: typing.Type[typing.Union[MyModel1, MyModel2]] = MyModel1
148+
reveal_type(kls.objects.custom_queryset_method()) # N: Revealed type is "Union[myapp.models.ModelQuerySet1, myapp.models.ModelQuerySet2]"
149+
reveal_type(kls.objects.all().custom_queryset_method()) # N: Revealed type is "Union[myapp.models.ModelQuerySet1, myapp.models.ModelQuerySet2]"
150+
reveal_type(kls.objects.returns_thing()) # N: Revealed type is "Union[builtins.int, builtins.str]"
151+
reveal_type(kls.objects.get()) # N: Revealed type is "Union[myapp.models.MyModel1, myapp.models.MyModel2]"
152+
installed_apps:
153+
- myapp
154+
files:
155+
- path: myapp/__init__.py
156+
- path: myapp/models.py
157+
content: |
158+
from django.db import models
159+
from typing import Sequence
160+
161+
class ModelQuerySet1(models.QuerySet["MyModel1"]):
162+
def custom_queryset_method(self) -> "ModelQuerySet1":
163+
return self.all()
164+
165+
def returns_thing(self) -> int:
166+
return 1
167+
168+
class ModelQuerySet2(models.QuerySet["MyModel2"]):
169+
def custom_queryset_method(self) -> "ModelQuerySet2":
170+
return self.all()
171+
172+
def returns_thing(self) -> str:
173+
return "asdf"
174+
175+
class MyModel1(models.Model):
176+
objects = ModelQuerySet1.as_manager()
177+
178+
class MyModel2(models.Model):
179+
objects = ModelQuerySet2.as_manager()
180+
143181
- case: handles_call_outside_of_model_class_definition
144182
main: |
145183
from myapp.models import MyModel, MyModelManager

tests/typecheck/managers/querysets/test_basic_methods.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@
5959
class User(models.Model):
6060
pass
6161
62+
- case: queryset_method_of_union
63+
main: |
64+
from myapp.models import MyModel1, MyModel2
65+
import typing
66+
kls: typing.Type[typing.Union[MyModel1, MyModel2]] = MyModel1
67+
reveal_type(kls.objects) # N: Revealed type is "Union[django.db.models.manager.Manager[myapp.models.MyModel1], django.db.models.manager.Manager[myapp.models.MyModel2]]"
68+
reveal_type(kls.objects.all()) # N: Revealed type is "Union[django.db.models.query._QuerySet[myapp.models.MyModel1, myapp.models.MyModel1], django.db.models.query._QuerySet[myapp.models.MyModel2, myapp.models.MyModel2]]"
69+
reveal_type(kls.objects.get()) # N: Revealed type is "Union[myapp.models.MyModel1, myapp.models.MyModel2]"
70+
installed_apps:
71+
- myapp
72+
files:
73+
- path: myapp/__init__.py
74+
- path: myapp/models.py
75+
content: |
76+
from django.db import models
77+
class MyModel1(models.Model):
78+
pass
79+
class MyModel2(models.Model):
80+
pass
81+
6282
- case: select_related_returns_queryset
6383
main: |
6484
from myapp.models import Book

tests/typecheck/managers/querysets/test_from_queryset.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,39 @@
276276
class MyModel(models.Model):
277277
objects = NewManager()
278278
279+
- case: from_queryset_with_manager_of_union
280+
main: |
281+
from myapp.models import MyModel1, MyModel2
282+
import typing
283+
kls: typing.Type[typing.Union[MyModel1, MyModel2]] = MyModel1
284+
reveal_type(kls.objects) # N: Revealed type is "Union[myapp.models.ManagerFromModelQuerySet1[myapp.models.MyModel1], myapp.models.ManagerFromModelQuerySet2[myapp.models.MyModel2]]"
285+
reveal_type(kls.objects.all()) # N: Revealed type is "Union[myapp.models.ModelQuerySet1[myapp.models.MyModel1], myapp.models.ModelQuerySet2[myapp.models.MyModel2]]"
286+
reveal_type(kls.objects.get()) # N: Revealed type is "Union[myapp.models.MyModel1, myapp.models.MyModel2]"
287+
reveal_type(kls.objects.queryset_method()) # N: Revealed type is "Union[builtins.int, builtins.str]"
288+
installed_apps:
289+
- myapp
290+
files:
291+
- path: myapp/__init__.py
292+
- path: myapp/models.py
293+
content: |
294+
from django.db import models
295+
296+
class ModelQuerySet1(models.QuerySet["MyModel1"]):
297+
def queryset_method(self) -> int:
298+
return 1
299+
300+
class ModelQuerySet2(models.QuerySet["MyModel2"]):
301+
def queryset_method(self) -> str:
302+
return 'hello'
303+
304+
NewManager1 = models.Manager.from_queryset(ModelQuerySet1)
305+
class MyModel1(models.Model):
306+
objects = NewManager1()
307+
308+
NewManager2 = models.Manager.from_queryset(ModelQuerySet2)
309+
class MyModel2(models.Model):
310+
objects = NewManager2()
311+
279312
- case: from_queryset_returns_intersection_of_manager_and_queryset
280313
main: |
281314
from myapp.models import MyModel, NewManager

tests/typecheck/managers/querysets/test_union_type.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
1111
reveal_type(model_cls) # N: Revealed type is "Union[Type[myapp.models.Order], Type[myapp.models.User]]"
1212
reveal_type(model_cls.objects) # N: Revealed type is "Union[myapp.models.ManagerFromMyQuerySet[myapp.models.Order], myapp.models.ManagerFromMyQuerySet[myapp.models.User]]"
13-
model_cls.objects.my_method() # E: Unable to resolve return type of queryset/manager method "my_method" [misc]
13+
reveal_type(model_cls.objects.my_method()) # N: Revealed type is "myapp.models.MyQuerySet"
1414
installed_apps:
1515
- myapp
1616
files:

0 commit comments

Comments
 (0)