diff --git a/mypy_django_plugin/lib/fullnames.py b/mypy_django_plugin/lib/fullnames.py index 64bcd87..a3e4e48 100644 --- a/mypy_django_plugin/lib/fullnames.py +++ b/mypy_django_plugin/lib/fullnames.py @@ -32,4 +32,6 @@ RELATED_FIELDS_CLASSES = { FOREIGN_KEY_FULLNAME, ONETOONE_FIELD_FULLNAME, MANYTOMANY_FIELD_FULLNAME -} \ No newline at end of file +} + +MIGRATION_CLASS_FULLNAME = 'django.db.migrations.migration.Migration' diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py index c5320b6..ec3a7e9 100644 --- a/mypy_django_plugin/transformers/fields.py +++ b/mypy_django_plugin/transformers/fields.py @@ -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' diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py index d8119cc..12aadf4 100644 --- a/mypy_django_plugin/transformers/querysets.py +++ b/mypy_django_plugin/transformers/querysets.py @@ -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())