From f5ae7148dd772421af473aa74df2ad449c910ba3 Mon Sep 17 00:00:00 2001 From: ANtlord Date: Wed, 18 Sep 2019 09:27:39 +0300 Subject: [PATCH 01/55] Basic django model fields are infered as builtin types. --- jedi/plugins/stdlib.py | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index fdf7bb25..4c53594e 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -787,6 +787,69 @@ _implemented = { } +def new_django_dict_filter(cls): + filter_ = ParserTreeFilter(parent_context=cls.as_context()) + field, = filter_.values() + field_tree_instance, = field.infer() + + if field_tree_instance.name.string_name in ('CharField', 'TextField', 'EmailField'): + builtin_str, = cls.inference_state.builtins_module.py__getattribute__('str') + return [DictFilter({ + field.string_name: DjangoModelField(builtin_str, field).name + })] + + integer_field_classes = ('IntegerField', 'BigIntegerField', 'PositiveIntegerField', 'SmallIntegerField') + if field_tree_instance.name.string_name in integer_field_classes: + builtin_str, = cls.inference_state.builtins_module.py__getattribute__('int') + return [DictFilter({ + field.string_name: DjangoModelField(builtin_str, field).name + })] + + if field_tree_instance.name.string_name == 'FloatField': + builtin_str, = cls.inference_state.builtins_module.py__getattribute__('float') + return [DictFilter({ + field.string_name: DjangoModelField(builtin_str, field).name + })] + + + if field_tree_instance.name.string_name == 'BinaryField': + builtin_str, = cls.inference_state.builtins_module.py__getattribute__('bytes') + return [DictFilter({ + field.string_name: DjangoModelField(builtin_str, field).name + })] + + if field_tree_instance.name.string_name == 'BooleanField': + builtin_str, = cls.inference_state.builtins_module.py__getattribute__('bool') + return [DictFilter({ + field.string_name: DjangoModelField(builtin_str, field).name + })] + + if field_tree_instance.name.string_name == 'DecimalField': + # TODO: make decimal.Decimal filter + return + + if field_tree_instance.name.string_name == 'ForeignKey': + # TODO: infer related object class and make a filter for that class + return + + if field_tree_instance.name.string_name == 'TimeField': + # TODO: make time.time filter + return + + if field_tree_instance.name.string_name == 'DurationField': + # TODO: make datetime.timedelta filter + return + + if field_tree_instance.name.string_name == 'DateField': + # TODO: make datetime.date filter + return + + if field_tree_instance.name.string_name == 'DatetimeField': + # TODO: make datetime.datetime filter + return + + + def get_metaclass_filters(func): def wrapper(cls, metaclasses): for metaclass in metaclasses: @@ -796,10 +859,37 @@ def get_metaclass_filters(func): return [DictFilter({ name.string_name: EnumInstance(cls, name).name for name in filter_.values() })] + + if metaclass.py__name__() == 'ModelBase' \ + and metaclass.get_root_context().py__name__() == 'django.db.models.base': + django_dict_filter = new_django_dict_filter(cls) + if django_dict_filter is not None: + return django_dict_filter + return func(cls, metaclasses) return wrapper +class DjangoModelField(LazyValueWrapper): + def __init__(self, cls, name): + self.inference_state = cls.inference_state + self._cls = cls # Corresponds to super().__self__ + self._name = name + self.tree_node = self._name.tree_name + + @safe_property + def name(self): + return ValueName(self, self._name.tree_name) + + def _get_wrapped_value(self): + obj, = self._cls.execute_with_values() + return obj + + def get_filters(self, origin_scope=None): + for f in self._get_wrapped_value().get_filters(): + yield f + + class EnumInstance(LazyValueWrapper): def __init__(self, cls, name): self.inference_state = cls.inference_state From 659aaf6861fb238b3bd2c06e70a4ce2574051dee Mon Sep 17 00:00:00 2001 From: ANtlord Date: Thu, 19 Sep 2019 08:42:39 +0300 Subject: [PATCH 02/55] Naming corrections. --- jedi/plugins/stdlib.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 9e60cbab..98e8987c 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -787,35 +787,35 @@ def new_django_dict_filter(cls): field_tree_instance, = field.infer() if field_tree_instance.name.string_name in ('CharField', 'TextField', 'EmailField'): - builtin_str, = cls.inference_state.builtins_module.py__getattribute__('str') + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('str') return [DictFilter({ - field.string_name: DjangoModelField(builtin_str, field).name + field.string_name: DjangoModelField(model_instance_field_type, field).name })] integer_field_classes = ('IntegerField', 'BigIntegerField', 'PositiveIntegerField', 'SmallIntegerField') if field_tree_instance.name.string_name in integer_field_classes: - builtin_str, = cls.inference_state.builtins_module.py__getattribute__('int') + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('int') return [DictFilter({ - field.string_name: DjangoModelField(builtin_str, field).name + field.string_name: DjangoModelField(model_instance_field_type, field).name })] if field_tree_instance.name.string_name == 'FloatField': - builtin_str, = cls.inference_state.builtins_module.py__getattribute__('float') + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('float') return [DictFilter({ - field.string_name: DjangoModelField(builtin_str, field).name + field.string_name: DjangoModelField(model_instance_field_type, field).name })] if field_tree_instance.name.string_name == 'BinaryField': - builtin_str, = cls.inference_state.builtins_module.py__getattribute__('bytes') + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bytes') return [DictFilter({ - field.string_name: DjangoModelField(builtin_str, field).name + field.string_name: DjangoModelField(model_instance_field_type, field).name })] if field_tree_instance.name.string_name == 'BooleanField': - builtin_str, = cls.inference_state.builtins_module.py__getattribute__('bool') + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bool') return [DictFilter({ - field.string_name: DjangoModelField(builtin_str, field).name + field.string_name: DjangoModelField(model_instance_field_type, field).name })] if field_tree_instance.name.string_name == 'DecimalField': From fbeff007612fcd584db1efae94df33383ab3111d Mon Sep 17 00:00:00 2001 From: ANtlord Date: Fri, 6 Dec 2019 23:47:19 +0200 Subject: [PATCH 03/55] Decimal, DurationField, DateField, DateTimeField, DecimalField django types are resolved. --- jedi/plugins/stdlib.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 98e8987c..2c7cf98e 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -805,7 +805,6 @@ def new_django_dict_filter(cls): field.string_name: DjangoModelField(model_instance_field_type, field).name })] - if field_tree_instance.name.string_name == 'BinaryField': model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bytes') return [DictFilter({ @@ -819,28 +818,38 @@ def new_django_dict_filter(cls): })] if field_tree_instance.name.string_name == 'DecimalField': - # TODO: make decimal.Decimal filter - return + model_instance_field_type, = cls.inference_state.import_module(('decimal',)).py__getattribute__('Decimal') + return [DictFilter({ + field.string_name: DjangoModelField(model_instance_field_type, field).name + })] if field_tree_instance.name.string_name == 'ForeignKey': # TODO: infer related object class and make a filter for that class return if field_tree_instance.name.string_name == 'TimeField': - # TODO: make time.time filter - return + model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('time') + return [DictFilter({ + field.string_name: DjangoModelField(model_instance_field_type, field).name + })] if field_tree_instance.name.string_name == 'DurationField': - # TODO: make datetime.timedelta filter - return + model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('timedelta') + return [DictFilter({ + field.string_name: DjangoModelField(model_instance_field_type, field).name + })] if field_tree_instance.name.string_name == 'DateField': - # TODO: make datetime.date filter - return + model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('date') + return [DictFilter({ + field.string_name: DjangoModelField(model_instance_field_type, field).name + })] - if field_tree_instance.name.string_name == 'DatetimeField': - # TODO: make datetime.datetime filter - return + if field_tree_instance.name.string_name == 'DateTimeField': + model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('datetime') + return [DictFilter({ + field.string_name: DjangoModelField(model_instance_field_type, field).name + })] From 654475b7d6198a083c1ad9087bd10acf76dd795c Mon Sep 17 00:00:00 2001 From: ANtlord Date: Fri, 6 Dec 2019 23:58:13 +0200 Subject: [PATCH 04/55] Infering multiple fields is fixed. --- jedi/plugins/stdlib.py | 51 +++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 2c7cf98e..5074e476 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -781,47 +781,33 @@ _implemented = { } -def new_django_dict_filter(cls): - filter_ = ParserTreeFilter(parent_context=cls.as_context()) - field, = filter_.values() +def infer_django_field(cls, field): field_tree_instance, = field.infer() if field_tree_instance.name.string_name in ('CharField', 'TextField', 'EmailField'): model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('str') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name integer_field_classes = ('IntegerField', 'BigIntegerField', 'PositiveIntegerField', 'SmallIntegerField') if field_tree_instance.name.string_name in integer_field_classes: model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('int') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'FloatField': model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('float') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'BinaryField': model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bytes') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'BooleanField': model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bool') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'DecimalField': model_instance_field_type, = cls.inference_state.import_module(('decimal',)).py__getattribute__('Decimal') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'ForeignKey': # TODO: infer related object class and make a filter for that class @@ -829,30 +815,29 @@ def new_django_dict_filter(cls): if field_tree_instance.name.string_name == 'TimeField': model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('time') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'DurationField': model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('timedelta') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'DateField': model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('date') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'DateTimeField': model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('datetime') - return [DictFilter({ - field.string_name: DjangoModelField(model_instance_field_type, field).name - })] + return DjangoModelField(model_instance_field_type, field).name +def new_django_dict_filter(cls): + filter_ = ParserTreeFilter(parent_context=cls.as_context()) + return [DictFilter({ + f.string_name: infer_django_field(cls, f) for f in filter_.values() + })] + + def get_metaclass_filters(func): def wrapper(cls, metaclasses): for metaclass in metaclasses: From a6dfc130c9149bb4711a8d96f6f4e46293103d4c Mon Sep 17 00:00:00 2001 From: ANtlord Date: Thu, 16 Jan 2020 15:40:45 +0200 Subject: [PATCH 05/55] Foreign key is handled. --- jedi/plugins/stdlib.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 5074e476..3193d84f 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -21,7 +21,7 @@ from jedi.inference.arguments import \ from jedi.inference import analysis from jedi.inference import compiled from jedi.inference.value.instance import \ - AnonymousMethodExecutionContext, MethodExecutionContext + AnonymousMethodExecutionContext, MethodExecutionContext, TreeInstance from jedi.inference.base_value import ContextualizedNode, \ NO_VALUES, ValueSet, ValueWrapper, LazyValueWrapper from jedi.inference.value import ClassValue, ModuleValue @@ -810,8 +810,22 @@ def infer_django_field(cls, field): return DjangoModelField(model_instance_field_type, field).name if field_tree_instance.name.string_name == 'ForeignKey': - # TODO: infer related object class and make a filter for that class - return + if isinstance(field_tree_instance, TreeInstance): + argument_iterator = field_tree_instance._arguments.unpack() + key, lazy_values = next(argument_iterator, (None, None)) + if key is None and lazy_values is not None: + # TODO: it has only one element in current state. Handle rest of elements. + for value in lazy_values.infer(): + string = value.get_safe_value(default=None) + if value.name.string_name == 'str': + foreign_key_class_name = value._compiled_obj.get_safe_value() + # TODO: it has only one element in current state. Handle rest of elements. + for v in cls.parent_context.py__getattribute__(foreign_key_class_name): + return DjangoModelField(v, field).name + else: + return DjangoModelField(value, field).name + + raise Exception('Should be handled') if field_tree_instance.name.string_name == 'TimeField': model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('time') From c61ca0d27be9dfc832911aa01234a2302e5e187c Mon Sep 17 00:00:00 2001 From: ANtlord Date: Sun, 19 Jan 2020 18:46:28 +0200 Subject: [PATCH 06/55] Infering of django model fields is moved to a dedicated module. --- jedi/plugins/django.py | 95 ++++++++++++++++++++++++++++++++++++++++++ jedi/plugins/stdlib.py | 95 ++---------------------------------------- 2 files changed, 98 insertions(+), 92 deletions(-) create mode 100644 jedi/plugins/django.py diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py new file mode 100644 index 00000000..0090ef06 --- /dev/null +++ b/jedi/plugins/django.py @@ -0,0 +1,95 @@ +""" +Module provides infering of Django model fields. +""" +from jedi.inference.base_value import LazyValueWrapper +from jedi.inference.utils import safe_property +from jedi.inference.filters import ParserTreeFilter, DictFilter +from jedi.inference.names import ValueName +from jedi.inference.value.instance import TreeInstance + + +def new_dict_filter(cls): + filter_ = ParserTreeFilter(parent_context=cls.as_context()) + return [DictFilter({ + f.string_name: _infer_field(cls, f) for f in filter_.values() + })] + + +class DjangoModelField(LazyValueWrapper): + def __init__(self, cls, name): + self.inference_state = cls.inference_state + self._cls = cls # Corresponds to super().__self__ + self._name = name + self.tree_node = self._name.tree_name + + @safe_property + def name(self): + return ValueName(self, self._name.tree_name) + + def _get_wrapped_value(self): + obj, = self._cls.execute_with_values() + return obj + + +def _infer_field(cls, field): + field_tree_instance, = field.infer() + + if field_tree_instance.name.string_name in ('CharField', 'TextField', 'EmailField'): + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('str') + return DjangoModelField(model_instance_field_type, field).name + + integer_field_classes = ('IntegerField', 'BigIntegerField', 'PositiveIntegerField', 'SmallIntegerField') + if field_tree_instance.name.string_name in integer_field_classes: + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('int') + return DjangoModelField(model_instance_field_type, field).name + + if field_tree_instance.name.string_name == 'FloatField': + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('float') + return DjangoModelField(model_instance_field_type, field).name + + if field_tree_instance.name.string_name == 'BinaryField': + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bytes') + return DjangoModelField(model_instance_field_type, field).name + + if field_tree_instance.name.string_name == 'BooleanField': + model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bool') + return DjangoModelField(model_instance_field_type, field).name + + if field_tree_instance.name.string_name == 'DecimalField': + model_instance_field_type, = cls.inference_state.import_module(('decimal',)).py__getattribute__('Decimal') + return DjangoModelField(model_instance_field_type, field).name + + if field_tree_instance.name.string_name == 'ForeignKey': + if isinstance(field_tree_instance, TreeInstance): + argument_iterator = field_tree_instance._arguments.unpack() + key, lazy_values = next(argument_iterator, (None, None)) + if key is None and lazy_values is not None: + # TODO: it has only one element in current state. Handle rest of elements. + for value in lazy_values.infer(): + string = value.get_safe_value(default=None) + if value.name.string_name == 'str': + foreign_key_class_name = value._compiled_obj.get_safe_value() + # TODO: it has only one element in current state. Handle rest of elements. + for v in cls.parent_context.py__getattribute__(foreign_key_class_name): + return DjangoModelField(v, field).name + else: + return DjangoModelField(value, field).name + + raise Exception('Should be handled') + + if field_tree_instance.name.string_name == 'TimeField': + model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('time') + return DjangoModelField(model_instance_field_type, field).name + + if field_tree_instance.name.string_name == 'DurationField': + model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('timedelta') + return DjangoModelField(model_instance_field_type, field).name + + if field_tree_instance.name.string_name == 'DateField': + model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('date') + return DjangoModelField(model_instance_field_type, field).name + + if field_tree_instance.name.string_name == 'DateTimeField': + model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('datetime') + return DjangoModelField(model_instance_field_type, field).name + diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 3193d84f..a97cc4e9 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -35,6 +35,8 @@ from jedi.inference.filters import AttributeOverwrite, publish_method, \ ParserTreeFilter, DictFilter from jedi.inference.signature import AbstractSignature, SignatureWrapper +from . import django + # Copied from Python 3.6's stdlib. _NAMEDTUPLE_CLASS_TEMPLATE = """\ @@ -781,77 +783,6 @@ _implemented = { } -def infer_django_field(cls, field): - field_tree_instance, = field.infer() - - if field_tree_instance.name.string_name in ('CharField', 'TextField', 'EmailField'): - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('str') - return DjangoModelField(model_instance_field_type, field).name - - integer_field_classes = ('IntegerField', 'BigIntegerField', 'PositiveIntegerField', 'SmallIntegerField') - if field_tree_instance.name.string_name in integer_field_classes: - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('int') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'FloatField': - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('float') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'BinaryField': - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bytes') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'BooleanField': - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bool') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'DecimalField': - model_instance_field_type, = cls.inference_state.import_module(('decimal',)).py__getattribute__('Decimal') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'ForeignKey': - if isinstance(field_tree_instance, TreeInstance): - argument_iterator = field_tree_instance._arguments.unpack() - key, lazy_values = next(argument_iterator, (None, None)) - if key is None and lazy_values is not None: - # TODO: it has only one element in current state. Handle rest of elements. - for value in lazy_values.infer(): - string = value.get_safe_value(default=None) - if value.name.string_name == 'str': - foreign_key_class_name = value._compiled_obj.get_safe_value() - # TODO: it has only one element in current state. Handle rest of elements. - for v in cls.parent_context.py__getattribute__(foreign_key_class_name): - return DjangoModelField(v, field).name - else: - return DjangoModelField(value, field).name - - raise Exception('Should be handled') - - if field_tree_instance.name.string_name == 'TimeField': - model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('time') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'DurationField': - model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('timedelta') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'DateField': - model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('date') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'DateTimeField': - model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('datetime') - return DjangoModelField(model_instance_field_type, field).name - - - -def new_django_dict_filter(cls): - filter_ = ParserTreeFilter(parent_context=cls.as_context()) - return [DictFilter({ - f.string_name: infer_django_field(cls, f) for f in filter_.values() - })] - - def get_metaclass_filters(func): def wrapper(cls, metaclasses): for metaclass in metaclasses: @@ -864,7 +795,7 @@ def get_metaclass_filters(func): if metaclass.py__name__() == 'ModelBase' \ and metaclass.get_root_context().py__name__() == 'django.db.models.base': - django_dict_filter = new_django_dict_filter(cls) + django_dict_filter = django.new_dict_filter(cls) if django_dict_filter is not None: return django_dict_filter @@ -872,26 +803,6 @@ def get_metaclass_filters(func): return wrapper -class DjangoModelField(LazyValueWrapper): - def __init__(self, cls, name): - self.inference_state = cls.inference_state - self._cls = cls # Corresponds to super().__self__ - self._name = name - self.tree_node = self._name.tree_name - - @safe_property - def name(self): - return ValueName(self, self._name.tree_name) - - def _get_wrapped_value(self): - obj, = self._cls.execute_with_values() - return obj - - def get_filters(self, origin_scope=None): - for f in self._get_wrapped_value().get_filters(): - yield f - - class EnumInstance(LazyValueWrapper): def __init__(self, cls, name): self.inference_state = cls.inference_state From 7287d67e7a45bd1bb2eedd2bf72dc4107c20c478 Mon Sep 17 00:00:00 2001 From: ANtlord Date: Tue, 21 Jan 2020 21:12:38 +0200 Subject: [PATCH 07/55] Functions infers type of Django model field is refactored. --- jedi/plugins/django.py | 69 +++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 0090ef06..e5f839c6 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -30,34 +30,38 @@ class DjangoModelField(LazyValueWrapper): obj, = self._cls.execute_with_values() return obj +mapping = { + 'IntegerField': (None, 'int'), + 'BigIntegerField': (None, 'int'), + 'PositiveIntegerField': (None, 'int'), + 'SmallIntegerField': (None, 'int'), + 'CharField': (None, 'str'), + 'TextField': (None, 'str'), + 'EmailField': (None, 'str'), + 'FloatField': (None, 'float'), + 'BinaryField': (None, 'bytes'), + 'BooleanField': (None, 'bool'), + 'DecimalField': ('decimal', 'Decimal'), + 'TimeField': ('datetime', 'time'), + 'DurationField': ('datetime', 'timedelta'), + 'DateField': ('datetime', 'date'), + 'DateTimeField': ('datetime', 'datetime'), +} def _infer_field(cls, field): field_tree_instance, = field.infer() - if field_tree_instance.name.string_name in ('CharField', 'TextField', 'EmailField'): - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('str') - return DjangoModelField(model_instance_field_type, field).name - - integer_field_classes = ('IntegerField', 'BigIntegerField', 'PositiveIntegerField', 'SmallIntegerField') - if field_tree_instance.name.string_name in integer_field_classes: - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('int') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'FloatField': - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('float') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'BinaryField': - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bytes') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'BooleanField': - model_instance_field_type, = cls.inference_state.builtins_module.py__getattribute__('bool') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'DecimalField': - model_instance_field_type, = cls.inference_state.import_module(('decimal',)).py__getattribute__('Decimal') - return DjangoModelField(model_instance_field_type, field).name + try: + module_name, attribute_name = mapping[field_tree_instance.name.string_name] + except KeyError: + pass + else: + if module_name is None: + module = cls.inference_state.builtins_module + else: + module = cls.inference_state.import_module((module_name,)) + attribute, = module.py__getattribute__(attribute_name) + return DjangoModelField(attribute, field).name if field_tree_instance.name.string_name == 'ForeignKey': if isinstance(field_tree_instance, TreeInstance): @@ -76,20 +80,3 @@ def _infer_field(cls, field): return DjangoModelField(value, field).name raise Exception('Should be handled') - - if field_tree_instance.name.string_name == 'TimeField': - model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('time') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'DurationField': - model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('timedelta') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'DateField': - model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('date') - return DjangoModelField(model_instance_field_type, field).name - - if field_tree_instance.name.string_name == 'DateTimeField': - model_instance_field_type, = cls.inference_state.import_module(('datetime',)).py__getattribute__('datetime') - return DjangoModelField(model_instance_field_type, field).name - From 2a86f7d82fc9af349e28c2e6b51b5d831d832c41 Mon Sep 17 00:00:00 2001 From: ANtlord Date: Tue, 21 Jan 2020 21:21:43 +0200 Subject: [PATCH 08/55] Django-plugin related code is removed from stdlib-plugin. --- jedi/plugins/django.py | 13 +++++++++++++ jedi/plugins/registry.py | 3 ++- jedi/plugins/stdlib.py | 9 --------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index e5f839c6..cacb9e75 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -80,3 +80,16 @@ def _infer_field(cls, field): return DjangoModelField(value, field).name raise Exception('Should be handled') + + +def get_metaclass_filters(func): + def wrapper(cls, metaclasses): + for metaclass in metaclasses: + if metaclass.py__name__() == 'ModelBase' \ + and metaclass.get_root_context().py__name__() == 'django.db.models.base': + django_dict_filter = new_dict_filter(cls) + if django_dict_filter is not None: + return django_dict_filter + + return func(cls, metaclasses) + return wrapper diff --git a/jedi/plugins/registry.py b/jedi/plugins/registry.py index 23913244..66d653ce 100644 --- a/jedi/plugins/registry.py +++ b/jedi/plugins/registry.py @@ -4,7 +4,8 @@ This is not a plugin, this is just the place were plugins are registered. from jedi.plugins import stdlib from jedi.plugins import flask +from jedi.plugins import django from jedi.plugins import plugin_manager -plugin_manager.register(stdlib, flask) +plugin_manager.register(stdlib, flask, django) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index a97cc4e9..88684efd 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -35,8 +35,6 @@ from jedi.inference.filters import AttributeOverwrite, publish_method, \ ParserTreeFilter, DictFilter from jedi.inference.signature import AbstractSignature, SignatureWrapper -from . import django - # Copied from Python 3.6's stdlib. _NAMEDTUPLE_CLASS_TEMPLATE = """\ @@ -792,13 +790,6 @@ def get_metaclass_filters(func): return [DictFilter({ name.string_name: EnumInstance(cls, name).name for name in filter_.values() })] - - if metaclass.py__name__() == 'ModelBase' \ - and metaclass.get_root_context().py__name__() == 'django.db.models.base': - django_dict_filter = django.new_dict_filter(cls) - if django_dict_filter is not None: - return django_dict_filter - return func(cls, metaclasses) return wrapper From ddcd48edd8e2fa5dfd09969426c727a38b872ebd Mon Sep 17 00:00:00 2001 From: ANtlord Date: Wed, 22 Jan 2020 20:55:25 +0200 Subject: [PATCH 09/55] Typeshed submodule checked out to d386452 --- jedi/third_party/typeshed | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/third_party/typeshed b/jedi/third_party/typeshed index 3319cadf..d3864524 160000 --- a/jedi/third_party/typeshed +++ b/jedi/third_party/typeshed @@ -1 +1 @@ -Subproject commit 3319cadf85012328f8a12b15da4eecc8de0cf305 +Subproject commit d38645247816f862cafeed21a8f4466d306aacf3 From 8440e1719fe491e40a7a1940dceb6ef2b83754df Mon Sep 17 00:00:00 2001 From: ANtlord Date: Wed, 22 Jan 2020 20:57:17 +0200 Subject: [PATCH 10/55] Unuseful changes are rolled back. --- jedi/plugins/registry.py | 2 +- jedi/plugins/stdlib.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/plugins/registry.py b/jedi/plugins/registry.py index 06e4aede..c1a0b749 100644 --- a/jedi/plugins/registry.py +++ b/jedi/plugins/registry.py @@ -4,9 +4,9 @@ This is not a plugin, this is just the place were plugins are registered. from jedi.plugins import stdlib from jedi.plugins import flask +from jedi.plugins import pytest from jedi.plugins import django from jedi.plugins import plugin_manager -from jedi.plugins import pytest plugin_manager.register(stdlib, flask, pytest, django) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index c32535e1..448f2b50 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -21,7 +21,7 @@ from jedi.inference.arguments import \ from jedi.inference import analysis from jedi.inference import compiled from jedi.inference.value.instance import \ - AnonymousMethodExecutionContext, MethodExecutionContext, TreeInstance + AnonymousMethodExecutionContext, MethodExecutionContext from jedi.inference.base_value import ContextualizedNode, \ NO_VALUES, ValueSet, ValueWrapper, LazyValueWrapper from jedi.inference.value import ClassValue, ModuleValue From 3718d62e24c6ba365fa22aa73f2b49c5cce6b20f Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 18 Apr 2020 11:23:12 +0200 Subject: [PATCH 11/55] Make sure that calling Jedi with a random argument in CLI results in errors --- jedi/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jedi/__main__.py b/jedi/__main__.py index 2c94da2f..d4cdfd4a 100644 --- a/jedi/__main__.py +++ b/jedi/__main__.py @@ -61,3 +61,5 @@ elif len(sys.argv) > 1 and sys.argv[1] == 'linter': _start_linter() elif len(sys.argv) > 1 and sys.argv[1] == '_complete': _complete() +else: + print('Command not implemented: %s' % sys.argv[1]) From 10b2de2c3fbcc314e801cce3e4f57c87dd3c7de5 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 18 Apr 2020 11:23:25 +0200 Subject: [PATCH 12/55] Make the linter completely private --- jedi/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/__main__.py b/jedi/__main__.py index d4cdfd4a..a02b9a1f 100644 --- a/jedi/__main__.py +++ b/jedi/__main__.py @@ -57,7 +57,7 @@ if len(sys.argv) == 2 and sys.argv[1] == 'repl': # don't want to use __main__ only for repl yet, maybe we want to use it for # something else. So just use the keyword ``repl`` for now. print(join(dirname(abspath(__file__)), 'api', 'replstartup.py')) -elif len(sys.argv) > 1 and sys.argv[1] == 'linter': +elif len(sys.argv) > 1 and sys.argv[1] == '_linter': _start_linter() elif len(sys.argv) > 1 and sys.argv[1] == '_complete': _complete() From 851e0d59f0d80dd64d75c31cf88ae7b480be05dc Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 18 Apr 2020 12:19:14 +0200 Subject: [PATCH 13/55] Better developer tools --- jedi/__main__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/jedi/__main__.py b/jedi/__main__.py index a02b9a1f..6b442533 100644 --- a/jedi/__main__.py +++ b/jedi/__main__.py @@ -44,13 +44,20 @@ def _complete(): import jedi import pdb + if '-d' in sys.argv: + sys.argv.remove('-d') + jedi.set_debug_function() + try: - for c in jedi.Script(sys.argv[2]).complete(): + completions = jedi.Script(sys.argv[2]).complete() + for c in completions: c.docstring() c.type except Exception as e: - print(e) + print(repr(e)) pdb.post_mortem() + else: + print(completions) if len(sys.argv) == 2 and sys.argv[1] == 'repl': From d48575c8c5b1e52d0e4703ad7838b43d236cffc5 Mon Sep 17 00:00:00 2001 From: ANtlord Date: Sat, 18 Apr 2020 16:13:48 +0300 Subject: [PATCH 14/55] Simple tests of Django plugin are added. --- jedi/plugins/django.py | 45 +++++++++++---------- test/test_inference/test_django.py | 64 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 test/test_inference/test_django.py diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index cacb9e75..03a14c5d 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -1,5 +1,5 @@ """ -Module provides infering of Django model fields. +Module is used to infer Django model fields. """ from jedi.inference.base_value import LazyValueWrapper from jedi.inference.utils import safe_property @@ -10,9 +10,8 @@ from jedi.inference.value.instance import TreeInstance def new_dict_filter(cls): filter_ = ParserTreeFilter(parent_context=cls.as_context()) - return [DictFilter({ - f.string_name: _infer_field(cls, f) for f in filter_.values() - })] + res = {f.string_name: _infer_field(cls, f) for f in filter_.values()} + return [DictFilter({x: y for x, y in res.items() if y is not None})] class DjangoModelField(LazyValueWrapper): @@ -30,6 +29,7 @@ class DjangoModelField(LazyValueWrapper): obj, = self._cls.execute_with_values() return obj + mapping = { 'IntegerField': (None, 'int'), 'BigIntegerField': (None, 'int'), @@ -48,38 +48,41 @@ mapping = { 'DateTimeField': ('datetime', 'datetime'), } + +def _infer_scalar_field(cls, field, field_tree_instance): + if field_tree_instance.name.string_name not in mapping: + return None + + module_name, attribute_name = mapping[field_tree_instance.name.string_name] + if module_name is None: + module = cls.inference_state.builtins_module + else: + module = cls.inference_state.import_module((module_name,)) + + attribute, = module.py__getattribute__(attribute_name) + return DjangoModelField(attribute, field).name + + def _infer_field(cls, field): field_tree_instance, = field.infer() - - try: - module_name, attribute_name = mapping[field_tree_instance.name.string_name] - except KeyError: - pass - else: - if module_name is None: - module = cls.inference_state.builtins_module - else: - module = cls.inference_state.import_module((module_name,)) - attribute, = module.py__getattribute__(attribute_name) - return DjangoModelField(attribute, field).name + scalar_field = _infer_scalar_field(cls, field, field_tree_instance) + if scalar_field: + return scalar_field if field_tree_instance.name.string_name == 'ForeignKey': if isinstance(field_tree_instance, TreeInstance): argument_iterator = field_tree_instance._arguments.unpack() key, lazy_values = next(argument_iterator, (None, None)) if key is None and lazy_values is not None: - # TODO: it has only one element in current state. Handle rest of elements. for value in lazy_values.infer(): - string = value.get_safe_value(default=None) if value.name.string_name == 'str': - foreign_key_class_name = value._compiled_obj.get_safe_value() - # TODO: it has only one element in current state. Handle rest of elements. + foreign_key_class_name = value.get_safe_value() for v in cls.parent_context.py__getattribute__(foreign_key_class_name): return DjangoModelField(v, field).name else: return DjangoModelField(value, field).name - raise Exception('Should be handled') + print('TODO: {}'.format(field)) def get_metaclass_filters(func): diff --git a/test/test_inference/test_django.py b/test/test_inference/test_django.py new file mode 100644 index 00000000..16c9ee52 --- /dev/null +++ b/test/test_inference/test_django.py @@ -0,0 +1,64 @@ +import pytest +import datetime +import decimal + +source_tpl_basic_types = ''' +from django.db import models + +class BusinessModel(models.Model): + {0} = {1} + +p1 = BusinessModel() +p1_field = p1.{0} +p1_field.''' + + +source_tpl_foreign_key = ''' +from django.db import models + +class Category(models.Model): + category_name = models.CharField() + +class BusinessModel(models.Model): + category = models.ForeignKey(Category) + +p1 = BusinessModel() +p1_field = p1.category +p1_field.''' + + +@pytest.mark.parametrize('field_name, field_model_type, expected_fields', [ + ('integer_field', 'models.IntegerField()', dir(int)), + ('big_integer_field', 'models.BigIntegerField()', dir(int)), + ('positive_integer_field', 'models.PositiveIntegerField()', dir(int)), + ('small_integer_field', 'models.SmallIntegerField()', dir(int)), + ('char_field', 'models.CharField()', dir(str)), + ('text_field', 'models.TextField()', dir(str)), + ('email_field', 'models.EmailField()', dir(str)), + ('float_field', 'models.FloatField()', dir(float)), + ('binary_field', 'models.BinaryField()', dir(bytes)), + ('boolean_field', 'models.BooleanField()', dir(bool)), + ('decimal_field', 'models.DecimalField()', dir(decimal.Decimal)), + ('time_field', 'models.TimeField()', dir(datetime.time)), + ('duration_field', 'models.DurationField()', dir(datetime.timedelta)), + ('date_field', 'models.DateField()', dir(datetime.date)), + ('date_time_field', 'models.DateTimeField()', dir(datetime.datetime)), +]) +def test_basic_types( + field_name, + field_model_type, + expected_fields, + Script, +): + source = source_tpl_basic_types.format(field_name, field_model_type) + result = Script(source).complete() + result = {x.name for x in result} + expected_fields_public = [x for x in expected_fields if x[0] != '_'] + for field in expected_fields_public: + assert field in result + + +def test_foreign_key(Script): + result = Script(source_tpl_foreign_key).complete() + result = {x.name for x in result} + assert 'category_name' in result From 09950233e77c2ec02757bab9997f63c52cdc7dce Mon Sep 17 00:00:00 2001 From: ANtlord Date: Sat, 18 Apr 2020 18:36:04 +0300 Subject: [PATCH 15/55] Django is designated in test dependencies. --- jedi/plugins/django.py | 3 +++ setup.py | 1 + 2 files changed, 4 insertions(+) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 03a14c5d..8ebd73b4 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -1,5 +1,8 @@ """ Module is used to infer Django model fields. +Bugs: + - Can't infer User model. + - Can't infer ManyToManyField. """ from jedi.inference.base_value import LazyValueWrapper from jedi.inference.utils import safe_property diff --git a/setup.py b/setup.py index b73fea79..54418153 100755 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ setup(name='jedi', 'docopt', # coloroma for colored debug output 'colorama', + 'Django', ], 'qa': [ 'flake8==3.7.9', From 1d3082249f9f839ee0db43c962c5cdd000f72c9f Mon Sep 17 00:00:00 2001 From: ANtlord Date: Sat, 18 Apr 2020 18:51:12 +0300 Subject: [PATCH 16/55] Debug information corrections. --- jedi/plugins/django.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 8ebd73b4..0b518c00 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -85,7 +85,9 @@ def _infer_field(cls, field): else: return DjangoModelField(value, field).name - print('TODO: {}'.format(field)) + print('django plugin: fail to infer `{}` from class `{}`'.format( + field.string_name, cls.name.string_name, + )) def get_metaclass_filters(func): From 1c4a2edbdb0493fafa51d0c7a74a46dd722da851 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 18 Apr 2020 19:43:47 +0100 Subject: [PATCH 17/55] Fix construction of nested generic tuple return types Unfortunately this appears to show up a separate bug. --- jedi/inference/gradual/base.py | 2 +- test/completion/pep0484_generic_parameters.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/jedi/inference/gradual/base.py b/jedi/inference/gradual/base.py index 5d7be602..70cfa447 100644 --- a/jedi/inference/gradual/base.py +++ b/jedi/inference/gradual/base.py @@ -99,7 +99,7 @@ class DefineGenericBase(LazyValueWrapper): for generic_set in self.get_generics(): values = NO_VALUES for generic in generic_set: - if isinstance(generic, (GenericClass, TypeVar)): + if isinstance(generic, (DefineGenericBase, TypeVar)): result = generic.define_generics(type_var_dict) values |= result if result != ValueSet({generic}): diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index 89572d99..dabe8d7c 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -6,6 +6,7 @@ from typing import ( Iterable, List, Mapping, + Tuple, Type, TypeVar, Union, @@ -59,6 +60,26 @@ for b in list_type_t_to_list_t(list_of_int_type): b +# Test construction of nested generic tuple return parameters +def list_t_to_list_tuple_t(the_list: List[T]) -> List[Tuple[T]]: + return [(x,) for x in the_list] + + +x1t = list_t_to_list_tuple_t(list_of_ints)[0][0] +#? int() +x1t + + +for c1 in list_t_to_list_tuple_t(list_of_ints): + #? int() + c1[0] + + +for c2, in list_t_to_list_tuple_t(list_of_ints): + #? int() + c2 + + def foo(x: T) -> T: return x From 7ebbf9da447df90e41a655501a09e31cd3c140f7 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 18 Apr 2020 22:56:46 +0100 Subject: [PATCH 18/55] Make this test case obey typing rules in Python Unfortunately I can't recall exactly what it was that this test case was trying to validate, however on a second look it turns out that it was working by accident and did not represent a valid use of generic type vars in Python (which cannot be used completely unbound as this was). --- test/completion/pep0484_generic_parameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index dabe8d7c..a80d0346 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -80,11 +80,11 @@ for c2, in list_t_to_list_tuple_t(list_of_ints): c2 -def foo(x: T) -> T: +def foo(x: int) -> int: return x -list_of_funcs = [foo] # type: List[Callable[[T], T]] +list_of_funcs = [foo] # type: List[Callable[[int], int]] def list_func_t_to_list_func_type_t(the_list: List[Callable[[T], T]]) -> List[Callable[[Type[T]], T]]: def adapt(func: Callable[[T], T]) -> Callable[[Type[T]], T]: From 2ac806e39fc969b2bdcb9462807e743de1dc6493 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 19 Apr 2020 12:50:24 +0100 Subject: [PATCH 19/55] Add test which demonstrates incomplete generic Callable handling --- test/completion/pep0484_generic_parameters.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index a80d0346..4f0f0d1f 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -80,6 +80,7 @@ for c2, in list_t_to_list_tuple_t(list_of_ints): c2 +# Test handling of nested callables def foo(x: int) -> int: return x @@ -99,6 +100,21 @@ for b in list_func_t_to_list_func_type_t(list_of_funcs): b(int) +def bar(*a, **k) -> int: + return len(a) + len(k) + + +list_of_funcs_2 = [bar] # type: List[Callable[..., int]] + +def list_func_t_passthrough(the_list: List[Callable[..., T]]) -> List[Callable[..., T]]: + return the_list + + +for b in list_func_t_passthrough(list_of_funcs_2): + #? int() + b(None, x="x") + + mapping_int_str = {42: 'a'} # type: Dict[int, str] # Test that mappings (that have more than one parameter) are handled From f8e7447d3589d791556d81776f9cc1dc1c30ca5d Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 19 Apr 2020 13:12:54 +0100 Subject: [PATCH 20/55] Add handling of nested generic callables Previously tests for these were passing somewhat by accident, however this commit's parent adds a case which showed that the handling was missing. Note that this also relies on the recent fix for nested tuples which changed the `isinstance` check in `define_generics`. --- jedi/inference/gradual/typing.py | 21 ++++++++++++++++--- test/completion/pep0484_generic_mismatches.py | 20 +++++++----------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index ab0b449b..50ab8e83 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -217,9 +217,24 @@ class TypingClassValueWithIndex(_TypingClassMixin, TypingValueWithIndex): elif annotation_name == 'Callable': if len(annotation_generics) == 2: - return annotation_generics[1].infer_type_vars( - value_set.execute_annotation(), - ) + if is_class_value: + # This only applies if we are comparing something like + # List[Callable[..., T]] with Iterable[Callable[..., T]]. + # First, Jedi tries to match List/Iterable. After that we + # will land here, because is_class_value will be True at + # that point. Obviously we also compare below that both + # sides are `Callable`. + for element in value_set: + element_name = element.py__name__() + if element_name == 'Callable': + merge_type_var_dicts( + type_var_dict, + merge_pairwise_generics(self, element), + ) + else: + return annotation_generics[1].infer_type_vars( + value_set.execute_annotation(), + ) elif annotation_name == 'Tuple': tuple_annotation, = self.execute_annotation() diff --git a/test/completion/pep0484_generic_mismatches.py b/test/completion/pep0484_generic_mismatches.py index 47add048..fbd3c55a 100644 --- a/test/completion/pep0484_generic_mismatches.py +++ b/test/completion/pep0484_generic_mismatches.py @@ -206,40 +206,36 @@ for a in list_func_t_to_list_t(12): a -# The following are all actually wrong, however we're mainly testing here that -# we don't error when processing invalid values, rather than that we get the -# right output. - x0 = list_func_t_to_list_t(["abc"])[0] -#? str() +#? x0 x2 = list_func_t_to_list_t([tpl])[0] -#? tuple() +#? x2 x3 = list_func_t_to_list_t([tpl_typed])[0] -#? tuple() +#? x3 x4 = list_func_t_to_list_t([collection])[0] -#? dict() +#? x4 x5 = list_func_t_to_list_t([collection_typed])[0] -#? dict() +#? x5 x6 = list_func_t_to_list_t([custom_generic])[0] -#? CustomGeneric() +#? x6 x7 = list_func_t_to_list_t([plain_instance])[0] -#? PlainClass() +#? x7 for a in list_func_t_to_list_t([12]): - #? int() + #? a From df76b2462ede414fdb6d9acfb79b4f46b2e3563d Mon Sep 17 00:00:00 2001 From: ANtlord Date: Mon, 20 Apr 2020 10:31:03 +0300 Subject: [PATCH 21/55] Review corrections. --- test/completion/django.py | 65 ++++++++++++++++++++++++++++++ test/test_inference/test_django.py | 64 ----------------------------- 2 files changed, 65 insertions(+), 64 deletions(-) create mode 100644 test/completion/django.py delete mode 100644 test/test_inference/test_django.py diff --git a/test/completion/django.py b/test/completion/django.py new file mode 100644 index 00000000..7840491c --- /dev/null +++ b/test/completion/django.py @@ -0,0 +1,65 @@ +import pytest +import datetime +import decimal + +from django.db import models + + +class Category(models.Model): + category_name = models.CharField() + + +class BusinessModel(models.Model): + category_fk = models.ForeignKey(Category) + integer_field = models.IntegerField() + big_integer_field = models.BigIntegerField() + positive_integer_field = models.PositiveIntegerField() + small_integer_field = models.SmallIntegerField() + char_field = models.CharField() + text_field = models.TextField() + email_field = models.EmailField() + float_field = models.FloatField() + binary_field = models.BinaryField() + boolean_field = models.BooleanField() + decimal_field = models.DecimalField() + time_field = models.TimeField() + duration_field = models.DurationField() + date_field = models.DateField() + date_time_field = models.DateTimeField() + + +model_instance = BusinessModel() +#? int() +model_instance.integer_field +#? int() +model_instance.big_integer_field +#? int() +model_instance.positive_integer_field +#? int() +model_instance.small_integer_field +#? str() +model_instance.char_field +#? str() +model_instance.text_field +#? str() +model_instance.email_field +#? float() +model_instance.float_field +#? bytes() +model_instance.binary_field +#? bool() +model_instance.boolean_field +#? decimal.Decimal() +model_instance.decimal_field +#? datetime.time() +model_instance.time_field +#? datetime.timedelta() +model_instance.duration_field +#? datetime.date() +model_instance.date_field +#? datetime.datetime() +model_instance.date_time_field +#? Category() +model_instance.category_fk +#? str() +model_instance.category_fk.category_name diff --git a/test/test_inference/test_django.py b/test/test_inference/test_django.py deleted file mode 100644 index 16c9ee52..00000000 --- a/test/test_inference/test_django.py +++ /dev/null @@ -1,64 +0,0 @@ -import pytest -import datetime -import decimal - -source_tpl_basic_types = ''' -from django.db import models - -class BusinessModel(models.Model): - {0} = {1} - -p1 = BusinessModel() -p1_field = p1.{0} -p1_field.''' - - -source_tpl_foreign_key = ''' -from django.db import models - -class Category(models.Model): - category_name = models.CharField() - -class BusinessModel(models.Model): - category = models.ForeignKey(Category) - -p1 = BusinessModel() -p1_field = p1.category -p1_field.''' - - -@pytest.mark.parametrize('field_name, field_model_type, expected_fields', [ - ('integer_field', 'models.IntegerField()', dir(int)), - ('big_integer_field', 'models.BigIntegerField()', dir(int)), - ('positive_integer_field', 'models.PositiveIntegerField()', dir(int)), - ('small_integer_field', 'models.SmallIntegerField()', dir(int)), - ('char_field', 'models.CharField()', dir(str)), - ('text_field', 'models.TextField()', dir(str)), - ('email_field', 'models.EmailField()', dir(str)), - ('float_field', 'models.FloatField()', dir(float)), - ('binary_field', 'models.BinaryField()', dir(bytes)), - ('boolean_field', 'models.BooleanField()', dir(bool)), - ('decimal_field', 'models.DecimalField()', dir(decimal.Decimal)), - ('time_field', 'models.TimeField()', dir(datetime.time)), - ('duration_field', 'models.DurationField()', dir(datetime.timedelta)), - ('date_field', 'models.DateField()', dir(datetime.date)), - ('date_time_field', 'models.DateTimeField()', dir(datetime.datetime)), -]) -def test_basic_types( - field_name, - field_model_type, - expected_fields, - Script, -): - source = source_tpl_basic_types.format(field_name, field_model_type) - result = Script(source).complete() - result = {x.name for x in result} - expected_fields_public = [x for x in expected_fields if x[0] != '_'] - for field in expected_fields_public: - assert field in result - - -def test_foreign_key(Script): - result = Script(source_tpl_foreign_key).complete() - result = {x.name for x in result} - assert 'category_name' in result From b5c1c6d414cc2746493e06928872907b0b22a7b4 Mon Sep 17 00:00:00 2001 From: ANtlord Date: Tue, 21 Apr 2020 10:56:22 +0300 Subject: [PATCH 22/55] Django plugin test of ManyToManyField is added and marked for future implementation. --- test/completion/django.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/completion/django.py b/test/completion/django.py index 7840491c..6a50626f 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -5,6 +5,10 @@ import decimal from django.db import models +class Tag(models.Model): + tag_name = models.CharField() + + class Category(models.Model): category_name = models.CharField() @@ -26,6 +30,7 @@ class BusinessModel(models.Model): duration_field = models.DurationField() date_field = models.DateField() date_time_field = models.DateTimeField() + tags_m2m = models.ManyToManyField(Tag) model_instance = BusinessModel() @@ -63,3 +68,6 @@ model_instance.date_time_field model_instance.category_fk #? str() model_instance.category_fk.category_name +# TODO: implement many to many field support +model_instance.tags_m2m + From 086728365cb991164f63e94f13427052e32757fa Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 21 Apr 2020 23:36:00 +0200 Subject: [PATCH 23/55] Make Django test optional --- conftest.py | 6 ++++++ test/test_integration.py | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 91e772b3..4c6abf25 100644 --- a/conftest.py +++ b/conftest.py @@ -147,6 +147,12 @@ def has_typing(environment): return bool(script.infer()) +@pytest.fixture(scope='session') +def has_django(environment): + script = jedi.Script('import django', environment=environment) + return bool(script.infer()) + + @pytest.fixture(scope='session') def jedi_path(): return os.path.dirname(__file__) diff --git a/test/test_integration.py b/test/test_integration.py index 378fe893..ffa9cfe3 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -36,7 +36,7 @@ unspecified = %s """ % (case, sorted(d - a), sorted(a - d)) -def test_completion(case, monkeypatch, environment, has_typing): +def test_completion(case, monkeypatch, environment, has_typing, has_django): skip_reason = case.get_skip_reason(environment) if skip_reason is not None: pytest.skip(skip_reason) @@ -47,6 +47,8 @@ def test_completion(case, monkeypatch, environment, has_typing): _CONTAINS_TYPING = ('pep0484_typing', 'pep0484_comments', 'pep0526_variables') if not has_typing and any(x in case.path for x in _CONTAINS_TYPING): pytest.skip('Needs the typing module installed to run this test.') + if not has_django and case.path.endswith('django.py'): + pytest.skip('Needs django to be installed to run this test.') repo_root = helpers.root_dir monkeypatch.chdir(os.path.join(repo_root, 'jedi')) case.run(assert_case_equal, environment) From 89ad9a500b26e5aea0603646363af2a4d360a5bb Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 21 Apr 2020 23:41:54 +0200 Subject: [PATCH 24/55] Use debug instead of print for Django and fix indentation, see #1467 --- jedi/plugins/django.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 0b518c00..b3c4dae8 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -4,6 +4,7 @@ Bugs: - Can't infer User model. - Can't infer ManyToManyField. """ +from jedi import debug from jedi.inference.base_value import LazyValueWrapper from jedi.inference.utils import safe_property from jedi.inference.filters import ParserTreeFilter, DictFilter @@ -74,20 +75,19 @@ def _infer_field(cls, field): if field_tree_instance.name.string_name == 'ForeignKey': if isinstance(field_tree_instance, TreeInstance): - argument_iterator = field_tree_instance._arguments.unpack() - key, lazy_values = next(argument_iterator, (None, None)) - if key is None and lazy_values is not None: - for value in lazy_values.infer(): - if value.name.string_name == 'str': - foreign_key_class_name = value.get_safe_value() - for v in cls.parent_context.py__getattribute__(foreign_key_class_name): - return DjangoModelField(v, field).name - else: - return DjangoModelField(value, field).name + argument_iterator = field_tree_instance._arguments.unpack() + key, lazy_values = next(argument_iterator, (None, None)) + if key is None and lazy_values is not None: + for value in lazy_values.infer(): + if value.name.string_name == 'str': + foreign_key_class_name = value.get_safe_value() + for v in cls.parent_context.py__getattribute__(foreign_key_class_name): + return DjangoModelField(v, field).name + else: + return DjangoModelField(value, field).name - print('django plugin: fail to infer `{}` from class `{}`'.format( - field.string_name, cls.name.string_name, - )) + debug.dbg('django plugin: fail to infer `%s` from class `%s`', + field.string_name, cls.name.string_name) def get_metaclass_filters(func): From d96887b1029aa523d6262e401cb0b465282fc661 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 21 Apr 2020 23:43:59 +0200 Subject: [PATCH 25/55] Remove old third party django tests --- test/completion/thirdparty/django_.py | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 test/completion/thirdparty/django_.py diff --git a/test/completion/thirdparty/django_.py b/test/completion/thirdparty/django_.py deleted file mode 100644 index 9914a6d1..00000000 --- a/test/completion/thirdparty/django_.py +++ /dev/null @@ -1,11 +0,0 @@ -#! ['class ObjectDoesNotExist'] -from django.core.exceptions import ObjectDoesNotExist -import django - -#? ['get_version'] -django.get_version - -from django.conf import settings - -#? ['configured'] -settings.configured From df307b8edac3d114ec234ffaf7211af3801ea6f8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 00:05:35 +0200 Subject: [PATCH 26/55] Refactor a few things for django --- jedi/plugins/django.py | 19 +++++++++---------- test/completion/django.py | 4 +--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index b3c4dae8..434d9286 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -12,16 +12,10 @@ from jedi.inference.names import ValueName from jedi.inference.value.instance import TreeInstance -def new_dict_filter(cls): - filter_ = ParserTreeFilter(parent_context=cls.as_context()) - res = {f.string_name: _infer_field(cls, f) for f in filter_.values()} - return [DictFilter({x: y for x, y in res.items() if y is not None})] - - class DjangoModelField(LazyValueWrapper): def __init__(self, cls, name): self.inference_state = cls.inference_state - self._cls = cls # Corresponds to super().__self__ + self._cls = cls self._name = name self.tree_node = self._name.tree_name @@ -88,6 +82,13 @@ def _infer_field(cls, field): debug.dbg('django plugin: fail to infer `%s` from class `%s`', field.string_name, cls.name.string_name) + return None + + +def _new_dict_filter(cls): + filter_ = ParserTreeFilter(parent_context=cls.as_context()) + res = {f.string_name: _infer_field(cls, f) for f in filter_.values()} + return DictFilter({x: y for x, y in res.items() if y is not None}) def get_metaclass_filters(func): @@ -95,9 +96,7 @@ def get_metaclass_filters(func): for metaclass in metaclasses: if metaclass.py__name__() == 'ModelBase' \ and metaclass.get_root_context().py__name__() == 'django.db.models.base': - django_dict_filter = new_dict_filter(cls) - if django_dict_filter is not None: - return django_dict_filter + return [_new_dict_filter(cls)] return func(cls, metaclasses) return wrapper diff --git a/test/completion/django.py b/test/completion/django.py index 6a50626f..23ad6f43 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -1,4 +1,3 @@ -import pytest import datetime import decimal @@ -68,6 +67,5 @@ model_instance.date_time_field model_instance.category_fk #? str() model_instance.category_fk.category_name -# TODO: implement many to many field support +#? models.ManyToManyField() model_instance.tags_m2m - From 1a89fafce4444d506a8abc61bb1470fee987ce4b Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 00:15:35 +0200 Subject: [PATCH 27/55] Some other small refactorings --- jedi/plugins/django.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 434d9286..b299933f 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -58,7 +58,7 @@ def _infer_scalar_field(cls, field, field_tree_instance): module = cls.inference_state.import_module((module_name,)) attribute, = module.py__getattribute__(attribute_name) - return DjangoModelField(attribute, field).name + return DjangoModelField(attribute, field) def _infer_field(cls, field): @@ -76,9 +76,9 @@ def _infer_field(cls, field): if value.name.string_name == 'str': foreign_key_class_name = value.get_safe_value() for v in cls.parent_context.py__getattribute__(foreign_key_class_name): - return DjangoModelField(v, field).name + return DjangoModelField(v, field) else: - return DjangoModelField(value, field).name + return DjangoModelField(value, field) debug.dbg('django plugin: fail to infer `%s` from class `%s`', field.string_name, cls.name.string_name) @@ -86,9 +86,13 @@ def _infer_field(cls, field): def _new_dict_filter(cls): - filter_ = ParserTreeFilter(parent_context=cls.as_context()) - res = {f.string_name: _infer_field(cls, f) for f in filter_.values()} - return DictFilter({x: y for x, y in res.items() if y is not None}) + def iterate(): + filter_ = ParserTreeFilter(parent_context=cls.as_context()) + for f in filter_.values(): + django_field = _infer_field(cls, f) + if django_field is not None: + yield f.string_name, django_field.name + return DictFilter(dict(iterate())) def get_metaclass_filters(func): From ba4e3393d35c0e7057bd9eeefc65542f5b8ad374 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 00:27:06 +0200 Subject: [PATCH 28/55] Fix ForeignKey issues with invalid values --- jedi/plugins/django.py | 8 +++++--- test/completion/django.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index b299933f..1ab59464 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -48,10 +48,11 @@ mapping = { def _infer_scalar_field(cls, field, field_tree_instance): - if field_tree_instance.name.string_name not in mapping: + try: + module_name, attribute_name = mapping[field_tree_instance.name.string_name] + except KeyError: return None - module_name, attribute_name = mapping[field_tree_instance.name.string_name] if module_name is None: module = cls.inference_state.builtins_module else: @@ -69,6 +70,7 @@ def _infer_field(cls, field): if field_tree_instance.name.string_name == 'ForeignKey': if isinstance(field_tree_instance, TreeInstance): + # TODO private access.. argument_iterator = field_tree_instance._arguments.unpack() key, lazy_values = next(argument_iterator, (None, None)) if key is None and lazy_values is not None: @@ -77,7 +79,7 @@ def _infer_field(cls, field): foreign_key_class_name = value.get_safe_value() for v in cls.parent_context.py__getattribute__(foreign_key_class_name): return DjangoModelField(v, field) - else: + elif value.is_class(): return DjangoModelField(value, field) debug.dbg('django plugin: fail to infer `%s` from class `%s`', diff --git a/test/completion/django.py b/test/completion/django.py index 23ad6f43..8d56915c 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -14,6 +14,8 @@ class Category(models.Model): class BusinessModel(models.Model): category_fk = models.ForeignKey(Category) + category_fk2 = models.ForeignKey('Category') + category_fk3 = models.ForeignKey(1) integer_field = models.IntegerField() big_integer_field = models.BigIntegerField() positive_integer_field = models.PositiveIntegerField() @@ -63,9 +65,17 @@ model_instance.duration_field model_instance.date_field #? datetime.datetime() model_instance.date_time_field + #? Category() model_instance.category_fk #? str() model_instance.category_fk.category_name +#? Category() +model_instance.category_fk2 +#? str() +model_instance.category_fk2.category_name +#? models.ForeignKey() +model_instance.category_fk3 + #? models.ManyToManyField() model_instance.tags_m2m From 7756792bba6cf6d4254917fd2515e269101dd9ef Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 00:33:51 +0200 Subject: [PATCH 29/55] Fix another issue with foreign keys --- jedi/plugins/django.py | 5 +++-- test/completion/django.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 1ab59464..e8ea9665 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -77,8 +77,9 @@ def _infer_field(cls, field): for value in lazy_values.infer(): if value.name.string_name == 'str': foreign_key_class_name = value.get_safe_value() - for v in cls.parent_context.py__getattribute__(foreign_key_class_name): - return DjangoModelField(v, field) + for v in cls.get_root_context().py__getattribute__(foreign_key_class_name): + if v.is_class(): + return DjangoModelField(v, field) elif value.is_class(): return DjangoModelField(value, field) diff --git a/test/completion/django.py b/test/completion/django.py index 8d56915c..b89e67d2 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -16,6 +16,7 @@ class BusinessModel(models.Model): category_fk = models.ForeignKey(Category) category_fk2 = models.ForeignKey('Category') category_fk3 = models.ForeignKey(1) + category_fk4 = models.ForeignKey('models') integer_field = models.IntegerField() big_integer_field = models.BigIntegerField() positive_integer_field = models.PositiveIntegerField() @@ -76,6 +77,8 @@ model_instance.category_fk2 model_instance.category_fk2.category_name #? models.ForeignKey() model_instance.category_fk3 +#? models.ForeignKey() +model_instance.category_fk4 #? models.ManyToManyField() model_instance.tags_m2m From 17eeb737675d8e5a9073b42e649608d5f31c697c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 00:41:59 +0200 Subject: [PATCH 30/55] Some nitpicks --- jedi/plugins/django.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index e8ea9665..c2b95026 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -47,54 +47,55 @@ mapping = { } -def _infer_scalar_field(cls, field, field_tree_instance): +def _infer_scalar_field(inference_state, field_name, field_tree_instance): try: - module_name, attribute_name = mapping[field_tree_instance.name.string_name] + module_name, attribute_name = mapping[field_tree_instance.py__name__()] except KeyError: return None if module_name is None: - module = cls.inference_state.builtins_module + module = inference_state.builtins_module else: - module = cls.inference_state.import_module((module_name,)) + module = inference_state.import_module((module_name,)) attribute, = module.py__getattribute__(attribute_name) - return DjangoModelField(attribute, field) + return DjangoModelField(attribute, field_name) -def _infer_field(cls, field): - field_tree_instance, = field.infer() - scalar_field = _infer_scalar_field(cls, field, field_tree_instance) +def _infer_field(cls, field_name): + inference_state = cls.inference_state + field_tree_instance, = field_name.infer() + scalar_field = _infer_scalar_field(inference_state, field_name, field_tree_instance) if scalar_field: return scalar_field - if field_tree_instance.name.string_name == 'ForeignKey': + if field_tree_instance.py__name__() == 'ForeignKey': if isinstance(field_tree_instance, TreeInstance): # TODO private access.. argument_iterator = field_tree_instance._arguments.unpack() key, lazy_values = next(argument_iterator, (None, None)) if key is None and lazy_values is not None: for value in lazy_values.infer(): - if value.name.string_name == 'str': + if value.py__name__() == 'str': foreign_key_class_name = value.get_safe_value() for v in cls.get_root_context().py__getattribute__(foreign_key_class_name): if v.is_class(): - return DjangoModelField(v, field) + return DjangoModelField(v, field_name) elif value.is_class(): - return DjangoModelField(value, field) + return DjangoModelField(value, field_name) debug.dbg('django plugin: fail to infer `%s` from class `%s`', - field.string_name, cls.name.string_name) + field_name.string_name, cls.py__name__()) return None def _new_dict_filter(cls): def iterate(): filter_ = ParserTreeFilter(parent_context=cls.as_context()) - for f in filter_.values(): - django_field = _infer_field(cls, f) + for name in filter_.values(): + django_field = _infer_field(cls, name) if django_field is not None: - yield f.string_name, django_field.name + yield name.string_name, django_field.name return DictFilter(dict(iterate())) From f9176578ea19ce07cd1cd340fbfbe21f965324ce Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 00:54:43 +0200 Subject: [PATCH 31/55] Fix another django modelfield issue --- jedi/plugins/django.py | 41 ++++++++++++++++++++------------------- test/completion/django.py | 10 ++++++++++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index c2b95026..5100eef2 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -58,31 +58,32 @@ def _infer_scalar_field(inference_state, field_name, field_tree_instance): else: module = inference_state.import_module((module_name,)) - attribute, = module.py__getattribute__(attribute_name) - return DjangoModelField(attribute, field_name) + for attribute in module.py__getattribute__(attribute_name): + return DjangoModelField(attribute, field_name) def _infer_field(cls, field_name): inference_state = cls.inference_state - field_tree_instance, = field_name.infer() - scalar_field = _infer_scalar_field(inference_state, field_name, field_tree_instance) - if scalar_field: - return scalar_field + for field_tree_instance in field_name.infer(): + scalar_field = _infer_scalar_field(inference_state, field_name, field_tree_instance) + if scalar_field: + return scalar_field - if field_tree_instance.py__name__() == 'ForeignKey': - if isinstance(field_tree_instance, TreeInstance): - # TODO private access.. - argument_iterator = field_tree_instance._arguments.unpack() - key, lazy_values = next(argument_iterator, (None, None)) - if key is None and lazy_values is not None: - for value in lazy_values.infer(): - if value.py__name__() == 'str': - foreign_key_class_name = value.get_safe_value() - for v in cls.get_root_context().py__getattribute__(foreign_key_class_name): - if v.is_class(): - return DjangoModelField(v, field_name) - elif value.is_class(): - return DjangoModelField(value, field_name) + if field_tree_instance.py__name__() == 'ForeignKey': + if isinstance(field_tree_instance, TreeInstance): + # TODO private access.. + argument_iterator = field_tree_instance._arguments.unpack() + key, lazy_values = next(argument_iterator, (None, None)) + if key is None and lazy_values is not None: + for value in lazy_values.infer(): + if value.py__name__() == 'str': + foreign_key_class_name = value.get_safe_value() + module = cls.get_root_context() + for v in module.py__getattribute__(foreign_key_class_name): + if v.is_class(): + return DjangoModelField(v, field_name) + elif value.is_class(): + return DjangoModelField(value, field_name) debug.dbg('django plugin: fail to infer `%s` from class `%s`', field_name.string_name, cls.py__name__()) diff --git a/test/completion/django.py b/test/completion/django.py index b89e67d2..6bdc2eea 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -17,6 +17,7 @@ class BusinessModel(models.Model): category_fk2 = models.ForeignKey('Category') category_fk3 = models.ForeignKey(1) category_fk4 = models.ForeignKey('models') + category_fk5 = models.ForeignKey() integer_field = models.IntegerField() big_integer_field = models.BigIntegerField() positive_integer_field = models.PositiveIntegerField() @@ -34,6 +35,8 @@ class BusinessModel(models.Model): date_time_field = models.DateTimeField() tags_m2m = models.ManyToManyField(Tag) + unidentifiable = NOT_FOUND + model_instance = BusinessModel() #? int() @@ -79,6 +82,13 @@ model_instance.category_fk2.category_name model_instance.category_fk3 #? models.ForeignKey() model_instance.category_fk4 +#? models.ForeignKey() +model_instance.category_fk5 #? models.ManyToManyField() model_instance.tags_m2m + +#? +model_instance.unidentifiable +#! ['unidentifiable = NOT_FOUND'] +model_instance.unidentifiable From f3eaa418bb4e72170030e4ec69fe78bf689dfcc4 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 09:32:31 +0200 Subject: [PATCH 32/55] Work with a NameWrapper, so Django goto works better --- jedi/plugins/django.py | 57 +++++++++++++++++---------------------- test/completion/django.py | 7 ++++- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 5100eef2..630a3d08 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -5,29 +5,13 @@ Bugs: - Can't infer ManyToManyField. """ from jedi import debug -from jedi.inference.base_value import LazyValueWrapper +from jedi.inference.base_value import LazyValueWrapper, ValueSet, NO_VALUES from jedi.inference.utils import safe_property from jedi.inference.filters import ParserTreeFilter, DictFilter -from jedi.inference.names import ValueName +from jedi.inference.names import ValueName, NameWrapper from jedi.inference.value.instance import TreeInstance -class DjangoModelField(LazyValueWrapper): - def __init__(self, cls, name): - self.inference_state = cls.inference_state - self._cls = cls - self._name = name - self.tree_node = self._name.tree_name - - @safe_property - def name(self): - return ValueName(self, self._name.tree_name) - - def _get_wrapped_value(self): - obj, = self._cls.execute_with_values() - return obj - - mapping = { 'IntegerField': (None, 'int'), 'BigIntegerField': (None, 'int'), @@ -59,14 +43,14 @@ def _infer_scalar_field(inference_state, field_name, field_tree_instance): module = inference_state.import_module((module_name,)) for attribute in module.py__getattribute__(attribute_name): - return DjangoModelField(attribute, field_name) + return attribute.execute_with_values() def _infer_field(cls, field_name): 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) - if scalar_field: + if scalar_field is not None: return scalar_field if field_tree_instance.py__name__() == 'ForeignKey': @@ -79,25 +63,34 @@ def _infer_field(cls, field_name): if value.py__name__() == 'str': foreign_key_class_name = value.get_safe_value() module = cls.get_root_context() - for v in module.py__getattribute__(foreign_key_class_name): - if v.is_class(): - return DjangoModelField(v, field_name) + return ValueSet.from_sets( + v.execute_with_values() + for v in module.py__getattribute__(foreign_key_class_name) + if v.is_class() + ) elif value.is_class(): - return DjangoModelField(value, field_name) + return value.execute_with_values() debug.dbg('django plugin: fail to infer `%s` from class `%s`', field_name.string_name, cls.py__name__()) - return None + return field_name.infer() + + +class DjangoModelName(NameWrapper): + def __init__(self, cls, name): + super(DjangoModelName, self).__init__(name) + self._cls = cls + + def infer(self): + return _infer_field(self._cls, self._wrapped_name) def _new_dict_filter(cls): - def iterate(): - filter_ = ParserTreeFilter(parent_context=cls.as_context()) - for name in filter_.values(): - django_field = _infer_field(cls, name) - if django_field is not None: - yield name.string_name, django_field.name - return DictFilter(dict(iterate())) + filter_ = ParserTreeFilter(parent_context=cls.as_context()) + return DictFilter({ + name.string_name: DjangoModelName(cls, name) + for name in filter_.values() + }) def get_metaclass_filters(func): diff --git a/test/completion/django.py b/test/completion/django.py index 6bdc2eea..f089d956 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -18,6 +18,7 @@ class BusinessModel(models.Model): category_fk3 = models.ForeignKey(1) category_fk4 = models.ForeignKey('models') category_fk5 = models.ForeignKey() + integer_field = models.IntegerField() big_integer_field = models.BigIntegerField() positive_integer_field = models.PositiveIntegerField() @@ -70,6 +71,10 @@ model_instance.date_field #? datetime.datetime() model_instance.date_time_field +#! ['category_fk = models.ForeignKey(Category)'] +model_instance.category_fk +#! ['category_name = models.CharField()'] +model_instance.category_fk.category_name #? Category() model_instance.category_fk #? str() @@ -80,7 +85,7 @@ model_instance.category_fk2 model_instance.category_fk2.category_name #? models.ForeignKey() model_instance.category_fk3 -#? models.ForeignKey() +#? model_instance.category_fk4 #? models.ForeignKey() model_instance.category_fk5 From f3152a8c2b33897574cd99d53ae994611d5ef538 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 09:44:31 +0200 Subject: [PATCH 33/55] Django is not supported for Python 2 --- test/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_integration.py b/test/test_integration.py index ffa9cfe3..0cc1636b 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -47,7 +47,7 @@ def test_completion(case, monkeypatch, environment, has_typing, has_django): _CONTAINS_TYPING = ('pep0484_typing', 'pep0484_comments', 'pep0526_variables') if not has_typing and any(x in case.path for x in _CONTAINS_TYPING): pytest.skip('Needs the typing module installed to run this test.') - if not has_django and case.path.endswith('django.py'): + if (not has_django or environment.version_info.major == 2) and case.path.endswith('django.py'): pytest.skip('Needs django to be installed to run this test.') repo_root = helpers.root_dir monkeypatch.chdir(os.path.join(repo_root, 'jedi')) From 94d374c9ceeb16ea1c6fe49b8e039d415c7dbe5a Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 17:32:40 +0200 Subject: [PATCH 34/55] Fix a small issue with the help method, fixes #1556 --- test/test_api/test_documentation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_api/test_documentation.py b/test/test_api/test_documentation.py index 2aa6140a..584666b7 100644 --- a/test/test_api/test_documentation.py +++ b/test/test_api/test_documentation.py @@ -115,3 +115,8 @@ def test_docstring_decorator(goto_or_help_or_infer, skip_python2): doc = d.docstring() assert doc == 'FunctionType(*args: Any, **kwargs: Any) -> Any\n\nhello' + + +@pytest.mark.parametrize('code', ['', '\n', ' ']) +def test_empty(Script, code): + assert not Script(code).help(1, 0) From 0f39135ae58c941c0e00d3354828a77fabb324ee Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 22 Apr 2020 23:14:40 +0200 Subject: [PATCH 35/55] Start changelog for 0.17.1 --- CHANGELOG.rst | 6 ++++++ jedi/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 161e85ed..683e533c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,12 @@ Changelog --------- +Unreleased: 0.17.1 (2020-04-) ++++++++++++++++++++ + +- Django ``Model`` meta class support +- A few bugfixes + 0.17.0 (2020-04-14) +++++++++++++++++++ diff --git a/jedi/__init__.py b/jedi/__init__.py index 6824efe3..f15ac9f5 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -27,7 +27,7 @@ ad load """ -__version__ = '0.17.0' +__version__ = '0.17.1' from jedi.api import Script, Interpreter, set_debug_function, \ preload_module, names From 784f9ff081177495f7c1d94f8fe5ea2dba732bc0 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 23 Apr 2020 10:10:58 +0200 Subject: [PATCH 36/55] Actually fix #1556, forgot to add this in 94d374c9ceeb16ea1c6fe49b8e039d415c7dbe5a --- conftest.py | 2 +- jedi/api/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index 4c6abf25..08c5e81a 100644 --- a/conftest.py +++ b/conftest.py @@ -13,7 +13,7 @@ from test.helpers import test_dir collect_ignore = [ 'setup.py', - '__main__.py', + 'jedi/__main__.py', 'jedi/inference/compiled/subprocess/__main__.py', 'build/', 'test/examples', diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 591dd19e..47e046e4 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -472,7 +472,7 @@ class Script(object): if definitions: return definitions leaf = self._module_node.get_leaf_for_position((line, column)) - if leaf.type in ('keyword', 'operator', 'error_leaf'): + if leaf is not None and leaf.type in ('keyword', 'operator', 'error_leaf'): reserved = self._inference_state.grammar._pgen_grammar.reserved_syntax_strings.keys() if leaf.value in reserved: name = KeywordName(self._inference_state, leaf.value) From be82d5ff364b0aaf0ef3e309e2e5196fc6f97bbf Mon Sep 17 00:00:00 2001 From: Josh Bax Date: Thu, 23 Apr 2020 14:05:11 -0700 Subject: [PATCH 37/55] Remove a redundant check from Name.desc_with_module --- jedi/api/classes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jedi/api/classes.py b/jedi/api/classes.py index a57a1613..274ac898 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -734,8 +734,7 @@ class Name(BaseName): DeprecationWarning, stacklevel=2 ) - position = '' if self.in_builtin_module else '@%s' % self.line - return "%s:%s%s" % (self.module_name, self.description, position) + return "%s:%s" % (self.module_name, self.description) @memoize_method def defined_names(self): From 912fe68069e163d8f05262e59464dd65faea2e11 Mon Sep 17 00:00:00 2001 From: Josh Bax Date: Thu, 23 Apr 2020 14:32:32 -0700 Subject: [PATCH 38/55] Fix typos in api.classes docstrings --- jedi/api/classes.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 274ac898..e859b674 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -94,7 +94,11 @@ class BaseName(object): @property def module_path(self): - """Shows the file path of a module. e.g. ``/usr/lib/python2.7/os.py``""" + """ + Shows the file path of a module. e.g. ``/usr/lib/python2.7/os.py`` + + :rtype: str or None + """ module = self._get_module_context() if module.is_stub() or not module.is_compiled(): # Compiled modules should not return a module path even if they @@ -168,7 +172,7 @@ class BaseName(object): >>> defs[3] 'function' - Valid values for are ``module``, ``class``, ``instance``, ``function``, + Valid values for type are ``module``, ``class``, ``instance``, ``function``, ``param``, ``path``, ``keyword`` and ``statement``. """ @@ -245,8 +249,8 @@ class BaseName(object): Document for function f. Notice that useful extra information is added to the actual - docstring. For function, it is signature. If you need - actual docstring, use ``raw=True`` instead. + docstring, e.g. function signatures are prepended to their docstrings. + If you need the actual docstring, use ``raw=True`` instead. >>> print(script.infer(1, len('def f'))[0].docstring(raw=True)) Document for function f. @@ -665,7 +669,7 @@ class Completion(BaseName): def docstring(self, raw=False, fast=True): """ - Documentated under :meth:`BaseName.docstring`. + Documented under :meth:`BaseName.docstring`. """ if self._like_name_length >= 3: # In this case we can just resolve the like name, because we @@ -703,7 +707,7 @@ class Completion(BaseName): @property def type(self): """ - Documentated under :meth:`BaseName.type`. + Documented under :meth:`BaseName.type`. """ # Purely a speed optimization. if self._cached_name is not None: @@ -797,7 +801,7 @@ class BaseSignature(Name): Returns a text representation of the signature. This could for example look like ``foo(bar, baz: int, **kwargs)``. - :return str + :rtype: str """ return self._signature.to_string() @@ -864,7 +868,7 @@ class ParamName(Name): Returns a simple representation of a param, like ``f: Callable[..., Any]``. - :rtype: :class:`str` + :rtype: str """ return self._name.to_string() From 11eb4f8fde4bafd26e8a024f967715064e259cae Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 23 Apr 2020 23:52:51 +0200 Subject: [PATCH 39/55] Remove unused imports --- jedi/plugins/django.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 630a3d08..16f9eb41 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -5,10 +5,9 @@ Bugs: - Can't infer ManyToManyField. """ from jedi import debug -from jedi.inference.base_value import LazyValueWrapper, ValueSet, NO_VALUES -from jedi.inference.utils import safe_property +from jedi.inference.base_value import ValueSet from jedi.inference.filters import ParserTreeFilter, DictFilter -from jedi.inference.names import ValueName, NameWrapper +from jedi.inference.names import NameWrapper from jedi.inference.value.instance import TreeInstance From 2e1284f04453d54edb5a149d88ec994daaa9d854 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 00:24:26 +0200 Subject: [PATCH 40/55] Fix a recursion error issue --- jedi/inference/value/iterable.py | 6 +++++- test/completion/recursion.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/jedi/inference/value/iterable.py b/jedi/inference/value/iterable.py index 3fc39e52..d23ca13e 100644 --- a/jedi/inference/value/iterable.py +++ b/jedi/inference/value/iterable.py @@ -192,13 +192,17 @@ class Sequence(LazyAttributeOverwrite, IterableMixin): def _get_generics(self): return (self.merge_types_of_iterate().py__class__(),) + @inference_state_method_cache(default=()) + def _cached_generics(self): + return self._get_generics() + def _get_wrapped_value(self): from jedi.inference.gradual.base import GenericClass from jedi.inference.gradual.generics import TupleGenericManager klass = compiled.builtin_from_name(self.inference_state, self.array_type) c, = GenericClass( klass, - TupleGenericManager(self._get_generics()) + TupleGenericManager(self._cached_generics()) ).execute_annotation() return c diff --git a/test/completion/recursion.py b/test/completion/recursion.py index 1e882bfa..e595497f 100644 --- a/test/completion/recursion.py +++ b/test/completion/recursion.py @@ -103,3 +103,15 @@ while True: bar = bar # type: bar #? int() bar + + +class Comprehension: + def __init__(self, foo): + self.foo = foo + + def update(self): + self.foo = (self.foo,) + + +#? int() tuple() +Comprehension(1).foo[0] From 6d927d502e2eeb0e5ee263151880ce607990913b Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 00:38:15 +0200 Subject: [PATCH 41/55] Make sure that infering the Django User model works --- jedi/plugins/django.py | 1 - test/completion/django.py | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 16f9eb41..606fe32c 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -1,7 +1,6 @@ """ Module is used to infer Django model fields. Bugs: - - Can't infer User model. - Can't infer ManyToManyField. """ from jedi import debug diff --git a/test/completion/django.py b/test/completion/django.py index f089d956..4baf9ec2 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -2,6 +2,11 @@ import datetime import decimal from django.db import models +from django.contrib.auth.models import User + + +#? str() +User().email class Tag(models.Model): @@ -38,6 +43,9 @@ class BusinessModel(models.Model): unidentifiable = NOT_FOUND +# ----------------- +# Model attribute inference +# ----------------- model_instance = BusinessModel() #? int() From 6bff30fbbb348d66a5f87e7bdc4afe951ed8e602 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 13:43:52 +0200 Subject: [PATCH 42/55] Include Django stubs as a third party repo --- .gitmodules | 3 +++ jedi/inference/gradual/typeshed.py | 9 +++++++++ jedi/plugins/django.py | 22 ++++++++++++++++++++-- jedi/third_party/django-stubs | 1 + test/completion/django.py | 11 +++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 160000 jedi/third_party/django-stubs diff --git a/.gitmodules b/.gitmodules index 3ce8ff23..368cba0a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "jedi/third_party/typeshed"] path = jedi/third_party/typeshed 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 diff --git a/jedi/inference/gradual/typeshed.py b/jedi/inference/gradual/typeshed.py index 005a398f..bcf839a0 100644 --- a/jedi/inference/gradual/typeshed.py +++ b/jedi/inference/gradual/typeshed.py @@ -12,6 +12,8 @@ from jedi.inference.value import ModuleValue _jedi_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) TYPESHED_PATH = os.path.join(_jedi_path, 'third_party', 'typeshed') +DJANGO_INIT_PATH = os.path.join(_jedi_path, 'third_party', 'django-stubs', + 'django-stubs', '__init__.pyi') _IMPORT_MAP = dict( _collections='collections', @@ -173,6 +175,13 @@ def _try_to_load_stub(inference_state, import_names, python_value_set, ) if m is not None: return m + if import_names[0] == 'django': + return _try_to_load_stub_from_file( + inference_state, + python_value_set, + file_io=FileIO(DJANGO_INIT_PATH), + import_names=import_names, + ) # 2. Try to load pyi files next to py files. for c in python_value_set: diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 606fe32c..3c0b3cbe 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -8,6 +8,8 @@ from jedi.inference.base_value import ValueSet from jedi.inference.filters import ParserTreeFilter, DictFilter from jedi.inference.names import NameWrapper from jedi.inference.value.instance import TreeInstance +from jedi.inference.gradual.base import GenericClass +from jedi.inference.gradual.generics import TupleGenericManager mapping = { @@ -83,12 +85,28 @@ class DjangoModelName(NameWrapper): return _infer_field(self._cls, self._wrapped_name) +def _create_manager_for(cls): + managers = cls.inference_state.import_module( + ('django', 'db', 'models', 'manager') + ).py__getattribute__('BaseManager') + for m in managers: + if m.is_class() and not m.is_compiled(): + generics_manager = TupleGenericManager((ValueSet([cls]),)) + for c in GenericClass(m, generics_manager).execute_annotation(): + return c + return None + + def _new_dict_filter(cls): filter_ = ParserTreeFilter(parent_context=cls.as_context()) - return DictFilter({ + dct = { name.string_name: DjangoModelName(cls, name) for name in filter_.values() - }) + } + manager = _create_manager_for(cls) + if manager: + dct['objects'] = manager.name + return DictFilter(dct) def get_metaclass_filters(func): diff --git a/jedi/third_party/django-stubs b/jedi/third_party/django-stubs new file mode 160000 index 00000000..92c8dfc9 --- /dev/null +++ b/jedi/third_party/django-stubs @@ -0,0 +1 @@ +Subproject commit 92c8dfc93f840b936e33eb3f1770293627ac0f15 diff --git a/test/completion/django.py b/test/completion/django.py index 4baf9ec2..d0349601 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -105,3 +105,14 @@ model_instance.tags_m2m model_instance.unidentifiable #! ['unidentifiable = NOT_FOUND'] model_instance.unidentifiable + +# ----------------- +# Queries +# ----------------- + +#? models.query.QuerySet.filter +model_instance.objects.filter +#? BusinessModel() None +model_instance.objects.filter().first() +#? str() +model_instance.objects.get().char_field From f6803bce2c3e9433f991d3005d6fa78d0b64f788 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 16:17:47 +0200 Subject: [PATCH 43/55] Infer many to many fields --- jedi/plugins/django.py | 53 +++++++++++++++++++++++---------------- test/completion/django.py | 10 +++++--- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 3c0b3cbe..0400c6a9 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -1,10 +1,8 @@ """ Module is used to infer Django model fields. -Bugs: - - Can't infer ManyToManyField. """ from jedi import debug -from jedi.inference.base_value import ValueSet +from jedi.inference.base_value import ValueSet, iterator_to_value_set from jedi.inference.filters import ParserTreeFilter, DictFilter from jedi.inference.names import NameWrapper from jedi.inference.value.instance import TreeInstance @@ -46,6 +44,24 @@ def _infer_scalar_field(inference_state, field_name, field_tree_instance): return attribute.execute_with_values() +@iterator_to_value_set +def _get_foreign_key_values(cls, field_tree_instance): + if isinstance(field_tree_instance, TreeInstance): + # TODO private access.. + argument_iterator = field_tree_instance._arguments.unpack() + key, lazy_values = next(argument_iterator, (None, None)) + if key is None and lazy_values is not None: + for value in lazy_values.infer(): + if value.py__name__() == 'str': + foreign_key_class_name = value.get_safe_value() + module = cls.get_root_context() + for v in module.py__getattribute__(foreign_key_class_name): + if v.is_class(): + yield v + elif value.is_class(): + yield value + + def _infer_field(cls, field_name): inference_state = cls.inference_state for field_tree_instance in field_name.infer(): @@ -53,23 +69,16 @@ def _infer_field(cls, field_name): if scalar_field is not None: return scalar_field - if field_tree_instance.py__name__() == 'ForeignKey': - if isinstance(field_tree_instance, TreeInstance): - # TODO private access.. - argument_iterator = field_tree_instance._arguments.unpack() - key, lazy_values = next(argument_iterator, (None, None)) - if key is None and lazy_values is not None: - for value in lazy_values.infer(): - if value.py__name__() == 'str': - foreign_key_class_name = value.get_safe_value() - module = cls.get_root_context() - return ValueSet.from_sets( - v.execute_with_values() - for v in module.py__getattribute__(foreign_key_class_name) - if v.is_class() - ) - elif value.is_class(): - return value.execute_with_values() + name = field_tree_instance.py__name__() + is_many_to_many = name == 'ManyToManyField' + if name == 'ForeignKey' or is_many_to_many: + values = _get_foreign_key_values(cls, field_tree_instance) + if is_many_to_many: + return ValueSet(filter(None, [ + _create_manager_for(v, 'RelatedManager') for v in values + ])) + else: + return values.execute_with_values() debug.dbg('django plugin: fail to infer `%s` from class `%s`', field_name.string_name, cls.py__name__()) @@ -85,10 +94,10 @@ class DjangoModelName(NameWrapper): return _infer_field(self._cls, self._wrapped_name) -def _create_manager_for(cls): +def _create_manager_for(cls, manager_cls='BaseManager'): managers = cls.inference_state.import_module( ('django', 'db', 'models', 'manager') - ).py__getattribute__('BaseManager') + ).py__getattribute__(manager_cls) for m in managers: if m.is_class() and not m.is_compiled(): generics_manager = TupleGenericManager((ValueSet([cls]),)) diff --git a/test/completion/django.py b/test/completion/django.py index d0349601..4c2cf6ec 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -91,15 +91,19 @@ model_instance.category_fk.category_name model_instance.category_fk2 #? str() model_instance.category_fk2.category_name -#? models.ForeignKey() +#? model_instance.category_fk3 #? model_instance.category_fk4 -#? models.ForeignKey() +#? model_instance.category_fk5 -#? models.ManyToManyField() +#? models.manager.RelatedManager() model_instance.tags_m2m +#? Tag() +model_instance.tags_m2m.get() +#? ['add'] +model_instance.tags_m2m.add #? model_instance.unidentifiable From bf8b58aeeb00d6a925f4f761fcb8592a5316233c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 16:25:27 +0200 Subject: [PATCH 44/55] Some more django query tests --- test/completion/django.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/completion/django.py b/test/completion/django.py index 4c2cf6ec..64f5f6bb 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -120,3 +120,7 @@ model_instance.objects.filter model_instance.objects.filter().first() #? str() model_instance.objects.get().char_field +#? int() +model_instance.objects.update(x='') +#? BusinessModel() +model_instance.objects.create() From 857e0fc00e0b70104238fbbd6f8f5e2639f68d80 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 18:05:02 +0200 Subject: [PATCH 45/55] Include Django stubs license in Jedi package --- MANIFEST.in | 1 + setup.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 94869628..3b76f251 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,7 @@ include requirements.txt include jedi/parser/python/grammar*.txt recursive-include jedi/third_party *.pyi include jedi/third_party/typeshed/LICENSE +include jedi/third_party/django-stubs/LICENSE.txt include jedi/third_party/typeshed/README recursive-include test * recursive-include docs * diff --git a/setup.py b/setup.py index 54418153..da96c6b9 100755 --- a/setup.py +++ b/setup.py @@ -19,6 +19,8 @@ with open('requirements.txt') as f: assert os.path.isfile("jedi/third_party/typeshed/LICENSE"), \ "Please download the typeshed submodule first (Hint: git submodule update --init)" +assert os.path.isfile("jedi/third_party/django-stubs/LICENSE.txt"), \ + "Please download the django-stubs submodule first (Hint: git submodule update --init)" setup(name='jedi', version=version, From 9d5eb285230505514135b54951d797dd6947e9dc Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 18:05:37 +0200 Subject: [PATCH 46/55] Mention django stubs support in README --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 683e533c..aa9c689b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,8 @@ Unreleased: 0.17.1 (2020-04-) +++++++++++++++++++ - Django ``Model`` meta class support +- Added Django Stubs to Jedi, thanks to all contributors of the + `Django Stubs `_ project - A few bugfixes 0.17.0 (2020-04-14) From 9b58bf6199f6e7dbc3b2dda417002c4e7307a477 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 20:19:13 +0200 Subject: [PATCH 47/55] Pin the Django test dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index da96c6b9..3e592481 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup(name='jedi', 'docopt', # coloroma for colored debug output 'colorama', - 'Django', + 'Django==3.0.5', # For now pin this. ], 'qa': [ 'flake8==3.7.9', From 92623232c3a27d7ea1393a20d74110cfa15f335c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 21:52:47 +0200 Subject: [PATCH 48/55] Make sure Django User inference works --- jedi/inference/value/klass.py | 11 ++++++----- jedi/plugins/django.py | 3 ++- test/completion/django.py | 13 +++++++++---- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 55e38e56..89bc7925 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -186,11 +186,12 @@ class ClassMixin(object): mro.append(cls_new) yield cls_new - def get_filters(self, origin_scope=None, is_instance=False): - metaclasses = self.get_metaclasses() - if metaclasses: - for f in self.get_metaclass_filters(metaclasses): - yield f + def get_filters(self, origin_scope=None, is_instance=False, include_metaclasses=True): + if include_metaclasses: + metaclasses = self.get_metaclasses() + if metaclasses: + for f in self.get_metaclass_filters(metaclasses): + yield f for cls in self.py__mro__(): if cls.is_compiled(): diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index 0400c6a9..ab4c273a 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -107,9 +107,10 @@ def _create_manager_for(cls, manager_cls='BaseManager'): def _new_dict_filter(cls): - filter_ = ParserTreeFilter(parent_context=cls.as_context()) + filters = cls.get_filters(is_instance=True, include_metaclasses=False) dct = { name.string_name: DjangoModelName(cls, name) + for filter_ in filters for name in filter_.values() } manager = _create_manager_for(cls) diff --git a/test/completion/django.py b/test/completion/django.py index 64f5f6bb..8d8ca44b 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -5,10 +5,6 @@ from django.db import models from django.contrib.auth.models import User -#? str() -User().email - - class Tag(models.Model): tag_name = models.CharField() @@ -124,3 +120,12 @@ model_instance.objects.get().char_field model_instance.objects.update(x='') #? BusinessModel() model_instance.objects.create() + +# ----------------- +# Django Auth +# ----------------- + +#? str() +User().email +#? str() +User.objects.get().email From c761dded359c30d8c1680fae8dc4ee141023eab6 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 21:59:13 +0200 Subject: [PATCH 49/55] Properly implement inheritance for Django models --- jedi/plugins/django.py | 2 +- test/completion/django.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index ab4c273a..6d50153c 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -110,7 +110,7 @@ def _new_dict_filter(cls): filters = cls.get_filters(is_instance=True, include_metaclasses=False) dct = { name.string_name: DjangoModelName(cls, name) - for filter_ in filters + for filter_ in reversed(list(filters)) for name in filter_.values() } manager = _create_manager_for(cls) diff --git a/test/completion/django.py b/test/completion/django.py index 8d8ca44b..87a6421a 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -44,6 +44,7 @@ class BusinessModel(models.Model): # ----------------- model_instance = BusinessModel() + #? int() model_instance.integer_field #? int() @@ -121,6 +122,31 @@ model_instance.objects.update(x='') #? BusinessModel() model_instance.objects.create() +# ----------------- +# Inheritance +# ----------------- + +class Inherited(BusinessModel): + text_field = models.IntegerField() + new_field = models.FloatField() + +inherited = Inherited() +#? int() +inherited.text_field +#? str() +inherited.char_field +#? float() +inherited.new_field + +#? str() +inherited.category_fk2.category_name +#? str() +inherited.objects.get().char_field +#? int() +inherited.objects.get().text_field +#? float() +inherited.objects.get().new_field + # ----------------- # Django Auth # ----------------- From a3a147f02808f326c08632a51cfa0225547e1ef5 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 22:49:10 +0200 Subject: [PATCH 50/55] Make sure that Django's values/values_list is tested (though not implemented --- test/completion/django.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/completion/django.py b/test/completion/django.py index 87a6421a..295908d7 100644 --- a/test/completion/django.py +++ b/test/completion/django.py @@ -155,3 +155,14 @@ inherited.objects.get().new_field User().email #? str() User.objects.get().email + +# ----------------- +# values & values_list (dave is too lazy to implement it) +# ----------------- + +#? +model_instance.objects.values_list('char_field')[0] +#? dict() +model_instance.objects.values('char_field')[0] +#? +model_instance.objects.values('char_field')[0]['char_field'] From e6d8a955d2896b86591dcef97d9d4e33ea75c08a Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 25 Apr 2020 23:25:51 +0200 Subject: [PATCH 51/55] Pin Django in a different way so tests can work everywhere --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3e592481..cafb8b75 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ setup(name='jedi', 'docopt', # coloroma for colored debug output 'colorama', - 'Django==3.0.5', # For now pin this. + 'Django<3.1', # For now pin this. ], 'qa': [ 'flake8==3.7.9', From 97fb95ec0c08a8b51a088129dca62c161d1ed4e8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 26 Apr 2020 00:21:01 +0200 Subject: [PATCH 52/55] Don't display unnecessary help, fixes #1557 --- jedi/api/__init__.py | 15 +++++++++++++-- test/test_api/test_documentation.py | 10 ++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 47e046e4..7531598a 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -473,8 +473,19 @@ class Script(object): return definitions leaf = self._module_node.get_leaf_for_position((line, column)) if leaf is not None and leaf.type in ('keyword', 'operator', 'error_leaf'): - reserved = self._inference_state.grammar._pgen_grammar.reserved_syntax_strings.keys() - if leaf.value in reserved: + def need_pydoc(): + if leaf.value in ('(', ')', '[', ']'): + if leaf.parent.type == 'trailer': + return False + if leaf.parent.type == 'atom': + return False + grammar = self._inference_state.grammar + # This parso stuff is not public, but since I control it, this + # is fine :-) ~dave + reserved = grammar._pgen_grammar.reserved_syntax_strings.keys() + return leaf.value in reserved + + if need_pydoc(): name = KeywordName(self._inference_state, leaf.value) return [classes.Name(self._inference_state, name)] return [] diff --git a/test/test_api/test_documentation.py b/test/test_api/test_documentation.py index 584666b7..1fe8bfd9 100644 --- a/test/test_api/test_documentation.py +++ b/test/test_api/test_documentation.py @@ -120,3 +120,13 @@ def test_docstring_decorator(goto_or_help_or_infer, skip_python2): @pytest.mark.parametrize('code', ['', '\n', ' ']) def test_empty(Script, code): assert not Script(code).help(1, 0) + + +@pytest.mark.parametrize('code', ['f()', '(bar or baz)', 'f[3]']) +def test_no_help_for_operator(Script, code): + assert not Script(code).help() + + +@pytest.mark.parametrize('code', ['()', '(1,)', '[]', '[1]', 'f[]']) +def test_help_for_operator(Script, code): + assert Script(code).help() From 7fd5c8af8ff04711599fea10ca8babe51b280464 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 26 Apr 2020 00:33:04 +0200 Subject: [PATCH 53/55] Allow files for get_default_project, fixes #1552 --- jedi/_compatibility.py | 7 +++++++ jedi/api/project.py | 4 +++- test/test_api/test_project.py | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index a7a53bdc..13c1975c 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -339,6 +339,13 @@ try: except NameError: PermissionError = IOError +try: + NotADirectoryError = NotADirectoryError +except NameError: + class NotADirectoryError(Exception): + # Don't implement this for Python 2 anymore. + pass + def no_unicode_pprint(dct): """ diff --git a/jedi/api/project.py b/jedi/api/project.py index 005d6827..e01a87a3 100644 --- a/jedi/api/project.py +++ b/jedi/api/project.py @@ -13,7 +13,7 @@ import json import sys from jedi._compatibility import FileNotFoundError, PermissionError, \ - IsADirectoryError + IsADirectoryError, NotADirectoryError from jedi import debug from jedi.api.environment import get_cached_default_environment, create_environment from jedi.api.exceptions import WrongVersion @@ -383,6 +383,8 @@ def get_default_project(path=None): return Project.load(dir) except (FileNotFoundError, IsADirectoryError, PermissionError): pass + except NotADirectoryError: + continue if first_no_init_file is None: if os.path.exists(os.path.join(dir, '__init__.py')): diff --git a/test/test_api/test_project.py b/test/test_api/test_project.py index c21579a5..48e83378 100644 --- a/test/test_api/test_project.py +++ b/test/test_api/test_project.py @@ -20,6 +20,12 @@ def test_django_default_project(Script): assert script._inference_state.project._django is True +def test_django_default_project_of_file(Script): + project = get_default_project(__file__) + d = os.path.dirname + assert project._path == d(d(d(__file__))) + + def test_interpreter_project_path(): # Run from anywhere it should be the cwd. dir = os.path.join(root_dir, 'test') From 612fd23777e9fcfa6cc92fb8d5ed45acc5406bf7 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 26 Apr 2020 00:55:27 +0100 Subject: [PATCH 54/55] Support accessing the py__class__ of a NewType The test here is a bit contrived, the actual place I found this was in using a NewType as a type within a NamedTuple. However due to https://github.com/davidhalter/jedi/issues/1560 that currently also fails for other reasons. This still feels useful to fix on its own though. --- jedi/inference/gradual/typing.py | 4 ++++ test/completion/pep0484_typing.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index 50ab8e83..6b3e675b 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -413,6 +413,10 @@ class NewType(Value): self._type_value_set = type_value_set self.tree_node = tree_node + def py__class__(self): + c, = self._type_value_set.py__class__() + return c + def py__call__(self, arguments): return self._type_value_set.execute_annotation() diff --git a/test/completion/pep0484_typing.py b/test/completion/pep0484_typing.py index 8060270b..cc5dd71f 100644 --- a/test/completion/pep0484_typing.py +++ b/test/completion/pep0484_typing.py @@ -283,6 +283,18 @@ def testnewtype2(y): y #? [] y. + +# The type of a NewType is equivalent to the type of its underlying type. +MyInt = typing.NewType('MyInt', int) +x = type(MyInt) +#? type.mro +x.mro + +PlainInt = int +y = type(PlainInt) +#? type.mro +y.mro + # python > 2.7 class TestDefaultDict(typing.DefaultDict[str, int]): From 8c3fd9900924c3d7b84bd09dee5cd7cefd25b0e9 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 26 Apr 2020 01:05:04 +0100 Subject: [PATCH 55/55] Tell sith that goto_assignments is now goto --- sith.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sith.py b/sith.py index 4718b8c1..dcdc39e3 100755 --- a/sith.py +++ b/sith.py @@ -123,7 +123,7 @@ class TestCase(object): with open(self.path) as f: self.script = jedi.Script(f.read(), path=self.path) kwargs = {} - if self.operation == 'goto_assignments': + if self.operation == 'goto': kwargs['follow_imports'] = random.choice([False, True]) self.objects = getattr(self.script, self.operation)(self.line, self.column, **kwargs)