Skip to content

Commit ade48b6

Browse files
authored
Add support for BaseManager.from_queryset() (#251)
* add support for BaseManager.from_queryset() * cleanups * lint fixes
1 parent b8f2902 commit ade48b6

File tree

5 files changed

+275
-93
lines changed

5 files changed

+275
-93
lines changed

mypy_django_plugin/lib/helpers.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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

66
from django.db.models.fields import Field
@@ -10,13 +10,15 @@
1010
from mypy.checker import TypeChecker
1111
from mypy.mro import calculate_mro
1212
from mypy.nodes import (
13-
GDEF, MDEF, Block, ClassDef, Expression, MemberExpr, MypyFile, NameExpr, StrExpr, SymbolNode, SymbolTable,
14-
SymbolTableNode, TypeInfo, Var,
13+
GDEF, MDEF, Argument, Block, ClassDef, Expression, FuncDef, MemberExpr, MypyFile, NameExpr, StrExpr, SymbolNode,
14+
SymbolTable, SymbolTableNode, TypeInfo, Var,
1515
)
1616
from mypy.plugin import (
17-
AttributeContext, CheckerPluginInterface, FunctionContext, MethodContext,
17+
AttributeContext, CheckerPluginInterface, ClassDefContext, DynamicClassDefContext, FunctionContext, MethodContext,
1818
)
19-
from mypy.types import AnyType, Instance, NoneTyp, TupleType
19+
from mypy.plugins.common import add_method
20+
from mypy.semanal import SemanticAnalyzer
21+
from mypy.types import AnyType, CallableType, Instance, NoneTyp, TupleType
2022
from mypy.types import Type as MypyType
2123
from mypy.types import TypedDictType, TypeOfAny, UnionType
2224

@@ -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: 13 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,15 @@ 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 create_new_manager_class_from_from_queryset_method
255+
return None
256+
245257

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

0 commit comments

Comments
 (0)