Skip to content

Commit 59ebe6f

Browse files
authored
fill QuerySet generics using the manager's model type (#2281)
1 parent a28717d commit 59ebe6f

File tree

4 files changed

+119
-66
lines changed

4 files changed

+119
-66
lines changed

mypy_django_plugin/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
)
3838
from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute
3939
from mypy_django_plugin.transformers.managers import (
40+
construct_as_manager_instance,
4041
create_new_manager_class_from_as_manager_method,
4142
create_new_manager_class_from_from_queryset_method,
4243
reparametrize_any_manager_hook,
@@ -208,6 +209,10 @@ def get_method_hook(self, fullname: str) -> Optional[Callable[[MethodContext], M
208209
fullnames.REVERSE_MANY_TO_ONE_DESCRIPTOR: manytoone.refine_many_to_one_related_manager,
209210
}
210211
return hooks.get(class_fullname)
212+
elif method_name == "as_manager":
213+
info = self._get_typeinfo_or_none(class_fullname)
214+
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
215+
return partial(construct_as_manager_instance, info=info)
211216

212217
if method_name in self.manager_and_queryset_method_hooks:
213218
info = self._get_typeinfo_or_none(class_fullname)

mypy_django_plugin/transformers/managers.py

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
StrExpr,
1616
SymbolTableNode,
1717
TypeInfo,
18-
Var,
1918
)
20-
from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext
19+
from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext, MethodContext
2120
from mypy.semanal import SemanticAnalyzer
2221
from mypy.semanal_shared import has_placeholder
22+
from mypy.subtypes import find_member
2323
from mypy.types import (
2424
AnyType,
2525
CallableType,
@@ -28,6 +28,7 @@
2828
Overloaded,
2929
ProperType,
3030
TypeOfAny,
31+
TypeType,
3132
TypeVarType,
3233
UnionType,
3334
get_proper_type,
@@ -121,15 +122,11 @@ def _process_dynamic_method(
121122
variables = method_type.variables
122123
ret_type = method_type.ret_type
123124

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]])
125+
manager_model = find_member("model", manager_instance, manager_instance)
126+
assert isinstance(manager_model, TypeType), manager_model
127+
manager_model_type = manager_model.item
128+
129+
queryset_instance = Instance(queryset_info, (manager_model_type,) * len(queryset_info.type_vars))
133130

134131
# For methods on the manager that return a queryset we need to override the
135132
# return type to be the actual queryset class, not the base QuerySet that's
@@ -554,23 +551,9 @@ def create_new_manager_class_from_as_manager_method(ctx: DynamicClassDefContext)
554551
manager_name=manager_class_name,
555552
manager_base=manager_base,
556553
)
554+
queryset_info.metadata.setdefault("django_as_manager_names", {})
555+
queryset_info.metadata["django_as_manager_names"][semanal_api.cur_mod_id] = new_manager_info.name
557556

558-
# Whenever `<QuerySet>.as_manager()` isn't called at class level, we want to ensure
559-
# that the variable is an instance of our generated manager. Instead of the return
560-
# value of `.as_manager()`. Though model argument is populated as `Any`.
561-
# `transformers.models.AddManagers` will populate a model's manager(s), when it
562-
# finds it on class level.
563-
var = Var(name=ctx.name, type=Instance(new_manager_info, [AnyType(TypeOfAny.from_omitted_generics)]))
564-
var.info = new_manager_info
565-
var._fullname = f"{current_module.fullname}.{ctx.name}"
566-
var.is_inferred = True
567-
# Note: Order of `add_symbol_table_node` calls matters. Depending on what level
568-
# we've found the `.as_manager()` call. Point here being that we want to replace the
569-
# `.as_manager` return value with our newly created manager.
570-
added = semanal_api.add_symbol_table_node(
571-
ctx.name, SymbolTableNode(semanal_api.current_symbol_kind(), var, plugin_generated=True)
572-
)
573-
assert added
574557
# Add the new manager to the current module
575558
added = semanal_api.add_symbol_table_node(
576559
# We'll use `new_manager_info.name` instead of `manager_class_name` here
@@ -582,6 +565,26 @@ def create_new_manager_class_from_as_manager_method(ctx: DynamicClassDefContext)
582565
assert added
583566

584567

568+
def construct_as_manager_instance(ctx: MethodContext, *, info: TypeInfo) -> MypyType:
569+
api = helpers.get_typechecker_api(ctx)
570+
module = helpers.get_current_module(api)
571+
try:
572+
manager_name = info.metadata["django_as_manager_names"][module.fullname]
573+
except KeyError:
574+
return ctx.default_return_type
575+
576+
manager_node = api.lookup(manager_name)
577+
if not isinstance(manager_node.node, TypeInfo):
578+
return ctx.default_return_type
579+
580+
# Whenever `<QuerySet>.as_manager()` isn't called at class level, we want to ensure
581+
# that the variable is an instance of our generated manager. Instead of the return
582+
# value of `.as_manager()`. Though model argument is populated as `Any`.
583+
# `transformers.models.AddManagers` will populate a model's manager(s), when it
584+
# finds it on class level.
585+
return Instance(manager_node.node, [AnyType(TypeOfAny.from_omitted_generics)])
586+
587+
585588
def reparametrize_any_manager_hook(ctx: ClassDefContext) -> None:
586589
"""
587590
Add implicit generics to manager classes that are defined without generic.

tests/typecheck/managers/querysets/test_as_manager.yml

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
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: ...
@@ -64,9 +66,12 @@
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):
@@ -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:
@@ -280,7 +285,7 @@
280285
objects = MyModelQuerySet.as_manager()
281286
282287
class MyOtherModel(models.Model):
283-
objects = _MyModelQuerySet2.as_manager() # type: ignore
288+
objects = _MyModelQuerySet2.as_manager()
284289
285290
- case: handles_type_vars
286291
main: |
@@ -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:

0 commit comments

Comments
 (0)