Disable monkeypatches, add dependencies via new hook (#60)

* code cleanups, disable monkeypatches, move to add_additional_deps

* disable incremental mode for tests

* add pip-wheel-metadata

* move some code from get_base_hook to get_attribute_hook to reduce dependencies

* simplify values/values_list tests and code

* disable cache for some tests failing with incremental mode

* enable incremental mode for tests typechecking

* pin mypy version

* fix tests

* lint

* fix internal crashes
This commit is contained in:
Maxim Kurnikov
2019-04-12 14:54:00 +03:00
committed by GitHub
parent 13d19017b7
commit aeb435c8b3
24 changed files with 801 additions and 607 deletions

View File

@@ -1,29 +1,33 @@
import os
from functools import partial
from typing import Callable, Dict, Optional, Union, cast
from typing import Callable, Dict, List, Optional, Tuple, cast
from mypy.nodes import MemberExpr, NameExpr, TypeInfo
from mypy.nodes import MypyFile, NameExpr, TypeInfo
from mypy.options import Options
from mypy.plugin import (
AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin,
AnalyzeTypeContext)
from mypy.types import (
AnyType, CallableType, Instance, NoneTyp, Type, TypeOfAny, TypeType, UnionType,
AnalyzeTypeContext, AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin,
)
from mypy.types import AnyType, Instance, Type, TypeOfAny
from mypy_django_plugin import helpers, monkeypatch
from mypy_django_plugin import helpers
from mypy_django_plugin.config import Config
from mypy_django_plugin.transformers import fields, init_create
from mypy_django_plugin.transformers.forms import (
make_meta_nested_class_inherit_from_any,
extract_proper_type_for_get_form, extract_proper_type_for_get_form_class, make_meta_nested_class_inherit_from_any,
)
from mypy_django_plugin.transformers.migrations import (
determine_model_cls_from_string_for_migrations, get_string_value_from_expr,
determine_model_cls_from_string_for_migrations,
)
from mypy_django_plugin.transformers.models import process_model_class
from mypy_django_plugin.transformers.queryset import extract_proper_type_for_values_and_values_list
from mypy_django_plugin.transformers.queryset import (
extract_proper_type_for_queryset_values, extract_proper_type_queryset_values_list,
set_first_generic_param_as_default_for_second,
)
from mypy_django_plugin.transformers.related import (
determine_type_of_related_manager, extract_and_return_primary_key_of_bound_related_field_parameter,
)
from mypy_django_plugin.transformers.settings import (
AddSettingValuesToDjangoConfObject, get_settings_metadata,
get_type_of_setting, return_user_model_hook,
)
@@ -35,20 +39,21 @@ def transform_model_class(ctx: ClassDefContext) -> None:
pass
else:
if sym is not None and isinstance(sym.node, TypeInfo):
sym.node.metadata['django']['model_bases'][ctx.cls.fullname] = 1
helpers.get_django_metadata(sym.node)['model_bases'][ctx.cls.fullname] = 1
process_model_class(ctx)
def transform_manager_class(ctx: ClassDefContext) -> None:
sym = ctx.api.lookup_fully_qualified_or_none(helpers.MANAGER_CLASS_FULLNAME)
if sym is not None and isinstance(sym.node, TypeInfo):
sym.node.metadata['django']['manager_bases'][ctx.cls.fullname] = 1
helpers.get_django_metadata(sym.node)['manager_bases'][ctx.cls.fullname] = 1
def transform_form_class(ctx: ClassDefContext) -> None:
sym = ctx.api.lookup_fully_qualified_or_none(helpers.BASEFORM_CLASS_FULLNAME)
if sym is not None and isinstance(sym.node, TypeInfo):
sym.node.metadata['django']['baseform_bases'][ctx.cls.fullname] = 1
helpers.get_django_metadata(sym.node)['baseform_bases'][ctx.cls.fullname] = 1
make_meta_nested_class_inherit_from_any(ctx)
@@ -83,123 +88,31 @@ def determine_proper_manager_type(ctx: FunctionContext) -> Type:
return ret
def set_first_generic_param_as_default_for_second(fullname: str, ctx: AnalyzeTypeContext) -> Type:
if not ctx.type.args:
try:
return ctx.api.named_type(fullname, [AnyType(TypeOfAny.explicit),
AnyType(TypeOfAny.explicit)])
except KeyError:
# really should never happen
return AnyType(TypeOfAny.explicit)
def return_type_for_id_field(ctx: AttributeContext) -> Type:
if not isinstance(ctx.type, Instance):
return AnyType(TypeOfAny.from_error)
args = ctx.type.args
if len(args) == 1:
args = [args[0], args[0]]
analyzed_args = [ctx.api.analyze_type(arg) for arg in args]
try:
return ctx.api.named_type(fullname, analyzed_args)
except KeyError:
# really should never happen
return AnyType(TypeOfAny.explicit)
def return_user_model_hook(ctx: FunctionContext) -> Type:
from mypy.checker import TypeChecker
api = cast(TypeChecker, ctx.api)
setting_expr = helpers.get_setting_expr(api, 'AUTH_USER_MODEL')
if setting_expr is None:
return ctx.default_return_type
model_path = get_string_value_from_expr(setting_expr)
if model_path is None:
return ctx.default_return_type
app_label, _, model_class_name = model_path.rpartition('.')
if app_label is None:
return ctx.default_return_type
model_fullname = helpers.get_model_fullname(app_label, model_class_name,
all_modules=api.modules)
if model_fullname is None:
api.fail(f'"{app_label}.{model_class_name}" model class is not imported so far. Try to import it '
f'(under if TYPE_CHECKING) at the beginning of the current file',
context=ctx.context)
return ctx.default_return_type
model_info = helpers.lookup_fully_qualified_generic(model_fullname,
all_modules=api.modules)
if model_info is None or not isinstance(model_info, TypeInfo):
return ctx.default_return_type
return TypeType(Instance(model_info, []))
def _extract_referred_to_type_info(typ: Union[UnionType, Instance]) -> Optional[TypeInfo]:
if isinstance(typ, Instance):
return typ.type
else:
# should be Union[TYPE, None]
typ = helpers.make_required(typ)
if isinstance(typ, Instance):
return typ.type
return None
def extract_and_return_primary_key_of_bound_related_field_parameter(ctx: AttributeContext) -> Type:
if not isinstance(ctx.default_attr_type, Instance) or not (ctx.default_attr_type.type.fullname() == 'builtins.int'):
return ctx.default_attr_type
if not isinstance(ctx.type, Instance) or not ctx.type.type.has_base(helpers.MODEL_CLASS_FULLNAME):
return ctx.default_attr_type
field_name = ctx.context.name.split('_')[0]
sym = ctx.type.type.get(field_name)
if sym and isinstance(sym.type, Instance) and len(sym.type.args) > 0:
referred_to = sym.type.args[1]
if isinstance(referred_to, AnyType):
return AnyType(TypeOfAny.implementation_artifact)
model_type = _extract_referred_to_type_info(referred_to)
if model_type is None:
return AnyType(TypeOfAny.implementation_artifact)
primary_key_type = helpers.extract_primary_key_type_for_get(model_type)
if primary_key_type:
return primary_key_type
is_nullable = helpers.is_field_nullable(ctx.type.type, field_name)
if is_nullable:
return helpers.make_optional(ctx.default_attr_type)
return ctx.default_attr_type
def return_integer_type_for_id_for_non_defined_primary_key_in_models(ctx: AttributeContext) -> Type:
if isinstance(ctx.type, Instance) and ctx.type.type.has_base(helpers.MODEL_CLASS_FULLNAME):
model_info = ctx.type.type # type: TypeInfo
primary_key_field_name = helpers.get_primary_key_field_name(model_info)
if not primary_key_field_name:
# no field with primary_key=True, just return id as int
return ctx.api.named_generic_type('builtins.int', [])
return ctx.default_attr_type
if primary_key_field_name != 'id':
# there's field with primary_key=True, but it's name is not 'id', fail
ctx.api.fail("Default primary key 'id' is not defined", ctx.context)
return AnyType(TypeOfAny.from_error)
class ExtractSettingType:
def __init__(self, module_fullname: str):
self.module_fullname = module_fullname
primary_key_sym = model_info.get(primary_key_field_name)
if primary_key_sym and isinstance(primary_key_sym.type, Instance):
pass
def __call__(self, ctx: AttributeContext) -> Type:
from mypy.checker import TypeChecker
# try to parse field type out of primary key field
field_type = helpers.extract_field_getter_type(primary_key_sym.type)
if field_type:
return field_type
api = cast(TypeChecker, ctx.api)
original_module = api.modules.get(self.module_fullname)
if original_module is None:
return ctx.default_attr_type
definition = ctx.context
if isinstance(definition, MemberExpr):
sym = original_module.names.get(definition.name)
if sym and sym.type:
return sym.type
return ctx.default_attr_type
return primary_key_sym.type
def transform_form_view(ctx: ClassDefContext) -> None:
@@ -208,80 +121,10 @@ def transform_form_view(ctx: ClassDefContext) -> None:
helpers.get_django_metadata(ctx.cls.info)['form_class'] = form_class_value.fullname
def extract_proper_type_for_get_form_class(ctx: MethodContext) -> Type:
object_type = ctx.type
if not isinstance(object_type, Instance):
return ctx.default_return_type
form_class_fullname = helpers.get_django_metadata(object_type.type).get('form_class', None)
if not form_class_fullname:
return ctx.default_return_type
return TypeType(ctx.api.named_generic_type(form_class_fullname, []))
def extract_proper_type_for_get_form(ctx: MethodContext) -> Type:
object_type = ctx.type
if not isinstance(object_type, Instance):
return ctx.default_return_type
form_class_type = helpers.get_argument_type_by_name(ctx, 'form_class')
if form_class_type is None or isinstance(form_class_type, NoneTyp):
# extract from specified form_class in metadata
form_class_fullname = helpers.get_django_metadata(object_type.type).get('form_class', None)
if not form_class_fullname:
return ctx.default_return_type
return ctx.api.named_generic_type(form_class_fullname, [])
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
return ctx.default_return_type
def extract_proper_type_for_values_list(ctx: MethodContext) -> Type:
object_type = ctx.type
if not isinstance(object_type, Instance):
return ctx.default_return_type
flat = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'flat'))
named = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'named'))
ret = ctx.default_return_type
any_type = AnyType(TypeOfAny.implementation_artifact)
if named and flat:
ctx.api.fail("'flat' and 'named' can't be used together.", ctx.context)
return ret
elif named:
# TODO: Fill in namedtuple fields/types
row_arg = ctx.api.named_generic_type('typing.NamedTuple', [])
elif flat:
# TODO: Figure out row_arg type dependent on the argument passed in
if len(ctx.args[0]) > 1:
ctx.api.fail("'flat' is not valid when values_list is called with more than one field.", ctx.context)
return ret
row_arg = any_type
else:
# TODO: Figure out tuple argument types dependent on the arguments passed in
row_arg = ctx.api.named_generic_type('builtins.tuple', [any_type])
first_arg = ret.args[0] if len(ret.args) > 0 else any_type
new_type_args = [first_arg, row_arg]
return helpers.reparametrize_instance(ret, new_type_args)
class DjangoPlugin(Plugin):
def __init__(self, options: Options) -> None:
super().__init__(options)
monkeypatch.restore_original_load_graph()
monkeypatch.restore_original_dependencies_handling()
config_fpath = os.environ.get('MYPY_DJANGO_CONFIG', 'mypy_django.ini')
if config_fpath and os.path.exists(config_fpath):
self.config = Config.from_config_file(config_fpath)
@@ -293,16 +136,6 @@ class DjangoPlugin(Plugin):
if 'DJANGO_SETTINGS_MODULE' in os.environ:
self.django_settings_module = os.environ['DJANGO_SETTINGS_MODULE']
settings_modules = ['django.conf.global_settings']
if self.django_settings_module:
settings_modules.append(self.django_settings_module)
auto_imports = ['mypy_extensions']
auto_imports.extend(settings_modules)
monkeypatch.add_modules_as_a_source_seed_files(auto_imports)
monkeypatch.inject_modules_as_dependencies_for_django_conf_settings(settings_modules)
def _get_current_model_bases(self) -> Dict[str, int]:
model_sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
@@ -337,40 +170,70 @@ class DjangoPlugin(Plugin):
else:
return {}
def _get_settings_modules_in_order_of_priority(self) -> List[str]:
settings_modules = []
if self.django_settings_module:
settings_modules.append(self.django_settings_module)
settings_modules.append('django.conf.global_settings')
return settings_modules
def _get_typeinfo_or_none(self, class_name: str) -> Optional[TypeInfo]:
sym = self.lookup_fully_qualified(class_name)
if sym is not None and isinstance(sym.node, TypeInfo):
return sym.node
return None
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
if file.fullname() == 'django.conf' and self.django_settings_module:
return [(10, self.django_settings_module, -1)]
if file.fullname() == 'django.db.models.query':
return [(10, 'mypy_extensions', -1)]
return []
def get_function_hook(self, fullname: str
) -> Optional[Callable[[FunctionContext], Type]]:
if fullname == 'django.contrib.auth.get_user_model':
return return_user_model_hook
return partial(return_user_model_hook,
settings_modules=self._get_settings_modules_in_order_of_priority())
manager_bases = self._get_current_manager_bases()
if fullname in manager_bases:
return determine_proper_manager_type
sym = self.lookup_fully_qualified(fullname)
if sym is not None and isinstance(sym.node, TypeInfo):
if sym.node.has_base(helpers.FIELD_FULLNAME):
info = self._get_typeinfo_or_none(fullname)
if info:
if info.has_base(helpers.FIELD_FULLNAME):
return fields.adjust_return_type_of_field_instantiation
if sym.node.metadata.get('django', {}).get('generated_init'):
if helpers.get_django_metadata(info).get('generated_init'):
return init_create.redefine_and_typecheck_model_init
def get_method_hook(self, fullname: str
) -> Optional[Callable[[MethodContext], Type]]:
class_name, _, method_name = fullname.rpartition('.')
if method_name == 'get_form_class':
sym = self.lookup_fully_qualified(class_name)
if sym and isinstance(sym.node, TypeInfo) and sym.node.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME):
info = self._get_typeinfo_or_none(class_name)
if info and info.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME):
return extract_proper_type_for_get_form_class
if method_name == 'get_form':
sym = self.lookup_fully_qualified(class_name)
if sym and isinstance(sym.node, TypeInfo) and sym.node.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME):
info = self._get_typeinfo_or_none(class_name)
if info and info.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME):
return extract_proper_type_for_get_form
if method_name in ('values', 'values_list'):
sym = self.lookup_fully_qualified(class_name)
if sym and isinstance(sym.node, TypeInfo) and sym.node.has_base(helpers.QUERYSET_CLASS_FULLNAME):
return partial(extract_proper_type_for_values_and_values_list, method_name)
if method_name == 'values':
model_info = self._get_typeinfo_or_none(class_name)
if model_info and model_info.has_base(helpers.QUERYSET_CLASS_FULLNAME):
return extract_proper_type_for_queryset_values
if method_name == 'values_list':
model_info = self._get_typeinfo_or_none(class_name)
if model_info and model_info.has_base(helpers.QUERYSET_CLASS_FULLNAME):
return extract_proper_type_queryset_values_list
if fullname in {'django.apps.registry.Apps.get_model',
'django.db.migrations.state.StateApps.get_model'}:
@@ -384,13 +247,6 @@ class DjangoPlugin(Plugin):
def get_base_class_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS:
settings_modules = ['django.conf.global_settings']
if self.django_settings_module:
settings_modules.append(self.django_settings_module)
return AddSettingValuesToDjangoConfObject(settings_modules,
self.config.ignore_missing_settings)
if fullname in self._get_current_model_bases():
return transform_model_class
@@ -400,25 +256,34 @@ class DjangoPlugin(Plugin):
if fullname in self._get_current_form_bases():
return transform_form_class
sym = self.lookup_fully_qualified(fullname)
if sym and isinstance(sym.node, TypeInfo) and sym.node.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME):
info = self._get_typeinfo_or_none(fullname)
if info and info.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME):
return transform_form_view
return None
def get_attribute_hook(self, fullname: str
) -> Optional[Callable[[AttributeContext], Type]]:
if fullname == 'builtins.object.id':
return return_integer_type_for_id_for_non_defined_primary_key_in_models
class_name, _, attr_name = fullname.rpartition('.')
if class_name == helpers.DUMMY_SETTINGS_BASE_CLASS:
return partial(get_type_of_setting,
setting_name=attr_name,
settings_modules=self._get_settings_modules_in_order_of_priority(),
ignore_missing_settings=self.config.ignore_missing_settings)
module, _, name = fullname.rpartition('.')
sym = self.lookup_fully_qualified('django.conf.LazySettings')
if sym and isinstance(sym.node, TypeInfo):
metadata = get_settings_metadata(sym.node)
if module == 'builtins.object' and name in metadata:
return ExtractSettingType(module_fullname=metadata[name])
if class_name in self._get_current_model_bases():
if attr_name == 'id':
return return_type_for_id_field
return extract_and_return_primary_key_of_bound_related_field_parameter
model_info = self._get_typeinfo_or_none(class_name)
if model_info:
related_managers = helpers.get_related_managers_metadata(model_info)
if attr_name in related_managers:
return partial(determine_type_of_related_manager,
related_manager_name=attr_name)
if attr_name.endswith('_id'):
return extract_and_return_primary_key_of_bound_related_field_parameter
def get_type_analyze_hook(self, fullname: str
) -> Optional[Callable[[AnalyzeTypeContext], Type]]: