mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-08 21:14:49 +08:00
* Instead of using Literal types, overload QuerySet.values_list in the plugin. Fixes #43. - Add a couple of extra type checks that Django makes: 1) 'flat' and 'named' can't be used together. 2) 'flat' is not valid when values_list is called with more than one field. * Determine better row types for values_list/values based on fields specified. - In the case of values_list, we use a Row type with either a single primitive, Tuple, or NamedTuple. - In the case of values, we use a TypedDict. - In both cases, Any is used as a fallback for individual fields if those fields cannot be resolved. A couple other fixes I made along the way: - Don't create reverse relation for ForeignKeys with related_name='+' - Don't skip creating other related managers in AddRelatedManagers if a dynamic value is encountered for related_name parameter, or if the type cannot be determined. * Fix for TypedDict so that they are considered anonymous. * Clean up some comments. * Implement making TypedDict anonymous in a way that doesn't crash sometimes. * Fix flake8 errors. * Remove even uglier hack about making TypedDict anonymous. * Address review comments. Write a few better comments inside tests. * Fix crash when running with mypyc ("interpreted classes cannot inherit from compiled") due to the way I extended TypedDictType. - Implemented the hack in another way that works on mypyc. - Added a couple extra tests of accessing 'id' / 'pk' via values_list. * Fix flake8 errors. * Support annotation expressions (use type Any) for TypedDicts row types returned by values_list. - Bonus points: handle values_list gracefully (use type Any) where Tuples are returned where some of the fields arguments are not string literals.
189 lines
7.5 KiB
Python
189 lines
7.5 KiB
Python
from typing import Optional, cast
|
|
|
|
from mypy.checker import TypeChecker
|
|
from mypy.nodes import ListExpr, NameExpr, StrExpr, TupleExpr, TypeInfo
|
|
from mypy.plugin import FunctionContext
|
|
from mypy.types import (
|
|
AnyType, CallableType, Instance, TupleType, Type, UnionType,
|
|
)
|
|
|
|
from mypy_django_plugin import helpers
|
|
|
|
|
|
def extract_referred_to_type(ctx: FunctionContext) -> 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}',
|
|
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
|
|
try:
|
|
model_fullname = helpers.get_model_fullname_from_string(to_arg_expr.value,
|
|
all_modules=api.modules)
|
|
except helpers.SelfReference:
|
|
model_fullname = api.tscope.classes[-1].fullname()
|
|
|
|
except helpers.SameFileModel as exc:
|
|
model_fullname = api.tscope.classes[-1].module_name + '.' + exc.model_cls_name
|
|
|
|
if model_fullname is None:
|
|
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):
|
|
return None
|
|
return Instance(model_info, [])
|
|
|
|
referred_to_type = arg_type.ret_type
|
|
if not isinstance(referred_to_type, Instance):
|
|
return None
|
|
if not referred_to_type.type.has_base(helpers.MODEL_CLASS_FULLNAME):
|
|
ctx.api.msg.fail(f'to= parameter value must be '
|
|
f'a subclass of {helpers.MODEL_CLASS_FULLNAME}',
|
|
context=ctx.context)
|
|
return None
|
|
|
|
return referred_to_type
|
|
|
|
|
|
def convert_any_to_type(typ: Type, referred_to_type: Type) -> Type:
|
|
if isinstance(typ, UnionType):
|
|
converted_items = []
|
|
for item in typ.items:
|
|
converted_items.append(convert_any_to_type(item, referred_to_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(referred_to_type)
|
|
else:
|
|
args.append(default_arg)
|
|
return helpers.reparametrize_instance(typ, args)
|
|
|
|
if isinstance(typ, AnyType):
|
|
return referred_to_type
|
|
|
|
return typ
|
|
|
|
|
|
def fill_descriptor_types_for_related_field(ctx: FunctionContext) -> Type:
|
|
default_return_type = set_descriptor_types_for_field(ctx)
|
|
referred_to_type = extract_referred_to_type(ctx)
|
|
if referred_to_type is None:
|
|
return default_return_type
|
|
|
|
# replace Any with referred_to_type
|
|
args = []
|
|
for default_arg in default_return_type.args:
|
|
args.append(convert_any_to_type(default_arg, referred_to_type))
|
|
|
|
return helpers.reparametrize_instance(ctx.default_return_type, new_args=args)
|
|
|
|
|
|
def set_descriptor_types_for_field(ctx: FunctionContext) -> Instance:
|
|
default_return_type = cast(Instance, ctx.default_return_type)
|
|
is_nullable = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'null'))
|
|
set_type = helpers.get_private_descriptor_type(default_return_type.type, '_pyi_private_set_type',
|
|
is_nullable=is_nullable)
|
|
get_type = helpers.get_private_descriptor_type(default_return_type.type, '_pyi_private_get_type',
|
|
is_nullable=is_nullable)
|
|
return helpers.reparametrize_instance(default_return_type, [set_type, get_type])
|
|
|
|
|
|
def determine_type_of_array_field(ctx: FunctionContext) -> Type:
|
|
default_return_type = set_descriptor_types_for_field(ctx)
|
|
|
|
base_field_arg_type = helpers.get_argument_type_by_name(ctx, 'base_field')
|
|
if not base_field_arg_type or not isinstance(base_field_arg_type, Instance):
|
|
return default_return_type
|
|
|
|
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))
|
|
|
|
return helpers.reparametrize_instance(default_return_type, args)
|
|
|
|
|
|
def transform_into_proper_return_type(ctx: FunctionContext) -> Type:
|
|
default_return_type = ctx.default_return_type
|
|
if not isinstance(default_return_type, Instance):
|
|
return default_return_type
|
|
|
|
if helpers.has_any_of_bases(default_return_type.type, (helpers.FOREIGN_KEY_FULLNAME,
|
|
helpers.ONETOONE_FIELD_FULLNAME,
|
|
helpers.MANYTOMANY_FIELD_FULLNAME)):
|
|
return fill_descriptor_types_for_related_field(ctx)
|
|
|
|
if default_return_type.type.has_base(helpers.ARRAY_FIELD_FULLNAME):
|
|
return determine_type_of_array_field(ctx)
|
|
|
|
return set_descriptor_types_for_field(ctx)
|
|
|
|
|
|
def adjust_return_type_of_field_instantiation(ctx: FunctionContext) -> Type:
|
|
record_field_properties_into_outer_model_class(ctx)
|
|
return transform_into_proper_return_type(ctx)
|
|
|
|
|
|
def record_field_properties_into_outer_model_class(ctx: FunctionContext) -> None:
|
|
api = cast(TypeChecker, ctx.api)
|
|
outer_model = api.scope.active_class()
|
|
if outer_model is None or not outer_model.has_base(helpers.MODEL_CLASS_FULLNAME):
|
|
# outside models.Model class, undetermined
|
|
return
|
|
|
|
field_name = None
|
|
for name_expr, stmt in helpers.iter_over_assignments(outer_model.defn):
|
|
if stmt == ctx.context and isinstance(name_expr, NameExpr):
|
|
field_name = name_expr.name
|
|
break
|
|
if field_name is None:
|
|
return
|
|
|
|
fields_metadata = outer_model.metadata.setdefault('django', {}).setdefault('fields', {})
|
|
|
|
# primary key
|
|
is_primary_key = False
|
|
primary_key_arg = helpers.get_argument_by_name(ctx, 'primary_key')
|
|
if primary_key_arg:
|
|
is_primary_key = helpers.parse_bool(primary_key_arg)
|
|
fields_metadata[field_name] = {'primary_key': is_primary_key}
|
|
|
|
# choices
|
|
choices_arg = helpers.get_argument_by_name(ctx, 'choices')
|
|
if choices_arg and isinstance(choices_arg, (TupleExpr, ListExpr)):
|
|
# iterable of 2 element tuples of two kinds
|
|
_, analyzed_choices = 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):
|
|
fields_metadata[field_name]['choices'] = first_element_type.type.fullname()
|
|
|
|
# nullability
|
|
null_arg = helpers.get_argument_by_name(ctx, 'null')
|
|
is_nullable = False
|
|
if null_arg:
|
|
is_nullable = helpers.parse_bool(null_arg)
|
|
fields_metadata[field_name]['null'] = is_nullable
|
|
|
|
# is_blankable
|
|
blank_arg = helpers.get_argument_by_name(ctx, 'blank')
|
|
is_blankable = False
|
|
if blank_arg:
|
|
is_blankable = helpers.parse_bool(blank_arg)
|
|
fields_metadata[field_name]['blank'] = is_blankable
|
|
|
|
# default
|
|
default_arg = helpers.get_argument_by_name(ctx, 'default')
|
|
if default_arg and not helpers.is_none_expr(default_arg):
|
|
fields_metadata[field_name]['default_specified'] = True
|