diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 094a8cb2..1cb647c9 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -626,46 +626,6 @@ class Interpreter(Script): interpreter.add_namespaces_to_parser(self._evaluator, namespaces, self._parser.module()) - def _simple_complete(self, path, dot, like): - user_stmt = self._parser.user_stmt_with_whitespace() - is_simple_path = not path or re.search('^[\w][\w\d.]*$', path) - if isinstance(user_stmt, tree.Import) or not is_simple_path: - return super(Interpreter, self)._simple_complete(path, dot, like) - else: - # TODO Remove this branch? The above branch should be fast enough IMO. - class NamespaceModule(object): - def __getattr__(_, name): - for n in self.namespaces: - try: - return n[name] - except KeyError: - pass - raise AttributeError() - - def __dir__(_): - gen = (n.keys() for n in self.namespaces) - return list(set(chain.from_iterable(gen))) - - paths = path.split('.') if path else [] - - namespaces = (NamespaceModule(), builtins) - for p in paths: - old, namespaces = namespaces, [] - for n in old: - try: - namespaces.append(getattr(n, p)) - except Exception: - pass - - completion_names = [] - for namespace in namespaces: - for name in dir(namespace): - if name.lower().startswith(like.lower()): - scope = self._parser.module() - n = FakeName(name, scope) - completion_names.append(n) - return completion_names - def defined_names(source, path=None, encoding='utf-8'): """ diff --git a/jedi/api/interpreter.py b/jedi/api/interpreter.py index 7325c9bc..c858c462 100644 --- a/jedi/api/interpreter.py +++ b/jedi/api/interpreter.py @@ -3,13 +3,13 @@ TODO Some parts of this module are still not well documented. """ import inspect import re +import sys from jedi._compatibility import builtins from jedi import debug from jedi.common import source_to_unicode from jedi.cache import underscore_memoization from jedi.evaluate import compiled -from jedi.evaluate.compiled.fake import get_module from jedi.parser import tree as pt from jedi.parser import load_grammar from jedi.parser.fast import FastParser @@ -42,6 +42,9 @@ class LazyName(helpers.FakeName): def parent(self): """ Creating fake statements for the interpreter. + + Here we are trying to link back to Python code, if possible. This means + we try to find the python module for a name (not the builtin). """ obj = self._value parser_path = [] @@ -63,10 +66,10 @@ class LazyName(helpers.FakeName): # Unfortunately in some cases like `int` there's no __module__ module = builtins else: - # TODO this import is wrong. Yields x for x.y.z instead of z - module = __import__(module_name) + # If we put anything into fromlist, it will just return the + # latest name. + module = __import__(module_name, fromlist=['']) parser_path = names - raw_module = get_module(self._value) found = [] try: @@ -74,17 +77,36 @@ class LazyName(helpers.FakeName): except AttributeError: pass else: - # cut the `c` from `.pyc` - path = re.sub('c$', '', path) + # Find the corresponding Python file for an interpreted one. + path = re.sub(r'\.pyc$', '.py', path) + if path.endswith('.py'): with open(path) as f: source = source_to_unicode(f.read()) mod = FastParser(load_grammar(), source, path).module + mod = self._evaluator.wrap(mod) + + # We have to make sure that the modules that we import are also + # part of evaluator.modules. + for module_name, module in sys.modules.items(): + try: + iterated_path = module.__file__ + except AttributeError: + pass + else: + if iterated_path == path: + self._evaluator.modules[module_name] = mod + break + else: + raise NotImplementedError('This should not happen, a module ' + 'should be part of sys.modules.') + if parser_path: assert len(parser_path) == 1 - found = self._evaluator.find_types(mod, parser_path[0], search_global=True) + found = list(self._evaluator.find_types(mod, parser_path[0], + search_global=True)) else: - found = [self._evaluator.wrap(mod)] + found = [mod] if not found: debug.warning('Interpreter lookup failed in global scope for %s', @@ -94,11 +116,14 @@ class LazyName(helpers.FakeName): evaluated = compiled.create(self._evaluator, obj) found = [evaluated] - content = iterable.AlreadyEvaluated(found) - stmt = pt.ExprStmt([self, pt.Operator(pt.zero_position_modifier, - '=', (0, 0), ''), content]) - stmt.parent = self._module - return stmt + if len(found) > 1 or True: + content = iterable.AlreadyEvaluated(found) + stmt = pt.ExprStmt([self, pt.Operator(pt.zero_position_modifier, + '=', (0, 0), ''), content]) + stmt.parent = self._module + return stmt + else: + return found[0] @parent.setter def parent(self, value): diff --git a/jedi/evaluate/compiled/__init__.py b/jedi/evaluate/compiled/__init__.py index d3bac367..f9b4d314 100644 --- a/jedi/evaluate/compiled/__init__.py +++ b/jedi/evaluate/compiled/__init__.py @@ -113,7 +113,7 @@ class CompiledObject(Base): elif inspect.ismodule(cls): return 'module' elif inspect.isbuiltin(cls) or inspect.ismethod(cls) \ - or inspect.ismethoddescriptor(cls): + or inspect.ismethoddescriptor(cls) or inspect.isfunction(cls): return 'function' @property diff --git a/jedi/evaluate/compiled/fake.py b/jedi/evaluate/compiled/fake.py index 8b3a7d23..37c2cff4 100644 --- a/jedi/evaluate/compiled/fake.py +++ b/jedi/evaluate/compiled/fake.py @@ -6,6 +6,7 @@ mixing in Python code, the autocompletion should work much better for builtins. import os import inspect +import types from jedi._compatibility import is_py3, builtins, unicode from jedi.parser import ParserWithRecovery, load_grammar @@ -15,6 +16,28 @@ from jedi.evaluate.helpers import FakeName modules = {} +MethodDescriptorType = type(str.replace) +# These are not considered classes and access is granted even though they have +# a __class__ attribute. +NOT_CLASS_TYPES = ( + types.BuiltinFunctionType, + types.CodeType, + types.DynamicClassAttribute, + types.FrameType, + types.FunctionType, + types.GeneratorType, + types.GetSetDescriptorType, + types.LambdaType, + types.MappingProxyType, + types.MemberDescriptorType, + types.MethodType, + types.ModuleType, + types.SimpleNamespace, + types.TracebackType, + MethodDescriptorType +) + + def _load_faked_module(module): module_name = module.__name__ if module_name == '__builtin__' and not is_py3: @@ -134,7 +157,9 @@ def get_faked(module, obj, name=None): def is_class_instance(obj): """Like inspect.* methods.""" - return not (inspect.isclass(obj) or inspect.ismodule(obj) - or inspect.isbuiltin(obj) or inspect.ismethod(obj) - or inspect.ismethoddescriptor(obj) or inspect.iscode(obj) - or inspect.isgenerator(obj)) + try: + cls = obj.__class__ + except AttributeError: + return False + else: + return cls != type and not issubclass(cls, NOT_CLASS_TYPES) diff --git a/jedi/utils.py b/jedi/utils.py index 6b48ef49..a09f954a 100644 --- a/jedi/utils.py +++ b/jedi/utils.py @@ -88,7 +88,7 @@ def setup_readline(namespace_module=__main__): try: import readline except ImportError: - print("Module readline not available.") + print("Jedi: Module readline not available.") else: readline.set_completer(JediRL().complete) readline.parse_and_bind("tab: complete") diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 25653558..b54d9e5f 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -7,6 +7,34 @@ import jedi from jedi._compatibility import is_py33 +def get_completion(source, namespace): + i = jedi.Interpreter(source, [namespace]) + completions = i.completions() + assert len(completions) == 1 + return completions[0] + + +def test_builtin_details(): + import keyword + + class EmptyClass: + pass + + variable = EmptyClass() + + def func(): + pass + + cls = get_completion('EmptyClass', locals()) + var = get_completion('variable', locals()) + f = get_completion('func', locals()) + m = get_completion('keyword', locals()) + assert cls.type == 'class' + assert var.type == 'instance' + assert f.type == 'function' + assert m.type == 'module' + + class TestInterpreterAPI(TestCase): def check_interpreter_complete(self, source, namespace, completions, **kwds):