cleanups, fallback to Any in some corner cases

This commit is contained in:
Maxim Kurnikov
2019-07-18 02:58:25 +03:00
parent 0e72b2e6fc
commit 03b59b872d
3 changed files with 23 additions and 178 deletions

View File

@@ -33,3 +33,5 @@ RELATED_FIELDS_CLASSES = {
ONETOONE_FIELD_FULLNAME,
MANYTOMANY_FIELD_FULLNAME
}
MIGRATION_CLASS_FULLNAME = 'django.db.migrations.migration.Migration'

View File

@@ -1,82 +1,13 @@
from typing import Optional, Tuple, cast
from mypy.checker import TypeChecker
from mypy.nodes import StrExpr, TypeInfo, Expression
from mypy.nodes import TypeInfo
from mypy.plugin import FunctionContext
from mypy.types import AnyType, CallableType, Instance, Type as MypyType, UnionType, TypeOfAny
from mypy.types import AnyType, CallableType, Instance, Type as MypyType, TypeOfAny
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers
def extract_referred_to_type(ctx: FunctionContext, django_context: DjangoContext) -> Optional[Instance]:
api = cast(TypeChecker, ctx.api)
if 'to' not in ctx.callee_arg_names:
api.msg.fail(f'to= parameter must be set for {ctx.context.callee.fullname!r}',
context=ctx.context)
return None
arg_type = ctx.arg_types[ctx.callee_arg_names.index('to')][0]
if not isinstance(arg_type, CallableType):
to_arg_expr = ctx.args[ctx.callee_arg_names.index('to')][0]
if not isinstance(to_arg_expr, StrExpr):
# not string, not supported
return None
model_string = to_arg_expr.value
if model_string == 'self':
model_fullname = api.tscope.classes[-1].fullname()
elif '.' not in model_string:
model_fullname = api.tscope.classes[-1].module_name + '.' + model_string
else:
if django_context.app_models is not None and model_string in django_context.app_models:
model_fullname = django_context.app_models[model_string]
else:
ctx.api.fail(f'Cannot find referenced model for {model_string!r}', context=ctx.context)
return None
model_info = helpers.lookup_fully_qualified_generic(model_fullname, all_modules=api.modules)
if model_info is None or not isinstance(model_info, TypeInfo):
raise helpers.IncompleteDefnException(model_fullname)
return Instance(model_info, [])
referred_to_type = arg_type.ret_type
assert isinstance(referred_to_type, Instance)
if not referred_to_type.type.has_base(fullnames.MODEL_CLASS_FULLNAME):
ctx.api.msg.fail(f'to= parameter value must be a subclass of {fullnames.MODEL_CLASS_FULLNAME!r}',
context=ctx.context)
return None
return referred_to_type
def convert_any_to_type(typ: MypyType, replacement_type: MypyType) -> MypyType:
"""
Converts any encountered Any (in typ itself, or in generic parameters) into referred_to_type
"""
if isinstance(typ, UnionType):
converted_items = []
for item in typ.items:
converted_items.append(convert_any_to_type(item, replacement_type))
return UnionType.make_union(converted_items,
line=typ.line, column=typ.column)
if isinstance(typ, Instance):
args = []
for default_arg in typ.args:
if isinstance(default_arg, AnyType):
args.append(replacement_type)
else:
args.append(default_arg)
return helpers.reparametrize_instance(typ, args)
if isinstance(typ, AnyType):
return replacement_type
return typ
def get_referred_to_model_fullname(ctx: FunctionContext, django_context: DjangoContext) -> Optional[str]:
to_arg_type = helpers.get_call_argument_type_by_name(ctx, 'to')
if isinstance(to_arg_type, CallableType):
@@ -97,6 +28,10 @@ def get_referred_to_model_fullname(ctx: FunctionContext, django_context: DjangoC
return outer_model_info.fullname()
if '.' not in model_string:
# same file class
current_module = ctx.api.tree
if model_string not in current_module.names:
ctx.api.fail(f'No model {model_string!r} defined in the current module', ctx.context)
return None
return outer_model_info.module_name + '.' + model_string
app_label, model_name = model_string.split('.')
@@ -121,19 +56,15 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context
return AnyType(TypeOfAny.from_error)
referred_to_typeinfo = helpers.lookup_fully_qualified_generic(referred_to_fullname, ctx.api.modules)
if referred_to_typeinfo is None:
ctx.api.fail(f'Cannot resolve {referred_to_fullname!r}. Please, report it to package developers.',
ctx.context)
return AnyType(TypeOfAny.from_error)
assert isinstance(referred_to_typeinfo, TypeInfo), f'Cannot resolve {referred_to_fullname!r}'
referred_to_type = Instance(referred_to_typeinfo, [])
default_related_field_type = set_descriptor_types_for_field(ctx)
# replace Any with referred_to_type
args = []
for default_arg in default_related_field_type.args:
args.append(convert_any_to_type(default_arg, referred_to_type))
args.append(helpers.convert_any_to_type(default_arg, referred_to_type))
return helpers.reparametrize_instance(default_related_field_type, new_args=args)
@@ -157,6 +88,12 @@ def transform_into_proper_return_type(ctx: FunctionContext, django_context: Djan
default_return_type = ctx.default_return_type
assert isinstance(default_return_type, Instance)
# bail out if we're inside migration, not supported yet
active_class = ctx.api.scope.active_class()
if active_class is not None:
if active_class.has_base(fullnames.MIGRATION_CLASS_FULLNAME):
return ctx.default_return_type
if helpers.has_any_of_bases(default_return_type.type, fullnames.RELATED_FIELDS_CLASSES):
return fill_descriptor_types_for_related_field(ctx, django_context)
@@ -176,104 +113,6 @@ def determine_type_of_array_field(ctx: FunctionContext, django_context: DjangoCo
base_type = base_field_arg_type.args[1] # extract __get__ type
args = []
for default_arg in default_return_type.args:
args.append(convert_any_to_type(default_arg, base_type))
args.append(helpers.convert_any_to_type(default_arg, base_type))
return helpers.reparametrize_instance(default_return_type, args)
# def _parse_choices_type(ctx: FunctionContext, choices_arg: Expression) -> Optional[str]:
# if isinstance(choices_arg, (TupleExpr, ListExpr)):
# # iterable of 2 element tuples of two kinds
# _, analyzed_choices = ctx.api.analyze_iterable_item_type(choices_arg)
# if isinstance(analyzed_choices, TupleType):
# first_element_type = analyzed_choices.items[0]
# if isinstance(first_element_type, Instance):
# return first_element_type.type.fullname()
# def _parse_referenced_model(ctx: FunctionContext, to_arg: Expression) -> Optional[TypeInfo]:
# if isinstance(to_arg, NameExpr) and isinstance(to_arg.node, TypeInfo):
# # reference to the model class
# return to_arg.node
#
# elif isinstance(to_arg, StrExpr):
# referenced_model_info = helpers.get_model_info(to_arg.value, ctx.api.modules)
# if referenced_model_info is not None:
# return referenced_model_info
# def parse_field_init_arguments_into_model_metadata(ctx: FunctionContext) -> None:
# outer_model = ctx.api.scope.active_class()
# if outer_model is None or not outer_model.has_base(fullnames.MODEL_CLASS_FULLNAME):
# # outside models.Model class, undetermined
# return
#
# # Determine name of the current field
# for attr_name, stmt in helpers.iter_over_class_level_assignments(outer_model.defn):
# if stmt == ctx.context:
# field_name = attr_name
# break
# else:
# return
#
# model_fields_metadata = metadata.get_fields_metadata(outer_model)
#
# # primary key
# is_primary_key = False
# primary_key_arg = helpers.get_call_argument_by_name(ctx, 'primary_key')
# if primary_key_arg:
# is_primary_key = helpers.parse_bool(primary_key_arg)
# model_fields_metadata[field_name] = {'primary_key': is_primary_key}
#
# # choices
# choices_arg = helpers.get_call_argument_by_name(ctx, 'choices')
# if choices_arg:
# choices_type_fullname = _parse_choices_type(ctx.api, choices_arg)
# if choices_type_fullname:
# model_fields_metadata[field_name]['choices_type'] = choices_type_fullname
#
# # nullability
# null_arg = helpers.get_call_argument_by_name(ctx, 'null')
# is_nullable = False
# if null_arg:
# is_nullable = helpers.parse_bool(null_arg)
# model_fields_metadata[field_name]['null'] = is_nullable
#
# # is_blankable
# blank_arg = helpers.get_call_argument_by_name(ctx, 'blank')
# is_blankable = False
# if blank_arg:
# is_blankable = helpers.parse_bool(blank_arg)
# model_fields_metadata[field_name]['blank'] = is_blankable
#
# # default
# default_arg = helpers.get_call_argument_by_name(ctx, 'default')
# if default_arg and not helpers.is_none_expr(default_arg):
# model_fields_metadata[field_name]['default_specified'] = True
#
# if helpers.has_any_of_bases(ctx.default_return_type.type, fullnames.RELATED_FIELDS_CLASSES):
# # to
# to_arg = helpers.get_call_argument_by_name(ctx, 'to')
# if to_arg:
# referenced_model = _parse_referenced_model(ctx, to_arg)
# if referenced_model is not None:
# model_fields_metadata[field_name]['to'] = referenced_model.fullname()
# else:
# model_fields_metadata[field_name]['to'] = to_arg.value
# # referenced_model = to_arg.value
# # raise helpers.IncompleteDefnException()
#
# # model_fields_metadata[field_name]['to'] = referenced_model.fullname()
# # if referenced_model is not None:
# # model_fields_metadata[field_name]['to'] = referenced_model.fullname()
# # else:
# # assert isinstance(to_arg, StrExpr)
# # model_fields_metadata[field_name]['to'] = to_arg.value
#
# # related_name
# related_name_arg = helpers.get_call_argument_by_name(ctx, 'related_name')
# if related_name_arg:
# if isinstance(related_name_arg, StrExpr):
# model_fields_metadata[field_name]['related_name'] = related_name_arg.value
# else:
# model_fields_metadata[field_name]['related_name'] = outer_model.name().lower() + '_set'

View File

@@ -108,8 +108,12 @@ def get_values_list_row_type(ctx: MethodContext, django_context: DjangoContext,
def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
# called on the Instance
assert isinstance(ctx.type, Instance)
assert isinstance(ctx.type.args[0], Instance)
# bail if queryset of Any or other non-instances
if not isinstance(ctx.type.args[0], Instance):
return AnyType(TypeOfAny.from_omitted_generics)
model_type = ctx.type.args[0]
model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname())