Skip to content

Commit 197f0e3

Browse files
authored
Fix Self typed custom queryset methods incompatible with base queryset type (#1840) (#1852)
1 parent bfa4590 commit 197f0e3

File tree

3 files changed

+57
-20
lines changed

3 files changed

+57
-20
lines changed

mypy_django_plugin/transformers/managers.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,21 @@ def _process_dynamic_method(
109109
variables = method_type.variables
110110
ret_type = method_type.ret_type
111111

112+
if not is_fallback_queryset:
113+
queryset_instance = Instance(queryset_info, manager_instance.args)
114+
else:
115+
# The fallback queryset inherits _QuerySet, which has two generics
116+
# instead of the one exposed on QuerySet. That means that we need
117+
# to add the model twice. In real code it's not possible to inherit
118+
# from _QuerySet, as it doesn't exist at runtime, so this fix is
119+
# only needed for plugin-generated querysets.
120+
queryset_instance = Instance(queryset_info, [manager_instance.args[0], manager_instance.args[0]])
121+
112122
# For methods on the manager that return a queryset we need to override the
113123
# return type to be the actual queryset class, not the base QuerySet that's
114124
# used by the typing stubs.
115125
if method_name in MANAGER_METHODS_RETURNING_QUERYSET:
116-
if not is_fallback_queryset:
117-
ret_type = Instance(queryset_info, manager_instance.args)
118-
else:
119-
# The fallback queryset inherits _QuerySet, which has two generics
120-
# instead of the one exposed on QuerySet. That means that we need
121-
# to add the model twice. In real code it's not possible to inherit
122-
# from _QuerySet, as it doesn't exist at runtime, so this fix is
123-
# only needed for pluign-generated querysets.
124-
ret_type = Instance(queryset_info, [manager_instance.args[0], manager_instance.args[0]])
126+
ret_type = queryset_instance
125127
variables = []
126128
args_types = method_type.arg_types[1:]
127129
if _has_compatible_type_vars(base_that_has_method):
@@ -138,7 +140,7 @@ def _process_dynamic_method(
138140
]
139141
if base_that_has_method.self_type:
140142
# Manages -> Self returns
141-
ret_type = _replace_type_var(ret_type, base_that_has_method.self_type.fullname, manager_instance)
143+
ret_type = _replace_type_var(ret_type, base_that_has_method.self_type.fullname, queryset_instance)
142144

143145
# Drop any 'self' argument as our manager is already initialized
144146
return method_type.copy_modified(

tests/typecheck/managers/querysets/test_as_manager.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
- case: self_return_management
22
main: |
3-
from myapp.models import MyModel
4-
reveal_type(MyModel.objects.example_simple()) # N: Revealed type is "myapp.models.ManagerFromMyQuerySet[myapp.models.MyModel]"
5-
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "builtins.list[myapp.models.ManagerFromMyQuerySet[myapp.models.MyModel]]"
3+
from myapp.models import MyModel, MyModelWithoutSelf
4+
reveal_type(MyModel.objects.example_simple()) # N: Revealed type is "myapp.models.MyQuerySet[myapp.models.MyModel]"
5+
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "builtins.list[myapp.models.MyQuerySet[myapp.models.MyModel]]"
66
reveal_type(MyModel.objects.example_simple().just_int()) # N: Revealed type is "builtins.int"
7-
reveal_type(MyModel.objects.example_dict()) # N: Revealed type is "builtins.dict[builtins.str, myapp.models.ManagerFromMyQuerySet[myapp.models.MyModel]]"
7+
reveal_type(MyModel.objects.example_dict()) # N: Revealed type is "builtins.dict[builtins.str, myapp.models.MyQuerySet[myapp.models.MyModel]]"
8+
reveal_type(MyModelWithoutSelf.objects.method()) # N: Revealed type is "myapp.models.QuerySetWithoutSelf"
89
910
installed_apps:
1011
- myapp
@@ -26,6 +27,13 @@
2627
2728
class MyModel(models.Model):
2829
objects = MyQuerySet.as_manager()
30+
31+
class QuerySetWithoutSelf(models.QuerySet["MyModelWithoutSelf"]):
32+
def method(self) -> "QuerySetWithoutSelf":
33+
return self
34+
35+
class MyModelWithoutSelf(models.Model):
36+
objects = QuerySetWithoutSelf.as_manager()
2937
- case: declares_manager_type_like_django
3038
main: |
3139
from myapp.models import MyModel
@@ -192,6 +200,8 @@
192200
reveal_type(MyOtherModel.objects.dummy_override()) # N: Revealed type is "myapp.models.MyOtherModel"
193201
reveal_type(MyOtherModel.objects.example_mixin(MyOtherModel())) # N: Revealed type is "myapp.models.MyOtherModel"
194202
reveal_type(MyOtherModel.objects.example_other_mixin()) # N: Revealed type is "myapp.models.MyOtherModel"
203+
reveal_type(MyOtherModel.objects.test_self()) # N: Revealed type is "myapp.models._MyModelQuerySet2[myapp.models.MyOtherModel]"
204+
reveal_type(MyOtherModel.objects.test_sub_self()) # N: Revealed type is "myapp.models._MyModelQuerySet2[myapp.models.MyOtherModel]"
195205
installed_apps:
196206
- myapp
197207
files:
@@ -200,6 +210,7 @@
200210
content: |
201211
from typing import TypeVar, Generic
202212
from django.db import models
213+
from typing_extensions import Self
203214
204215
T = TypeVar("T", bound=models.Model)
205216
T_2 = TypeVar("T_2", bound=models.Model)
@@ -215,12 +226,14 @@
215226
def override(self) -> T: ...
216227
def override2(self) -> T: ...
217228
def dummy_override(self) -> int: ...
229+
def test_sub_self(self) -> Self: ...
218230
219231
class _MyModelQuerySet2(SomeMixin, _MyModelQuerySet[T_2]):
220232
def example_2(self) -> T_2: ...
221233
def override(self) -> T_2: ...
222234
def override2(self) -> T_2: ...
223235
def dummy_override(self) -> T_2: ... # type: ignore[override]
236+
def test_self(self) -> Self: ...
224237
225238
class MyModelQuerySet(_MyModelQuerySet2["MyModel"]):
226239
def override(self) -> "MyModel": ...

tests/typecheck/managers/querysets/test_from_queryset.yml

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
- case: from_queryset_self_return_management
22
main: |
3-
from myapp.models import MyModel
4-
reveal_type(MyModel.objects.example_simple()) # N: Revealed type is "myapp.models.BaseManagerFromModelQuerySet[myapp.models.MyModel]"
5-
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "builtins.list[myapp.models.BaseManagerFromModelQuerySet[myapp.models.MyModel]]"
3+
from myapp.models import MyModel, MyModelWithoutSelf
4+
reveal_type(MyModel.objects.example_simple()) # N: Revealed type is "myapp.models.MyQuerySet[myapp.models.MyModel]"
5+
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "builtins.list[myapp.models.MyQuerySet[myapp.models.MyModel]]"
6+
reveal_type(MyModel.objects.example_simple().just_int()) # N: Revealed type is "builtins.int"
7+
reveal_type(MyModel.objects.example_dict()) # N: Revealed type is "builtins.dict[builtins.str, myapp.models.MyQuerySet[myapp.models.MyModel]]"
8+
reveal_type(MyModel.objects.test_custom_manager()) # N: Revealed type is "myapp.models.CustomManagerFromMyQuerySet[myapp.models.MyModel]"
9+
reveal_type(MyModelWithoutSelf.objects.method()) # N: Revealed type is "myapp.models.QuerySetWithoutSelf"
610
installed_apps:
711
- myapp
812
files:
@@ -11,16 +15,34 @@
1115
content: |
1216
from django.db import models
1317
from django.db.models.manager import BaseManager
18+
from typing import List, Dict
1419
from typing_extensions import Self
15-
from typing import List
1620
17-
class ModelQuerySet(models.QuerySet):
21+
class CustomManager(BaseManager):
22+
def test_custom_manager(self) -> Self: ...
23+
24+
class BaseQuerySet(models.QuerySet):
25+
def example_dict(self) -> Dict[str, Self]: ...
26+
27+
class MyQuerySet(BaseQuerySet):
1828
def example_simple(self) -> Self: ...
1929
def example_list(self) -> List[Self]: ...
20-
NewManager = BaseManager.from_queryset(ModelQuerySet)
30+
def just_int(self) -> int: ...
31+
32+
NewManager = CustomManager.from_queryset(MyQuerySet)
33+
2134
class MyModel(models.Model):
2235
objects = NewManager()
2336
37+
class QuerySetWithoutSelf(models.QuerySet["MyModelWithoutSelf"]):
38+
def method(self) -> "QuerySetWithoutSelf":
39+
return self
40+
41+
ManagerWithoutSelf = BaseManager.from_queryset(QuerySetWithoutSelf)
42+
43+
class MyModelWithoutSelf(models.Model):
44+
objects = ManagerWithoutSelf()
45+
2446
- case: from_queryset_with_base_manager
2547
main: |
2648
from myapp.models import MyModel

0 commit comments

Comments
 (0)