diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 8664f0d7..36c7f0f5 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -291,17 +291,6 @@ Usage:: """ -class Python3Method(object): - def __init__(self, func): - self.func = func - - def __get__(self, obj, objtype): - if obj is None: - return lambda *args, **kwargs: self.func(*args, **kwargs) - else: - return lambda *args, **kwargs: self.func(obj, *args, **kwargs) - - def use_metaclass(meta, *bases): """ Create a class with a metaclass. """ if not bases: diff --git a/jedi/api/interpreter.py b/jedi/api/interpreter.py index c8a88988..515e0078 100644 --- a/jedi/api/interpreter.py +++ b/jedi/api/interpreter.py @@ -6,7 +6,7 @@ from jedi.evaluate.context import ModuleContext from jedi.evaluate import compiled from jedi.evaluate.compiled import mixed from jedi.evaluate.compiled.access import create_access_path -from jedi.evaluate.base_context import Context +from jedi.evaluate.base_context import ContextWrapper def _create(evaluator, obj): @@ -20,41 +20,28 @@ class NamespaceObject(object): self.__dict__ = dct -class MixedModuleContext(Context): - # TODO use ContextWrapper! +class MixedModuleContext(ContextWrapper): type = 'mixed_module' def __init__(self, evaluator, tree_module, namespaces, file_io, code_lines): - self.evaluator = evaluator - self._namespaces = namespaces - - self._namespace_objects = [NamespaceObject(n) for n in namespaces] - self._module_context = ModuleContext( + module_context = ModuleContext( evaluator, tree_module, file_io=file_io, string_names=('__main__',), code_lines=code_lines ) - self.tree_node = tree_module + super(MixedModuleContext, self).__init__(module_context) + self._namespace_objects = [NamespaceObject(n) for n in namespaces] def get_filters(self, *args, **kwargs): - for filter in self._module_context.get_filters(*args, **kwargs): + for filter in self._wrapped_context.get_filters(*args, **kwargs): yield filter for namespace_obj in self._namespace_objects: compiled_object = _create(self.evaluator, namespace_obj) mixed_object = mixed.MixedObject( - self.evaluator, - parent_context=self, compiled_object=compiled_object, - tree_context=self._module_context + tree_context=self._wrapped_context ) for filter in mixed_object.get_filters(*args, **kwargs): yield filter - - @property - def code_lines(self): - return self._module_context.code_lines - - def __getattr__(self, name): - return getattr(self._module_context, name) diff --git a/jedi/evaluate/base_context.py b/jedi/evaluate/base_context.py index 496efb5a..08b6ade0 100644 --- a/jedi/evaluate/base_context.py +++ b/jedi/evaluate/base_context.py @@ -11,7 +11,7 @@ from operator import add from parso.python.tree import ExprStmt, CompFor from jedi import debug -from jedi._compatibility import Python3Method, zip_longest, unicode +from jedi._compatibility import zip_longest, unicode from jedi.parser_utils import clean_scope_docstring from jedi.common import BaseContextSet, BaseContext from jedi.evaluate.helpers import SimpleGetItemNotFound, execute_evaluated @@ -51,7 +51,6 @@ class HelperContextMixin(object): for lazy_context in self.iterate(contextualized_node, is_async) ) - @Python3Method def py__getattribute__(self, name_or_str, name_context=None, position=None, search_global=False, is_goto=False, analysis_errors=True): diff --git a/jedi/evaluate/compiled/access.py b/jedi/evaluate/compiled/access.py index 84b7b9b4..b7165c0d 100644 --- a/jedi/evaluate/compiled/access.py +++ b/jedi/evaluate/compiled/access.py @@ -305,16 +305,26 @@ class DirectObjectAccess(object): return True, True return True, False - def getattr(self, name, default=_sentinel): + def getattr_paths(self, name, default=_sentinel): try: - return self._create_access(getattr(self._obj, name)) + return_obj = getattr(self._obj, name) except AttributeError: # Happens e.g. in properties of # PyQt4.QtGui.QStyleOptionComboBox.currentText # -> just set it to None if default is _sentinel: raise - return self._create_access(default) + return_obj = default + access = self._create_access(return_obj) + if inspect.ismodule(self._obj): + return [access] + + module = inspect.getmodule(return_obj) + if module is None: + module = inspect.getmodule(type(return_obj)) + if module is None: + module = builtins + return [self._create_access(module), access] def get_safe_value(self): if type(self._obj) in (bool, bytes, float, int, str, unicode, slice): diff --git a/jedi/evaluate/compiled/context.py b/jedi/evaluate/compiled/context.py index 4e6ba4f6..035e2bd2 100644 --- a/jedi/evaluate/compiled/context.py +++ b/jedi/evaluate/compiled/context.py @@ -36,7 +36,7 @@ class CheckAttribute(object): return self # This might raise an AttributeError. That's wanted. - instance.access_handle.getattr(self.check_name) + instance.access_handle.getattr_paths(self.check_name) return partial(self.func, instance) @@ -219,7 +219,7 @@ class CompiledObject(Context): try: # TODO wtf is this? this is exactly the same as the thing # below. It uses getattr as well. - self.evaluator.builtins_module.access_handle.getattr(name) + self.evaluator.builtins_module.access_handle.getattr_paths(name) except AttributeError: continue else: @@ -486,13 +486,17 @@ def _parse_function_doc(doc): def _create_from_name(evaluator, compiled_object, name): - access = compiled_object.access_handle.getattr(name, default=None) + access_paths = compiled_object.access_handle.getattr_paths(name, default=None) parent_context = compiled_object if parent_context.is_class(): parent_context = parent_context.parent_context - return create_cached_compiled_object( - evaluator, access, parent_context=parent_context - ) + + context = None + for access_path in access_paths: + context = create_cached_compiled_object( + evaluator, access_path, parent_context=context + ) + return context def _normalize_create_args(func): diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index 3a96ce29..61a1a713 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -11,7 +11,7 @@ from jedi import settings from jedi.evaluate import compiled from jedi.cache import underscore_memoization from jedi.file_io import FileIO -from jedi.evaluate.base_context import Context, ContextSet +from jedi.evaluate.base_context import ContextSet, ContextWrapper from jedi.evaluate.context import ModuleContext from jedi.evaluate.cache import evaluator_function_cache from jedi.evaluate.helpers import execute_evaluated @@ -21,7 +21,7 @@ from jedi.evaluate.compiled.context import create_cached_compiled_object from jedi.evaluate.gradual.conversion import to_stub -class MixedObject(object): +class MixedObject(ContextWrapper): """ A ``MixedObject`` is used in two ways: @@ -38,27 +38,19 @@ class MixedObject(object): fewer special cases, because we in Python you don't have the same freedoms to modify the runtime. """ - def __init__(self, evaluator, parent_context, compiled_object, tree_context): - self.evaluator = evaluator - self.parent_context = parent_context + def __init__(self, compiled_object, tree_context): + super(MixedObject, self).__init__(tree_context) self.compiled_object = compiled_object - self._context = tree_context self.access_handle = compiled_object.access_handle - # We have to overwrite everything that has to do with trailers, name - # lookups and filters to make it possible to route name lookups towards - # compiled objects and the rest towards tree node contexts. - def py__getattribute__(*args, **kwargs): - return Context.py__getattribute__(*args, **kwargs) - def get_filters(self, *args, **kwargs): yield MixedObjectFilter(self.evaluator, self) def __repr__(self): - return '<%s: %s>' % (type(self).__name__, self.access_handle.get_repr()) - - def __getattr__(self, name): - return getattr(self._context, name) + return '<%s: %s>' % ( + type(self).__name__, + self.access_handle.get_repr() + ) class MixedName(compiled.CompiledName): @@ -80,10 +72,15 @@ class MixedName(compiled.CompiledName): @underscore_memoization def infer(self): - access_handle = self.parent_context.access_handle # TODO use logic from compiled.CompiledObjectFilter - access_handle = access_handle.getattr(self.string_name, default=None) - return _create(self._evaluator, access_handle, parent_context=self.parent_context) + access_paths = self.parent_context.access_handle.getattr_paths( + self.string_name, + default=None + ) + context = None + for access in access_paths: + return _create(self._evaluator, access, parent_context=context) + return context @property def api_type(self): @@ -227,10 +224,6 @@ def _create(evaluator, access_handle, parent_context, *args): tree_context, = execute_evaluated(tree_context) return ContextSet({ - MixedObject( - evaluator, - parent_context, - compiled_object, - tree_context=c, - ) for c in to_stub(tree_context) or [tree_context] + MixedObject(compiled_object, tree_context=c) + for c in to_stub(tree_context) or [tree_context] }) diff --git a/test/conftest.py b/test/conftest.py index 7291600a..45bb48e0 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,6 +9,7 @@ from . import run from . import refactor import jedi +from jedi.api.environment import InterpreterEnvironment from jedi.evaluate.analysis import Warning @@ -163,3 +164,8 @@ def cwd_tmpdir(monkeypatch, tmpdir): @pytest.fixture def evaluator(Script): return Script('')._evaluator + + +@pytest.fixture +def same_process_evaluator(Script): + return Script('', environment=InterpreterEnvironment())._evaluator diff --git a/test/test_evaluate/test_compiled.py b/test/test_evaluate/test_compiled.py index b80b9add..1c6f5190 100644 --- a/test/test_evaluate/test_compiled.py +++ b/test/test_evaluate/test_compiled.py @@ -3,6 +3,7 @@ from textwrap import dedent import pytest from jedi.evaluate import compiled +from jedi.evaluate.compiled.access import DirectObjectAccess from jedi.evaluate.helpers import execute_evaluated from jedi.evaluate.gradual.conversion import stub_to_actual_context_set @@ -101,3 +102,39 @@ def test_getitem_on_none(Script): assert not script.goto_definitions() issue, = script._evaluator.analysis assert issue.name == 'type-error-not-subscriptable' + + +@pytest.mark.parametrize( + 'attribute, expected_name, expected_parent', [ + ('x', 'int', 'builtins'), + ('y', 'int', 'builtins'), + ('z', 'bool', 'builtins'), + ('cos', 'cos', 'math'), + ('dec', 'Decimal', 'decimal'), + ('dt', 'datetime', 'datetime'), + ] +) +def test_parent_context(same_process_evaluator, attribute, expected_name, expected_parent): + import math + import decimal + import datetime + + class C: + x = 1 + y = int + z = True + cos = math.cos + dec = decimal.Decimal(1) + dt = datetime.datetime(2000, 1, 1) + + o = compiled.CompiledObject( + same_process_evaluator, + DirectObjectAccess(same_process_evaluator, C) + ) + x, = o.py__getattribute__(attribute) + assert x.py__name__() == expected_name + module_name = x.parent_context.py__name__() + if module_name == '__builtin__': + module_name = 'builtins' # Python 2 + assert module_name == expected_parent + assert x.parent_context.parent_context is None