Skip to content

Commit ad71834

Browse files
committed
Implement support for <QuerySet>.as_manager()
1 parent 2a6f464 commit ad71834

File tree

8 files changed

+524
-118
lines changed

8 files changed

+524
-118
lines changed

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,7 @@ And then use `AuthenticatedHttpRequest` instead of the standard `HttpRequest` fo
140140

141141
### My QuerySet methods are returning Any rather than my Model
142142

143-
`QuerySet.as_manager()` is not currently supported.
144-
145-
If you are using `MyQuerySet.as_manager()`, then your `Manager`/`QuerySet` methods will all not be linked to your model.
143+
If you are using `MyQuerySet.as_manager()`:
146144

147145
Example:
148146

@@ -156,12 +154,12 @@ class MyModel(models.Model):
156154
bar = models.IntegerField()
157155
objects = MyModelQuerySet.as_manager()
158156

159-
def use_my_model():
160-
foo = MyModel.objects.get(id=1) # This is `Any` but it should be `MyModel`
161-
return foo.xyz # No error, but there should be
157+
def use_my_model() -> int:
158+
foo = MyModel.objects.get(id=1) # Should now be `MyModel`
159+
return foo.xyz # Gives an error
162160
```
163161

164-
There is a workaround: use `Manager.from_queryset` instead.
162+
Or if you're using `Manager.from_queryset`:
165163

166164
Example:
167165

@@ -177,11 +175,14 @@ class MyModel(models.Model):
177175
bar = models.IntegerField()
178176
objects = MyModelManager()
179177

180-
def use_my_model():
181-
foo = MyModel.objects.get(id=1)
178+
def use_my_model() -> int:
179+
foo = MyModel.objects.get(id=1) # Should now be `MyModel`
182180
return foo.xyz # Gives an error
183181
```
184182

183+
Take note that `.from_queryset` needs to be placed at _module level_ to annotate
184+
types correctly. Calling `.from_queryset` in class definition will yield an error.
185+
185186
### How do I annotate cases where I called QuerySet.annotate?
186187

187188
Django-stubs provides a special type, `django_stubs_ext.WithAnnotations[Model]`, which indicates that the `Model` has

mypy_django_plugin/lib/helpers.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -376,33 +376,31 @@ def bind_or_analyze_type(t: MypyType, api: SemanticAnalyzer, module_name: Option
376376

377377

378378
def copy_method_to_another_class(
379-
ctx: ClassDefContext,
379+
api: SemanticAnalyzer,
380+
cls: ClassDef,
380381
self_type: Instance,
381382
new_method_name: str,
382383
method_node: FuncDef,
383384
return_type: Optional[MypyType] = None,
384385
original_module_name: Optional[str] = None,
385386
) -> None:
386-
semanal_api = get_semanal_api(ctx)
387387
if method_node.type is None:
388-
if not semanal_api.final_iteration:
389-
semanal_api.defer()
388+
if not api.final_iteration:
389+
api.defer()
390390
return
391391

392392
arguments, return_type = build_unannotated_method_args(method_node)
393-
add_method_to_class(
394-
semanal_api, ctx.cls, new_method_name, args=arguments, return_type=return_type, self_type=self_type
395-
)
393+
add_method_to_class(api, cls, new_method_name, args=arguments, return_type=return_type, self_type=self_type)
396394
return
397395

398396
method_type = method_node.type
399397
if not isinstance(method_type, CallableType):
400-
if not semanal_api.final_iteration:
401-
semanal_api.defer()
398+
if not api.final_iteration:
399+
api.defer()
402400
return
403401

404402
if return_type is None:
405-
return_type = bind_or_analyze_type(method_type.ret_type, semanal_api, original_module_name)
403+
return_type = bind_or_analyze_type(method_type.ret_type, api, original_module_name)
406404
if return_type is None:
407405
return
408406

@@ -415,7 +413,7 @@ def copy_method_to_another_class(
415413
zip(method_type.arg_types[1:], method_type.arg_kinds[1:], method_type.arg_names[1:]),
416414
start=1,
417415
):
418-
bound_arg_type = bind_or_analyze_type(arg_type, semanal_api, original_module_name)
416+
bound_arg_type = bind_or_analyze_type(arg_type, api, original_module_name)
419417
if bound_arg_type is None:
420418
return
421419
if arg_name is None and hasattr(method_node, "arguments"):
@@ -431,9 +429,7 @@ def copy_method_to_another_class(
431429
)
432430
)
433431

434-
add_method_to_class(
435-
semanal_api, ctx.cls, new_method_name, args=arguments, return_type=return_type, self_type=self_type
436-
)
432+
add_method_to_class(api, cls, new_method_name, args=arguments, return_type=return_type, self_type=self_type)
437433

438434

439435
def add_new_manager_base(api: SemanticAnalyzerPluginInterface, fullname: str) -> None:

mypy_django_plugin/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from mypy_django_plugin.lib import fullnames, helpers
2424
from mypy_django_plugin.transformers import fields, forms, init_create, meta, querysets, request, settings
2525
from mypy_django_plugin.transformers.managers import (
26+
create_new_manager_class_from_as_manager_method,
2627
create_new_manager_class_from_from_queryset_method,
2728
fail_if_manager_type_created_in_model_body,
2829
resolve_manager_method,
@@ -303,11 +304,16 @@ def get_type_analyze_hook(self, fullname: str) -> Optional[Callable[[AnalyzeType
303304

304305
def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicClassDefContext], None]]:
305306
# Create a new manager class definition when a manager's '.from_queryset' classmethod is called
306-
if fullname.endswith("from_queryset"):
307+
if fullname.endswith(".from_queryset"):
307308
class_name, _, _ = fullname.rpartition(".")
308309
info = self._get_typeinfo_or_none(class_name)
309310
if info and info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME):
310311
return create_new_manager_class_from_from_queryset_method
312+
elif fullname.endswith(".as_manager"):
313+
class_name, _, _ = fullname.rpartition(".")
314+
info = self._get_typeinfo_or_none(class_name)
315+
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
316+
return create_new_manager_class_from_as_manager_method
311317
return None
312318

313319

0 commit comments

Comments
 (0)