Remove side effects when accessing jedi from the interpreter.

Note that there is http://bugs.python.org/issue31184.
Fixes #925.
This commit is contained in:
Dave Halter
2017-08-12 22:48:49 +02:00
parent b26b8a1749
commit 88cfb2cb91
4 changed files with 149 additions and 10 deletions

View File

@@ -8,6 +8,11 @@ from jedi.evaluate.compiled import mixed
from jedi.evaluate.context import Context from jedi.evaluate.context import Context
class NamespaceObject():
def __init__(self, dct):
self.__dict__ = dct
class MixedModuleContext(Context): class MixedModuleContext(Context):
resets_positions = True resets_positions = True
type = 'mixed_module' type = 'mixed_module'
@@ -16,7 +21,7 @@ class MixedModuleContext(Context):
self.evaluator = evaluator self.evaluator = evaluator
self._namespaces = namespaces 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._module_context = ModuleContext(evaluator, tree_module, path=path)
self.tree_node = tree_module self.tree_node = tree_module

View File

@@ -5,6 +5,7 @@ import inspect
import re import re
import sys import sys
import os import os
import types
from functools import partial from functools import partial
from jedi._compatibility import builtins as _builtins, unicode 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, \ from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition, \
ContextNameMixin ContextNameMixin
from jedi.evaluate.context import Context, LazyKnownContext from jedi.evaluate.context import Context, LazyKnownContext
from jedi.evaluate.compiled.getattr_static import getattr_static
from . import fake 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))) _path_re = re.compile('(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep)))
del _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): class CheckAttribute(object):
"""Raises an AttributeError if the attribute X isn't available.""" """Raises an AttributeError if the attribute X isn't available."""
@@ -297,16 +316,17 @@ class CompiledObjectFilter(AbstractFilter):
name = str(name) name = str(name)
obj = self._compiled_object.obj obj = self._compiled_object.obj
try: try:
getattr(obj, name) attr, is_get_descriptor = getattr_static(obj, name)
if self._is_instance and name not in dir(obj):
return []
except AttributeError: except AttributeError:
return [] return []
except Exception: else:
# This is a bit ugly. We're basically returning this to make if is_get_descriptor \
# lookups possible without having the actual attribute. However and not type(attr) in ALLOWED_DESCRIPTOR_ACCESS:
# this makes proper completion possible. # In case of descriptors that have get methods we cannot return
return [EmptyCompiledName(self._evaluator, name)] # 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)] return [self._create_name(name)]
def values(self): def values(self):

View File

@@ -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)

View File

@@ -2,7 +2,6 @@
Tests of ``jedi.api.Interpreter``. Tests of ``jedi.api.Interpreter``.
""" """
from ..helpers import TestCase
import jedi import jedi
from jedi._compatibility import is_py33 from jedi._compatibility import is_py33
from jedi.evaluate.compiled import mixed from jedi.evaluate.compiled import mixed
@@ -179,15 +178,20 @@ def test_getitem_side_effects():
def test_property_error(): def test_property_error():
lst = []
class Foo3(): class Foo3():
@property @property
def bar(self): def bar(self):
lst.append(1)
raise ValueError raise ValueError
foo = Foo3() foo = Foo3()
_assert_interpreter_complete('foo.bar', locals(), ['bar']) _assert_interpreter_complete('foo.bar', locals(), ['bar'])
_assert_interpreter_complete('foo.bar.baz', locals(), []) _assert_interpreter_complete('foo.bar.baz', locals(), [])
# There should not be side effects
assert lst == []
def test_param_completion(): def test_param_completion():
def foo(bar): def foo(bar):