From 669b70b2cd7ff9349c33c641d571f7da6730c5e7 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Mon, 18 May 2020 22:37:23 +0100 Subject: [PATCH 01/16] Validate instance methods on Django models --- test/completion/django.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/completion/django.py b/test/completion/django.py index 88a508d9..16d31202 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -49,6 +49,9 @@ class BusinessModel(models.Model): unidentifiable = NOT_FOUND + def method(self): + return 42 + # ----------------- # Model attribute inference # ----------------- @@ -132,6 +135,11 @@ model_instance.unidentifiable #! ['unidentifiable = NOT_FOUND'] model_instance.unidentifiable +#? int() +model_instance.method() +#! ['def method'] +model_instance.method + # ----------------- # Queries # ----------------- From c36904d983fabf49f89c89c128ada3732b10f73b Mon Sep 17 00:00:00 2001 From: Peter Law Date: Fri, 22 May 2020 12:33:03 +0100 Subject: [PATCH 02/16] Support custom managers in Django models For the moment this support is limited to just Model.objects replacements and does not use the custom manager for ForeignKey related managers. --- jedi/plugins/django.py | 42 ++++++++++++++++++++++++++++++++++----- test/completion/django.py | 19 ++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 43cbaf01..83be0db9 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -110,15 +110,47 @@ def _create_manager_for(cls, manager_cls='BaseManager'): def _new_dict_filter(cls): - filters = cls.get_filters(is_instance=True, include_metaclasses=False) + 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=True, include_metaclasses=False)) dct = { name.string_name: DjangoModelName(cls, name) - for filter_ in reversed(list(filters)) + for filter_ in reversed(filters) for name in filter_.values() } - manager = _create_manager_for(cls) - if manager: - dct['objects'] = manager.name + + manager_name = get_manager_name(filters) + if manager_name: + dct['objects'] = manager_name + return DictFilter(dct) diff --git a/test/completion/django.py b/test/completion/django.py index 16d31202..6ed34ce1 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -6,9 +6,18 @@ from django.db import models from django.contrib.auth.models import User +class TagManager(models.Manager): + def specially_filtered_tags(self): + return self.all() + + class Tag(models.Model): tag_name = models.CharField() + objects = TagManager() + + custom_objects = TagManager() + class Category(models.Model): category_name = models.CharField() @@ -155,6 +164,16 @@ model_instance.objects.update(x='') #? BusinessModel() model_instance.objects.create() +# ----------------- +# Custom object manager +# ----------------- + +#? TagManager() +Tag.objects + +#? TagManager() +Tag.custom_objects + # ----------------- # Inheritance # ----------------- From 574b79029632d5d8bbbf816ec81dd8433cc6d8e7 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 6 Jun 2020 01:23:14 +0200 Subject: [PATCH 03/16] Make it possible to use inheritance on generics without always specifying type vars, see also discussion in #1593 --- jedi/inference/base_value.py | 3 +++ jedi/inference/gradual/base.py | 17 ++++++++++++++--- jedi/inference/value/klass.py | 4 ++++ jedi/plugins/django.py | 2 +- test/completion/pep0484_generic_passthroughs.py | 16 ++++++++++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index dfa2c230..075de49a 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -174,6 +174,9 @@ class Value(HelperValueMixin): def is_class(self): return False + def is_class_mixin(self): + return False + def is_instance(self): return False diff --git a/jedi/inference/gradual/base.py b/jedi/inference/gradual/base.py index e7940800..44564f65 100644 --- a/jedi/inference/gradual/base.py +++ b/jedi/inference/gradual/base.py @@ -190,7 +190,7 @@ class GenericClass(ClassMixin, DefineGenericBaseClass): @to_list def py__bases__(self): for base in self._wrapped_value.py__bases__(): - yield _LazyGenericBaseClass(self, base) + yield _LazyGenericBaseClass(self, base, self._generics_manager) def _create_instance_with_generics(self, generics_manager): return GenericClass(self._class_value, generics_manager) @@ -241,9 +241,10 @@ class GenericClass(ClassMixin, DefineGenericBaseClass): class _LazyGenericBaseClass(object): - def __init__(self, class_value, lazy_base_class): + def __init__(self, class_value, lazy_base_class, generics_manager): self._class_value = class_value self._lazy_base_class = lazy_base_class + self._generics_manager = generics_manager @iterator_to_value_set def infer(self): @@ -256,7 +257,17 @@ class _LazyGenericBaseClass(object): TupleGenericManager(tuple(self._remap_type_vars(base))), ) else: - yield base + if base.is_class_mixin(): + # This case basically allows classes like `class Foo(List)` + # to be used like `Foo[int]`. The generics are not + # necessary and can be used later. + yield GenericClass.create_cached( + base.inference_state, + base, + self._generics_manager, + ) + else: + yield base def _remap_type_vars(self, base): from jedi.inference.gradual.type_var import TypeVar diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index ec51b24a..4a47da0e 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -135,6 +135,9 @@ class ClassMixin(object): def is_class(self): return True + def is_class_mixin(self): + return True + def py__call__(self, arguments=None): from jedi.inference.value import TreeInstance @@ -314,6 +317,7 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase def py__getitem__(self, index_value_set, contextualized_node): from jedi.inference.gradual.base import GenericClass if not index_value_set: + debug.warning('Class indexes inferred to nothing. Returning class instead') return ValueSet([self]) return ValueSet( GenericClass( diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 43cbaf01..89bc38c6 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -102,7 +102,7 @@ def _create_manager_for(cls, manager_cls='BaseManager'): ('django', 'db', 'models', 'manager') ).py__getattribute__(manager_cls) for m in managers: - if m.is_class() and not m.is_compiled(): + if m.is_class_mixin(): generics_manager = TupleGenericManager((ValueSet([cls]),)) for c in GenericClass(m, generics_manager).execute_annotation(): return c diff --git a/test/completion/pep0484_generic_passthroughs.py b/test/completion/pep0484_generic_passthroughs.py index 3ccdae7a..4fccc5ed 100644 --- a/test/completion/pep0484_generic_passthroughs.py +++ b/test/completion/pep0484_generic_passthroughs.py @@ -126,3 +126,19 @@ for p in typed_bound_generic_passthrough(untyped_list_str): for q in typed_bound_generic_passthrough(typed_list_str): #? str() q + + +class CustomList(List): + def get_first(self): + return self[0] + + +#? str() +CustomList[str]()[0] +#? str() +CustomList[str]().get_first() + +#? str() +typed_fully_generic_passthrough(CustomList[str]())[0] +#? +typed_list_generic_passthrough(CustomList[str])[0] From 34cc8e9ad7e367441270cc883d337d0b7cc5474f Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 Jun 2020 14:18:45 +0200 Subject: [PATCH 04/16] Properly handle __get__ in properties/partials --- jedi/plugins/stdlib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index be6a2eae..2e113715 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -395,7 +395,7 @@ class PropertyObject(AttributeOverwrite, ValueWrapper): def py__get__(self, instance, class_value): if instance is None: - return NO_VALUES + return ValueSet([self]) return self._function.execute_with_values(instance) @publish_method('deleter') @@ -518,6 +518,8 @@ class PartialObject(ValueWrapper): class PartialMethodObject(PartialObject): def py__get__(self, instance, class_value): + if instance is None: + return ValueSet([self]) return ValueSet([PartialObject(self._actual_value, self._arguments, instance)]) From 9adcf3d2337dff1b3e5fefff5f8d99bafdb36f3d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 Jun 2020 14:54:26 +0200 Subject: [PATCH 05/16] Make sure meta class filters can distinguish between classes and instances --- jedi/inference/value/klass.py | 6 +++--- jedi/plugins/django.py | 34 ++++++++++++++++++++++++---------- jedi/plugins/stdlib.py | 4 ++-- test/completion/django.py | 9 +++++++++ 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 4a47da0e..6f60d4b8 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -193,7 +193,7 @@ class ClassMixin(object): if include_metaclasses: metaclasses = self.get_metaclasses() if metaclasses: - for f in self.get_metaclass_filters(metaclasses): + for f in self.get_metaclass_filters(metaclasses, is_instance): yield f for cls in self.py__mro__(): @@ -361,8 +361,8 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase return ValueSet({self}) @plugin_manager.decorate() - def get_metaclass_filters(self, metaclass): - debug.dbg('Unprocessed metaclass %s', metaclass) + def get_metaclass_filters(self, metaclass, is_instance): + debug.warning('Unprocessed metaclass %s', metaclass) return [] @inference_state_method_cache(default=NO_VALUES) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 12c1fdb7..b8aaa126 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -32,12 +32,21 @@ mapping = { } -def _infer_scalar_field(inference_state, field_name, field_tree_instance): +def _get_deferred_attributes(inference_state): + return inference_state.import_module( + ('django', 'db', 'models', 'query_utils') + ).py__getattribute__('DeferredAttribute').execute_annotation() + + +def _infer_scalar_field(inference_state, field_name, field_tree_instance, is_instance): try: module_name, attribute_name = mapping[field_tree_instance.py__name__()] except KeyError: return None + if not is_instance: + return _get_deferred_attributes(inference_state) + if module_name is None: module = inference_state.builtins_module else: @@ -65,16 +74,20 @@ def _get_foreign_key_values(cls, field_tree_instance): yield value -def _infer_field(cls, field_name): +def _infer_field(cls, field_name, is_instance): inference_state = cls.inference_state for field_tree_instance in field_name.infer(): - scalar_field = _infer_scalar_field(inference_state, field_name, field_tree_instance) + scalar_field = _infer_scalar_field( + inference_state, field_name, field_tree_instance, is_instance) if scalar_field is not None: return scalar_field name = field_tree_instance.py__name__() is_many_to_many = name == 'ManyToManyField' if name in ('ForeignKey', 'OneToOneField') or is_many_to_many: + if not is_instance: + return _get_deferred_attributes(inference_state) + values = _get_foreign_key_values(cls, field_tree_instance) if is_many_to_many: return ValueSet(filter(None, [ @@ -89,12 +102,13 @@ def _infer_field(cls, field_name): class DjangoModelName(NameWrapper): - def __init__(self, cls, name): + def __init__(self, cls, name, is_instance): super(DjangoModelName, self).__init__(name) self._cls = cls + self._is_instance = is_instance def infer(self): - return _infer_field(self._cls, self._wrapped_name) + return _infer_field(self._cls, self._wrapped_name, self._is_instance) def _create_manager_for(cls, manager_cls='BaseManager'): @@ -109,7 +123,7 @@ def _create_manager_for(cls, manager_cls='BaseManager'): return None -def _new_dict_filter(cls): +def _new_dict_filter(cls, is_instance): def get_manager_name(filters): for f in filters: names = f.get('objects') @@ -142,7 +156,7 @@ def _new_dict_filter(cls): filters = list(cls.get_filters(is_instance=True, include_metaclasses=False)) dct = { - name.string_name: DjangoModelName(cls, name) + name.string_name: DjangoModelName(cls, name, is_instance) for filter_ in reversed(filters) for name in filter_.values() } @@ -155,11 +169,11 @@ def _new_dict_filter(cls): def get_metaclass_filters(func): - def wrapper(cls, metaclasses): + 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': - return [_new_dict_filter(cls)] + return [_new_dict_filter(cls, is_instance)] - return func(cls, metaclasses) + return func(cls, metaclasses, is_instance) return wrapper diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 2e113715..475cd6a8 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -801,7 +801,7 @@ _implemented = { def get_metaclass_filters(func): - def wrapper(cls, metaclasses): + def wrapper(cls, metaclasses, is_instance): for metaclass in metaclasses: if metaclass.py__name__() == 'EnumMeta' \ and metaclass.get_root_context().py__name__() == 'enum': @@ -809,7 +809,7 @@ def get_metaclass_filters(func): return [DictFilter({ name.string_name: EnumInstance(cls, name).name for name in filter_.values() })] - return func(cls, metaclasses) + return func(cls, metaclasses, is_instance) return wrapper diff --git a/test/completion/django.py b/test/completion/django.py index 6ed34ce1..6df0c3a3 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -4,6 +4,7 @@ import uuid from django.db import models from django.contrib.auth.models import User +from django.db.models.query_utils import DeferredAttribute class TagManager(models.Manager): @@ -58,6 +59,9 @@ class BusinessModel(models.Model): unidentifiable = NOT_FOUND + #? models.IntegerField() + integer_field + def method(self): return 42 @@ -65,6 +69,11 @@ class BusinessModel(models.Model): # Model attribute inference # ----------------- +#? DeferredAttribute() +BusinessModel.integer_field +#? DeferredAttribute() +BusinessModel.tags_m2m + model_instance = BusinessModel() #? int() From c9a21adc5fade5bd42e93b33c28ee6403da8eb6f Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 Jun 2020 15:01:12 +0200 Subject: [PATCH 06/16] Make sure py__get__ is applied properly for Django metaclasses --- jedi/inference/value/klass.py | 5 +++-- jedi/plugins/django.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 6f60d4b8..5c45bb5f 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -189,7 +189,8 @@ class ClassMixin(object): mro.append(cls_new) yield cls_new - def get_filters(self, origin_scope=None, is_instance=False, include_metaclasses=True): + def get_filters(self, origin_scope=None, is_instance=False, + include_metaclasses=True, include_type_when_class=True): if include_metaclasses: metaclasses = self.get_metaclasses() if metaclasses: @@ -206,7 +207,7 @@ class ClassMixin(object): origin_scope=origin_scope, is_instance=is_instance ) - if not is_instance: + if not is_instance and include_type_when_class: from jedi.inference.compiled import builtin_from_name type_ = builtin_from_name(self.inference_state, u'type') assert isinstance(type_, ClassValue) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index b8aaa126..69ba6139 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -154,7 +154,11 @@ def _new_dict_filter(cls, is_instance): if manager: return manager.name - filters = list(cls.get_filters(is_instance=True, include_metaclasses=False)) + filters = list(cls.get_filters( + is_instance=is_instance, + include_metaclasses=False, + include_type_when_class=False) + ) dct = { name.string_name: DjangoModelName(cls, name, is_instance) for filter_ in reversed(filters) From cd6113c2c33802844c69c43d57ca5e2e202aaf43 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 8 Jun 2020 00:11:45 +0200 Subject: [PATCH 07/16] Move with_generics and define_generics to ClassMixin --- jedi/inference/gradual/base.py | 2 +- jedi/inference/gradual/typing.py | 4 +-- jedi/inference/value/klass.py | 60 ++++++++++++++++---------------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/jedi/inference/gradual/base.py b/jedi/inference/gradual/base.py index 44564f65..f5c05d17 100644 --- a/jedi/inference/gradual/base.py +++ b/jedi/inference/gradual/base.py @@ -151,7 +151,7 @@ class DefineGenericBaseClass(LazyValueWrapper): ) -class GenericClass(ClassMixin, DefineGenericBaseClass): +class GenericClass(DefineGenericBaseClass, ClassMixin): """ A class that is defined with generics, might be something simple like: diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index 0f3cd5f8..e3a95009 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -203,7 +203,7 @@ class _TypingClassMixin(ClassMixin): return ValueName(self, self._tree_name) -class TypingClassWithGenerics(_TypingClassMixin, ProxyWithGenerics): +class TypingClassWithGenerics(ProxyWithGenerics, _TypingClassMixin): def infer_type_vars(self, value_set): type_var_dict = {} annotation_generics = self.get_generics() @@ -240,7 +240,7 @@ class TypingClassWithGenerics(_TypingClassMixin, ProxyWithGenerics): ) -class ProxyTypingClassValue(_TypingClassMixin, ProxyTypingValue): +class ProxyTypingClassValue(ProxyTypingValue, _TypingClassMixin): index_class = TypingClassWithGenerics diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 5c45bb5f..9c2102ff 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -271,6 +271,36 @@ class ClassMixin(object): return True return False + def with_generics(self, generics_tuple): + from jedi.inference.gradual.base import GenericClass + return GenericClass( + self, + TupleGenericManager(generics_tuple) + ) + + def define_generics(self, type_var_dict): + from jedi.inference.gradual.base import GenericClass + + def remap_type_vars(): + """ + The TypeVars in the resulting classes have sometimes different names + and we need to check for that, e.g. a signature can be: + + def iter(iterable: Iterable[_T]) -> Iterator[_T]: ... + + However, the iterator is defined as Iterator[_T_co], which means it has + a different type var name. + """ + for type_var in self.list_type_vars(): + yield type_var_dict.get(type_var.py__name__(), NO_VALUES) + + if type_var_dict: + return ValueSet([GenericClass( + self, + TupleGenericManager(tuple(remap_type_vars())) + )]) + return ValueSet({self}) + class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase)): api_type = u'class' @@ -331,36 +361,6 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase for index_value in index_value_set ) - def with_generics(self, generics_tuple): - from jedi.inference.gradual.base import GenericClass - return GenericClass( - self, - TupleGenericManager(generics_tuple) - ) - - def define_generics(self, type_var_dict): - from jedi.inference.gradual.base import GenericClass - - def remap_type_vars(): - """ - The TypeVars in the resulting classes have sometimes different names - and we need to check for that, e.g. a signature can be: - - def iter(iterable: Iterable[_T]) -> Iterator[_T]: ... - - However, the iterator is defined as Iterator[_T_co], which means it has - a different type var name. - """ - for type_var in self.list_type_vars(): - yield type_var_dict.get(type_var.py__name__(), NO_VALUES) - - if type_var_dict: - return ValueSet([GenericClass( - self, - TupleGenericManager(tuple(remap_type_vars())) - )]) - return ValueSet({self}) - @plugin_manager.decorate() def get_metaclass_filters(self, metaclass, is_instance): debug.warning('Unprocessed metaclass %s', metaclass) From d4f0424ddc1415dee7ded4dc9f7af044a6ae71ab Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 8 Jun 2020 00:58:38 +0200 Subject: [PATCH 08/16] Move py__getitem__ from Class to ClassMixin --- jedi/inference/value/klass.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 9c2102ff..1a8cac2f 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -271,6 +271,22 @@ class ClassMixin(object): return True return False + def py__getitem__(self, index_value_set, contextualized_node): + from jedi.inference.gradual.base import GenericClass + if not index_value_set: + debug.warning('Class indexes inferred to nothing. Returning class instead') + return ValueSet([self]) + return ValueSet( + GenericClass( + self, + LazyGenericManager( + context_of_index=contextualized_node.context, + index_value=index_value, + ) + ) + for index_value in index_value_set + ) + def with_generics(self, generics_tuple): from jedi.inference.gradual.base import GenericClass return GenericClass( @@ -345,22 +361,6 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase self.inference_state.builtins_module.py__getattribute__('object') )] - def py__getitem__(self, index_value_set, contextualized_node): - from jedi.inference.gradual.base import GenericClass - if not index_value_set: - debug.warning('Class indexes inferred to nothing. Returning class instead') - return ValueSet([self]) - return ValueSet( - GenericClass( - self, - LazyGenericManager( - context_of_index=contextualized_node.context, - index_value=index_value, - ) - ) - for index_value in index_value_set - ) - @plugin_manager.decorate() def get_metaclass_filters(self, metaclass, is_instance): debug.warning('Unprocessed metaclass %s', metaclass) From 6d0d75c7d98d2a458c6539c43b72bb613a7c280d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 8 Jun 2020 09:15:33 +0200 Subject: [PATCH 09/16] @publish_method should provide arguments --- jedi/inference/filters.py | 4 ++-- jedi/inference/value/iterable.py | 14 +++++++------- jedi/plugins/stdlib.py | 7 +++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/jedi/inference/filters.py b/jedi/inference/filters.py index e7dc3849..00e594f7 100644 --- a/jedi/inference/filters.py +++ b/jedi/inference/filters.py @@ -255,7 +255,7 @@ class _BuiltinMappedMethod(ValueWrapper): def py__call__(self, arguments): # TODO add TypeError if params are given/or not correct. - return self._method(self._value) + return self._method(self._value, arguments) class SpecialMethodFilter(DictFilter): @@ -330,7 +330,7 @@ class _AttributeOverwriteMixin(object): def get_filters(self, *args, **kwargs): yield SpecialMethodFilter(self, self.overwritten_methods, self._wrapped_value) - for filter in self._wrapped_value.get_filters(): + for filter in self._wrapped_value.get_filters(*args, **kwargs): yield filter diff --git a/jedi/inference/value/iterable.py b/jedi/inference/value/iterable.py index d23ca13e..43692c57 100644 --- a/jedi/inference/value/iterable.py +++ b/jedi/inference/value/iterable.py @@ -58,13 +58,13 @@ class GeneratorBase(LazyAttributeOverwrite, IterableMixin): return True @publish_method('__iter__') - def py__iter__(self, contextualized_node=None): + def _iter(self, arguments): return ValueSet([self]) @publish_method('send') @publish_method('next', python_version_match=2) @publish_method('__next__', python_version_match=3) - def py__next__(self): + def py__next__(self, arguments): return ValueSet.from_sets(lazy_value.infer() for lazy_value in self.py__iter__()) def py__stop_iteration_returns(self): @@ -290,12 +290,12 @@ class DictComprehension(ComprehensionMixin, Sequence, _DictKeyMixin): return ValueSet.from_sets(values for keys, values in self._iterate()) @publish_method('values') - def _imitate_values(self): + def _imitate_values(self, arguments): lazy_value = LazyKnownValues(self._dict_values()) return ValueSet([FakeList(self.inference_state, [lazy_value])]) @publish_method('items') - def _imitate_items(self): + def _imitate_items(self, arguments): lazy_values = [ LazyKnownValue( FakeTuple( @@ -457,12 +457,12 @@ class DictLiteralValue(_DictMixin, SequenceLiteralValue, _DictKeyMixin): yield LazyKnownValues(types) @publish_method('values') - def _imitate_values(self): + def _imitate_values(self, arguments): lazy_value = LazyKnownValues(self._dict_values()) return ValueSet([FakeList(self.inference_state, [lazy_value])]) @publish_method('items') - def _imitate_items(self): + def _imitate_items(self, arguments): lazy_values = [ LazyKnownValue(FakeTuple( self.inference_state, @@ -552,7 +552,7 @@ class FakeDict(_DictMixin, Sequence, _DictKeyMixin): return lazy_value.infer() @publish_method('values') - def _values(self): + def _values(self, arguments): return ValueSet([FakeTuple( self.inference_state, [LazyKnownValues(self._dict_values())] diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 475cd6a8..8bb35ad1 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -260,13 +260,12 @@ class ReversedObject(AttributeOverwrite): super(ReversedObject, self).__init__(reversed_obj) self._iter_list = iter_list - @publish_method('__iter__') - def py__iter__(self, contextualized_node=None): + def py__iter__(self, contextualized_node): return self._iter_list @publish_method('next', python_version_match=2) @publish_method('__next__', python_version_match=3) - def py__next__(self): + def py__next__(self, arguments): return ValueSet.from_sets( lazy_value.infer() for lazy_value in self._iter_list ) @@ -401,7 +400,7 @@ class PropertyObject(AttributeOverwrite, ValueWrapper): @publish_method('deleter') @publish_method('getter') @publish_method('setter') - def _return_self(self): + def _return_self(self, arguments): return ValueSet({self}) From a2108de2c00d62bc138ef409a244e1f951c0adc9 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 9 Jun 2020 23:26:39 +0200 Subject: [PATCH 10/16] Use py__get__ for Django Model.objects This includes the fix in https://github.com/typeddjango/django-stubs/pull/394 --- jedi/inference/base_value.py | 3 ++ jedi/inference/gradual/base.py | 6 +++ jedi/inference/value/instance.py | 5 ++ jedi/inference/value/klass.py | 8 ++-- jedi/plugins/django.py | 78 +++++++++++++++++--------------- test/completion/django.py | 45 +++++++++++++----- 6 files changed, 93 insertions(+), 52 deletions(-) diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index 075de49a..6cd48329 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -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 diff --git a/jedi/inference/gradual/base.py b/jedi/inference/gradual/base.py index f5c05d17..68fab7f1 100644 --- a/jedi/inference/gradual/base.py +++ b/jedi/inference/gradual/base.py @@ -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): diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index bc06cfd4..5435c791 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -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: diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 1a8cac2f..46802ad8 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -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 ) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 69ba6139..5004ddb1 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 _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 diff --git a/test/completion/django.py b/test/completion/django.py index 6df0c3a3..f2ed27f7 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -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'] From c4de9ae2d39adc6973abe82a5ecb9f2ddb28e00d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 12 Jun 2020 19:30:49 +0200 Subject: [PATCH 11/16] Use a customized django-stubs --- .gitmodules | 2 +- jedi/third_party/django-stubs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 368cba0a..1a59e543 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/davidhalter/typeshed.git [submodule "jedi/third_party/django-stubs"] path = jedi/third_party/django-stubs - url = https://github.com/typeddjango/django-stubs + url = https://github.com/davidhalter/django-stubs diff --git a/jedi/third_party/django-stubs b/jedi/third_party/django-stubs index 92c8dfc9..fd057010 160000 --- a/jedi/third_party/django-stubs +++ b/jedi/third_party/django-stubs @@ -1 +1 @@ -Subproject commit 92c8dfc93f840b936e33eb3f1770293627ac0f15 +Subproject commit fd057010f6cbf176f57d1099e82be46d39b99cb9 From 365d725bc18281de00b1d72b99ca95957d0143c8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 13 Jun 2020 00:26:12 +0200 Subject: [PATCH 12/16] Fix a small issue that was inadvertently changed --- jedi/inference/value/klass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 46802ad8..8d93a120 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -201,7 +201,7 @@ class ClassMixin(object): yield filter else: yield ClassFilter( - cls, node_context=self.as_context(), + self, node_context=cls.as_context(), origin_scope=origin_scope, is_instance=is_instance ) From b165596a6ea2a977466646115b0fd7102fb2a7d7 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 13 Jun 2020 14:25:52 +0200 Subject: [PATCH 13/16] Avoid doing a call twice for now reason --- jedi/plugins/django.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 5004ddb1..8e7f4e29 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -79,7 +79,8 @@ def _get_foreign_key_values(cls, field_tree_instance): def _infer_field(cls, field_name, is_instance): inference_state = cls.inference_state - for field_tree_instance in field_name.infer(): + result = field_name.infer() + for field_tree_instance in result: scalar_field = _infer_scalar_field( inference_state, field_name, field_tree_instance, is_instance) if scalar_field is not None: @@ -101,7 +102,7 @@ def _infer_field(cls, field_name, is_instance): debug.dbg('django plugin: fail to infer `%s` from class `%s`', field_name.string_name, cls.py__name__()) - return field_name.infer() + return result class DjangoModelName(NameWrapper): From 3415ccbb734f8454c4ee46521744e773a4fafdde Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 13 Jun 2020 16:18:47 +0200 Subject: [PATCH 14/16] Add support for Django signatures, fixes parts of #1587 --- jedi/inference/value/klass.py | 13 ++++- jedi/plugins/django.py | 95 ++++++++++++++++++++++++++++++----- test/completion/django.py | 23 +++++++++ 3 files changed, 116 insertions(+), 15 deletions(-) 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) From 9de5ab2037a5727a7df873bf0597214f7f3ca58d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 13 Jun 2020 20:55:31 +0200 Subject: [PATCH 15/16] Make it possible to complete on QuerySet methods, fixes #1587 --- jedi/plugins/django.py | 31 +++++++++++++++++++++++++++++++ test/completion/django.py | 17 +++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 6e262a12..098262df 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -13,6 +13,7 @@ from jedi.inference.value.klass import ClassMixin from jedi.inference.gradual.base import GenericClass from jedi.inference.gradual.generics import TupleGenericManager from jedi.inference.signature import AbstractSignature +from jedi.inference.value.function import FunctionMixin mapping = { @@ -171,6 +172,17 @@ def get_metaclass_filters(func): def tree_name_to_values(func): def wrapper(inference_state, context, tree_name): result = func(inference_state, context, tree_name) + if tree_name.value in ('create', 'filter', 'exclude', 'update', 'get', + 'get_or_create', 'update_or_create'): + for v in result: + if v.get_qualified_names() == ('_BaseQuerySet', tree_name.value) \ + and v.parent_context.is_module() \ + and v.parent_context.py__name__() == 'django.db.models.query': + qs = context.get_value() + generics = qs.get_generics() + if len(generics) >= 1: + return ValueSet(QuerySetMethodWrapper(v, model) + for model in generics[0]) 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) @@ -257,3 +269,22 @@ class DjangoParamName(BaseTreeParamName): def infer(self): return self._field_name.infer() + + +class QuerySetMethodWrapper(ValueWrapper): + def __init__(self, method, model_cls): + super(QuerySetMethodWrapper, self).__init__(method) + self._model_cls = model_cls + + def py__get__(self, instance, class_value): + return ValueSet({QuerySetBoundMethodWrapper(v, self._model_cls) + for v in self._wrapped_value.py__get__(instance, class_value)}) + + +class QuerySetBoundMethodWrapper(ValueWrapper): + def __init__(self, method, model_cls): + super(QuerySetBoundMethodWrapper, self).__init__(method) + self._model_cls = model_cls + + def get_signatures(self): + return _get_signatures(self._model_cls) diff --git a/test/completion/django.py b/test/completion/django.py index 5387e990..533cab6e 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -273,3 +273,20 @@ Inherited(category_fk) Inherited(attached_o2) #? 18 ['tags_m2m='] Inherited(tags_m2m) + +#? 32 ['tags_m2m='] +Inherited.objects.create(tags_m2) +#? 32 ['tags_m2m='] +Inherited.objects.filter(tags_m2) +#? 35 ['char_field='] +Inherited.objects.exclude(char_fiel) +#? 34 ['char_field='] +Inherited.objects.update(char_fiel) +#? 32 ['email_field='] +Inherited.objects.get(email_fiel) +#? 44 ['category_fk2='] +Inherited.objects.get_or_create(category_fk2) +#? 44 ['uuid_field='] +Inherited.objects.update_or_create(uuid_fiel) +#? 48 ['char_field='] +Inherited.objects.exclude(pk=3).filter(char_fiel) From 1702a6340ee00349bd407bbc53c7dc7aa44431be Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 14 Jun 2020 22:23:08 +0200 Subject: [PATCH 16/16] Document a special case in Django a bit better --- jedi/plugins/django.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 098262df..88a3bfae 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -13,7 +13,6 @@ from jedi.inference.value.klass import ClassMixin from jedi.inference.gradual.base import GenericClass from jedi.inference.gradual.generics import TupleGenericManager from jedi.inference.signature import AbstractSignature -from jedi.inference.value.function import FunctionMixin mapping = { @@ -37,6 +36,9 @@ mapping = { 'UUIDField': ('uuid', 'UUID'), } +_FILTER_LIKE_METHODS = ('create', 'filter', 'exclude', 'update', 'get', + 'get_or_create', 'update_or_create') + @inference_state_function_cache() def _get_deferred_attributes(inference_state): @@ -172,8 +174,10 @@ def get_metaclass_filters(func): def tree_name_to_values(func): def wrapper(inference_state, context, tree_name): result = func(inference_state, context, tree_name) - if tree_name.value in ('create', 'filter', 'exclude', 'update', 'get', - 'get_or_create', 'update_or_create'): + if tree_name.value in _FILTER_LIKE_METHODS: + # Here we try to overwrite stuff like User.objects.filter. We need + # this to make sure that keyword param completion works on these + # kind of methods. for v in result: if v.get_qualified_names() == ('_BaseQuerySet', tree_name.value) \ and v.parent_context.is_module() \ @@ -183,11 +187,12 @@ def tree_name_to_values(func): if len(generics) >= 1: return ValueSet(QuerySetMethodWrapper(v, model) for model in generics[0]) - if tree_name.value == 'BaseManager' and context.is_module() \ + + elif 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() \ + elif 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