diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index 075de49a3..6cd483291 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -240,6 +240,9 @@ def py__get__(self, instance, class_value): 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 diff --git a/jedi/inference/gradual/base.py b/jedi/inference/gradual/base.py index f5c05d17a..68fab7f1a 100644 --- a/jedi/inference/gradual/base.py +++ b/jedi/inference/gradual/base.py @@ -200,6 +200,9 @@ def is_sub_class_of(self, class_value): 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 @@ def _remap_type_vars(self, base): 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): diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index bc06cfd49..5435c7919 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -288,6 +288,11 @@ def py__get__(self, instance, class_value): """ # 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: diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 1a8cac2fe..46802ad8e 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -114,8 +114,6 @@ def _access_possible(self, name): 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 @@ def is_class(self): 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 @@ def get_filters(self, origin_scope=None, is_instance=False, 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 @@ def get_filters(self, origin_scope=None, is_instance=False, 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 ) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 69ba61395..5004ddb14 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -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 get_manager_name(filters): 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 wrapper(cls, metaclasses, is_instance): 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 diff --git a/test/completion/django.py b/test/completion/django.py index 6df0c3a33..f2ed27f7f 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -162,16 +162,22 @@ def method(self): # 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 @@ def method(self): #? 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 @@ class Inherited(BusinessModel): #? 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 @@ class Inherited(BusinessModel): # ----------------- #? -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']