diff --git a/.gitignore b/.gitignore index ada8cd6b..fc6b9070 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ jedi.egg-info/ record.json /.cache/ /.pytest_cache +/venv/ diff --git a/AUTHORS.txt b/AUTHORS.txt index f30d0c01..0cd22f7e 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -55,5 +55,6 @@ Max Woerner Chase (@mwchase) Johannes Maria Frank (@jmfrank63) Shane Steinert-Threlkeld (@shanest) Tim Gates (@timgates42) +Lior Goldberg (@goldberglior) Note: (@user) means a github user name. diff --git a/README.rst b/README.rst index 0381785f..0880404d 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,7 @@ the CPython REPL you have to install it. Jedi can currently be used with the following editors/projects: - Vim (jedi-vim_, YouCompleteMe_, deoplete-jedi_, completor.vim_) +- `Visual Studio Code`_ (via `Python Extension `_) - Emacs (Jedi.el_, company-mode_, elpy_, anaconda-mode_, ycmd_) - Sublime Text (SublimeJEDI_ [ST2 + ST3], anaconda_ [only ST3]) - TextMate_ (Not sure if it's actually working) @@ -50,7 +51,6 @@ Jedi can currently be used with the following editors/projects: `_] - Atom_ (autocomplete-python-jedi_) - `GNOME Builder`_ (with support for GObject Introspection) -- `Visual Studio Code`_ (via `Python Extension `_) - Gedit (gedi_) - wdb_ - Web Debugger - `Eric IDE`_ (Available as a plugin) diff --git a/docs/docs/usage.rst b/docs/docs/usage.rst index bc30af35..3ebbb752 100644 --- a/docs/docs/usage.rst +++ b/docs/docs/usage.rst @@ -23,6 +23,10 @@ Vim: - YouCompleteMe_ - deoplete-jedi_ +Visual Studio Code: + +- `Python Extension`_ + Emacs: - Jedi.el_ @@ -48,10 +52,6 @@ Kate: `__, you have to enable it, though. -Visual Studio Code: - -- `Python Extension`_ - Atom: - autocomplete-python-jedi_ @@ -114,4 +114,4 @@ Using a custom ``$HOME/.pythonrc.py`` .. _GNOME Builder: https://wiki.gnome.org/Apps/Builder/ .. _gedi: https://github.com/isamert/gedi .. _Eric IDE: https://eric-ide.python-projects.org -.. _Python Extension: https://marketplace.visualstudio.com/items?itemName=donjayamanne.python +.. _Python Extension: https://marketplace.visualstudio.com/items?itemName=ms-python.python diff --git a/jedi/__init__.py b/jedi/__init__.py index eb2e814c..d0140404 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -40,7 +40,7 @@ from jedi.api import Script, Interpreter, set_debug_function, \ from jedi import settings from jedi.api.environment import find_virtualenvs, find_system_environments, \ get_default_environment, InvalidPythonEnvironment, create_environment, \ - get_system_environment + get_system_environment, InterpreterEnvironment from jedi.api.project import Project, get_default_project from jedi.api.exceptions import InternalError, RefactoringError # Finally load the internal plugins. This is only internal. diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 3aecf0b5..6b13f897 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -335,7 +335,8 @@ class Script(object): ) defs = [classes.Definition(self._inference_state, d) for d in set(names)] - return helpers.sorted_definitions(defs) + # Avoid duplicates + return list(set(helpers.sorted_definitions(defs))) @no_py2_support def search(self, string, **kwargs): diff --git a/jedi/api/file_name.py b/jedi/api/file_name.py index 96f10249..0a2161a3 100644 --- a/jedi/api/file_name.py +++ b/jedi/api/file_name.py @@ -17,6 +17,8 @@ def complete_file_name(inference_state, module_context, start_leaf, quote, strin like_name_length = len(os.path.basename(string)) addition = _get_string_additions(module_context, start_leaf) + if string.startswith('~'): + string = os.path.expanduser(string) if addition is None: return string = addition + string diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index 05134773..4d14f1a9 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -258,6 +258,7 @@ class Value(HelperValueMixin, BaseValue): def _as_context(self): raise NotImplementedError('Not all values need to be converted to contexts: %s', self) + @property def name(self): raise NotImplementedError diff --git a/jedi/inference/compiled/access.py b/jedi/inference/compiled/access.py index f5844227..99bd843c 100644 --- a/jedi/inference/compiled/access.py +++ b/jedi/inference/compiled/access.py @@ -484,6 +484,11 @@ class DirectObjectAccess(object): def needs_type_completions(self): return inspect.isclass(self._obj) and self._obj != type + def _annotation_to_str(self, annotation): + if isinstance(annotation, type): + return str(annotation.__name__) + return str(annotation) + def get_signature_params(self): return [ SignatureParam( @@ -493,7 +498,7 @@ class DirectObjectAccess(object): default_string=repr(p.default), has_annotation=p.annotation is not p.empty, annotation=self._create_access_path(p.annotation), - annotation_string=str(p.annotation), + annotation_string=self._annotation_to_str(p.annotation), kind_name=str(p.kind) ) for p in self._get_signature().parameters.values() ] diff --git a/jedi/inference/compiled/getattr_static.py b/jedi/inference/compiled/getattr_static.py index b86a46f4..7225aabd 100644 --- a/jedi/inference/compiled/getattr_static.py +++ b/jedi/inference/compiled/getattr_static.py @@ -6,6 +6,7 @@ information returned to enable Jedi to make decisions. import types +from jedi import debug from jedi._compatibility import py_version _sentinel = object() @@ -54,7 +55,14 @@ def _shadowed_dict_newstyle(klass): def _static_getmro_newstyle(klass): - return type.__dict__['__mro__'].__get__(klass) + mro = type.__dict__['__mro__'].__get__(klass) + if not isinstance(mro, (tuple, list)): + # There are unfortunately no tests for this, I was not able to + # reproduce this in pure Python. However should still solve the issue + # raised in GH #1517. + debug.warning('mro of %s returned %s, should be a tuple' % (klass, mro)) + return () + return mro if py_version >= 30: diff --git a/jedi/inference/compiled/subprocess/__init__.py b/jedi/inference/compiled/subprocess/__init__.py index bad1803c..246cffa5 100644 --- a/jedi/inference/compiled/subprocess/__init__.py +++ b/jedi/inference/compiled/subprocess/__init__.py @@ -297,7 +297,7 @@ class Listener(object): try: inference_state = self._inference_states[inference_state_id] except KeyError: - from jedi.api.environment import InterpreterEnvironment + from jedi import InterpreterEnvironment inference_state = InferenceState( # The project is not actually needed. Nothing should need to # access it. diff --git a/jedi/inference/gradual/typeshed.py b/jedi/inference/gradual/typeshed.py index 4db73801..48692aa0 100644 --- a/jedi/inference/gradual/typeshed.py +++ b/jedi/inference/gradual/typeshed.py @@ -130,6 +130,9 @@ def import_module_decorator(func): def try_to_load_stub_cached(inference_state, import_names, *args, **kwargs): + if import_names is None: + return None + try: return inference_state.stub_module_cache[import_names] except KeyError: diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index 3b3cdf17..85a34396 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -5,8 +5,11 @@ values. This file deals with all the typing.py cases. """ +import itertools + +from jedi._compatibility import unicode from jedi import debug -from jedi.inference.compiled import builtin_from_name +from jedi.inference.compiled import builtin_from_name, create_simple_object from jedi.inference.base_value import ValueSet, NO_VALUES, Value, \ LazyValueWrapper from jedi.inference.lazy_value import LazyKnownValues @@ -81,7 +84,8 @@ class TypingModuleName(NameWrapper): elif name == 'TypedDict': # TODO doesn't even exist in typeshed/typing.py, yet. But will be # added soon. - pass + yield TypedDictBase.create_cached( + inference_state, self.parent_context, self.tree_name) elif name in ('no_type_check', 'no_type_check_decorator'): # This is not necessary, as long as we are not doing type checking. for c in self._wrapped_name.infer(): # Fuck my life Python 2 @@ -339,3 +343,47 @@ class CastFunction(BaseTypingValue): @repack_with_argument_clinic('type, object, /') def py__call__(self, type_value_set, object_value_set): return type_value_set.execute_annotation() + + +class TypedDictBase(BaseTypingValue): + """ + This class has no responsibilities and is just here to make sure that typed + dicts can be identified. + """ + + +class TypedDict(LazyValueWrapper): + """Represents the instance version of ``TypedDictClass``.""" + def __init__(self, definition_class): + self.inference_state = definition_class.inference_state + self.parent_context = definition_class.parent_context + self.tree_node = definition_class.tree_node + self._definition_class = definition_class + + @property + def name(self): + return ValueName(self, self.tree_node.name) + + def py__simple_getitem__(self, index): + if isinstance(index, unicode): + return ValueSet.from_sets( + name.infer() + for filter in self._definition_class.get_filters(is_instance=True) + for name in filter.get(index) + ) + return NO_VALUES + + def get_key_values(self): + filtered_values = itertools.chain.from_iterable(( + f.values() + for f in self._definition_class.get_filters(is_instance=True) + )) + return ValueSet({ + create_simple_object(self.inference_state, v.string_name) + for v in filtered_values + }) + + def _get_wrapped_value(self): + d, = self.inference_state.builtins_module.py__getattribute__('dict') + result, = d.execute_with_values() + return result diff --git a/jedi/inference/param.py b/jedi/inference/param.py index 27bd60a4..c1ce541a 100644 --- a/jedi/inference/param.py +++ b/jedi/inference/param.py @@ -51,6 +51,25 @@ class ExecutedParamName(ParamName): def get_executed_param_names_and_issues(function_value, arguments): + """ + Return a tuple of: + - a list of `ExecutedParamName`s corresponding to the arguments of the + function execution `function_value`, containing the inferred value of + those arguments (whether explicit or default) + - a list of the issues encountered while building that list + + For example, given: + ``` + def foo(a, b, c=None, d='d'): ... + + foo(42, c='c') + ``` + + Then for the execution of `foo`, this will return a tuple containing: + - a list with entries for each parameter a, b, c & d; the entries for a, + c, & d will have their values (42, 'c' and 'd' respectively) included. + - a list with a single entry about the lack of a value for `b` + """ def too_many_args(argument): m = _error_argument_count(funcdef, len(unpacked_va)) # Just report an error for the first param that is not needed (like @@ -207,6 +226,23 @@ def get_executed_param_names_and_issues(function_value, arguments): def get_executed_param_names(function_value, arguments): + """ + Return a list of `ExecutedParamName`s corresponding to the arguments of the + function execution `function_value`, containing the inferred value of those + arguments (whether explicit or default). Any issues building this list (for + example required arguments which are missing in the invocation) are ignored. + + For example, given: + ``` + def foo(a, b, c=None, d='d'): ... + + foo(42, c='c') + ``` + + Then for the execution of `foo`, this will return a list containing entries + for each parameter a, b, c & d; the entries for a, c, & d will have their + values (42, 'c' and 'd' respectively) included. + """ return get_executed_param_names_and_issues(function_value, arguments)[0] diff --git a/jedi/inference/syntax_tree.py b/jedi/inference/syntax_tree.py index e1442e10..8a0b2656 100644 --- a/jedi/inference/syntax_tree.py +++ b/jedi/inference/syntax_tree.py @@ -356,6 +356,12 @@ def infer_atom(context, atom): def infer_expr_stmt(context, stmt, seek_name=None): with recursion.execution_allowed(context.inference_state, stmt) as allowed: if allowed: + if seek_name is not None: + pep0484_values = \ + annotation.find_type_from_comment_hint_assign(context, stmt, seek_name) + if pep0484_values: + return pep0484_values + return _infer_expr_stmt(context, stmt, seek_name) return NO_VALUES @@ -632,23 +638,6 @@ def _infer_comparison_part(inference_state, context, left, operator, right): return result -def _remove_statements(context, stmt, name): - """ - This is the part where statements are being stripped. - - Due to lazy type inference, statements like a = func; b = a; b() have to be - inferred. - - TODO merge with infer_expr_stmt? - """ - pep0484_values = \ - annotation.find_type_from_comment_hint_assign(context, stmt, name) - if pep0484_values: - return pep0484_values - - return infer_expr_stmt(context, stmt, seek_name=name) - - @plugin_manager.decorate() def tree_name_to_values(inference_state, context, tree_name): value_set = NO_VALUES @@ -713,7 +702,7 @@ def tree_name_to_values(inference_state, context, tree_name): n = TreeNameDefinition(context, tree_name) types = check_tuple_assignments(n, for_types) elif typ == 'expr_stmt': - types = _remove_statements(context, node, tree_name) + types = infer_expr_stmt(context, node, tree_name) elif typ == 'with_stmt': value_managers = context.infer_node(node.get_test_node_from_name(tree_name)) enter_methods = value_managers.py__getattribute__(u'__enter__') diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index 2eca9f9d..b884b14d 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -547,10 +547,10 @@ class InstanceClassFilter(AbstractFilter): self._class_filter = class_filter def get(self, name): - return self._convert(self._class_filter.get(name, from_instance=True)) + return self._convert(self._class_filter.get(name)) def values(self): - return self._convert(self._class_filter.values(from_instance=True)) + return self._convert(self._class_filter.values()) def _convert(self, names): return [ @@ -586,7 +586,7 @@ class SelfAttributeFilter(ClassFilter): if trailer.type == 'trailer' \ and len(trailer.parent.children) == 2 \ and trailer.children[0] == '.': - if name.is_definition() and self._access_possible(name, from_instance=True): + if name.is_definition() and self._access_possible(name): # TODO filter non-self assignments instead of this bad # filter. if self._is_in_right_scope(trailer.parent.children[0], name): diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 37b743b3..13a483e3 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -38,11 +38,11 @@ py__doc__() Returns the docstring for a value. """ from jedi import debug from jedi._compatibility import use_metaclass -from jedi.parser_utils import get_cached_parent_scope +from jedi.parser_utils import get_cached_parent_scope, expr_is_dotted from jedi.inference.cache import inference_state_method_cache, CachedMetaClass, \ inference_state_method_generator_cache from jedi.inference import compiled -from jedi.inference.lazy_value import LazyKnownValues +from jedi.inference.lazy_value import LazyKnownValues, LazyTreeValue from jedi.inference.filters import ParserTreeFilter from jedi.inference.names import TreeNameDefinition, ValueName from jedi.inference.arguments import unpack_arglist, ValuesArguments @@ -104,27 +104,31 @@ class ClassFilter(ParserTreeFilter): node = get_cached_parent_scope(self._used_names, node) return False - def _access_possible(self, name, from_instance=False): + def _access_possible(self, name): # Filter for ClassVar variables # TODO this is not properly done, yet. It just checks for the string # ClassVar in the annotation, which can be quite imprecise. If we # wanted to do this correct, we would have to infer the ClassVar. - if not from_instance: + if not self._is_instance: expr_stmt = name.get_definition() if expr_stmt is not None and expr_stmt.type == 'expr_stmt': annassign = expr_stmt.children[1] if annassign.type == 'annassign': # TODO this is not proper matching - if 'ClassVar' not in annassign.children[1].get_code(): + + # If there is an =, the variable is obviously also + # defined on the class. + if 'ClassVar' not in annassign.children[1].get_code() \ + and '=' not in annassign.children: return False # Filter for name mangling of private variables like __foo return not name.value.startswith('__') or name.value.endswith('__') \ or self._equals_origin_scope() - def _filter(self, names, from_instance=False): + def _filter(self, names): names = super(ClassFilter, self)._filter(names) - return [name for name in names if self._access_possible(name, from_instance)] + return [name for name in names if self._access_possible(name)] class ClassMixin(object): @@ -133,6 +137,10 @@ class ClassMixin(object): def py__call__(self, arguments=None): from jedi.inference.value import TreeInstance + + from jedi.inference.gradual.typing import TypedDict + if self.is_typeddict(): + return ValueSet([TypedDict(self)]) return ValueSet([TreeInstance(self.inference_state, self.parent_context, self, arguments)]) def py__class__(self): @@ -226,6 +234,36 @@ class ClassMixin(object): return 'Type[%s]' % self.py__name__() return self.py__name__() + @inference_state_method_cache(default=False) + def is_typeddict(self): + # TODO Do a proper mro resolution. Currently we are just listing + # classes. However, it's a complicated algorithm. + from jedi.inference.gradual.typing import TypedDictBase + for lazy_cls in self.py__bases__(): + if not isinstance(lazy_cls, LazyTreeValue): + return False + tree_node = lazy_cls.data + # Only resolve simple classes, stuff like Iterable[str] are more + # intensive to resolve and if generics are involved, we know it's + # not a TypedDict. + if not expr_is_dotted(tree_node): + return False + + for cls in lazy_cls.infer(): + if isinstance(cls, TypedDictBase): + return True + try: + method = cls.is_typeddict + except AttributeError: + # We're only dealing with simple classes, so just returning + # here should be fine. This only happens with e.g. compiled + # classes. + return False + else: + if method(): + return True + return False + class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase)): api_type = u'class' diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index 0f7ba429..7f69a80c 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -293,6 +293,25 @@ def cut_value_at_position(leaf, position): return ''.join(lines) +def expr_is_dotted(node): + """ + Checks if a path looks like `name` or `name.foo.bar` and not `name()`. + """ + if node.type == 'atom': + if len(node.children) == 3 and node.children[0] == '(': + return expr_is_dotted(node.children[1]) + return False + if node.type == 'atom_expr': + children = node.children + if children[0] == 'await': + return False + if not expr_is_dotted(children[0]): + return False + # Check trailers + return all(c.children[0] == '.' for c in children[1:]) + return node.type == 'name' + + def _function_is_x_method(method_name): def wrapper(function_node): """ diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 448f2b50..f3ebd35d 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -44,8 +44,6 @@ from operator import itemgetter as _itemgetter from collections import OrderedDict class {typename}(tuple): - '{typename}({arg_list})' - __slots__ = () _fields = {field_names!r} diff --git a/sith.py b/sith.py index abbfca11..746bac4a 100755 --- a/sith.py +++ b/sith.py @@ -20,8 +20,7 @@ Run a specific operation ./sith.py run -Where operation is one of completions, goto_assignments, goto_definitions, -usages, or call_signatures. +Where operation is one of complete, goto, infer, get_references or get_signatures. Note: Line numbers start at 1; columns start at 0 (this is consistent with many text editors, including Emacs). @@ -95,6 +94,7 @@ class TestCase(object): args = json.load(f) return cls(*args) + # Changing this? Also update the module docstring above. operations = ['complete', 'goto', 'infer', 'get_references', 'get_signatures'] @classmethod @@ -151,13 +151,13 @@ class TestCase(object): # Three lines ought to be enough lower = lineno - show if lineno - show > 0 else 0 prefix = ' |' - for i, line in enumerate(self.script._source.split('\n')[lower:lineno]): + for i, line in enumerate(self.script._code.split('\n')[lower:lineno]): print(prefix, lower + i + 1, line) - print(prefix, ' ', ' ' * (column + len(str(lineno))), '^') + print(prefix, ' ' * (column + len(str(lineno))), '^') def show_operation(self): print("%s:\n" % self.operation.capitalize()) - if self.operation == 'completions': + if self.operation == 'complete': self.show_completions() else: self.show_definitions() diff --git a/test/completion/basic.py b/test/completion/basic.py index 3ff919ca..b0e71bad 100644 --- a/test/completion/basic.py +++ b/test/completion/basic.py @@ -422,3 +422,11 @@ with Foo() as f3: #? 6 Foo with Foo() as f3: f3 + +# ----------------- +# Avoiding multiple definitions +# ----------------- + +some_array = ['', ''] +#! ['def upper'] +some_array[some_not_defined_index].upper diff --git a/test/completion/pep0484_typing.py b/test/completion/pep0484_typing.py index 1ddc0abf..eec6515b 100644 --- a/test/completion/pep0484_typing.py +++ b/test/completion/pep0484_typing.py @@ -499,3 +499,89 @@ def dynamic_annotation(x: int): #? int() dynamic_annotation('') + +# ------------------------- +# TypeDict +# ------------------------- + +# python >= 3.8 + +class Foo(typing.TypedDict): + foo: str + bar: typing.List[float] + an_int: int + #! ['foo: str'] + foo + #? str() + foo + #? int() + an_int + +def typed_dict_test_foo(arg: Foo): + a_string = arg['foo'] + a_list_of_floats = arg['bar'] + an_int = arg['an_int'] + + #? str() + a_string + #? list() + a_list_of_floats + #? float() + a_list_of_floats[0] + #? int() + an_int + + #? ['isupper'] + a_string.isuppe + #? ['pop'] + a_list_of_floats.po + #? ['as_integer_ratio'] + an_int.as_integer_rati + +#! ['class Foo'] +d: Foo +#? str() +d['foo'] +#? float() +d['bar'][0] +#? +d['baz'] + +#? +d.foo +#? +d.bar +#! [] +d.foo + +#? [] +Foo.set +#? ['setdefault'] +d.setdefaul +#? [] +Foo.setdefaul + +#? 5 ["'foo"] +d['fo'] +#? 5 ['"bar"'] +d["bar"] + +class Bar(Foo): + another_variable: int + + #? int() + another_variable + #? + an_int + +def typed_dict_test_foo(arg: Bar): + #? str() + arg['foo'] + #? list() + arg['bar'] + #? float() + arg['bar'][0] + #? int() + arg['an_int'] + #? int() + arg['another_variable'] diff --git a/test/completion/pep0526_variables.py b/test/completion/pep0526_variables.py index 6af77d91..39168339 100644 --- a/test/completion/pep0526_variables.py +++ b/test/completion/pep0526_variables.py @@ -57,7 +57,7 @@ Foo.baz Foo().baz class VarClass: - var_instance1: int = 1 + var_instance1: int = '' var_instance2: float var_class1: typing.ClassVar[str] = 1 var_class2: typing.ClassVar[bytes] @@ -77,9 +77,9 @@ class VarClass: self.var_ -#? ['var_class1', 'var_class2'] +#? ['var_class1', 'var_class2', 'var_instance1'] VarClass.var_ -#? +#? int() VarClass.var_instance1 #? VarClass.var_instance2 diff --git a/test/completion/recursion.py b/test/completion/recursion.py index ebbd69e3..1e882bfa 100644 --- a/test/completion/recursion.py +++ b/test/completion/recursion.py @@ -91,3 +91,15 @@ class B: for i in self.a(i): #? yield i + + +foo = int +foo = foo # type: foo +#? int +foo + +while True: + bar = int + bar = bar # type: bar + #? int() + bar diff --git a/test/conftest.py b/test/conftest.py index 6700cc55..26254000 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -8,7 +8,7 @@ import pytest from . import helpers from . import run from . import refactor -from jedi.api.environment import InterpreterEnvironment, get_system_environment +from jedi import InterpreterEnvironment, get_system_environment from jedi.inference.compiled.value import create_from_access_path from jedi.inference.imports import _load_python_module from jedi.file_io import KnownContentFileIO diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 3b6dfc70..6e1c9393 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -1,4 +1,4 @@ -from os.path import join, sep as s, dirname +from os.path import join, sep as s, dirname, expanduser import os import sys from textwrap import dedent @@ -7,6 +7,7 @@ import pytest from ..helpers import root_dir from jedi.api.helpers import _start_match, _fuzzy_match +from jedi._compatibility import scandir def test_in_whitespace(Script): @@ -86,6 +87,18 @@ def test_loading_unicode_files_with_bad_global_charset(Script, monkeypatch, tmpd s.complete(line=2, column=4) +def test_complete_expanduser(Script): + possibilities = scandir(expanduser('~')) + non_dots = [p for p in possibilities if not p.name.startswith('.') and len(p.name) > 1] + item = non_dots[0] + line = "'~%s%s'" % (os.sep, item.name) + s = Script(line, line=1, column=len(line)-1) + expected_name = item.name + if item.is_dir(): + expected_name += os.path.sep + assert expected_name in [c.name for c in s.completions()] + + def test_fake_subnodes(Script): """ Test the number of subnodes of a fake object. diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 1ec0b741..e32bc72a 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -342,7 +342,7 @@ def test_completion_params(): @pytest.mark.skipif('py_version < 33', reason='inspect.signature was created in 3.3.') def test_completion_param_annotations(): - # Need to define this function not directly in Python. Otherwise Jedi is to + # Need to define this function not directly in Python. Otherwise Jedi is too # clever and uses the Python code instead of the signature object. code = 'def foo(a: 1, b: str, c: int = 1.0) -> bytes: pass' exec_(code, locals()) @@ -354,6 +354,10 @@ def test_completion_param_annotations(): assert [d.name for d in b.infer()] == ['str'] assert {d.name for d in c.infer()} == {'int', 'float'} + assert a.description == 'param a: 1' + assert b.description == 'param b: str' + assert c.description == 'param c: int=1.0' + d, = jedi.Interpreter('foo()', [locals()]).infer() assert d.name == 'bytes' diff --git a/test/test_inference/test_stdlib.py b/test/test_inference/test_stdlib.py index e17a88b3..0e910d7f 100644 --- a/test/test_inference/test_stdlib.py +++ b/test/test_inference/test_stdlib.py @@ -77,6 +77,7 @@ def test_namedtuple_infer(Script): assert d1.get_line_code() == "class Foo(tuple):\n" assert d1.module_path is None + assert d1.docstring() == 'Foo(id, timestamp, gps_timestamp, attributes)' def test_re_sub(Script, environment): diff --git a/test/test_integration.py b/test/test_integration.py index 3bb3ed66..378fe893 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -41,6 +41,9 @@ def test_completion(case, monkeypatch, environment, has_typing): if skip_reason is not None: pytest.skip(skip_reason) + if 'pep0484_typing' in case.path and sys.version_info[0] == 2: + pytest.skip('ditch python 2 finally') + _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.')