Skip to content

Commit 7df54c3

Browse files
committed
add support for BaseManager.from_queryset()
1 parent b8f2902 commit 7df54c3

File tree

5 files changed

+279
-93
lines changed

5 files changed

+279
-93
lines changed

mypy_django_plugin/lib/helpers.py

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
from collections import OrderedDict
22
from typing import (
3-
TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Set, Union, cast,
3+
TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union,
44
)
55

6-
from django.db.models.fields import Field
76
from django.db.models.fields.related import RelatedField
87
from django.db.models.fields.reverse_related import ForeignObjectRel
98
from mypy import checker
109
from mypy.checker import TypeChecker
1110
from mypy.mro import calculate_mro
1211
from mypy.nodes import (
13-
GDEF, MDEF, Block, ClassDef, Expression, MemberExpr, MypyFile, NameExpr, StrExpr, SymbolNode, SymbolTable,
14-
SymbolTableNode, TypeInfo, Var,
12+
GDEF, MDEF, Argument, Block, ClassDef, Expression, FuncDef, MemberExpr, MypyFile, NameExpr, StrExpr, SymbolNode,
13+
SymbolTable, SymbolTableNode, TypeInfo, Var,
1514
)
1615
from mypy.plugin import (
17-
AttributeContext, CheckerPluginInterface, FunctionContext, MethodContext,
16+
AttributeContext, CheckerPluginInterface, ClassDefContext, DynamicClassDefContext, FunctionContext, MethodContext,
1817
)
19-
from mypy.types import AnyType, Instance, NoneTyp, TupleType
18+
from mypy.plugins.common import add_method
19+
from mypy.semanal import SemanticAnalyzer
20+
from mypy.types import AnyType, CallableType, Instance, NoneTyp, TupleType
2021
from mypy.types import Type as MypyType
2122
from mypy.types import TypedDictType, TypeOfAny, UnionType
2223

24+
from django.db.models.fields import Field
2325
from mypy_django_plugin.lib import fullnames
2426

2527
if TYPE_CHECKING:
@@ -55,7 +57,7 @@ def lookup_fully_qualified_generic(name: str, all_modules: Dict[str, MypyFile])
5557
return sym.node
5658

5759

58-
def lookup_fully_qualified_typeinfo(api: TypeChecker, fullname: str) -> Optional[TypeInfo]:
60+
def lookup_fully_qualified_typeinfo(api: Union[TypeChecker, SemanticAnalyzer], fullname: str) -> Optional[TypeInfo]:
5961
node = lookup_fully_qualified_generic(fullname, api.modules)
6062
if not isinstance(node, TypeInfo):
6163
return None
@@ -173,8 +175,11 @@ def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]
173175
return None
174176

175177

176-
def add_new_class_for_module(module: MypyFile, name: str, bases: List[Instance],
177-
fields: 'OrderedDict[str, MypyType]') -> TypeInfo:
178+
def add_new_class_for_module(module: MypyFile,
179+
name: str,
180+
bases: List[Instance],
181+
fields: Optional[Dict[str, MypyType]] = None
182+
) -> TypeInfo:
178183
new_class_unique_name = checker.gen_unique_name(name, module.names)
179184

180185
# make new class expression
@@ -188,11 +193,12 @@ def add_new_class_for_module(module: MypyFile, name: str, bases: List[Instance],
188193
new_typeinfo.calculate_metaclass_type()
189194

190195
# add fields
191-
for field_name, field_type in fields.items():
192-
var = Var(field_name, type=field_type)
193-
var.info = new_typeinfo
194-
var._fullname = new_typeinfo.fullname + '.' + field_name
195-
new_typeinfo.names[field_name] = SymbolTableNode(MDEF, var, plugin_generated=True)
196+
if fields:
197+
for field_name, field_type in fields.items():
198+
var = Var(field_name, type=field_type)
199+
var.info = new_typeinfo
200+
var._fullname = new_typeinfo.fullname + '.' + field_name
201+
new_typeinfo.names[field_name] = SymbolTableNode(MDEF, var, plugin_generated=True)
196202

197203
classdef.info = new_typeinfo
198204
module.names[new_class_unique_name] = SymbolTableNode(GDEF, new_typeinfo, plugin_generated=True)
@@ -269,10 +275,16 @@ def resolve_string_attribute_value(attr_expr: Expression, ctx: Union[FunctionCon
269275
return None
270276

271277

278+
def get_semanal_api(ctx: Union[ClassDefContext, DynamicClassDefContext]) -> SemanticAnalyzer:
279+
if not isinstance(ctx.api, SemanticAnalyzer):
280+
raise ValueError('Not a SemanticAnalyzer')
281+
return ctx.api
282+
283+
272284
def get_typechecker_api(ctx: Union[AttributeContext, MethodContext, FunctionContext]) -> TypeChecker:
273285
if not isinstance(ctx.api, TypeChecker):
274286
raise ValueError('Not a TypeChecker')
275-
return cast(TypeChecker, ctx.api)
287+
return ctx.api
276288

277289

278290
def is_model_subclass_info(info: TypeInfo, django_context: 'DjangoContext') -> bool:
@@ -298,3 +310,28 @@ def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType) -> No
298310
var.is_inferred = True
299311
info.names[name] = SymbolTableNode(MDEF, var,
300312
plugin_generated=True)
313+
314+
315+
def _prepare_new_method_arguments(node: FuncDef) -> Tuple[List[Argument], MypyType]:
316+
arguments = []
317+
for argument in node.arguments[1:]:
318+
if argument.type_annotation is None:
319+
argument.type_annotation = AnyType(TypeOfAny.unannotated)
320+
arguments.append(argument)
321+
322+
if isinstance(node.type, CallableType):
323+
return_type = node.type.ret_type
324+
else:
325+
return_type = AnyType(TypeOfAny.unannotated)
326+
327+
return arguments, return_type
328+
329+
330+
def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance,
331+
new_method_name: str, method_node: FuncDef) -> None:
332+
arguments, return_type = _prepare_new_method_arguments(method_node)
333+
add_method(ctx,
334+
new_method_name,
335+
args=arguments,
336+
return_type=return_type,
337+
self_type=self_type)

mypy_django_plugin/main.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from mypy.nodes import MypyFile, TypeInfo
88
from mypy.options import Options
99
from mypy.plugin import (
10-
AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin,
10+
AttributeContext, ClassDefContext, DynamicClassDefContext, FunctionContext, MethodContext, Plugin,
1111
)
1212
from mypy.types import Type as MypyType
1313

@@ -17,6 +17,9 @@
1717
from mypy_django_plugin.transformers import (
1818
fields, forms, init_create, meta, querysets, request, settings,
1919
)
20+
from mypy_django_plugin.transformers.managers import (
21+
create_new_manager_class_from_from_queryset_method,
22+
)
2023
from mypy_django_plugin.transformers.models import process_model_class
2124

2225

@@ -242,6 +245,16 @@ def get_attribute_hook(self, fullname: str
242245
return partial(request.set_auth_user_model_as_type_for_request_user, django_context=self.django_context)
243246
return None
244247

248+
def get_dynamic_class_hook(self, fullname: str
249+
) -> Optional[Callable[[DynamicClassDefContext], None]]:
250+
if fullname.endswith('from_queryset'):
251+
class_name, _, _ = fullname.rpartition('.')
252+
info = self._get_typeinfo_or_none(class_name)
253+
if info and info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME):
254+
return partial(create_new_manager_class_from_from_queryset_method,
255+
django_context=self.django_context)
256+
return None
257+
245258

246259
def plugin(version):
247260
return NewSemanalDjangoPlugin
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from mypy.nodes import (
2+
GDEF, FuncDef, MemberExpr, NameExpr, SymbolTableNode, TypeInfo,
3+
)
4+
from mypy.plugin import ClassDefContext, DynamicClassDefContext
5+
from mypy.types import AnyType, Instance, TypeOfAny
6+
7+
from mypy_django_plugin.django.context import DjangoContext
8+
from mypy_django_plugin.lib import helpers
9+
10+
11+
def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefContext,
12+
django_context: DjangoContext) -> None:
13+
semanal_api = helpers.get_semanal_api(ctx)
14+
# current_module = api.cur_mod_node
15+
# remove variable with name
16+
# if ctx.name in current_module.names:
17+
# del current_module.names[ctx.name]
18+
19+
assert isinstance(ctx.call.callee, MemberExpr)
20+
assert isinstance(ctx.call.callee.expr, NameExpr)
21+
base_manager_info = ctx.call.callee.expr.node
22+
if base_manager_info is None:
23+
if not semanal_api.final_iteration:
24+
semanal_api.defer()
25+
return
26+
27+
assert isinstance(base_manager_info, TypeInfo)
28+
new_manager_info = semanal_api.basic_new_typeinfo(ctx.name,
29+
basetype_or_fallback=Instance(base_manager_info,
30+
[AnyType(TypeOfAny.unannotated)]))
31+
new_manager_info.line = ctx.call.line
32+
new_manager_info.defn.line = ctx.call.line
33+
new_manager_info.metaclass_type = new_manager_info.calculate_metaclass_type()
34+
35+
current_module = semanal_api.cur_mod_node
36+
current_module.names[ctx.name] = SymbolTableNode(GDEF, new_manager_info,
37+
plugin_generated=True)
38+
passed_queryset = ctx.call.args[0]
39+
assert isinstance(passed_queryset, NameExpr)
40+
41+
derived_queryset_fullname = passed_queryset.fullname
42+
sym = semanal_api.lookup_fully_qualified_or_none(derived_queryset_fullname)
43+
assert sym is not None
44+
if sym.node is None:
45+
if not semanal_api.final_iteration:
46+
semanal_api.defer()
47+
else:
48+
# inherit from Any to prevent false-positives, if queryset class cannot be resolved
49+
new_manager_info.fallback_to_any = True
50+
return
51+
52+
derived_queryset_info = sym.node
53+
assert isinstance(derived_queryset_info, TypeInfo)
54+
55+
if len(ctx.call.args) > 1:
56+
custom_manager_generated_name = ctx.call.args[1].value
57+
else:
58+
custom_manager_generated_name = base_manager_info.name + 'From' + derived_queryset_info.name
59+
60+
custom_manager_generated_fullname = '.'.join(['django.db.models.manager', custom_manager_generated_name])
61+
if 'from_queryset_managers' not in base_manager_info.metadata:
62+
base_manager_info.metadata['from_queryset_managers'] = {}
63+
base_manager_info.metadata['from_queryset_managers'][custom_manager_generated_fullname] = new_manager_info.fullname
64+
65+
class_def_context = ClassDefContext(cls=new_manager_info.defn,
66+
reason=ctx.call, api=semanal_api)
67+
self_type = Instance(new_manager_info, [])
68+
for name, sym in derived_queryset_info.names.items():
69+
if isinstance(sym.node, FuncDef):
70+
helpers.copy_method_to_another_class(class_def_context,
71+
self_type,
72+
new_method_name=name,
73+
method_node=sym.node)

0 commit comments

Comments
 (0)