diff --git a/jedi/evaluate/analysis.py b/jedi/evaluate/analysis.py index be42e43d..47f1bbd5 100644 --- a/jedi/evaluate/analysis.py +++ b/jedi/evaluate/analysis.py @@ -118,7 +118,7 @@ def add_attribute_error(name_context, lookup_context, name): # Check for __getattr__/__getattribute__ existance and issue a warning # instead of an error, if that happens. typ = Error - if lookup_context.is_instance(): + if lookup_context.is_instance() and not lookup_context.is_compiled(): slot_names = lookup_context.get_function_slot_names(u'__getattr__') + \ lookup_context.get_function_slot_names(u'__getattribute__') for n in slot_names: diff --git a/jedi/evaluate/base_context.py b/jedi/evaluate/base_context.py index 98be287b..bf2aa848 100644 --- a/jedi/evaluate/base_context.py +++ b/jedi/evaluate/base_context.py @@ -19,6 +19,8 @@ from jedi.evaluate.utils import safe_property from jedi.evaluate.cache import evaluator_as_method_param_cache from jedi.cache import memoize_method +_sentinel = object() + class HelperContextMixin(object): def get_root_context(self): @@ -185,6 +187,11 @@ class Context(HelperContextMixin, BaseContext): return clean_scope_docstring(self.tree_node) return None + def get_safe_value(self, default=_sentinel): + if default is _sentinel: + raise ValueError("There exists no safe value for context %s" % self) + return default + def py__call__(self, arguments): debug.warning("no execution possible %s", self) return NO_CONTEXTS diff --git a/jedi/evaluate/compiled/access.py b/jedi/evaluate/compiled/access.py index 864de158..7d2e6809 100644 --- a/jedi/evaluate/compiled/access.py +++ b/jedi/evaluate/compiled/access.py @@ -282,6 +282,9 @@ class DirectObjectAccess(object): def is_module(self): return inspect.ismodule(self._obj) + def is_instance(self): + return _is_class_instance(self._obj) + def ismethoddescriptor(self): return inspect.ismethoddescriptor(self._obj) diff --git a/jedi/evaluate/compiled/context.py b/jedi/evaluate/compiled/context.py index b997c54f..ac019daa 100644 --- a/jedi/evaluate/compiled/context.py +++ b/jedi/evaluate/compiled/context.py @@ -110,6 +110,9 @@ class CompiledObject(Context): def is_stub(self): return False + def is_instance(self): + return self.access_handle.is_instance() + def py__doc__(self): return self.access_handle.py__doc__() diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index a941fd67..474c9951 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -14,12 +14,13 @@ from jedi.file_io import FileIO 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 from jedi.evaluate.compiled.getattr_static import getattr_static from jedi.evaluate.compiled.access import compiled_objects_cache from jedi.evaluate.compiled.context import create_cached_compiled_object from jedi.evaluate.gradual.conversion import to_stub +_sentinel = object() + class MixedObject(ContextWrapper): """ @@ -49,6 +50,12 @@ class MixedObject(ContextWrapper): def py__call__(self, arguments): return (to_stub(self._wrapped_context) or self._wrapped_context).py__call__(arguments) + def get_safe_value(self, default=_sentinel): + if default is _sentinel: + return self.compiled_object.get_safe_value() + else: + return self.compiled_object.get_safe_value(default) + def __repr__(self): return '<%s: %s>' % ( type(self).__name__, @@ -210,10 +217,15 @@ def _create(evaluator, access_handle, parent_context, *args): ) result = _find_syntax_node_name(evaluator, access_handle) - # 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 ContextSet({compiled_object}) + # TODO Care about generics from stuff like `[1]` and don't return like this. + python_object = access_handle.access._obj + if type(python_object) in (dict, list, tuple): + return ContextSet({compiled_object}) + + tree_contexts = to_stub(compiled_object) + if not tree_contexts: + return ContextSet({compiled_object}) else: module_node, tree_node, file_io, code_lines = result diff --git a/jedi/evaluate/context/instance.py b/jedi/evaluate/context/instance.py index 1e68d737..e05f2c24 100644 --- a/jedi/evaluate/context/instance.py +++ b/jedi/evaluate/context/instance.py @@ -157,7 +157,7 @@ class AbstractInstanceContext(Context): def iterate(): for generator in self.execute_function_slots(iter_slot_names): - if generator.is_instance(): + if generator.is_instance() and not generator.is_compiled(): # `__next__` logic. if self.evaluator.environment.version_info.major == 2: name = u'next' diff --git a/jedi/evaluate/finder.py b/jedi/evaluate/finder.py index 8f59868e..08d8d703 100644 --- a/jedi/evaluate/finder.py +++ b/jedi/evaluate/finder.py @@ -189,13 +189,13 @@ class NameFinder(object): contexts = ContextSet.from_sets(name.infer() for name in names) debug.dbg('finder._names_to_types: %s -> %s', names, contexts) - if not names and self._context.is_instance(): + if not names and self._context.is_instance() and not self._context.is_compiled(): # handling __getattr__ / __getattribute__ return self._check_getattr(self._context) # Add isinstance and other if/assert knowledge. if not contexts and isinstance(self._name, tree.Name) and \ - not self._name_context.is_instance(): + not self._name_context.is_instance() and not self._context.is_compiled(): flow_scope = self._name base_nodes = [self._name_context.tree_node] diff --git a/jedi/evaluate/helpers.py b/jedi/evaluate/helpers.py index 4c526014..122d05c6 100644 --- a/jedi/evaluate/helpers.py +++ b/jedi/evaluate/helpers.py @@ -207,10 +207,9 @@ def is_literal(context): def _get_safe_value_or_none(context, accept): - if context.is_compiled(): - value = context.get_safe_value(default=None) - if isinstance(value, accept): - return value + value = context.get_safe_value(default=None) + if isinstance(value, accept): + return value def get_int_or_none(context): diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index e75ef2de..659abee7 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -212,11 +212,12 @@ def builtins_getattr(objects, names, defaults=None): # follow the first param for obj in objects: for name in names: - if is_string(name): - return obj.py__getattribute__(force_unicode(name.get_safe_value())) - else: + string = get_str_or_none(name) + if string is None: debug.warning('getattr called without str') continue + else: + return obj.py__getattribute__(force_unicode(string)) return NO_CONTEXTS diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 0501866f..7b150a34 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -167,6 +167,14 @@ def test_list(): ['upper']) +def test_getattr(): + class Foo1: + bar = [] + baz = 'bar' + _assert_interpreter_complete('getattr(Foo1, baz).app', locals(), ['append']) + + +@pytest.mark.xfail(reason='For now slicing on strings is not supported for mixed objects') def test_slice(): class Foo1: bar = []