diff --git a/jedi/api/interpreter.py b/jedi/api/interpreter.py index 9f696eb4..1c6d085b 100644 --- a/jedi/api/interpreter.py +++ b/jedi/api/interpreter.py @@ -8,6 +8,11 @@ from jedi.evaluate.compiled import mixed from jedi.evaluate.context import Context +class NamespaceObject(): + def __init__(self, dct): + self.__dict__ = dct + + class MixedModuleContext(Context): resets_positions = True type = 'mixed_module' @@ -16,7 +21,7 @@ class MixedModuleContext(Context): self.evaluator = evaluator self._namespaces = namespaces - self._namespace_objects = [type('jedi_namespace', (), n) for n in namespaces] + self._namespace_objects = [NamespaceObject(n) for n in namespaces] self._module_context = ModuleContext(evaluator, tree_module, path=path) self.tree_node = tree_module diff --git a/jedi/evaluate/compiled/__init__.py b/jedi/evaluate/compiled/__init__.py index ab22a257..54e2829c 100644 --- a/jedi/evaluate/compiled/__init__.py +++ b/jedi/evaluate/compiled/__init__.py @@ -5,6 +5,7 @@ import inspect import re import sys import os +import types from functools import partial from jedi._compatibility import builtins as _builtins, unicode @@ -13,6 +14,7 @@ from jedi.cache import underscore_memoization, memoize_method from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition, \ ContextNameMixin from jedi.evaluate.context import Context, LazyKnownContext +from jedi.evaluate.compiled.getattr_static import getattr_static from . import fake @@ -22,6 +24,23 @@ if os.path.altsep is not None: _path_re = re.compile('(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep))) del _sep +# Those types don't exist in typing. +MethodDescriptorType = type(str.replace) +WrapperDescriptorType = type(set.__iter__) +# `object.__subclasshook__` is an already executed descriptor. +object_class_dict = type.__dict__["__dict__"].__get__(object) +ClassMethodDescriptorType = type(object_class_dict['__subclasshook__']) + +ALLOWED_DESCRIPTOR_ACCESS = ( + types.FunctionType, + types.GetSetDescriptorType, + types.MemberDescriptorType, + MethodDescriptorType, + WrapperDescriptorType, + ClassMethodDescriptorType, + staticmethod, + classmethod, +) class CheckAttribute(object): """Raises an AttributeError if the attribute X isn't available.""" @@ -297,16 +316,17 @@ class CompiledObjectFilter(AbstractFilter): name = str(name) obj = self._compiled_object.obj try: - getattr(obj, name) - if self._is_instance and name not in dir(obj): - return [] + attr, is_get_descriptor = getattr_static(obj, name) except AttributeError: return [] - except Exception: - # This is a bit ugly. We're basically returning this to make - # lookups possible without having the actual attribute. However - # this makes proper completion possible. - return [EmptyCompiledName(self._evaluator, name)] + else: + if is_get_descriptor \ + and not type(attr) in ALLOWED_DESCRIPTOR_ACCESS: + # In case of descriptors that have get methods we cannot return + # it's value, because that would mean code execution. + return [EmptyCompiledName(self._evaluator, name)] + if self._is_instance and name not in dir(obj): + return [] return [self._create_name(name)] def values(self): diff --git a/jedi/evaluate/compiled/getattr_static.py b/jedi/evaluate/compiled/getattr_static.py new file mode 100644 index 00000000..882c64d6 --- /dev/null +++ b/jedi/evaluate/compiled/getattr_static.py @@ -0,0 +1,110 @@ +""" +A static version of getattr. +This is a backport of the Python 3 code with a little bit of additional +information returned to enable Jedi to make decisions. +""" + +import types + +_sentinel = object() + +def _static_getmro(klass): + return type.__dict__['__mro__'].__get__(klass) + +def _check_instance(obj, attr): + instance_dict = {} + try: + instance_dict = object.__getattribute__(obj, "__dict__") + except AttributeError: + pass + return dict.get(instance_dict, attr, _sentinel) + + +def _check_class(klass, attr): + for entry in _static_getmro(klass): + if _shadowed_dict(type(entry)) is _sentinel: + try: + return entry.__dict__[attr] + except KeyError: + pass + return _sentinel + +def _is_type(obj): + try: + _static_getmro(obj) + except TypeError: + return False + return True + + +def _shadowed_dict(klass): + dict_attr = type.__dict__["__dict__"] + for entry in _static_getmro(klass): + try: + class_dict = dict_attr.__get__(entry)["__dict__"] + except KeyError: + pass + else: + if not (type(class_dict) is types.GetSetDescriptorType and + class_dict.__name__ == "__dict__" and + class_dict.__objclass__ is entry): + return class_dict + return _sentinel + + +def _safe_hasattr(obj, name): + return _check_class(type(obj), name) is not _sentinel + + +def _safe_is_data_descriptor(obj): + return (_safe_hasattr(obj, '__set__') or _safe_hasattr(obj, '__delete__')) + + +def getattr_static(obj, attr, default=_sentinel): + """Retrieve attributes without triggering dynamic lookup via the + descriptor protocol, __getattr__ or __getattribute__. + + Note: this function may not be able to retrieve all attributes + that getattr can fetch (like dynamically created attributes) + and may find attributes that getattr can't (like descriptors + that raise AttributeError). It can also return descriptor objects + instead of instance members in some cases. See the + documentation for details. + + Returns a tuple `(attr, is_get_descriptor)`. is_get_descripter means that + the attribute is a descriptor that has a `__get__` attribute. + """ + instance_result = _sentinel + if not _is_type(obj): + klass = type(obj) + dict_attr = _shadowed_dict(klass) + if (dict_attr is _sentinel or + type(dict_attr) is types.MemberDescriptorType): + instance_result = _check_instance(obj, attr) + else: + klass = obj + + klass_result = _check_class(klass, attr) + + if instance_result is not _sentinel and klass_result is not _sentinel: + if _safe_hasattr(klass_result, '__get__') \ + and _safe_is_data_descriptor(klass_result): + # A get/set descriptor has priority over everything. + return klass_result, True + + if instance_result is not _sentinel: + return instance_result, False + if klass_result is not _sentinel: + return klass_result, _safe_hasattr(klass_result, '__get__') + + if obj is klass: + # for types we check the metaclass too + for entry in _static_getmro(type(klass)): + if _shadowed_dict(type(entry)) is _sentinel: + try: + return entry.__dict__[attr], False + except KeyError: + pass + if default is not _sentinel: + return default, False + raise AttributeError(attr) diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 2cb68d33..df9d04bb 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -2,7 +2,6 @@ Tests of ``jedi.api.Interpreter``. """ -from ..helpers import TestCase import jedi from jedi._compatibility import is_py33 from jedi.evaluate.compiled import mixed @@ -179,15 +178,20 @@ def test_getitem_side_effects(): def test_property_error(): + lst = [] class Foo3(): @property def bar(self): + lst.append(1) raise ValueError foo = Foo3() _assert_interpreter_complete('foo.bar', locals(), ['bar']) _assert_interpreter_complete('foo.bar.baz', locals(), []) + # There should not be side effects + assert lst == [] + def test_param_completion(): def foo(bar):