-
-
Notifications
You must be signed in to change notification settings - Fork 487
New semanal-progress rebased on master #445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kszmigiel
wants to merge
76
commits into
typeddjango:master
Choose a base branch
from
kszmigiel:feature/plugin_cleanup
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,527
−1,169
Open
Changes from all commits
Commits
Show all changes
76 commits
Select commit
Hold shift + click to select a range
77daf90
refactor, fix method copying
mkurnikov db925ac
QuerySet.as_manager() support
mkurnikov 74ccc4d
allow manager classes nested inside model classes
mkurnikov f58cbea
lint fixes
mkurnikov 4bf5ec9
fix tests
mkurnikov 585273a
add two new as_manager tests
mkurnikov c0f41f3
wip
mkurnikov e9bdc50
wip
mkurnikov b51ec9d
remove as_manager support
mkurnikov 7d004ff
remove generics support for tests
mkurnikov 13cec79
remove as_manager/from_queryset leftovers and fix tests
mkurnikov c8641fb
remove files
mkurnikov 05e0f40
rebase
mkurnikov 4749d15
proper rebase
mkurnikov 59ff90e
FormCallback
mkurnikov 5093939
get_user_model support for new callbacks api
mkurnikov c0e5c46
lints
mkurnikov 56515da
isort
mkurnikov bf48ef2
remove one more irrelevant file
mkurnikov 196179b
disable test suites checking in CI
mkurnikov 59ce2ed
remove transformers/models
mkurnikov b4d8df1
more changes to new API
mkurnikov 774c14f
remove unused import
mkurnikov 4af54e7
remove some dead code
mkurnikov cca106d
lints
mkurnikov bd325e0
replace field callback with new API
mkurnikov fc60ceb
move meta to new API
mkurnikov 57ee7c9
move orm_lookups to new API
mkurnikov 43f488b
move init_create to new API
mkurnikov 6debf6d
move querysets to new API
mkurnikov 9e0f438
transformers2 -> transformers
mkurnikov d67a117
remove a bunch of dead code
mkurnikov a9fa3c0
add callback class for from_queryset()
04345bb
helpers
3b3f5e1
typos
c731711
method for copying methods
627fe61
cleanp after 1st CR
040c0d9
cleanup after 2nd CR, exceptions instead of returns
86ed790
fallback to Any if can not resolve from_queryset Manager
e8f1f19
implemented AddManagers.run_with_model_cls() from master
c3ee1a5
tests for from_queryset
deee98c
remove print
0c8735f
flak8 cleanup
707569b
reverted to old way of creating typeinfo, probably refactor needed on…
78aa41f
fix 2 of failing tests
2b75646
fix 3rd test
5a1aff7
fix the last test
0890d29
isort
3d1014a
fix first mypy plugin code error
e6cef08
another try to fix mypy for plugin code errors
f64932d
fix isort
818db9b
added type ignore comments in places where mypy doesnt resolve type c…
3a0c9a4
flake8
f048a28
error message formatted properly
8390de3
unused type ignore comment
e6d91ef
use of cast instead of type ignore
19b6eb6
fix issue #438 manager method is forward reference
51b021f
typo in test
c5f4256
fixed condition for defering
50fe47a
assert instead of return
3cad5e3
uncommented tests
kszmigiel 20ef55e
isort
kszmigiel 61a330c
fix test (note assertion on HttpRequest.user)
kszmigiel ab07ffc
isort and test revert
kszmigiel 1ae0d85
fix for bound_arg_type
kszmigiel c54eced
django typecheck fix (?)
kszmigiel 9b42185
fix typecheck test suite WIP
kszmigiel d92ea39
WIP django test suite bugfixing
kszmigiel 998cc56
remove comment to check tests
kszmigiel 125aa64
fix running tests, flak8
kszmigiel 6691967
add ignore error
kszmigiel 4d051e3
SmallAutoField stub
kszmigiel b4bf1d4
MemberExpr NameExpr return default
kszmigiel 6d034bd
fix error_context type
kszmigiel 4c3e588
add error ignore for test model inharitence
kszmigiel da09a4c
ignore error in test_abstract_inheritance
kszmigiel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
from typing import Dict, List, Optional, Set, Union | ||
|
||
from mypy import checker | ||
from mypy.checker import TypeChecker | ||
from mypy.mro import calculate_mro | ||
from mypy.nodes import ( | ||
GDEF, MDEF, Block, ClassDef, Expression, MypyFile, SymbolTable, SymbolTableNode, TypeInfo, Var, | ||
) | ||
from mypy.plugin import ( | ||
AttributeContext, CheckerPluginInterface, FunctionContext, MethodContext, | ||
) | ||
from mypy.types import AnyType, Instance, TupleType | ||
from mypy.types import Type as MypyType | ||
from mypy.types import TypedDictType, TypeOfAny | ||
|
||
from mypy_django_plugin.lib import helpers | ||
|
||
|
||
def add_new_class_for_current_module(current_module: MypyFile, | ||
name: str, | ||
bases: List[Instance], | ||
fields: Optional[Dict[str, MypyType]] = None | ||
) -> TypeInfo: | ||
new_class_unique_name = checker.gen_unique_name(name, current_module.names) | ||
|
||
# make new class expression | ||
classdef = ClassDef(new_class_unique_name, Block([])) | ||
classdef.fullname = current_module.fullname + '.' + new_class_unique_name | ||
|
||
# make new TypeInfo | ||
new_typeinfo = TypeInfo(SymbolTable(), classdef, current_module.fullname) | ||
new_typeinfo.bases = bases | ||
calculate_mro(new_typeinfo) | ||
new_typeinfo.calculate_metaclass_type() | ||
|
||
# add fields | ||
if fields: | ||
for field_name, field_type in fields.items(): | ||
var = Var(field_name, type=field_type) | ||
var.info = new_typeinfo | ||
var._fullname = new_typeinfo.fullname + '.' + field_name | ||
new_typeinfo.names[field_name] = SymbolTableNode(MDEF, var, plugin_generated=True) | ||
|
||
classdef.info = new_typeinfo | ||
current_module.names[new_class_unique_name] = SymbolTableNode(GDEF, new_typeinfo, plugin_generated=True) | ||
return new_typeinfo | ||
|
||
|
||
def make_oneoff_named_tuple(api: TypeChecker, name: str, fields: 'Dict[str, MypyType]') -> TupleType: | ||
current_module = helpers.get_current_module(api) | ||
namedtuple_info = add_new_class_for_current_module(current_module, name, | ||
bases=[api.named_generic_type('typing.NamedTuple', [])], | ||
fields=fields) | ||
return TupleType(list(fields.values()), fallback=Instance(namedtuple_info, [])) | ||
|
||
|
||
def make_tuple(api: 'TypeChecker', fields: List[MypyType]) -> TupleType: | ||
# fallback for tuples is any builtins.tuple instance | ||
fallback = api.named_generic_type('builtins.tuple', | ||
[AnyType(TypeOfAny.special_form)]) | ||
return TupleType(fields, fallback=fallback) | ||
|
||
|
||
def make_oneoff_typeddict(api: CheckerPluginInterface, fields: 'Dict[str, MypyType]', | ||
required_keys: Set[str]) -> TypedDictType: | ||
object_type = api.named_generic_type('mypy_extensions._TypedDict', []) | ||
typed_dict_type = TypedDictType(fields, # type: ignore | ||
required_keys=required_keys, | ||
fallback=object_type) | ||
return typed_dict_type | ||
|
||
|
||
def get_typechecker_api(ctx: Union[AttributeContext, MethodContext, FunctionContext]) -> TypeChecker: | ||
if not isinstance(ctx.api, TypeChecker): | ||
raise ValueError('Not a TypeChecker') | ||
return ctx.api | ||
|
||
|
||
def check_types_compatible(ctx: Union[FunctionContext, MethodContext], | ||
*, expected_type: MypyType, actual_type: MypyType, error_message: str) -> None: | ||
api = get_typechecker_api(ctx) | ||
api.check_subtype(actual_type, expected_type, | ||
ctx.context, error_message, | ||
'got', 'expected') | ||
|
||
|
||
def get_call_argument_by_name(ctx: Union[FunctionContext, MethodContext], name: str) -> Optional[Expression]: | ||
""" | ||
Return the expression for the specific argument. | ||
This helper should only be used with non-star arguments. | ||
""" | ||
if name not in ctx.callee_arg_names: | ||
return None | ||
idx = ctx.callee_arg_names.index(name) | ||
args = ctx.args[idx] | ||
if len(args) != 1: | ||
# Either an error or no value passed. | ||
return None | ||
return args[0] | ||
|
||
|
||
def get_call_argument_type_by_name(ctx: Union[FunctionContext, MethodContext], name: str) -> Optional[MypyType]: | ||
"""Return the type for the specific argument. | ||
This helper should only be used with non-star arguments. | ||
""" | ||
if name not in ctx.callee_arg_names: | ||
return None | ||
idx = ctx.callee_arg_names.index(name) | ||
arg_types = ctx.arg_types[idx] | ||
if len(arg_types) != 1: | ||
# Either an error or no value passed. | ||
return None | ||
return arg_types[0] | ||
|
||
|
||
def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType) -> None: | ||
# type=: type of the variable itself | ||
var = Var(name=name, type=sym_type) | ||
# var.info: type of the object variable is bound to | ||
var.info = info | ||
var._fullname = info.fullname + '.' + name | ||
var.is_initialized_in_class = True | ||
var.is_inferred = True | ||
info.names[name] = SymbolTableNode(MDEF, var, | ||
plugin_generated=True) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
def make_classes_generic(*klasses: type) -> None: | ||
for klass in klasses: | ||
def fake_classgetitem(cls, *args, **kwargs): | ||
return cls | ||
|
||
klass.__class_getitem__ = classmethod(fake_classgetitem) # type: ignore |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,47 @@ | ||
from typing import Optional | ||
|
||
from mypy.plugin import ClassDefContext, MethodContext | ||
from mypy.types import CallableType, Instance, NoneTyp | ||
from mypy.types import Type as MypyType | ||
from mypy.types import TypeType | ||
|
||
from mypy_django_plugin.lib import helpers | ||
from mypy_django_plugin.lib import chk_helpers, helpers | ||
|
||
|
||
def make_meta_nested_class_inherit_from_any(ctx: ClassDefContext) -> None: | ||
meta_node = helpers.get_nested_meta_node_for_current_class(ctx.cls.info) | ||
if meta_node is None: | ||
if not ctx.api.final_iteration: | ||
ctx.api.defer() | ||
else: | ||
class FormCallback(helpers.ClassDefPluginCallback): | ||
def modify_class_defn(self) -> None: | ||
meta_node = helpers.get_nested_meta_node_for_current_class(self.class_defn.info) | ||
if meta_node is None: | ||
return None | ||
meta_node.fallback_to_any = True | ||
|
||
|
||
def get_specified_form_class(object_type: Instance) -> Optional[TypeType]: | ||
form_class_sym = object_type.type.get('form_class') | ||
if form_class_sym and isinstance(form_class_sym.type, CallableType): | ||
return TypeType(form_class_sym.type.ret_type) | ||
return None | ||
class FormMethodCallback(helpers.GetMethodCallback): | ||
def get_specified_form_class(self) -> Optional[TypeType]: | ||
form_class_sym = self.callee_type.type.get('form_class') | ||
if form_class_sym and isinstance(form_class_sym.type, CallableType): | ||
return TypeType(form_class_sym.type.ret_type) | ||
return None | ||
|
||
|
||
def extract_proper_type_for_get_form(ctx: MethodContext) -> MypyType: | ||
object_type = ctx.type | ||
assert isinstance(object_type, Instance) | ||
class GetFormCallback(FormMethodCallback): | ||
def get_method_return_type(self) -> MypyType: | ||
form_class_type = chk_helpers.get_call_argument_type_by_name(self.ctx, 'form_class') | ||
if form_class_type is None or isinstance(form_class_type, NoneTyp): | ||
form_class_type = self.get_specified_form_class() | ||
|
||
form_class_type = helpers.get_call_argument_type_by_name(ctx, 'form_class') | ||
if form_class_type is None or isinstance(form_class_type, NoneTyp): | ||
form_class_type = get_specified_form_class(object_type) | ||
if isinstance(form_class_type, TypeType) and isinstance(form_class_type.item, Instance): | ||
return form_class_type.item | ||
|
||
if isinstance(form_class_type, TypeType) and isinstance(form_class_type.item, Instance): | ||
return form_class_type.item | ||
if isinstance(form_class_type, CallableType) and isinstance(form_class_type.ret_type, Instance): | ||
return form_class_type.ret_type | ||
|
||
if isinstance(form_class_type, CallableType) and isinstance(form_class_type.ret_type, Instance): | ||
return form_class_type.ret_type | ||
return self.default_return_type | ||
|
||
return ctx.default_return_type | ||
|
||
class GetFormClassCallback(FormMethodCallback): | ||
def get_method_return_type(self) -> MypyType: | ||
form_class_type = self.get_specified_form_class() | ||
if form_class_type is None: | ||
return self.default_return_type | ||
|
||
def extract_proper_type_for_get_form_class(ctx: MethodContext) -> MypyType: | ||
object_type = ctx.type | ||
assert isinstance(object_type, Instance) | ||
|
||
form_class_type = get_specified_form_class(object_type) | ||
if form_class_type is None: | ||
return ctx.default_return_type | ||
|
||
return form_class_type | ||
return form_class_type |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,77 +1,76 @@ | ||
from mypy.nodes import ( | ||
GDEF, FuncDef, MemberExpr, NameExpr, RefExpr, StrExpr, SymbolTableNode, TypeInfo, | ||
) | ||
from mypy.plugin import ClassDefContext, DynamicClassDefContext | ||
from mypy.types import AnyType, Instance, TypeOfAny | ||
from typing import List, Tuple | ||
|
||
from mypy.nodes import Argument, FuncDef, NameExpr, StrExpr, TypeInfo | ||
from mypy.plugin import ClassDefContext | ||
from mypy.types import AnyType, Instance | ||
from mypy.types import Type as MypyType | ||
from mypy.types import TypeOfAny | ||
|
||
from mypy_django_plugin.lib import fullnames, helpers | ||
|
||
|
||
def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefContext) -> None: | ||
semanal_api = helpers.get_semanal_api(ctx) | ||
|
||
callee = ctx.call.callee | ||
assert isinstance(callee, MemberExpr) | ||
assert isinstance(callee.expr, RefExpr) | ||
|
||
base_manager_info = callee.expr.node | ||
if base_manager_info is None: | ||
if not semanal_api.final_iteration: | ||
semanal_api.defer() | ||
return | ||
|
||
assert isinstance(base_manager_info, TypeInfo) | ||
new_manager_info = semanal_api.basic_new_typeinfo(ctx.name, | ||
basetype_or_fallback=Instance(base_manager_info, | ||
[AnyType(TypeOfAny.unannotated)])) | ||
new_manager_info.line = ctx.call.line | ||
new_manager_info.defn.line = ctx.call.line | ||
new_manager_info.metaclass_type = new_manager_info.calculate_metaclass_type() | ||
|
||
current_module = semanal_api.cur_mod_node | ||
current_module.names[ctx.name] = SymbolTableNode(GDEF, new_manager_info, | ||
plugin_generated=True) | ||
passed_queryset = ctx.call.args[0] | ||
assert isinstance(passed_queryset, NameExpr) | ||
|
||
derived_queryset_fullname = passed_queryset.fullname | ||
assert derived_queryset_fullname is not None | ||
|
||
sym = semanal_api.lookup_fully_qualified_or_none(derived_queryset_fullname) | ||
assert sym is not None | ||
if sym.node is None: | ||
if not semanal_api.final_iteration: | ||
semanal_api.defer() | ||
else: | ||
def build_unannotated_method_args(method_node: FuncDef) -> Tuple[List[Argument], MypyType]: | ||
prepared_arguments = [] | ||
for argument in method_node.arguments[1:]: | ||
argument.type_annotation = AnyType(TypeOfAny.unannotated) | ||
prepared_arguments.append(argument) | ||
return_type = AnyType(TypeOfAny.unannotated) | ||
return prepared_arguments, return_type | ||
|
||
|
||
class ManagerFromQuerySetCallback(helpers.DynamicClassFromMethodCallback): | ||
def create_new_dynamic_class(self) -> None: | ||
|
||
base_manager_info = self.callee.expr.node # type: ignore | ||
|
||
if base_manager_info is None and not self.defer_till_next_iteration(reason='base_manager_info is None'): | ||
# what exception should be thrown here? | ||
return | ||
|
||
assert isinstance(base_manager_info, TypeInfo) | ||
|
||
new_manager_info, current_module = self.generate_manager_info_and_module(base_manager_info) | ||
|
||
passed_queryset = self.call_expr.args[0] | ||
assert isinstance(passed_queryset, NameExpr) | ||
|
||
derived_queryset_fullname = passed_queryset.fullname | ||
assert derived_queryset_fullname is not None | ||
|
||
sym = self.semanal_api.lookup_fully_qualified_or_none(derived_queryset_fullname) | ||
assert sym is not None | ||
if sym.node is None and not self.defer_till_next_iteration(reason='sym.node is None'): | ||
# inherit from Any to prevent false-positives, if queryset class cannot be resolved | ||
new_manager_info.fallback_to_any = True | ||
return | ||
|
||
derived_queryset_info = sym.node | ||
assert isinstance(derived_queryset_info, TypeInfo) | ||
|
||
if len(ctx.call.args) > 1: | ||
expr = ctx.call.args[1] | ||
assert isinstance(expr, StrExpr) | ||
custom_manager_generated_name = expr.value | ||
else: | ||
custom_manager_generated_name = base_manager_info.name + 'From' + derived_queryset_info.name | ||
|
||
custom_manager_generated_fullname = '.'.join(['django.db.models.manager', custom_manager_generated_name]) | ||
if 'from_queryset_managers' not in base_manager_info.metadata: | ||
base_manager_info.metadata['from_queryset_managers'] = {} | ||
base_manager_info.metadata['from_queryset_managers'][custom_manager_generated_fullname] = new_manager_info.fullname | ||
|
||
class_def_context = ClassDefContext(cls=new_manager_info.defn, | ||
reason=ctx.call, api=semanal_api) | ||
self_type = Instance(new_manager_info, []) | ||
# we need to copy all methods in MRO before django.db.models.query.QuerySet | ||
for class_mro_info in derived_queryset_info.mro: | ||
if class_mro_info.fullname == fullnames.QUERYSET_CLASS_FULLNAME: | ||
break | ||
for name, sym in class_mro_info.names.items(): | ||
if isinstance(sym.node, FuncDef): | ||
helpers.copy_method_to_another_class(class_def_context, | ||
self_type, | ||
new_method_name=name, | ||
method_node=sym.node) | ||
return | ||
|
||
derived_queryset_info = sym.node | ||
assert isinstance(derived_queryset_info, TypeInfo) | ||
|
||
if len(self.call_expr.args) > 1: | ||
expr = self.call_expr.args[1] | ||
assert isinstance(expr, StrExpr) | ||
custom_manager_generated_name = expr.value | ||
else: | ||
custom_manager_generated_name = base_manager_info.name + 'From' + derived_queryset_info.name | ||
|
||
custom_manager_generated_fullname = '.'.join(['django.db.models.manager', custom_manager_generated_name]) | ||
if 'from_queryset_managers' not in base_manager_info.metadata: | ||
base_manager_info.metadata['from_queryset_managers'] = {} | ||
base_manager_info.metadata['from_queryset_managers'][ | ||
custom_manager_generated_fullname] = new_manager_info.fullname | ||
class_def_context = ClassDefContext( | ||
cls=new_manager_info.defn, | ||
reason=self.call_expr, api=self.semanal_api) | ||
self_type = Instance(new_manager_info, []) | ||
# we need to copy all methods in MRO before django.db.models.query.QuerySet | ||
for class_mro_info in derived_queryset_info.mro: | ||
if class_mro_info.fullname == fullnames.QUERYSET_CLASS_FULLNAME: | ||
break | ||
for name, sym in class_mro_info.names.items(): | ||
if isinstance(sym.node, FuncDef): | ||
self.copy_method_to_another_class( | ||
class_def_context, | ||
self_type, | ||
new_method_name=name, | ||
method_node=sym.node) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,46 @@ | ||
from django.core.exceptions import FieldDoesNotExist | ||
from mypy.plugin import MethodContext | ||
from mypy.types import AnyType, Instance | ||
from mypy.types import Type as MypyType | ||
from mypy.types import TypeOfAny | ||
|
||
from mypy_django_plugin.django.context import DjangoContext | ||
from mypy_django_plugin.lib import helpers | ||
from mypy_django_plugin.lib import chk_helpers, helpers | ||
|
||
|
||
def _get_field_instance(ctx: MethodContext, field_fullname: str) -> MypyType: | ||
field_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), | ||
field_fullname) | ||
if field_info is None: | ||
return AnyType(TypeOfAny.unannotated) | ||
return Instance(field_info, [AnyType(TypeOfAny.explicit), AnyType(TypeOfAny.explicit)]) | ||
class MetaGetFieldCallback(helpers.GetMethodCallback): | ||
def _get_field_instance(self, field_fullname: str) -> MypyType: | ||
field_info = helpers.lookup_fully_qualified_typeinfo(self.type_checker, field_fullname) | ||
if field_info is None: | ||
return AnyType(TypeOfAny.unannotated) | ||
return Instance(field_info, [AnyType(TypeOfAny.explicit), AnyType(TypeOfAny.explicit)]) | ||
|
||
def get_method_return_type(self) -> MypyType: | ||
# bail if list of generic params is empty | ||
if len(self.callee_type.args) == 0: | ||
return self.default_return_type | ||
|
||
def return_proper_field_type_from_get_field(ctx: MethodContext, django_context: DjangoContext) -> MypyType: | ||
# Options instance | ||
assert isinstance(ctx.type, Instance) | ||
model_type = self.callee_type.args[0] | ||
if not isinstance(model_type, Instance): | ||
return self.default_return_type | ||
|
||
# bail if list of generic params is empty | ||
if len(ctx.type.args) == 0: | ||
return ctx.default_return_type | ||
model_cls = self.django_context.get_model_class_by_fullname(model_type.type.fullname) | ||
if model_cls is None: | ||
return self.default_return_type | ||
|
||
model_type = ctx.type.args[0] | ||
if not isinstance(model_type, Instance): | ||
return ctx.default_return_type | ||
field_name_expr = chk_helpers.get_call_argument_by_name(self.ctx, 'field_name') | ||
if field_name_expr is None: | ||
return self.default_return_type | ||
|
||
model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname) | ||
if model_cls is None: | ||
return ctx.default_return_type | ||
field_name = helpers.resolve_string_attribute_value(field_name_expr, self.django_context) | ||
if field_name is None: | ||
return self.default_return_type | ||
|
||
field_name_expr = helpers.get_call_argument_by_name(ctx, 'field_name') | ||
if field_name_expr is None: | ||
return ctx.default_return_type | ||
try: | ||
field = model_cls._meta.get_field(field_name) | ||
except FieldDoesNotExist as exc: | ||
# if model is abstract, do not raise exception, skip false positives | ||
if not model_cls._meta.abstract: | ||
self.ctx.api.fail(exc.args[0], self.ctx.context) | ||
return AnyType(TypeOfAny.from_error) | ||
|
||
field_name = helpers.resolve_string_attribute_value(field_name_expr, django_context) | ||
if field_name is None: | ||
return ctx.default_return_type | ||
|
||
try: | ||
field = model_cls._meta.get_field(field_name) | ||
except FieldDoesNotExist as exc: | ||
# if model is abstract, do not raise exception, skip false positives | ||
if not model_cls._meta.abstract: | ||
ctx.api.fail(exc.args[0], ctx.context) | ||
return AnyType(TypeOfAny.from_error) | ||
|
||
field_fullname = helpers.get_class_fullname(field.__class__) | ||
return _get_field_instance(ctx, field_fullname) | ||
field_fullname = helpers.get_class_fullname(field.__class__) | ||
return self._get_field_instance(field_fullname) |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from typing import Union | ||
|
||
from mypy.nodes import MypyFile, TypeInfo | ||
|
||
|
||
class IncompleteDefnError(Exception): | ||
pass | ||
|
||
|
||
class TypeInfoNotFound(IncompleteDefnError): | ||
def __init__(self, fullname: str) -> None: | ||
super().__init__(f'It is final iteration and required type {fullname!r} is not ready yet.') | ||
|
||
|
||
class AttributeNotFound(IncompleteDefnError): | ||
def __init__(self, node: Union[TypeInfo, MypyFile], attrname: str) -> None: | ||
super().__init__(f'Attribute {attrname!r} is not defined for the {node.fullname!r}.') | ||
|
||
|
||
class NameNotFound(IncompleteDefnError): | ||
def __init__(self, name: str) -> None: | ||
super().__init__(f'Could not find {name!r} in the current activated namespaces') | ||
|
||
|
||
class SymbolAdditionNotPossible(Exception): | ||
pass | ||
|
||
|
||
def get_class_fullname(klass: type) -> str: | ||
return klass.__module__ + '.' + klass.__qualname__ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,51 +1,47 @@ | ||
from mypy.plugin import MethodContext | ||
from mypy.types import AnyType, Instance | ||
from mypy.types import Type as MypyType | ||
from mypy.types import TypeOfAny | ||
|
||
from mypy_django_plugin.django.context import DjangoContext | ||
from mypy_django_plugin.lib import fullnames, helpers | ||
from mypy_django_plugin.lib import chk_helpers, fullnames, helpers | ||
|
||
|
||
def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) -> MypyType: | ||
lookup_kwargs = ctx.arg_names[1] | ||
provided_lookup_types = ctx.arg_types[1] | ||
class QuerySetFilterTypecheckCallback(helpers.GetMethodCallback): | ||
def resolve_combinable_type(self, combinable_type: Instance) -> MypyType: | ||
if combinable_type.type.fullname != fullnames.F_EXPRESSION_FULLNAME: | ||
# Combinables aside from F expressions are unsupported | ||
return AnyType(TypeOfAny.explicit) | ||
|
||
assert isinstance(ctx.type, Instance) | ||
return self.django_context.resolve_f_expression_type(combinable_type) | ||
|
||
if not ctx.type.args or not isinstance(ctx.type.args[0], Instance): | ||
return ctx.default_return_type | ||
def get_method_return_type(self) -> MypyType: | ||
lookup_kwargs = self.ctx.arg_names[1] | ||
provided_lookup_types = self.ctx.arg_types[1] | ||
|
||
model_cls_fullname = ctx.type.args[0].type.fullname | ||
model_cls = django_context.get_model_class_by_fullname(model_cls_fullname) | ||
if model_cls is None: | ||
return ctx.default_return_type | ||
if not self.callee_type.args or not isinstance(self.callee_type.args[0], Instance): | ||
return self.default_return_type | ||
|
||
for lookup_kwarg, provided_type in zip(lookup_kwargs, provided_lookup_types): | ||
if lookup_kwarg is None: | ||
continue | ||
if (isinstance(provided_type, Instance) | ||
and provided_type.type.has_base('django.db.models.expressions.Combinable')): | ||
provided_type = resolve_combinable_type(provided_type, django_context) | ||
model_cls_fullname = self.callee_type.args[0].type.fullname | ||
model_cls = self.django_context.get_model_class_by_fullname(model_cls_fullname) | ||
if model_cls is None: | ||
return self.default_return_type | ||
|
||
lookup_type = django_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg) | ||
# Managers as provided_type is not supported yet | ||
if (isinstance(provided_type, Instance) | ||
and helpers.has_any_of_bases(provided_type.type, (fullnames.MANAGER_CLASS_FULLNAME, | ||
fullnames.QUERYSET_CLASS_FULLNAME))): | ||
return ctx.default_return_type | ||
for lookup_kwarg, provided_type in zip(lookup_kwargs, provided_lookup_types): | ||
if lookup_kwarg is None: | ||
continue | ||
if (isinstance(provided_type, Instance) | ||
and provided_type.type.has_base('django.db.models.expressions.Combinable')): | ||
provided_type = self.resolve_combinable_type(provided_type) | ||
|
||
helpers.check_types_compatible(ctx, | ||
expected_type=lookup_type, | ||
actual_type=provided_type, | ||
error_message=f'Incompatible type for lookup {lookup_kwarg!r}:') | ||
lookup_type = self.django_context.resolve_lookup_expected_type(self.ctx, model_cls, lookup_kwarg) | ||
# Managers as provided_type is not supported yet | ||
if (isinstance(provided_type, Instance) | ||
and helpers.has_any_of_bases(provided_type.type, (fullnames.MANAGER_CLASS_FULLNAME, | ||
fullnames.QUERYSET_CLASS_FULLNAME))): | ||
return self.default_return_type | ||
|
||
return ctx.default_return_type | ||
chk_helpers.check_types_compatible(self.ctx, | ||
expected_type=lookup_type, | ||
actual_type=provided_type, | ||
error_message=f'Incompatible type for lookup {lookup_kwarg!r}:') | ||
|
||
|
||
def resolve_combinable_type(combinable_type: Instance, django_context: DjangoContext) -> MypyType: | ||
if combinable_type.type.fullname != fullnames.F_EXPRESSION_FULLNAME: | ||
# Combinables aside from F expressions are unsupported | ||
return AnyType(TypeOfAny.explicit) | ||
|
||
return django_context.resolve_f_expression_type(combinable_type) | ||
return self.default_return_type |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
from django.db.models.fields.reverse_related import ( | ||
ForeignObjectRel, ManyToManyRel, ManyToOneRel, OneToOneRel, | ||
) | ||
from mypy.checker import gen_unique_name | ||
from mypy.types import Instance | ||
from mypy.types import Type as MypyType | ||
|
||
from mypy_django_plugin.lib import fullnames, helpers | ||
|
||
|
||
class GetRelatedManagerCallback(helpers.GetAttributeCallback): | ||
obj_type: Instance | ||
|
||
def get_related_manager_type(self, relation: ForeignObjectRel) -> MypyType: | ||
related_model_cls = self.django_context.get_field_related_model_cls(relation) | ||
if related_model_cls is None: | ||
# could not find a referenced model (maybe invalid to= value, or GenericForeignKey) | ||
# TODO: show error | ||
return self.default_attr_type | ||
|
||
related_model_info = self.lookup_typeinfo(helpers.get_class_fullname(related_model_cls)) | ||
if related_model_info is None: | ||
# TODO: show error | ||
return self.default_attr_type | ||
|
||
if isinstance(relation, OneToOneRel): | ||
return Instance(related_model_info, []) | ||
|
||
elif isinstance(relation, (ManyToOneRel, ManyToManyRel)): | ||
related_manager_info = self.lookup_typeinfo(fullnames.RELATED_MANAGER_CLASS) | ||
if related_manager_info is None: | ||
return self.default_attr_type | ||
|
||
# get type of default_manager for model | ||
default_manager_fullname = helpers.get_class_fullname(related_model_cls._meta.default_manager.__class__) | ||
default_manager_info = self.lookup_typeinfo(default_manager_fullname) | ||
if default_manager_info is None: | ||
return self.default_attr_type | ||
|
||
default_manager_type = Instance(default_manager_info, [Instance(related_model_info, [])]) | ||
related_manager_type = Instance(related_manager_info, | ||
[Instance(related_model_info, [])]) | ||
|
||
if (not isinstance(default_manager_type, Instance) | ||
or default_manager_type.type.fullname == fullnames.MANAGER_CLASS_FULLNAME): | ||
# if not defined or trivial -> just return RelatedManager[Model] | ||
return related_manager_type | ||
|
||
# make anonymous class | ||
name = gen_unique_name(related_model_cls.__name__ + '_' + 'RelatedManager', | ||
self.obj_type.type.names) | ||
bases = [related_manager_type, default_manager_type] | ||
new_manager_info = self.new_typeinfo(name, bases) | ||
return Instance(new_manager_info, []) | ||
|
||
return self.default_attr_type | ||
|
||
def get_attribute_type(self) -> MypyType: | ||
if not isinstance(self.obj_type, Instance): | ||
# it's probably a UnionType, do nothing for now | ||
return self.default_attr_type | ||
|
||
model_fullname = self.obj_type.type.fullname | ||
model_cls = self.django_context.get_model_class_by_fullname(model_fullname) | ||
if model_cls is None: | ||
return self.default_attr_type | ||
|
||
for reverse_manager_name, relation in self.django_context.get_model_relations(model_cls): | ||
if reverse_manager_name == self.name: | ||
return self.get_related_manager_type(relation) | ||
|
||
return self.default_attr_type |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,25 @@ | ||
from mypy.plugin import AttributeContext | ||
from mypy.types import Instance | ||
from mypy.types import Type as MypyType | ||
from mypy.types import UnionType | ||
|
||
from mypy_django_plugin.django.context import DjangoContext | ||
from mypy_django_plugin.lib import helpers | ||
|
||
|
||
def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_context: DjangoContext) -> MypyType: | ||
# Imported here because django isn't properly loaded yet when module is loaded | ||
from django.contrib.auth.base_user import AbstractBaseUser | ||
from django.contrib.auth.models import AnonymousUser | ||
class RequestUserModelCallback(helpers.GetAttributeCallback): | ||
def get_attribute_type(self) -> MypyType: | ||
auth_user_model = self.django_context.settings.AUTH_USER_MODEL | ||
user_cls = self.django_context.apps_registry.get_model(auth_user_model) | ||
user_info = helpers.lookup_class_typeinfo(self.type_checker, user_cls) | ||
|
||
abstract_base_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AbstractBaseUser) | ||
anonymous_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AnonymousUser) | ||
if user_info is None: | ||
return self.default_attr_type | ||
|
||
# This shouldn't be able to happen, as we managed to import the models above. | ||
assert abstract_base_user_info is not None | ||
assert anonymous_user_info is not None | ||
# Imported here because django isn't properly loaded yet when module is loaded | ||
from django.contrib.auth.models import AnonymousUser | ||
|
||
if ctx.default_attr_type != UnionType([Instance(abstract_base_user_info, []), Instance(anonymous_user_info, [])]): | ||
# Type has been changed from the default in django-stubs. | ||
# I.e. HttpRequest has been subclassed and user-type overridden, so let's leave it as is. | ||
return ctx.default_attr_type | ||
anonymous_user_info = helpers.lookup_class_typeinfo(self.type_checker, AnonymousUser) | ||
if anonymous_user_info is None: | ||
# This shouldn't be able to happen, as we managed to import the model above... | ||
return Instance(user_info, []) | ||
|
||
auth_user_model = django_context.settings.AUTH_USER_MODEL | ||
user_cls = django_context.apps_registry.get_model(auth_user_model) | ||
user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), user_cls) | ||
|
||
if user_info is None: | ||
return ctx.default_attr_type | ||
|
||
return UnionType([Instance(user_info, []), Instance(anonymous_user_info, [])]) | ||
return UnionType([Instance(user_info, []), Instance(anonymous_user_info, [])]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,45 @@ | ||
from mypy.nodes import MemberExpr | ||
from mypy.plugin import AttributeContext, FunctionContext | ||
from mypy.types import AnyType, Instance | ||
from mypy.types import Type as MypyType | ||
from mypy.types import TypeOfAny, TypeType | ||
|
||
from mypy_django_plugin.django.context import DjangoContext | ||
from mypy_django_plugin.lib import helpers | ||
from mypy_django_plugin.transformers import new_helpers | ||
|
||
|
||
def get_user_model_hook(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: | ||
auth_user_model = django_context.settings.AUTH_USER_MODEL | ||
model_cls = django_context.apps_registry.get_model(auth_user_model) | ||
model_cls_fullname = helpers.get_class_fullname(model_cls) | ||
class GetUserModelCallback(helpers.GetFunctionCallback): | ||
def get_function_return_type(self) -> MypyType: | ||
auth_user_model = self.django_context.settings.AUTH_USER_MODEL | ||
model_cls = self.django_context.apps_registry.get_model(auth_user_model) | ||
model_cls_fullname = new_helpers.get_class_fullname(model_cls) | ||
|
||
model_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), | ||
model_cls_fullname) | ||
if model_info is None: | ||
return AnyType(TypeOfAny.unannotated) | ||
model_info = helpers.lookup_fully_qualified_typeinfo(self.type_checker, model_cls_fullname) | ||
if model_info is None: | ||
return AnyType(TypeOfAny.unannotated) | ||
|
||
return TypeType(Instance(model_info, [])) | ||
return TypeType(Instance(model_info, [])) | ||
|
||
|
||
def get_type_of_settings_attribute(ctx: AttributeContext, django_context: DjangoContext) -> MypyType: | ||
assert isinstance(ctx.context, MemberExpr) | ||
setting_name = ctx.context.name | ||
if not hasattr(django_context.settings, setting_name): | ||
ctx.api.fail(f"'Settings' object has no attribute {setting_name!r}", ctx.context) | ||
return ctx.default_attr_type | ||
class GetTypeOfSettingsAttributeCallback(helpers.GetAttributeCallback): | ||
def get_attribute_type(self) -> MypyType: | ||
if not hasattr(self.django_context.settings, self.name): | ||
self.type_checker.fail(f"'Settings' object has no attribute {self.name!r}", self.ctx.context) | ||
return self.default_attr_type | ||
|
||
typechecker_api = helpers.get_typechecker_api(ctx) | ||
# first look for the setting in the project settings file, then global settings | ||
settings_module = self.type_checker.modules.get(self.django_context.django_settings_module) | ||
global_settings_module = self.type_checker.modules.get('django.conf.global_settings') | ||
for module in [settings_module, global_settings_module]: | ||
if module is not None: | ||
sym = module.names.get(self.name) | ||
if sym is not None and sym.type is not None: | ||
return sym.type | ||
|
||
# first look for the setting in the project settings file, then global settings | ||
settings_module = typechecker_api.modules.get(django_context.django_settings_module) | ||
global_settings_module = typechecker_api.modules.get('django.conf.global_settings') | ||
for module in [settings_module, global_settings_module]: | ||
if module is not None: | ||
sym = module.names.get(setting_name) | ||
if sym is not None and sym.type is not None: | ||
return sym.type | ||
# if by any reason it isn't present there, get type from django settings | ||
value = getattr(self.django_context.settings, self.name) | ||
value_fullname = helpers.get_class_fullname(value.__class__) | ||
|
||
# if by any reason it isn't present there, get type from django settings | ||
value = getattr(django_context.settings, setting_name) | ||
value_fullname = helpers.get_class_fullname(value.__class__) | ||
value_info = helpers.lookup_fully_qualified_typeinfo(self.type_checker, value_fullname) | ||
if value_info is None: | ||
return self.default_attr_type | ||
|
||
value_info = helpers.lookup_fully_qualified_typeinfo(typechecker_api, value_fullname) | ||
if value_info is None: | ||
return ctx.default_attr_type | ||
|
||
return Instance(value_info, []) | ||
return Instance(value_info, []) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will only affect
django>=3.1
We do need legacy support.