Specific return types for values and values list (#53)

* 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.
This commit is contained in:
Seth Yastrov
2019-03-25 12:53:09 +03:00
committed by Maxim Kurnikov
parent 5c6be7ad12
commit 5b455b729a
11 changed files with 648 additions and 72 deletions
+23 -5
View File
@@ -172,17 +172,35 @@ class AddRelatedManagers(ModelClassInitializer):
ref_to_fullname = module_name + '.' + exc.model_cls_name
if self.model_classdef.fullname == ref_to_fullname:
related_manager_name = defn.name.lower() + '_set'
related_name = defn.name.lower() + '_set'
if 'related_name' in rvalue.arg_names:
related_name_expr = rvalue.args[rvalue.arg_names.index('related_name')]
if not isinstance(related_name_expr, StrExpr):
return None
related_manager_name = related_name_expr.value
continue
related_name = related_name_expr.value
if related_name == '+':
# No backwards relation is desired
continue
if 'related_query_name' in rvalue.arg_names:
related_query_name_expr = rvalue.args[rvalue.arg_names.index('related_query_name')]
if not isinstance(related_query_name_expr, StrExpr):
related_query_name = None
else:
related_query_name = related_query_name_expr.value
# TODO: Handle defaulting to model name if related_name is not set
else:
# No related_query_name specified, default to related_name
related_query_name = related_name
typ = get_related_field_type(rvalue, self.api, defn.info)
if typ is None:
return None
self.add_new_node_to_model_class(related_manager_name, typ)
continue
self.add_new_node_to_model_class(related_name, typ)
if related_query_name is not None:
# Only create related_query_name if it is a string literal
helpers.get_lookups_metadata(self.model_classdef.info)[related_query_name] = {
'related_query_name_target': related_name
}
def iter_over_classdefs(module_file: MypyFile) -> Iterator[ClassDef]: