Add support for Django signatures, fixes parts of #1587

This commit is contained in:
Dave Halter
2020-06-13 16:18:47 +02:00
parent b165596a6e
commit 3415ccbb73
3 changed files with 116 additions and 15 deletions

View File

@@ -227,6 +227,11 @@ class ClassMixin(object):
# Since calling staticmethod without a function is illegal, the Jedi # Since calling staticmethod without a function is illegal, the Jedi
# plugin doesn't return anything. Therefore call directly and get what # plugin doesn't return anything. Therefore call directly and get what
# we want: An instance of staticmethod. # we want: An instance of staticmethod.
metaclasses = self.get_metaclasses()
if metaclasses:
sigs = self.get_metaclass_signatures(metaclasses)
if sigs:
return sigs
args = ValuesArguments([]) args = ValuesArguments([])
init_funcs = self.py__call__(args).py__getattribute__('__init__') init_funcs = self.py__call__(args).py__getattribute__('__init__')
return [sig.bind(self) for sig in init_funcs.get_signatures()] return [sig.bind(self) for sig in init_funcs.get_signatures()]
@@ -360,8 +365,8 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase
)] )]
@plugin_manager.decorate() @plugin_manager.decorate()
def get_metaclass_filters(self, metaclass, is_instance): def get_metaclass_filters(self, metaclasses, is_instance):
debug.warning('Unprocessed metaclass %s', metaclass) debug.warning('Unprocessed metaclass %s', metaclasses)
return [] return []
@inference_state_method_cache(default=NO_VALUES) @inference_state_method_cache(default=NO_VALUES)
@@ -381,3 +386,7 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase
if values: if values:
return values return values
return NO_VALUES return NO_VALUES
@plugin_manager.decorate()
def get_metaclass_signatures(self, metaclasses):
return []

View File

@@ -1,16 +1,18 @@
""" """
Module is used to infer Django model fields. Module is used to infer Django model fields.
""" """
from jedi._compatibility import Parameter
from jedi import debug from jedi import debug
from jedi.inference.cache import inference_state_function_cache
from jedi.inference.base_value import ValueSet, iterator_to_value_set, ValueWrapper from jedi.inference.base_value import ValueSet, iterator_to_value_set, ValueWrapper
from jedi.inference.filters import DictFilter, AttributeOverwrite, publish_method from jedi.inference.filters import DictFilter, AttributeOverwrite
from jedi.inference.names import NameWrapper from jedi.inference.names import NameWrapper, BaseTreeParamName
from jedi.inference.compiled.value import EmptyCompiledName 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.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 from jedi.inference.signature import AbstractSignature
mapping = { mapping = {
@@ -35,6 +37,7 @@ mapping = {
} }
@inference_state_function_cache()
def _get_deferred_attributes(inference_state): def _get_deferred_attributes(inference_state):
return inference_state.import_module( return inference_state.import_module(
('django', 'db', 'models', 'query_utils') ('django', 'db', 'models', 'query_utils')
@@ -150,17 +153,56 @@ def _new_dict_filter(cls, is_instance):
return DictFilter(dct) return DictFilter(dct)
def is_django_model_base(value):
return value.py__name__() == 'ModelBase' \
and value.get_root_context().py__name__() == 'django.db.models.base'
def get_metaclass_filters(func): def get_metaclass_filters(func):
def wrapper(cls, metaclasses, is_instance): def wrapper(cls, metaclasses, is_instance):
for metaclass in metaclasses: for metaclass in metaclasses:
if metaclass.py__name__() == 'ModelBase' \ if is_django_model_base(metaclass):
and metaclass.get_root_context().py__name__() == 'django.db.models.base':
return [_new_dict_filter(cls, is_instance)] return [_new_dict_filter(cls, is_instance)]
return func(cls, metaclasses, is_instance) return func(cls, metaclasses, is_instance)
return wrapper return wrapper
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)
if tree_name.value == 'Field' and context.is_module() \
and context.py__name__() == 'django.db.models.fields':
return ValueSet(FieldWrapper(r) for r in result)
return result
return wrapper
def _find_fields(cls):
for name in _new_dict_filter(cls, is_instance=False).values():
for value in name.infer():
if value.name.get_qualified_names(include_module_names=True) \
== ('django', 'db', 'models', 'query_utils', 'DeferredAttribute'):
yield name
def _get_signatures(cls):
return [DjangoModelSignature(cls, field_names=list(_find_fields(cls)))]
def get_metaclass_signatures(func):
def wrapper(cls, metaclasses):
for metaclass in metaclasses:
if is_django_model_base(metaclass):
return _get_signatures(cls)
return func(cls, metaclass)
return wrapper
class ManagerWrapper(ValueWrapper): class ManagerWrapper(ValueWrapper):
def py__getitem__(self, index_value_set, contextualized_node): def py__getitem__(self, index_value_set, contextualized_node):
return ValueSet( return ValueSet(
@@ -180,11 +222,38 @@ class GenericManagerWrapper(AttributeOverwrite, ClassMixin):
return self._wrapped_value.with_generics(generics_tuple) return self._wrapped_value.with_generics(generics_tuple)
def tree_name_to_values(func): class FieldWrapper(ValueWrapper):
def wrapper(inference_state, context, tree_name): def py__getitem__(self, index_value_set, contextualized_node):
result = func(inference_state, context, tree_name) return ValueSet(
if tree_name.value == 'BaseManager' and context.is_module() \ GenericFieldWrapper(generic)
and context.py__name__() == 'django.db.models.manager': for generic in self._wrapped_value.py__getitem__(
return ValueSet(ManagerWrapper(r) for r in result) index_value_set, contextualized_node)
return result )
return wrapper
class GenericFieldWrapper(AttributeOverwrite, ClassMixin):
def py__get__on_class(self, calling_instance, instance, class_value):
# This is mostly an optimization to avoid Jedi aborting inference,
# because of too many function executions of Field.__get__.
return ValueSet({calling_instance})
class DjangoModelSignature(AbstractSignature):
def __init__(self, value, field_names):
super(DjangoModelSignature, self).__init__(value)
self._field_names = field_names
def get_param_names(self, resolve_stars=False):
return [DjangoParamName(name) for name in self._field_names]
class DjangoParamName(BaseTreeParamName):
def __init__(self, field_name):
super(DjangoParamName, self).__init__(field_name.parent_context, field_name.tree_name)
self._field_name = field_name
def get_kind(self):
return Parameter.KEYWORD_ONLY
def infer(self):
return self._field_name.infer()

View File

@@ -73,6 +73,8 @@ class BusinessModel(models.Model):
BusinessModel.integer_field BusinessModel.integer_field
#? DeferredAttribute() #? DeferredAttribute()
BusinessModel.tags_m2m BusinessModel.tags_m2m
#? DeferredAttribute()
BusinessModel.email_field
model_instance = BusinessModel() model_instance = BusinessModel()
@@ -250,3 +252,24 @@ BusinessModel.objects.values_list('char_field')[0]
BusinessModel.objects.values('char_field')[0] BusinessModel.objects.values('char_field')[0]
#? #?
BusinessModel.objects.values('char_field')[0]['char_field'] BusinessModel.objects.values('char_field')[0]['char_field']
# -----------------
# Completion
# -----------------
#? 19 ['text_field=']
Inherited(text_fiel)
#? 18 ['new_field=']
Inherited(new_fiel)
#? 19 ['char_field=']
Inherited(char_fiel)
#? 19 ['email_field=']
Inherited(email_fie)
#? 19 []
Inherited(unidentif)
#? 21 ['category_fk=', 'category_fk2=', 'category_fk3=', 'category_fk4=', 'category_fk5=']
Inherited(category_fk)
#? 21 ['attached_o2o=']
Inherited(attached_o2)
#? 18 ['tags_m2m=']
Inherited(tags_m2m)