Skip to content

Commit

Permalink
Use py__get__ for Django Model.objects
Browse files Browse the repository at this point in the history
This includes the fix in typeddjango/django-stubs#394
  • Loading branch information
davidhalter committed Jun 9, 2020
1 parent 6d0d75c commit a2108de
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 52 deletions.
3 changes: 3 additions & 0 deletions jedi/inference/base_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions jedi/inference/gradual/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions jedi/inference/value/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 3 additions & 5 deletions jedi/inference/value/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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() \
Expand All @@ -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
Expand Down Expand Up @@ -195,15 +193,15 @@ 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():
for filter in cls.get_filters(is_instance=is_instance):
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
)
Expand Down
78 changes: 42 additions & 36 deletions jedi/plugins/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand All @@ -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)

Expand All @@ -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
45 changes: 34 additions & 11 deletions test/completion/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,26 +162,36 @@ 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
# -----------------

#? TagManager()
Tag.objects
#? Tag() None
Tag.objects.filter().first()

#? TagManager()
Tag.custom_objects
#? Tag() None
Tag.custom_objects.filter().first()

# -----------------
# Inheritance
Expand All @@ -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
Expand All @@ -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']

0 comments on commit a2108de

Please sign in to comment.