Use py__get__ for Django Model.objects

This includes the fix in https://github.com/typeddjango/django-stubs/pull/394
This commit is contained in:
Dave Halter
2020-06-09 23:26:39 +02:00
parent 6d0d75c7d9
commit a2108de2c0
6 changed files with 93 additions and 52 deletions

View File

@@ -240,6 +240,9 @@ class Value(HelperValueMixin):
debug.warning("No __get__ defined on %s", self)
return ValueSet([self])
def py__get__on_class(self, calling_instance, instance, class_value):
return NotImplemented
def get_qualified_names(self):
# Returns Optional[Tuple[str, ...]]
return None

View File

@@ -200,6 +200,9 @@ class GenericClass(DefineGenericBaseClass, ClassMixin):
return True
return self._class_value.is_sub_class_of(class_value)
def with_generics(self, generics_tuple):
return self._class_value.with_generics(generics_tuple)
def infer_type_vars(self, value_set):
# Circular
from jedi.inference.gradual.annotation import merge_pairwise_generics, merge_type_var_dicts
@@ -287,6 +290,9 @@ class _LazyGenericBaseClass(object):
new |= ValueSet([type_var])
yield new
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self._lazy_base_class)
class _GenericInstanceWrapper(ValueWrapper):
def py__stop_iteration_returns(self):

View File

@@ -288,6 +288,11 @@ class _BaseTreeInstance(AbstractInstanceValue):
"""
# Arguments in __get__ descriptors are obj, class.
# `method` is the new parent of the array, don't know if that's good.
for cls in self.class_value.py__mro__():
result = cls.py__get__on_class(self, instance, class_value)
if result is not NotImplemented:
return result
names = self.get_function_slot_names(u'__get__')
if names:
if instance is None:

View File

@@ -114,8 +114,6 @@ class ClassFilter(ParserTreeFilter):
if expr_stmt is not None and expr_stmt.type == 'expr_stmt':
annassign = expr_stmt.children[1]
if annassign.type == 'annassign':
# TODO this is not proper matching
# If there is an =, the variable is obviously also
# defined on the class.
if 'ClassVar' not in annassign.children[1].get_code() \
@@ -138,7 +136,7 @@ class ClassMixin(object):
def is_class_mixin(self):
return True
def py__call__(self, arguments=None):
def py__call__(self, arguments):
from jedi.inference.value import TreeInstance
from jedi.inference.gradual.typing import TypedDict
@@ -195,7 +193,7 @@ class ClassMixin(object):
metaclasses = self.get_metaclasses()
if metaclasses:
for f in self.get_metaclass_filters(metaclasses, is_instance):
yield f
yield f # Python 2..
for cls in self.py__mro__():
if cls.is_compiled():
@@ -203,7 +201,7 @@ class ClassMixin(object):
yield filter
else:
yield ClassFilter(
self, node_context=cls.as_context(),
cls, node_context=self.as_context(),
origin_scope=origin_scope,
is_instance=is_instance
)

View File

@@ -2,12 +2,15 @@
Module is used to infer Django model fields.
"""
from jedi import debug
from jedi.inference.base_value import ValueSet, iterator_to_value_set
from jedi.inference.filters import ParserTreeFilter, DictFilter
from jedi.inference.base_value import ValueSet, iterator_to_value_set, ValueWrapper
from jedi.inference.filters import DictFilter, AttributeOverwrite, publish_method
from jedi.inference.names import NameWrapper
from jedi.inference.compiled.value import EmptyCompiledName
from jedi.inference.value.instance import TreeInstance
from jedi.inference.value.klass import ClassMixin
from jedi.inference.gradual.base import GenericClass
from jedi.inference.gradual.generics import TupleGenericManager
from jedi.inference.arguments import repack_with_argument_clinic
mapping = {
@@ -124,36 +127,6 @@ def _create_manager_for(cls, manager_cls='BaseManager'):
def _new_dict_filter(cls, is_instance):
def get_manager_name(filters):
for f in filters:
names = f.get('objects')
if not names:
continue
# Found a match. Either the model has a custom manager, or we're
# now in django.db.models.Model. If the latter we need to use
# `_create_manager_for` because the manager we get from the
# stubs doesn't work right.
name = names[0] # The first name should be good enough.
parent = name.get_defining_qualified_value()
if parent.py__name__() == 'Model':
django_models_model, = cls.inference_state.import_module(
('django', 'db', 'models', 'base'),
).py__getattribute__('Model')
if django_models_model == parent:
# Don't want to use the value from the Django stubs, but
# we have found the point where they'd take precedence.
break
return name
manager = _create_manager_for(cls)
if manager:
return manager.name
filters = list(cls.get_filters(
is_instance=is_instance,
include_metaclasses=False,
@@ -164,10 +137,14 @@ def _new_dict_filter(cls, is_instance):
for filter_ in reversed(filters)
for name in filter_.values()
}
manager_name = get_manager_name(filters)
if manager_name:
dct['objects'] = manager_name
if is_instance:
# Replace the objects with a name that amounts to nothing when accessed
# in an instance. This is not perfect and still completes "objects" in
# that case, but it at least not inferes stuff like `.objects.filter`.
# It would be nicer to do that in a better way, so that it also doesn't
# show up in completions, but it's probably just not worth doing that
# for the extra amount of work.
dct['objects'] = EmptyCompiledName(cls.inference_state, 'objects')
return DictFilter(dct)
@@ -181,3 +158,32 @@ def get_metaclass_filters(func):
return func(cls, metaclasses, is_instance)
return wrapper
class ManagerWrapper(ValueWrapper):
def py__getitem__(self, index_value_set, contextualized_node):
return ValueSet(
GenericManagerWrapper(generic)
for generic in self._wrapped_value.py__getitem__(
index_value_set, contextualized_node)
)
class GenericManagerWrapper(AttributeOverwrite, ClassMixin):
def py__get__on_class(self, calling_instance, instance, class_value):
return calling_instance.class_value.with_generics(
(ValueSet({class_value}),)
).py__call__(calling_instance._arguments)
def with_generics(self, generics_tuple):
return self._wrapped_value.with_generics(generics_tuple)
def tree_name_to_values(func):
def wrapper(inference_state, context, tree_name):
result = func(inference_state, context, tree_name)
if tree_name.value == 'BaseManager' and context.is_module() \
and context.py__name__() == 'django.db.models.manager':
return ValueSet(ManagerWrapper(r) for r in result)
return result
return wrapper

View File

@@ -162,16 +162,22 @@ model_instance.method
# Queries
# -----------------
#? models.query.QuerySet.filter
#? ['objects']
model_instance.object
#?
model_instance.objects
#?
model_instance.objects.filter
#? models.query.QuerySet.filter
BusinessModel.objects.filter
#? BusinessModel() None
model_instance.objects.filter().first()
BusinessModel.objects.filter().first()
#? str()
model_instance.objects.get().char_field
BusinessModel.objects.get().char_field
#? int()
model_instance.objects.update(x='')
BusinessModel.objects.update(x='')
#? BusinessModel()
model_instance.objects.create()
BusinessModel.objects.create()
# -----------------
# Custom object manager
@@ -179,9 +185,13 @@ model_instance.objects.create()
#? TagManager()
Tag.objects
#? Tag() None
Tag.objects.filter().first()
#? TagManager()
Tag.custom_objects
#? Tag() None
Tag.custom_objects.filter().first()
# -----------------
# Inheritance
@@ -199,14 +209,27 @@ inherited.char_field
#? float()
inherited.new_field
#?
Inherited.category_fk2.category_name
#? str()
inherited.category_fk2.category_name
#? str()
inherited.objects.get().char_field
Inherited.objects.get().char_field
#? int()
inherited.objects.get().text_field
Inherited.objects.get().text_field
#? float()
inherited.objects.get().new_field
Inherited.objects.get().new_field
# -----------------
# Model methods
# -----------------
#? ['from_db']
Inherited.from_db
#? ['validate_unique']
Inherited.validate_uniqu
#? ['validate_unique']
Inherited().validate_unique
# -----------------
# Django Auth
@@ -222,8 +245,8 @@ User.objects.get().email
# -----------------
#?
model_instance.objects.values_list('char_field')[0]
BusinessModel.objects.values_list('char_field')[0]
#? dict()
model_instance.objects.values('char_field')[0]
BusinessModel.objects.values('char_field')[0]
#?
model_instance.objects.values('char_field')[0]['char_field']
BusinessModel.objects.values('char_field')[0]['char_field']