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 5ee509e8..8f6a3265 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/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/base_value.py b/jedi/inference/base_value.py index 96f43395..05134773 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -264,6 +264,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): """ @@ -414,6 +417,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/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..bda6c20b 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(type_var_dict) + 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/value/function.py b/jedi/inference/value/function.py index ae28ae9a..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() @@ -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 b7d79646..37b743b3 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/pep0484_typing.py b/test/completion/pep0484_typing.py index 9104ca11..4ebab2e9 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 a037133b..9be2b07d 100644 --- a/test/test_api/test_classes.py +++ b/test/test_api/test_classes.py @@ -559,3 +559,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_inference/test_gradual/test_conversion.py b/test/test_inference/test_gradual/test_conversion.py index c57cb188..fd05d265 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]