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/CHANGELOG.rst b/CHANGELOG.rst index 161e85ed..aa9c689b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,14 @@ Changelog --------- +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) +++++++++++++++++++ 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/conftest.py b/conftest.py index 91e772b3..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', @@ -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/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 diff --git a/jedi/__main__.py b/jedi/__main__.py index 2c94da2f..6b442533 100644 --- a/jedi/__main__.py +++ b/jedi/__main__.py @@ -44,20 +44,29 @@ 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': # 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() +else: + print('Command not implemented: %s' % sys.argv[1]) 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/__init__.py b/jedi/api/__init__.py index 591dd19e..7531598a 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -472,9 +472,20 @@ 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'): - reserved = self._inference_state.grammar._pgen_grammar.reserved_syntax_strings.keys() - if leaf.value in reserved: + if leaf is not None and leaf.type in ('keyword', 'operator', 'error_leaf'): + 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/jedi/api/classes.py b/jedi/api/classes.py index a57a1613..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: @@ -734,8 +738,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): @@ -798,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() @@ -865,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() 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/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/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/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index 66301d73..075dc0a2 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -226,9 +226,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.get_annotated_class_object() @@ -421,6 +436,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/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/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 new file mode 100644 index 00000000..6d50153c --- /dev/null +++ b/jedi/plugins/django.py @@ -0,0 +1,130 @@ +""" +Module is used to infer Django model fields. +""" +from jedi import debug +from jedi.inference.base_value import ValueSet, iterator_to_value_set +from jedi.inference.filters import ParserTreeFilter, DictFilter +from jedi.inference.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 = { + '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_scalar_field(inference_state, field_name, field_tree_instance): + try: + module_name, attribute_name = mapping[field_tree_instance.py__name__()] + except KeyError: + return None + + if module_name is None: + module = inference_state.builtins_module + else: + module = inference_state.import_module((module_name,)) + + for attribute in module.py__getattribute__(attribute_name): + 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(): + scalar_field = _infer_scalar_field(inference_state, field_name, field_tree_instance) + if scalar_field is not None: + return scalar_field + + 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__()) + 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 _create_manager_for(cls, manager_cls='BaseManager'): + managers = cls.inference_state.import_module( + ('django', 'db', 'models', 'manager') + ).py__getattribute__(manager_cls) + 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): + filters = cls.get_filters(is_instance=True, include_metaclasses=False) + dct = { + name.string_name: DjangoModelName(cls, name) + for filter_ in reversed(list(filters)) + for name in filter_.values() + } + manager = _create_manager_for(cls) + if manager: + dct['objects'] = manager.name + return DictFilter(dct) + + +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': + return [_new_dict_filter(cls)] + + return func(cls, metaclasses) + return wrapper diff --git a/jedi/plugins/registry.py b/jedi/plugins/registry.py index b39cbbe9..c1a0b749 100644 --- a/jedi/plugins/registry.py +++ b/jedi/plugins/registry.py @@ -5,7 +5,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 pytest +from jedi.plugins import django from jedi.plugins import plugin_manager -plugin_manager.register(stdlib, flask, pytest) +plugin_manager.register(stdlib, flask, pytest, django) 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/setup.py b/setup.py index b73fea79..cafb8b75 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, @@ -43,6 +45,7 @@ setup(name='jedi', 'docopt', # coloroma for colored debug output 'colorama', + 'Django<3.1', # For now pin this. ], 'qa': [ 'flake8==3.7.9', 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) diff --git a/test/completion/django.py b/test/completion/django.py new file mode 100644 index 00000000..295908d7 --- /dev/null +++ b/test/completion/django.py @@ -0,0 +1,168 @@ +import datetime +import decimal + +from django.db import models +from django.contrib.auth.models import User + + +class Tag(models.Model): + tag_name = models.CharField() + + +class Category(models.Model): + category_name = models.CharField() + + +class BusinessModel(models.Model): + category_fk = models.ForeignKey(Category) + 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() + 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() + tags_m2m = models.ManyToManyField(Tag) + + unidentifiable = NOT_FOUND + +# ----------------- +# Model attribute inference +# ----------------- + +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_fk = models.ForeignKey(Category)'] +model_instance.category_fk +#! ['category_name = models.CharField()'] +model_instance.category_fk.category_name +#? Category() +model_instance.category_fk +#? str() +model_instance.category_fk.category_name +#? Category() +model_instance.category_fk2 +#? str() +model_instance.category_fk2.category_name +#? +model_instance.category_fk3 +#? +model_instance.category_fk4 +#? +model_instance.category_fk5 + +#? models.manager.RelatedManager() +model_instance.tags_m2m +#? Tag() +model_instance.tags_m2m.get() +#? ['add'] +model_instance.tags_m2m.add + +#? +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 +#? int() +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 +# ----------------- + +#? str() +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'] diff --git a/test/completion/pep0484_generic_mismatches.py b/test/completion/pep0484_generic_mismatches.py index 64721b8c..c848dd10 100644 --- a/test/completion/pep0484_generic_mismatches.py +++ b/test/completion/pep0484_generic_mismatches.py @@ -207,40 +207,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 diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index e0b07e4a..9edcd061 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -60,6 +60,27 @@ 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 + + +# Test handling of nested tuple input parameters def list_tuple_t_to_tuple_list_t(the_list: List[Tuple[T]]) -> Tuple[List[T], ...]: return tuple(list(x) for x in the_list) @@ -82,11 +103,12 @@ for b in list_tuple_t_elipsis_to_tuple_list_t(list_of_int_tuple_elipsis): b[0] -def foo(x: T) -> T: +# Test handling of nested callables +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]: @@ -101,6 +123,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 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]): 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] 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 diff --git a/test/test_api/test_documentation.py b/test/test_api/test_documentation.py index 2aa6140a..1fe8bfd9 100644 --- a/test/test_api/test_documentation.py +++ b/test/test_api/test_documentation.py @@ -115,3 +115,18 @@ 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) + + +@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() 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') diff --git a/test/test_integration.py b/test/test_integration.py index 378fe893..0cc1636b 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 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')) case.run(assert_case_equal, environment)