From 97f342fc4c54465ea07c31e825055c67fa2d5c33 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 7 Jun 2019 01:33:37 +0200 Subject: [PATCH] Fix qualified names for CompiledObject --- jedi/evaluate/compiled/access.py | 16 ++++++ jedi/evaluate/compiled/context.py | 7 ++- jedi/evaluate/compiled/mixed.py | 77 ++++++++++++++++------------- jedi/evaluate/usages.py | 2 +- jedi/plugins/stdlib.py | 2 +- test/test_api/test_interpreter.py | 4 ++ test/test_evaluate/test_compiled.py | 33 +++++++++++-- 7 files changed, 99 insertions(+), 42 deletions(-) diff --git a/jedi/evaluate/compiled/access.py b/jedi/evaluate/compiled/access.py index d638991f..864de158 100644 --- a/jedi/evaluate/compiled/access.py +++ b/jedi/evaluate/compiled/access.py @@ -279,9 +279,25 @@ class DirectObjectAccess(object): def is_class(self): return inspect.isclass(self._obj) + def is_module(self): + return inspect.ismodule(self._obj) + def ismethoddescriptor(self): return inspect.ismethoddescriptor(self._obj) + def get_qualified_names(self): + def try_to_get_name(obj): + return getattr(obj, '__qualname__', getattr(obj, '__name__', None)) + + if self.is_module(): + return () + name = try_to_get_name(self._obj) + if name is None: + name = try_to_get_name(type(self._obj)) + if name is None: + return () + return name.split('.') + def dir(self): return list(map(force_unicode, dir(self._obj))) diff --git a/jedi/evaluate/compiled/context.py b/jedi/evaluate/compiled/context.py index fb1642ba..b997c54f 100644 --- a/jedi/evaluate/compiled/context.py +++ b/jedi/evaluate/compiled/context.py @@ -86,11 +86,11 @@ class CompiledObject(Context): # For modules name = self.py__name__() if name is None: - return [] + return () return tuple(name.split('.')) def get_qualified_names(self): - return self.string_names + return self.access_handle.get_qualified_names() def py__bool__(self): return self.access_handle.py__bool__() @@ -101,6 +101,9 @@ class CompiledObject(Context): def is_class(self): return self.access_handle.is_class() + def is_module(self): + return self.access_handle.is_module() + def is_compiled(self): return True diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index f1c8a42f..a941fd67 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -81,13 +81,15 @@ class MixedName(compiled.CompiledName): default=None ) assert len(access_paths) - context = None + contexts = [None] for access in access_paths: - if context is None or isinstance(context, MixedObject): - context = _create(self._evaluator, access, parent_context=context) - else: - context = create_cached_compiled_object(context.evaluator, access, context) - return ContextSet([context]) + contexts = ContextSet.from_sets( + _create(self._evaluator, access, parent_context=c) + if c is None or isinstance(c, MixedObject) + else ContextSet({create_cached_compiled_object(c.evaluator, access, c)}) + for c in contexts + ) + return contexts @property def api_type(self): @@ -211,35 +213,40 @@ def _create(evaluator, access_handle, parent_context, *args): # TODO use stub contexts here. If we do that we probably have to care about # generics from stuff like `[1]`. if result is None: - return compiled_object - - module_node, tree_node, file_io, code_lines = result - - if parent_context is None: - # TODO this __name__ is probably wrong. - name = compiled_object.get_root_context().py__name__() - string_names = tuple(name.split('.')) - module_context = ModuleContext( - evaluator, module_node, - file_io=file_io, - string_names=string_names, - code_lines=code_lines, - is_package=hasattr(compiled_object, 'py__path__'), - ) - if name is not None: - evaluator.module_cache.add(string_names, ContextSet([module_context])) + return ContextSet({compiled_object}) else: - assert parent_context.tree_node.get_root_node() == module_node - module_context = parent_context.get_root_context() + module_node, tree_node, file_io, code_lines = result - tree_context = module_context.create_context( - tree_node, - node_is_context=True, - node_is_object=True + if parent_context is None: + # TODO this __name__ is probably wrong. + name = compiled_object.get_root_context().py__name__() + string_names = tuple(name.split('.')) + module_context = ModuleContext( + evaluator, module_node, + file_io=file_io, + string_names=string_names, + code_lines=code_lines, + is_package=hasattr(compiled_object, 'py__path__'), + ) + if name is not None: + evaluator.module_cache.add(string_names, ContextSet([module_context])) + else: + assert parent_context.tree_node.get_root_node() == module_node + module_context = parent_context.get_root_context() + + tree_contexts = ContextSet({ + module_context.create_context( + tree_node, + node_is_context=True, + node_is_object=True + ) + }) + if tree_node.type == 'classdef': + if not access_handle.is_class(): + # Is an instance, not a class. + tree_contexts = tree_contexts.execute_evaluated() + + return ContextSet( + MixedObject(compiled_object, tree_context=tree_context) + for tree_context in tree_contexts ) - if tree_node.type == 'classdef': - if not access_handle.is_class(): - # Is an instance, not a class. - tree_context, = execute_evaluated(tree_context) - - return MixedObject(compiled_object, tree_context=tree_context) diff --git a/jedi/evaluate/usages.py b/jedi/evaluate/usages.py index 623caf4c..9c8d2651 100644 --- a/jedi/evaluate/usages.py +++ b/jedi/evaluate/usages.py @@ -38,7 +38,7 @@ def usages(module_context, tree_name): search_name = tree_name.value found_names = _find_names(module_context, tree_name) modules = set(d.get_root_context() for d in found_names.values()) - modules = set(m for m in modules if m.is_module()) + modules = set(m for m in modules if m.is_module() and not m.is_compiled()) non_matching_usage_maps = {} for m in imports.get_modules_containing_name(module_context.evaluator, modules, search_name): diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 03674b81..e75ef2de 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -110,7 +110,7 @@ class StdlibPlugin(BasePlugin): if context.parent_context == self._evaluator.builtins_module: module_name = 'builtins' elif context.parent_context is not None and context.parent_context.is_module(): - module_name = context.parent_context.name.string_name + module_name = context.parent_context.py__name__() else: return callback(context, arguments=arguments) diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index b26d3f00..0501866f 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -397,6 +397,9 @@ def test_sys_path_docstring(): # Was an issue in #1298 ('counter.setdefa', ['setdefault']), ('counter.pop().imag', []), # TODO stubs could make this better ('counter.keys())[0].uppe', []), + + ('string.upper().uppe', ['upper']), + ('"".upper().uppe', ['upper']), ] ) def test_simple_completions(code, completions): @@ -406,6 +409,7 @@ def test_simple_completions(code, completions): import collections deq = collections.deque([1]) counter = collections.Counter(['asdf']) + string = '' defs = jedi.Interpreter(code, [locals()]).completions() assert [d.name for d in defs] == completions diff --git a/test/test_evaluate/test_compiled.py b/test/test_evaluate/test_compiled.py index a04dfd4b..9d4d1fc1 100644 --- a/test/test_evaluate/test_compiled.py +++ b/test/test_evaluate/test_compiled.py @@ -1,4 +1,8 @@ from textwrap import dedent +import math +import sys +from collections import Counter +from datetime import datetime import pytest @@ -107,6 +111,7 @@ def test_getitem_on_none(Script): def _return_int(): return 1 + @pytest.mark.parametrize( 'attribute, expected_name, expected_parent', [ ('x', 'int', 'builtins'), @@ -119,9 +124,7 @@ def _return_int(): ] ) def test_parent_context(same_process_evaluator, attribute, expected_name, expected_parent): - import math import decimal - import datetime class C: x = 1 @@ -129,7 +132,7 @@ def test_parent_context(same_process_evaluator, attribute, expected_name, expect z = True cos = math.cos dec = decimal.Decimal(1) - dt = datetime.datetime(2000, 1, 1) + dt = datetime(2000, 1, 1) ret_int = _return_int o = compiled.CompiledObject( @@ -143,3 +146,27 @@ def test_parent_context(same_process_evaluator, attribute, expected_name, expect module_name = 'builtins' # Python 2 assert module_name == expected_parent assert x.parent_context.parent_context is None + + +@pytest.mark.skipif(sys.version_info[0] == 2, reason="Ignore Python 2, because EOL") +@pytest.mark.parametrize( + 'obj, expected_names', [ + ('', ['str']), + (str, ['str']), + (''.upper, ['str', 'upper']), + (str.upper, ['str', 'upper']), + + (math.cos, ['cos']), + + (Counter, ['Counter']), + (Counter(""), ['Counter']), + (Counter.most_common, ['Counter', 'most_common']), + (Counter("").most_common, ['Counter', 'most_common']), + ] +) +def test_qualified_names(same_process_evaluator, obj, expected_names): + o = compiled.CompiledObject( + same_process_evaluator, + DirectObjectAccess(same_process_evaluator, obj) + ) + assert o.get_qualified_names() == expected_names