diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 8d93a120..33510445 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -227,6 +227,11 @@ class ClassMixin(object): # Since calling staticmethod without a function is illegal, the Jedi # plugin doesn't return anything. Therefore call directly and get what # we want: An instance of staticmethod. + metaclasses = self.get_metaclasses() + if metaclasses: + sigs = self.get_metaclass_signatures(metaclasses) + if sigs: + return sigs args = ValuesArguments([]) init_funcs = self.py__call__(args).py__getattribute__('__init__') 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() - def get_metaclass_filters(self, metaclass, is_instance): - debug.warning('Unprocessed metaclass %s', metaclass) + def get_metaclass_filters(self, metaclasses, is_instance): + debug.warning('Unprocessed metaclass %s', metaclasses) return [] @inference_state_method_cache(default=NO_VALUES) @@ -381,3 +386,7 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase if values: return values return NO_VALUES + + @plugin_manager.decorate() + def get_metaclass_signatures(self, metaclasses): + return [] diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 8e7f4e29..6e262a12 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -1,16 +1,18 @@ """ Module is used to infer Django model fields. """ +from jedi._compatibility import Parameter 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.filters import DictFilter, AttributeOverwrite, publish_method -from jedi.inference.names import NameWrapper +from jedi.inference.filters import DictFilter, AttributeOverwrite +from jedi.inference.names import NameWrapper, BaseTreeParamName 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 +from jedi.inference.signature import AbstractSignature mapping = { @@ -35,6 +37,7 @@ mapping = { } +@inference_state_function_cache() def _get_deferred_attributes(inference_state): return inference_state.import_module( ('django', 'db', 'models', 'query_utils') @@ -150,17 +153,56 @@ def _new_dict_filter(cls, is_instance): 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 wrapper(cls, metaclasses, is_instance): for metaclass in metaclasses: - if metaclass.py__name__() == 'ModelBase' \ - and metaclass.get_root_context().py__name__() == 'django.db.models.base': + if is_django_model_base(metaclass): return [_new_dict_filter(cls, is_instance)] return func(cls, metaclasses, is_instance) 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): def py__getitem__(self, index_value_set, contextualized_node): return ValueSet( @@ -180,11 +222,38 @@ class GenericManagerWrapper(AttributeOverwrite, ClassMixin): 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 +class FieldWrapper(ValueWrapper): + def py__getitem__(self, index_value_set, contextualized_node): + return ValueSet( + GenericFieldWrapper(generic) + for generic in self._wrapped_value.py__getitem__( + index_value_set, contextualized_node) + ) + + +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() diff --git a/test/completion/django.py b/test/completion/django.py index f2ed27f7..5387e990 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -73,6 +73,8 @@ class BusinessModel(models.Model): BusinessModel.integer_field #? DeferredAttribute() BusinessModel.tags_m2m +#? DeferredAttribute() +BusinessModel.email_field 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]['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)