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) debug.warning("No __get__ defined on %s", self)
return ValueSet([self]) return ValueSet([self])
def py__get__on_class(self, calling_instance, instance, class_value):
return NotImplemented
def get_qualified_names(self): def get_qualified_names(self):
# Returns Optional[Tuple[str, ...]] # Returns Optional[Tuple[str, ...]]
return None return None

View File

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

View File

@@ -288,6 +288,11 @@ class _BaseTreeInstance(AbstractInstanceValue):
""" """
# Arguments in __get__ descriptors are obj, class. # Arguments in __get__ descriptors are obj, class.
# `method` is the new parent of the array, don't know if that's good. # `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__') names = self.get_function_slot_names(u'__get__')
if names: if names:
if instance is None: 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': if expr_stmt is not None and expr_stmt.type == 'expr_stmt':
annassign = expr_stmt.children[1] annassign = expr_stmt.children[1]
if annassign.type == 'annassign': if annassign.type == 'annassign':
# TODO this is not proper matching
# If there is an =, the variable is obviously also # If there is an =, the variable is obviously also
# defined on the class. # defined on the class.
if 'ClassVar' not in annassign.children[1].get_code() \ if 'ClassVar' not in annassign.children[1].get_code() \
@@ -138,7 +136,7 @@ class ClassMixin(object):
def is_class_mixin(self): def is_class_mixin(self):
return True return True
def py__call__(self, arguments=None): def py__call__(self, arguments):
from jedi.inference.value import TreeInstance from jedi.inference.value import TreeInstance
from jedi.inference.gradual.typing import TypedDict from jedi.inference.gradual.typing import TypedDict
@@ -195,7 +193,7 @@ class ClassMixin(object):
metaclasses = self.get_metaclasses() metaclasses = self.get_metaclasses()
if metaclasses: if metaclasses:
for f in self.get_metaclass_filters(metaclasses, is_instance): for f in self.get_metaclass_filters(metaclasses, is_instance):
yield f yield f # Python 2..
for cls in self.py__mro__(): for cls in self.py__mro__():
if cls.is_compiled(): if cls.is_compiled():
@@ -203,7 +201,7 @@ class ClassMixin(object):
yield filter yield filter
else: else:
yield ClassFilter( yield ClassFilter(
self, node_context=cls.as_context(), cls, node_context=self.as_context(),
origin_scope=origin_scope, origin_scope=origin_scope,
is_instance=is_instance is_instance=is_instance
) )

View File

@@ -2,12 +2,15 @@
Module is used to infer Django model fields. Module is used to infer Django model fields.
""" """
from jedi import debug from jedi import debug
from jedi.inference.base_value import ValueSet, iterator_to_value_set from jedi.inference.base_value import ValueSet, iterator_to_value_set, ValueWrapper
from jedi.inference.filters import ParserTreeFilter, DictFilter from jedi.inference.filters import DictFilter, AttributeOverwrite, publish_method
from jedi.inference.names import NameWrapper from jedi.inference.names import NameWrapper
from jedi.inference.compiled.value import EmptyCompiledName
from jedi.inference.value.instance import TreeInstance 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.base import GenericClass
from jedi.inference.gradual.generics import TupleGenericManager from jedi.inference.gradual.generics import TupleGenericManager
from jedi.inference.arguments import repack_with_argument_clinic
mapping = { mapping = {
@@ -124,36 +127,6 @@ def _create_manager_for(cls, manager_cls='BaseManager'):
def _new_dict_filter(cls, is_instance): 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( filters = list(cls.get_filters(
is_instance=is_instance, is_instance=is_instance,
include_metaclasses=False, include_metaclasses=False,
@@ -164,10 +137,14 @@ def _new_dict_filter(cls, is_instance):
for filter_ in reversed(filters) for filter_ in reversed(filters)
for name in filter_.values() for name in filter_.values()
} }
if is_instance:
manager_name = get_manager_name(filters) # Replace the objects with a name that amounts to nothing when accessed
if manager_name: # in an instance. This is not perfect and still completes "objects" in
dct['objects'] = manager_name # 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) return DictFilter(dct)
@@ -181,3 +158,32 @@ def get_metaclass_filters(func):
return func(cls, metaclasses, is_instance) return func(cls, metaclasses, is_instance)
return wrapper 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 # Queries
# ----------------- # -----------------
#? models.query.QuerySet.filter #? ['objects']
model_instance.object
#?
model_instance.objects
#?
model_instance.objects.filter model_instance.objects.filter
#? models.query.QuerySet.filter
BusinessModel.objects.filter
#? BusinessModel() None #? BusinessModel() None
model_instance.objects.filter().first() BusinessModel.objects.filter().first()
#? str() #? str()
model_instance.objects.get().char_field BusinessModel.objects.get().char_field
#? int() #? int()
model_instance.objects.update(x='') BusinessModel.objects.update(x='')
#? BusinessModel() #? BusinessModel()
model_instance.objects.create() BusinessModel.objects.create()
# ----------------- # -----------------
# Custom object manager # Custom object manager
@@ -179,9 +185,13 @@ model_instance.objects.create()
#? TagManager() #? TagManager()
Tag.objects Tag.objects
#? Tag() None
Tag.objects.filter().first()
#? TagManager() #? TagManager()
Tag.custom_objects Tag.custom_objects
#? Tag() None
Tag.custom_objects.filter().first()
# ----------------- # -----------------
# Inheritance # Inheritance
@@ -199,14 +209,27 @@ inherited.char_field
#? float() #? float()
inherited.new_field inherited.new_field
#?
Inherited.category_fk2.category_name
#? str() #? str()
inherited.category_fk2.category_name inherited.category_fk2.category_name
#? str() #? str()
inherited.objects.get().char_field Inherited.objects.get().char_field
#? int() #? int()
inherited.objects.get().text_field Inherited.objects.get().text_field
#? float() #? 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 # 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() #? 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']