diff --git a/CHANGELOG.rst b/CHANGELOG.rst index beabd71e..2d7bcccd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,9 @@ Changelog --------- +0.16.1 (2020--) ++++++++++++++++++++ + 0.16.0 (2020-01-26) +++++++++++++++++++ diff --git a/jedi/__init__.py b/jedi/__init__.py index efec776d..5ddb0d04 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -33,7 +33,7 @@ As you see Jedi is pretty simple and allows you to concentrate on writing a good text editor, while still having very good IDE features for Python. """ -__version__ = '0.16.0' +__version__ = '0.16.1' from jedi.api import Script, Interpreter, set_debug_function, \ preload_module, names diff --git a/jedi/__main__.py b/jedi/__main__.py index c1e58a62..14aa42cf 100644 --- a/jedi/__main__.py +++ b/jedi/__main__.py @@ -48,7 +48,8 @@ def _complete(): for c in jedi.Script(sys.argv[2]).complete(): c.docstring() c.type - except Exception: + except Exception as e: + print(e) pdb.post_mortem() diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 709cd518..d8950018 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -113,7 +113,12 @@ def find_module_py33(string, path=None, loader=None, full_name=None, is_global_s def _from_loader(loader, string): - is_package = loader.is_package(string) + try: + is_package_method = loader.is_package + except AttributeError: + is_package = False + else: + is_package = is_package_method(string) try: get_filename = loader.get_filename except AttributeError: @@ -123,7 +128,11 @@ def _from_loader(loader, string): # To avoid unicode and read bytes, "overwrite" loader.get_source if # possible. - f = type(loader).get_source + try: + f = type(loader).get_source + except AttributeError: + raise ImportError("get_source was not defined on loader") + if is_py3 and f is not importlib.machinery.SourceFileLoader.get_source: # Unfortunately we are reading unicode here, not bytes. # It seems hard to get bytes, because the zip importer diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 44751109..426f56ec 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -271,12 +271,12 @@ class Script(object): """ Return the first definition found, while optionally following imports. Multiple objects may be returned, because Python itself is a - dynamic language, which means depending on an option you can have two - different versions of a function. + dynamic language, which means you can have two different versions of a + function. :param follow_imports: The goto call will follow imports. - :param follow_builtin_imports: If follow_imports is True will decide if - it follow builtin imports. + :param follow_builtin_imports: If follow_imports is True will try to + look up names in builtins (i.e. compiled or extension modules). :param only_stubs: Only return stubs for this goto call. :param prefer_stubs: Prefer stubs to Python objects for this goto call. :rtype: list of :class:`classes.Definition` @@ -310,7 +310,7 @@ class Script(object): names = list(name.goto()) if follow_imports: - names = helpers.filter_follow_imports(names) + names = helpers.filter_follow_imports(names, follow_builtin_imports) names = convert_names( names, only_stubs=only_stubs, diff --git a/jedi/api/classes.py b/jedi/api/classes.py index b44e5eb8..b7aaadd3 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -513,6 +513,16 @@ class BaseDefinition(object): def execute(self): return _values_to_definitions(self._name.infer().execute_with_values()) + def get_type_hint(self): + """ + Returns type hints like ``Iterable[int]`` or ``Union[int, str]``. + + This method might be quite slow, especially for functions. The problem + is finding executions for those functions to return something like + ``Callable[[int, str], str]``. + """ + return self._name.infer().get_type_hint() + class Completion(BaseDefinition): """ diff --git a/jedi/inference/arguments.py b/jedi/inference/arguments.py index c9404478..c68dd469 100644 --- a/jedi/inference/arguments.py +++ b/jedi/inference/arguments.py @@ -131,15 +131,6 @@ def _parse_argument_clinic(string): class _AbstractArgumentsMixin(object): - def infer_all(self, funcdef=None): - """ - Inferes all arguments as a support for static analysis - (normally Jedi). - """ - for key, lazy_value in self.unpack(): - types = lazy_value.infer() - try_iter_content(types) - def unpack(self, funcdef=None): raise NotImplementedError diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index 3705d7bc..4d14f1a9 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -265,6 +265,9 @@ class Value(HelperValueMixin, BaseValue): def py__name__(self): return self.name.string_name + def get_type_hint(self, add_class_info=True): + return None + def iterate_values(values, contextualized_node=None, is_async=False): """ @@ -415,6 +418,26 @@ class ValueSet(BaseValueSet): def get_signatures(self): return [sig for c in self._set for sig in c.get_signatures()] + def get_type_hint(self, add_class_info=True): + t = [v.get_type_hint(add_class_info=add_class_info) for v in self._set] + type_hints = sorted(filter(None, t)) + if len(type_hints) == 1: + return type_hints[0] + + optional = 'None' in type_hints + if optional: + type_hints.remove('None') + + if len(type_hints) == 0: + return None + elif len(type_hints) == 1: + s = type_hints[0] + else: + s = 'Union[%s]' % ', '.join(type_hints) + if optional: + s = 'Optional[%s]' % s + return s + NO_VALUES = ValueSet([]) diff --git a/jedi/inference/compiled/access.py b/jedi/inference/compiled/access.py index f47ae773..f76a645f 100644 --- a/jedi/inference/compiled/access.py +++ b/jedi/inference/compiled/access.py @@ -570,4 +570,6 @@ def _is_class_instance(obj): except AttributeError: return False else: - return cls != type and not issubclass(cls, NOT_CLASS_TYPES) + # The isinstance check for cls is just there so issubclass doesn't + # raise an exception. + return cls != type and isinstance(cls, type) and not issubclass(cls, NOT_CLASS_TYPES) diff --git a/jedi/inference/compiled/value.py b/jedi/inference/compiled/value.py index fe8c03c2..bad5c73f 100644 --- a/jedi/inference/compiled/value.py +++ b/jedi/inference/compiled/value.py @@ -285,6 +285,11 @@ class CompiledValue(Value): for k in self.access_handle.get_key_paths() ] + def get_type_hint(self, add_class_info=True): + if self.access_handle.get_repr() in ('None', ""): + return 'None' + return None + class CompiledModule(CompiledValue): file_io = None # For modules diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 4624f560..1de87962 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -278,17 +278,17 @@ def infer_type_vars_for_execution(function, arguments, annotation_dict): def infer_return_for_callable(arguments, param_values, result_values): - result = NO_VALUES + all_type_vars = {} for pv in param_values: if pv.array_type == 'list': type_var_dict = infer_type_vars_for_callable(arguments, pv.py__iter__()) + all_type_vars.update(type_var_dict) - result |= ValueSet.from_sets( - v.define_generics(type_var_dict) - if isinstance(v, (DefineGenericBase, TypeVar)) else ValueSet({v}) - for v in result_values - ).execute_annotation() - return result + return ValueSet.from_sets( + v.define_generics(all_type_vars) + if isinstance(v, (DefineGenericBase, TypeVar)) else ValueSet({v}) + for v in result_values + ).execute_annotation() def infer_type_vars_for_callable(arguments, lazy_params): diff --git a/jedi/inference/gradual/base.py b/jedi/inference/gradual/base.py index e09766be..f9ccddb2 100644 --- a/jedi/inference/gradual/base.py +++ b/jedi/inference/gradual/base.py @@ -165,6 +165,18 @@ class GenericClass(ClassMixin, DefineGenericBase): def _get_wrapped_value(self): return self._class_value + def get_type_hint(self, add_class_info=True): + n = self.py__name__() + # Not sure if this is the best way to do this, but all of these types + # are a bit special in that they have type aliases and other ways to + # become lower case. It's probably better to make them upper case, + # because that's what you can use in annotations. + n = dict(list="List", dict="Dict", set="Set", tuple="Tuple").get(n, n) + s = n + self._generics_manager.get_type_hint() + if add_class_info: + return 'Type[%s]' % s + return s + def get_type_var_filter(self): return _TypeVarFilter(self.get_generics(), self.list_type_vars()) @@ -239,6 +251,9 @@ class _GenericInstanceWrapper(ValueWrapper): return ValueSet([builtin_from_name(self.inference_state, u'None')]) return self._wrapped_value.py__stop_iteration_returns() + def get_type_hint(self, add_class_info=True): + return self._wrapped_value.class_value.get_type_hint(add_class_info=False) + class _PseudoTreeNameClass(Value): """ diff --git a/jedi/inference/gradual/conversion.py b/jedi/inference/gradual/conversion.py index 074e29a7..541aa0d1 100644 --- a/jedi/inference/gradual/conversion.py +++ b/jedi/inference/gradual/conversion.py @@ -73,7 +73,13 @@ def _try_stub_to_python_names(names, prefer_stub_to_compiled=False): converted_names = converted.goto(name.get_public_name()) if converted_names: for n in converted_names: - yield n + if n.get_root_context().is_stub(): + # If it's a stub again, it means we're going in + # a circle. Probably some imports make it a + # stub again. + yield name + else: + yield n continue yield name diff --git a/jedi/inference/gradual/generics.py b/jedi/inference/gradual/generics.py index f0060e4f..6bd6aa63 100644 --- a/jedi/inference/gradual/generics.py +++ b/jedi/inference/gradual/generics.py @@ -31,6 +31,9 @@ class _AbstractGenericManager(object): debug.warning('No param #%s found for annotation %s', index, self) return NO_VALUES + def get_type_hint(self): + return '[%s]' % ', '.join(t.get_type_hint(add_class_info=False) for t in self.to_tuple()) + class LazyGenericManager(_AbstractGenericManager): def __init__(self, context_of_index, index_value): diff --git a/jedi/inference/lazy_value.py b/jedi/inference/lazy_value.py index fa2f9609..44f8d596 100644 --- a/jedi/inference/lazy_value.py +++ b/jedi/inference/lazy_value.py @@ -3,8 +3,10 @@ from jedi.common.utils import monkeypatch class AbstractLazyValue(object): - def __init__(self, data): + def __init__(self, data, min=1, max=1): self.data = data + self.min = min + self.max = max def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.data) @@ -26,16 +28,16 @@ class LazyKnownValues(AbstractLazyValue): class LazyUnknownValue(AbstractLazyValue): - def __init__(self): - super(LazyUnknownValue, self).__init__(None) + def __init__(self, min=1, max=1): + super(LazyUnknownValue, self).__init__(None, min, max) def infer(self): return NO_VALUES class LazyTreeValue(AbstractLazyValue): - def __init__(self, context, node): - super(LazyTreeValue, self).__init__(node) + def __init__(self, context, node, min=1, max=1): + super(LazyTreeValue, self).__init__(node, min, max) self.context = context # We need to save the predefined names. It's an unfortunate side effect # that needs to be tracked otherwise results will be wrong. diff --git a/jedi/inference/syntax_tree.py b/jedi/inference/syntax_tree.py index 51bc960a..bc902020 100644 --- a/jedi/inference/syntax_tree.py +++ b/jedi/inference/syntax_tree.py @@ -800,7 +800,8 @@ def check_tuple_assignments(name, value_set): if isinstance(index, slice): # For no star unpacking is not possible. return NO_VALUES - for _ in range(index + 1): + i = 0 + while i <= index: try: lazy_value = next(iterated) except StopIteration: @@ -809,6 +810,8 @@ def check_tuple_assignments(name, value_set): # index number is high. Therefore break if the loop is # finished. return NO_VALUES + else: + i += lazy_value.max value_set = lazy_value.infer() return value_set diff --git a/jedi/inference/value/function.py b/jedi/inference/value/function.py index fd3c2a04..44cdfd6c 100644 --- a/jedi/inference/value/function.py +++ b/jedi/inference/value/function.py @@ -86,6 +86,33 @@ class FunctionMixin(object): def py__name__(self): return self.name.string_name + def get_type_hint(self, add_class_info=True): + return_annotation = self.tree_node.annotation + if return_annotation is None: + def param_name_to_str(n): + s = n.string_name + annotation = n.infer().get_type_hint() + if annotation is not None: + s += ': ' + annotation + if n.default_node is not None: + s += '=' + n.default_node.get_code(include_prefix=False) + return s + + function_execution = self.as_context() + result = function_execution.infer() + return_hint = result.get_type_hint() + body = self.py__name__() + '(%s)' % ', '.join([ + param_name_to_str(n) + for n in function_execution.get_param_names() + ]) + if return_hint is None: + return body + else: + return_hint = return_annotation.get_code(include_prefix=False) + body = self.py__name__() + self.tree_node.children[2].get_code(include_prefix=False) + + return body + ' -> ' + return_hint + def py__call__(self, arguments): function_execution = self.as_context(arguments) return function_execution.infer() @@ -201,15 +228,15 @@ class BaseFunctionExecutionContext(ValueContext, TreeContextMixin): returns = funcdef.iter_return_stmts() for r in returns: - check = flow_analysis.reachability_check(self, funcdef, r) - if check is flow_analysis.UNREACHABLE: - debug.dbg('Return unreachable: %s', r) + if check_yields: + value_set |= ValueSet.from_sets( + lazy_value.infer() + for lazy_value in self._get_yield_lazy_value(r) + ) else: - if check_yields: - value_set |= ValueSet.from_sets( - lazy_value.infer() - for lazy_value in self._get_yield_lazy_value(r) - ) + check = flow_analysis.reachability_check(self, funcdef, r) + if check is flow_analysis.UNREACHABLE: + debug.dbg('Return unreachable: %s', r) else: try: children = r.children @@ -218,9 +245,9 @@ class BaseFunctionExecutionContext(ValueContext, TreeContextMixin): value_set |= ValueSet([ctx]) else: value_set |= self.infer_node(children[1]) - if check is flow_analysis.REACHABLE: - debug.dbg('Return reachable: %s', r) - break + if check is flow_analysis.REACHABLE: + debug.dbg('Return reachable: %s', r) + break return value_set def _get_yield_lazy_value(self, yield_expr): @@ -265,7 +292,7 @@ class BaseFunctionExecutionContext(ValueContext, TreeContextMixin): else: types = self.get_return_values(check_yields=True) if types: - yield LazyKnownValues(types) + yield LazyKnownValues(types, min=0, max=float('inf')) return last_for_stmt = for_stmt @@ -399,6 +426,9 @@ class OverloadedFunctionValue(FunctionMixin, ValueWrapper): def get_signature_functions(self): return self._overloaded_functions + def get_type_hint(self, add_class_info=True): + return 'Union[%s]' % ', '.join(f.get_type_hint() for f in self._overloaded_functions) + def _find_overload_functions(context, tree_node): def _is_overload_decorated(funcdef): diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index 858c778e..2eca9f9d 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -130,6 +130,9 @@ class AbstractInstanceValue(Value): for name in names ) + def get_type_hint(self, add_class_info=True): + return self.py__name__() + def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.class_value) diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index 743c17cf..764644d6 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -221,6 +221,11 @@ class ClassMixin(object): def _as_context(self): return ClassContext(self) + def get_type_hint(self, add_class_info=True): + if add_class_info: + return 'Type[%s]' % self.py__name__() + return self.py__name__() + class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase)): api_type = u'class' diff --git a/test/completion/basic.py b/test/completion/basic.py index b4006817..3ff919ca 100644 --- a/test/completion/basic.py +++ b/test/completion/basic.py @@ -209,11 +209,11 @@ if r: deleted_var = 3 del deleted_var -#? int() +#? deleted_var -#? ['deleted_var'] +#? [] deleted_var -#! ['deleted_var = 3'] +#! [] deleted_var # ----------------- diff --git a/test/completion/generators.py b/test/completion/generators.py index 1d5e5384..e36f3928 100644 --- a/test/completion/generators.py +++ b/test/completion/generators.py @@ -78,6 +78,28 @@ g = iter([1.0]) #? float() next(g) +x, y = Get() +#? int() str() +x +#? int() str() +x + +class Iter: + def __iter__(self): + yield "" + i = 0 + while True: + v = 1 + yield v + i += 1 +a, b, c = Iter() +#? str() int() +a +#? str() int() +b +#? str() int() +c + # ----------------- # __next__ @@ -134,7 +156,7 @@ a, b = simple() #? int() str() a # For now this is ok. -#? +#? int() str() b diff --git a/test/completion/pep0484_typing.py b/test/completion/pep0484_typing.py index 7a48aca9..8dc03d96 100644 --- a/test/completion/pep0484_typing.py +++ b/test/completion/pep0484_typing.py @@ -436,6 +436,12 @@ def the_callable() -> float: ... #? float() call3_pls()(the_callable)[0] +def call4_pls(fn: typing.Callable[..., TYPE_VARX]) -> typing.Callable[..., TYPE_VARX]: + return "" + +#? int() +call4_pls(lambda x: 1)() + # ------------------------- # TYPE_CHECKING # ------------------------- diff --git a/test/test_api/test_classes.py b/test/test_api/test_classes.py index 4a17c572..1d8b9ff4 100644 --- a/test/test_api/test_classes.py +++ b/test/test_api/test_classes.py @@ -558,3 +558,43 @@ def test_definition_goto_follow_imports(Script): assert follow.description == 'def dumps' assert follow.line != 1 assert follow.module_name == 'json' + + +@pytest.mark.parametrize( + 'code, expected', [ + ('1', 'int'), + ('x = None; x', 'None'), + ('n: Optional[str]; n', 'Optional[str]'), + ('n = None if xxxxx else ""; n', 'Optional[str]'), + ('n = None if xxxxx else str(); n', 'Optional[str]'), + ('n = None if xxxxx else str; n', 'Optional[Type[str]]'), + ('class Foo: pass\nFoo', 'Type[Foo]'), + ('class Foo: pass\nFoo()', 'Foo'), + + ('n: Type[List[int]]; n', 'Type[List[int]]'), + ('n: Type[List]; n', 'Type[list]'), + ('n: List; n', 'list'), + ('n: List[int]; n', 'List[int]'), + ('n: Iterable[int]; n', 'Iterable[int]'), + + ('n = [1]; n', 'List[int]'), + ('n = [1, ""]; n', 'List[Union[int, str]]'), + ('n = [1, str(), None]; n', 'List[Optional[Union[int, str]]]'), + ('n = {1, str()}; n', 'Set[Union[int, str]]'), + ('n = (1,); n', 'Tuple[int]'), + ('n = {1: ""}; n', 'Dict[int, str]'), + ('n = {1: "", 1.0: b""}; n', 'Dict[Union[float, int], Union[bytes, str]]'), + + ('n = next; n', 'Union[next(__i: Iterator[_T]) -> _T, ' + 'next(__i: Iterator[_T], default: _VT) -> Union[_T, _VT]]'), + ('abs', 'abs(__n: SupportsAbs[_T]) -> _T'), + ('def foo(x, y): return x if xxxx else y\nfoo(str(), 1)\nfoo', + 'foo(x: str, y: int) -> Union[int, str]'), + ('def foo(x, y = None): return x if xxxx else y\nfoo(str(), 1)\nfoo', + 'foo(x: str, y: int=None) -> Union[int, str]'), + ] +) +def test_get_type_hint(Script, code, expected, skip_pre_python36): + code = 'from typing import *\n' + code + d, = Script(code).goto() + assert d.get_type_hint() == expected diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 39ccb0da..7d5d183f 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -431,8 +431,9 @@ def test_completion_cache(Script, module_injector): assert cls.docstring() == 'foo()\n\ndoc2' -def test_typing_module_completions(Script): - for c in Script('import typing; typing.').completions(): +@pytest.mark.parametrize('module', ['typing', 'os']) +def test_module_completions(Script, module): + for c in Script('import {module}; {module}.'.format(module=module)).completions(): # Just make sure that there are no errors c.type c.docstring() diff --git a/test/test_api/test_full_name.py b/test/test_api/test_full_name.py index 4fdb861b..6858b6ca 100644 --- a/test/test_api/test_full_name.py +++ b/test/test_api/test_full_name.py @@ -112,7 +112,8 @@ def test_os_path(Script): def test_os_issues(Script): """Issue #873""" - assert [c.name for c in Script('import os\nos.nt''').complete()] == ['nt'] + # nt is not found, because it's deleted + assert [c.name for c in Script('import os\nos.nt''').complete()] == [] def test_param_name(Script): diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 160639eb..1ec0b741 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -530,6 +530,16 @@ def test__wrapped__(): assert c.line == syslogs_to_df.__wrapped__.__code__.co_firstlineno + 1 +@pytest.mark.skipif(sys.version_info[0] == 2, reason="Ignore Python 2, because EOL") +def test_illegal_class_instance(): + class X: + __class__ = 1 + X.__name__ = 'asdf' + d, = jedi.Interpreter('foo', [{'foo': X()}]).infer() + v, = d._name.infer() + assert not v.is_instance() + + @pytest.mark.skipif(sys.version_info[0] == 2, reason="Ignore Python 2, because EOL") @pytest.mark.parametrize('module_name', ['sys', 'time', 'unittest.mock']) def test_core_module_completes(module_name): diff --git a/test/test_inference/test_gradual/test_conversion.py b/test/test_inference/test_gradual/test_conversion.py index 123858d6..b63129ae 100644 --- a/test/test_inference/test_gradual/test_conversion.py +++ b/test/test_inference/test_gradual/test_conversion.py @@ -62,3 +62,11 @@ def test_goto_import(Script, skip_pre_python35): assert d.is_stub() d, = Script(code).goto() assert not d.is_stub() + + +def test_os_stat_result(Script): + d, = Script('import os; os.stat_result').goto() + assert d.is_stub() + n = d._name + # This should not be a different stub name + assert convert_names([n]) == [n]