Skip to content

Commit 9fa3404

Browse files
committed
fill QuerySet generics using the manager's model type
1 parent a28717d commit 9fa3404

File tree

3 files changed

+92
-49
lines changed

3 files changed

+92
-49
lines changed

mypy_django_plugin/transformers/managers.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext
2121
from mypy.semanal import SemanticAnalyzer
2222
from mypy.semanal_shared import has_placeholder
23+
from mypy.subtypes import find_member
2324
from mypy.types import (
2425
AnyType,
2526
CallableType,
@@ -28,6 +29,7 @@
2829
Overloaded,
2930
ProperType,
3031
TypeOfAny,
32+
TypeType,
3133
TypeVarType,
3234
UnionType,
3335
get_proper_type,
@@ -121,15 +123,11 @@ def _process_dynamic_method(
121123
variables = method_type.variables
122124
ret_type = method_type.ret_type
123125

124-
if not is_fallback_queryset:
125-
queryset_instance = Instance(queryset_info, manager_instance.args)
126-
else:
127-
# The fallback queryset inherits _QuerySet, which has two generics
128-
# instead of the one exposed on QuerySet. That means that we need
129-
# to add the model twice. In real code it's not possible to inherit
130-
# from _QuerySet, as it doesn't exist at runtime, so this fix is
131-
# only needed for plugin-generated querysets.
132-
queryset_instance = Instance(queryset_info, [manager_instance.args[0], manager_instance.args[0]])
126+
manager_model = find_member("model", manager_instance, manager_instance)
127+
assert isinstance(manager_model, TypeType), manager_model
128+
manager_model_type = manager_model.item
129+
130+
queryset_instance = Instance(queryset_info, (manager_model_type,) * len(queryset_info.type_vars))
133131

134132
# For methods on the manager that return a queryset we need to override the
135133
# return type to be the actual queryset class, not the base QuerySet that's

tests/typecheck/managers/querysets/test_as_manager.yml

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,21 @@
1414
- path: myapp/models.py
1515
content: |
1616
from django.db import models
17-
from typing import List, Dict
17+
from typing import List, Dict, TypeVar, ClassVar
1818
from typing_extensions import Self
1919
20-
class BaseQuerySet(models.QuerySet):
20+
M = TypeVar("M", bound=models.Model, covariant=True)
21+
22+
class BaseQuerySet(models.QuerySet[M]):
2123
def example_dict(self) -> Dict[str, Self]: ...
2224
23-
class MyQuerySet(BaseQuerySet):
25+
class MyQuerySet(BaseQuerySet[M]):
2426
def example_simple(self) -> Self: ...
2527
def example_list(self) -> List[Self]: ...
2628
def just_int(self) -> int: ...
2729
2830
class MyModel(models.Model):
29-
objects = MyQuerySet.as_manager()
31+
objects = MyQuerySet.as_manager() # type: ignore[var-annotated]
3032
3133
class QuerySetWithoutSelf(models.QuerySet["MyModelWithoutSelf"]):
3234
def method(self) -> "QuerySetWithoutSelf":
@@ -64,13 +66,16 @@
6466
- path: myapp/__init__.py
6567
- path: myapp/models.py
6668
content: |
69+
from typing import TypeVar
6770
from django.db import models
6871
69-
class MyQuerySet(models.QuerySet):
72+
M = TypeVar("M", bound=models.Model, covariant=True)
73+
74+
class MyQuerySet(models.QuerySet[M]):
7075
...
7176
7277
class MyModel(models.Model):
73-
objects = MyQuerySet.as_manager()
78+
objects = MyQuerySet.as_manager() # type: ignore[var-annotated]
7479
7580
- case: model_gets_generated_manager_as_default_manager
7681
main: |
@@ -183,7 +188,7 @@
183188
from myapp.models import MyModel, MyModelManager
184189
reveal_type(MyModelManager) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[Any]"
185190
reveal_type(MyModel.objects) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[myapp.models.MyModel]"
186-
reveal_type(MyModel.objects.all()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel]"
191+
reveal_type(MyModel.objects.all()) # N: Revealed type is "myapp.models.ModelQuerySet"
187192
installed_apps:
188193
- myapp
189194
files:
@@ -204,7 +209,7 @@
204209
from myapp.models import MyModel, ManagerFromModelQuerySet
205210
reveal_type(ManagerFromModelQuerySet) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet1[Any]"
206211
reveal_type(MyModel.objects) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet1[myapp.models.MyModel]"
207-
reveal_type(MyModel.objects.all()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel]"
212+
reveal_type(MyModel.objects.all()) # N: Revealed type is "myapp.models.ModelQuerySet"
208213
installed_apps:
209214
- myapp
210215
files:
@@ -346,8 +351,8 @@
346351
from myapp.models import MyModel
347352
reveal_type(MyModel.objects_1) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[myapp.models.MyModel]"
348353
reveal_type(MyModel.objects_2) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[myapp.models.MyModel]"
349-
reveal_type(MyModel.objects_1.all()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel]"
350-
reveal_type(MyModel.objects_2.all()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel]"
354+
reveal_type(MyModel.objects_1.all()) # N: Revealed type is "myapp.models.ModelQuerySet"
355+
reveal_type(MyModel.objects_2.all()) # N: Revealed type is "myapp.models.ModelQuerySet"
351356
installed_apps:
352357
- myapp
353358
files:

tests/typecheck/managers/querysets/test_from_queryset.yml

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@
1515
content: |
1616
from django.db import models
1717
from django.db.models.manager import BaseManager
18-
from typing import List, Dict
18+
from typing import List, Dict, TypeVar
1919
from typing_extensions import Self
2020
21-
class CustomManager(BaseManager):
21+
M = TypeVar("M", covariant=True, bound=models.Model)
22+
23+
class CustomManager(BaseManager[M]):
2224
def test_custom_manager(self) -> Self: ...
2325
24-
class BaseQuerySet(models.QuerySet):
26+
class BaseQuerySet(models.QuerySet[M]):
2527
def example_dict(self) -> Dict[str, Self]: ...
2628
27-
class MyQuerySet(BaseQuerySet):
29+
class MyQuerySet(BaseQuerySet[M]):
2830
def example_simple(self) -> Self: ...
2931
def example_list(self) -> List[Self]: ...
3032
def just_int(self) -> int: ...
@@ -82,10 +84,13 @@
8284
- path: myapp/__init__.py
8385
- path: myapp/models.py
8486
content: |
87+
from typing import TypeVar
8588
from django.db import models
8689
from django.db.models.manager import BaseManager
8790
88-
class ModelQuerySet(models.QuerySet):
91+
M = TypeVar("M", bound=models.Model, covariant=True)
92+
93+
class ModelQuerySet(models.QuerySet[M]):
8994
def queryset_method(self) -> str:
9095
return 'hello'
9196
NewManager = BaseManager.from_queryset(ModelQuerySet)
@@ -103,7 +108,7 @@
103108
reveal_type(MyModel.objects.queryset_method_3()) # N: Revealed type is "builtins.str"
104109
reveal_type(MyModel.objects.queryset_method_4([])) # N: Revealed type is "None"
105110
reveal_type(MyModel.objects.filter(id=1).queryset_method()) # N: Revealed type is "myapp.querysets.ModelQuerySet"
106-
reveal_type(MyModel.objects.filter(id=1)) # N: Revealed type is "myapp.querysets.ModelQuerySet[myapp.models.MyModel]"
111+
reveal_type(MyModel.objects.filter(id=1)) # N: Revealed type is "myapp.querysets.ModelQuerySet"
107112
installed_apps:
108113
- myapp
109114
files:
@@ -223,7 +228,7 @@
223228
reveal_type(MyModel.objects.queryset_method_3()) # N: Revealed type is "builtins.str"
224229
reveal_type(MyModel.objects.queryset_method_4([])) # N: Revealed type is "None"
225230
reveal_type(MyModel.objects.filter(id=1).queryset_method()) # N: Revealed type is "myapp.querysets.ModelQuerySet"
226-
reveal_type(MyModel.objects.filter(id=1)) # N: Revealed type is "myapp.querysets.ModelQuerySet[myapp.models.MyModel]"
231+
reveal_type(MyModel.objects.filter(id=1)) # N: Revealed type is "myapp.querysets.ModelQuerySet"
227232
installed_apps:
228233
- myapp
229234
files:
@@ -307,7 +312,7 @@
307312
import typing
308313
kls: typing.Type[typing.Union[MyModel1, MyModel2]] = MyModel1
309314
reveal_type(kls.objects) # N: Revealed type is "Union[myapp.models.ManagerFromModelQuerySet1[myapp.models.MyModel1], myapp.models.ManagerFromModelQuerySet2[myapp.models.MyModel2]]"
310-
reveal_type(kls.objects.all()) # N: Revealed type is "Union[myapp.models.ModelQuerySet1[myapp.models.MyModel1], myapp.models.ModelQuerySet2[myapp.models.MyModel2]]"
315+
reveal_type(kls.objects.all()) # N: Revealed type is "Union[myapp.models.ModelQuerySet1, myapp.models.ModelQuerySet2]"
311316
reveal_type(kls.objects.get()) # N: Revealed type is "Union[myapp.models.MyModel1, myapp.models.MyModel2]"
312317
reveal_type(kls.objects.queryset_method()) # N: Revealed type is "Union[builtins.int, builtins.str]"
313318
installed_apps:
@@ -580,9 +585,9 @@
580585
from myapp.models import MyModel
581586
reveal_type(MyModel.objects) # N: Revealed type is "myapp.models.MyManagerFromMyQuerySet[myapp.models.MyModel]"
582587
reveal_type(MyModel._default_manager) # N: Revealed type is "myapp.models.MyManagerFromMyQuerySet[myapp.models.MyModel]"
583-
reveal_type(MyModel.objects.all) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]"
588+
reveal_type(MyModel.objects.all) # N: Revealed type is "def () -> myapp.models.MyQuerySet"
584589
reveal_type(MyModel.objects.custom) # N: Revealed type is "def () -> myapp.models.MyQuerySet"
585-
reveal_type(MyModel.objects.all().filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
590+
reveal_type(MyModel.objects.all().filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet"
586591
reveal_type(MyModel.objects.custom().filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet"
587592
reveal_type(MyModel.objects2) # N: Revealed type is "myapp.models.MyManagerFromMyQuerySet[myapp.models.MyModel]"
588593
reveal_type(MyModel._default_manager) # N: Revealed type is "myapp.models.MyManagerFromMyQuerySet[myapp.models.MyModel]"
@@ -633,26 +638,26 @@
633638
- case: from_queryset_includes_methods_returning_queryset
634639
main: |
635640
from myapp.models import MyModel
636-
reveal_type(MyModel.objects.alias) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
637-
reveal_type(MyModel.objects.all) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]"
638-
reveal_type(MyModel.objects.annotate) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
639-
reveal_type(MyModel.objects.complex_filter) # N: Revealed type is "def (filter_obj: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
640-
reveal_type(MyModel.objects.defer) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
641-
reveal_type(MyModel.objects.difference) # N: Revealed type is "def (*other_qs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
642-
reveal_type(MyModel.objects.distinct) # N: Revealed type is "def (*field_names: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
643-
reveal_type(MyModel.objects.exclude) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
644-
reveal_type(MyModel.objects.extra) # N: Revealed type is "def (select: Union[builtins.dict[builtins.str, Any], None] =, where: Union[typing.Sequence[builtins.str], None] =, params: Union[typing.Sequence[Any], None] =, tables: Union[typing.Sequence[builtins.str], None] =, order_by: Union[typing.Sequence[builtins.str], None] =, select_params: Union[typing.Sequence[Any], None] =) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
645-
reveal_type(MyModel.objects.filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
646-
reveal_type(MyModel.objects.intersection) # N: Revealed type is "def (*other_qs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
647-
reveal_type(MyModel.objects.none) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]"
648-
reveal_type(MyModel.objects.only) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
649-
reveal_type(MyModel.objects.order_by) # N: Revealed type is "def (*field_names: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
650-
reveal_type(MyModel.objects.prefetch_related) # N: Revealed type is "def (*lookups: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
651-
reveal_type(MyModel.objects.reverse) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]"
652-
reveal_type(MyModel.objects.select_for_update) # N: Revealed type is "def (nowait: builtins.bool =, skip_locked: builtins.bool =, of: typing.Sequence[builtins.str] =, no_key: builtins.bool =) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
653-
reveal_type(MyModel.objects.select_related) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
654-
reveal_type(MyModel.objects.union) # N: Revealed type is "def (*other_qs: Any, all: builtins.bool =) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
655-
reveal_type(MyModel.objects.using) # N: Revealed type is "def (alias: Union[builtins.str, None]) -> myapp.models.MyQuerySet[myapp.models.MyModel]"
641+
reveal_type(MyModel.objects.alias) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet"
642+
reveal_type(MyModel.objects.all) # N: Revealed type is "def () -> myapp.models.MyQuerySet"
643+
reveal_type(MyModel.objects.annotate) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet"
644+
reveal_type(MyModel.objects.complex_filter) # N: Revealed type is "def (filter_obj: Any) -> myapp.models.MyQuerySet"
645+
reveal_type(MyModel.objects.defer) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet"
646+
reveal_type(MyModel.objects.difference) # N: Revealed type is "def (*other_qs: Any) -> myapp.models.MyQuerySet"
647+
reveal_type(MyModel.objects.distinct) # N: Revealed type is "def (*field_names: Any) -> myapp.models.MyQuerySet"
648+
reveal_type(MyModel.objects.exclude) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet"
649+
reveal_type(MyModel.objects.extra) # N: Revealed type is "def (select: Union[builtins.dict[builtins.str, Any], None] =, where: Union[typing.Sequence[builtins.str], None] =, params: Union[typing.Sequence[Any], None] =, tables: Union[typing.Sequence[builtins.str], None] =, order_by: Union[typing.Sequence[builtins.str], None] =, select_params: Union[typing.Sequence[Any], None] =) -> myapp.models.MyQuerySet"
650+
reveal_type(MyModel.objects.filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet"
651+
reveal_type(MyModel.objects.intersection) # N: Revealed type is "def (*other_qs: Any) -> myapp.models.MyQuerySet"
652+
reveal_type(MyModel.objects.none) # N: Revealed type is "def () -> myapp.models.MyQuerySet"
653+
reveal_type(MyModel.objects.only) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet"
654+
reveal_type(MyModel.objects.order_by) # N: Revealed type is "def (*field_names: Any) -> myapp.models.MyQuerySet"
655+
reveal_type(MyModel.objects.prefetch_related) # N: Revealed type is "def (*lookups: Any) -> myapp.models.MyQuerySet"
656+
reveal_type(MyModel.objects.reverse) # N: Revealed type is "def () -> myapp.models.MyQuerySet"
657+
reveal_type(MyModel.objects.select_for_update) # N: Revealed type is "def (nowait: builtins.bool =, skip_locked: builtins.bool =, of: typing.Sequence[builtins.str] =, no_key: builtins.bool =) -> myapp.models.MyQuerySet"
658+
reveal_type(MyModel.objects.select_related) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet"
659+
reveal_type(MyModel.objects.union) # N: Revealed type is "def (*other_qs: Any, all: builtins.bool =) -> myapp.models.MyQuerySet"
660+
reveal_type(MyModel.objects.using) # N: Revealed type is "def (alias: Union[builtins.str, None]) -> myapp.models.MyQuerySet"
656661
installed_apps:
657662
- myapp
658663
files:
@@ -882,6 +887,41 @@
882887
883888
class MCS(type): pass
884889
890+
- case: test_from_queryset_with_concrete_subclass
891+
main: |
892+
from myapp.models import Concrete
893+
reveal_type(Concrete.objects) # N: Revealed type is "myapp.models.ConcreteManager"
894+
reveal_type(Concrete.objects.get()) # N: Revealed type is "myapp.models.Concrete"
895+
reveal_type(Concrete.objects.all()) # N: Revealed type is "myapp.models.CustomQuerySet[myapp.models.Concrete, myapp.models.Concrete]"
896+
reveal_type(Concrete.objects.all().get()) # N: Revealed type is "myapp.models.Concrete"
897+
installed_apps:
898+
- myapp
899+
files:
900+
- path: myapp/__init__.py
901+
- path: myapp/models.py
902+
content: |
903+
from typing import ClassVar
904+
from typing_extensions import Self, TypeVar
905+
from django.db.models import Model, QuerySet
906+
from django.db.models.manager import Manager
907+
908+
M = TypeVar("M", bound=Model, covariant=True)
909+
D = TypeVar("D", covariant=True, default=M)
910+
911+
class CustomQuerySet(QuerySet[M, D]): pass
912+
913+
_base = Manager.from_queryset(CustomQuerySet)
914+
915+
class CustomBase(_base[M]): ...
916+
917+
class BaseModel(Model):
918+
objects: ClassVar[CustomBase[Self]] = CustomBase()
919+
920+
class ConcreteManager(CustomBase["Concrete"]): ...
921+
922+
class Concrete(BaseModel):
923+
objects: ClassVar[ConcreteManager] = ConcreteManager()
924+
885925
- case: test_queryset_arg_as_unsupported_expressions
886926
main: |
887927
from typing import Union, Generic, TypeVar

0 commit comments

Comments
 (0)