diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index bfdaf528..cef8c252 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -16,7 +16,7 @@ import parso from parso.python import tree from parso import python_bytes_to_unicode, split_lines -from jedi.parser_utils import get_executable_nodes, get_statement_of_position +from jedi.parser_utils import get_executable_nodes from jedi import debug from jedi import settings from jedi import cache diff --git a/jedi/evaluate/base_context.py b/jedi/evaluate/base_context.py index 693a99aa..24df628a 100644 --- a/jedi/evaluate/base_context.py +++ b/jedi/evaluate/base_context.py @@ -85,8 +85,13 @@ class Context(BaseContext): result = ContextSet() for index in index_contexts: - if isinstance(index, (CompiledObject, Slice)): + if isinstance(index, Slice): index = index.obj + if isinstance(index, CompiledObject): + try: + index = index.get_safe_value() + except ValueError: + pass if type(index) not in (float, int, str, unicode, slice, type(Ellipsis)): # If the index is not clearly defined, we have to get all the diff --git a/jedi/evaluate/compiled/__init__.py b/jedi/evaluate/compiled/__init__.py index 83d07fdb..e0401b57 100644 --- a/jedi/evaluate/compiled/__init__.py +++ b/jedi/evaluate/compiled/__init__.py @@ -16,6 +16,7 @@ from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition, \ from jedi.evaluate.base_context import Context, ContextSet from jedi.evaluate.lazy_context import LazyKnownContext from jedi.evaluate.compiled.getattr_static import getattr_static +from jedi.evaluate.compiled.access import DirectObjectAccess, _sentinel from . import fake @@ -25,23 +26,6 @@ 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.""" @@ -60,12 +44,10 @@ class CheckAttribute(object): # the __iter__ function as long as __getitem__ is defined (it will # just start with __getitem__(0). This is especially true for # Python 2 strings, where `str.__iter__` is not even defined. - try: - iter(instance.obj) - except TypeError: + if not instance.access.has_iter(): raise AttributeError else: - getattr(instance.obj, self.check_name) + instance.access.getattr(self.check_name) return partial(self.func, instance) @@ -73,9 +55,9 @@ class CompiledObject(Context): path = None # modules have this attribute - set it to None. used_names = lambda self: {} # To be consistent with modules. - def __init__(self, evaluator, obj, parent_context=None, faked_class=None): + def __init__(self, evaluator, access, parent_context=None, faked_class=None): super(CompiledObject, self).__init__(evaluator, parent_context) - self.obj = obj + self.access = access # This attribute will not be set for most classes, except for fakes. self.tree_node = faked_class @@ -85,7 +67,7 @@ class CompiledObject(Context): @CheckAttribute def py__call__(self, params): - if inspect.isclass(self.obj): + if self.access.is_class(): from jedi.evaluate.context import CompiledInstance return ContextSet(CompiledInstance(self.evaluator, self.parent_context, self, params)) else: @@ -97,26 +79,25 @@ class CompiledObject(Context): @CheckAttribute def py__mro__(self): - return (self,) + tuple(create(self.evaluator, cls) for cls in self.obj.__mro__[1:]) + return (self,) + tuple( + _create_from_access(self.evaluator, access) for access in self.access.py__mro__accesses() + ) @CheckAttribute def py__bases__(self): return tuple(create(self.evaluator, cls) for cls in self.obj.__bases__) def py__bool__(self): - return bool(self.obj) + return self.access.py__bool__() def py__file__(self): - try: - return self.obj.__file__ - except AttributeError: - return None + return self.access.py__file__() def is_class(self): - return inspect.isclass(self.obj) + return self.access.is_class() def py__doc__(self, include_call_signature=False): - return inspect.getdoc(self.obj) or '' + return self.access.py__doc__() def get_param_names(self): obj = self.obj @@ -153,7 +134,7 @@ class CompiledObject(Context): yield SignatureParamName(self, signature_param) def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__, repr(self.obj)) + return '<%s: %s>' % (self.__class__.__name__, self.access.get_repr()) @underscore_memoization def _parse_function_doc(self): @@ -165,16 +146,7 @@ class CompiledObject(Context): @property def api_type(self): - obj = self.obj - if inspect.isclass(obj): - return 'class' - elif inspect.ismodule(obj): - return 'module' - elif inspect.isbuiltin(obj) or inspect.ismethod(obj) \ - or inspect.ismethoddescriptor(obj) or inspect.isfunction(obj): - return 'function' - # Everything else... - return 'instance' + return self.access.get_api_type() @underscore_memoization def _cls(self): @@ -185,18 +157,6 @@ class CompiledObject(Context): # Ensures that a CompiledObject is returned that is not an instance (like list) return self - def _get_class(self): - if not fake.is_class_instance(self.obj) or \ - inspect.ismethoddescriptor(self.obj): # slots - return self.obj - - try: - return self.obj.__class__ - except AttributeError: - # happens with numpy.core.umath._UFUNC_API (you get it - # automatically by doing `import numpy`. - return type - def get_filters(self, search_global=False, is_instance=False, until_position=None, origin_scope=None): yield self._ensure_one_filter(is_instance) @@ -211,36 +171,25 @@ class CompiledObject(Context): @CheckAttribute def py__getitem__(self, index): - if type(self.obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): - # Get rid of side effects, we won't call custom `__getitem__`s. + access = self.access.py__getitem__(index) + if access is None: return ContextSet() - return ContextSet(create(self.evaluator, self.obj[index])) + return ContextSet(_create_from_access(self.evaluator, access)) @CheckAttribute - def py__iter__(self): - if type(self.obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): - # Get rid of side effects, we won't call custom `__getitem__`s. - return - - for i, part in enumerate(self.obj): - if i > 20: - # Should not go crazy with large iterators - break - yield LazyKnownContext(create(self.evaluator, part)) + def py__iter__list(self): + for access in self.access.py__iter__(): + yield _create_from_access(self.evaluator, access) def py__name__(self): - try: - return self._get_class().__name__ - except AttributeError: - return None + return self.access.py__name__() @property def name(self): - try: - name = self._get_class().__name__ - except AttributeError: - name = repr(self.obj) + name = self.py__name__() + if name is None: + name = self.access.get_repr() return CompiledContextName(self, name) def _execute_function(self, params): @@ -274,6 +223,15 @@ class CompiledObject(Context): create(self.evaluator, v) for v in self.obj.values() ) + def get_safe_value(self, default=_sentinel): + return self.access.get_safe_value(default=default) + + def execute_operation(self, other, operator): + return _create_from_access( + self.evaluator, + self.access.execute_operation(other.access, operator) + ) + class CompiledName(AbstractNameDefinition): def __init__(self, evaluator, parent_context, name): @@ -294,9 +252,8 @@ class CompiledName(AbstractNameDefinition): @underscore_memoization def infer(self): - module = self.parent_context.get_root_context() return ContextSet(_create_from_name( - self._evaluator, module, self.parent_context, self.string_name + self._evaluator, self.parent_context, self.string_name )) @@ -366,30 +323,20 @@ class CompiledObjectFilter(AbstractFilter): @memoize_method def get(self, name): name = str(name) - obj = self._compiled_object.obj - try: - attr, is_get_descriptor = getattr_static(obj, name) - except AttributeError: + if not self._compiled_object.access.is_allowed_getattr(name): + return [EmptyCompiledName(self._evaluator, name)] + + if self._is_instance and name not in self._compiled_object.access.dir(): return [] - 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): - obj = self._compiled_object.obj - names = [] - for name in dir(obj): + for name in self._compiled_object.access.dir(): names += self.get(name) # ``dir`` doesn't include the type names. - if not self._is_instance and inspect.isclass(obj) and obj != type: + if not self._is_instance and self._compiled_object.access.needs_type_completions(): for filter in create(self._evaluator, type).get_filters(): names += filter.values() return names @@ -533,24 +480,18 @@ def _parse_function_doc(doc): return param_str, ret -def _create_from_name(evaluator, module, compiled_object, name): - obj = compiled_object.obj +def _create_from_name(evaluator, compiled_object, name): faked = None + print(compiled_object.tree_node) try: - faked = fake.get_faked(evaluator, module, obj, parent_context=compiled_object, name=name) + faked = fake.get_faked_with_parent_context(compiled_object, name) if faked.type == 'funcdef': from jedi.evaluate.context.function import FunctionContext return FunctionContext(evaluator, compiled_object, faked) except fake.FakeDoesNotExist: pass - try: - obj = getattr(obj, name) - except AttributeError: - # Happens e.g. in properties of - # PyQt4.QtGui.QStyleOptionComboBox.currentText - # -> just set it to None - obj = None + obj = compiled_object.access.getattr(name, default=None) return create(evaluator, obj, parent_context=compiled_object, faked=faked) @@ -609,20 +550,45 @@ def create(evaluator, obj, parent_context=None, module=None, faked=None): A very weird interface class to this module. The more options provided the more acurate loading compiled objects is. """ + print('create', obj) + if isinstance(obj, DirectObjectAccess): + access = obj + else: + access = DirectObjectAccess(obj) if inspect.ismodule(obj): if parent_context is not None: # Modules don't have parents, be careful with caching: recurse. return create(evaluator, obj) - else: - if parent_context is None and obj is not _builtins: - return create(evaluator, obj, create(evaluator, _builtins)) - try: - faked = fake.get_faked(evaluator, module, obj, parent_context=parent_context) - if faked.type == 'funcdef': - from jedi.evaluate.context.function import FunctionContext - return FunctionContext(evaluator, parent_context, faked) - except fake.FakeDoesNotExist: - pass + #if parent_context is None and obj is not _builtins: + #return create(evaluator, obj, create(evaluator, _builtins)) - return CompiledObject(evaluator, obj, parent_context, faked) + if faked is None and parent_context is None: + access_tuples = access.get_access_path_tuples() + if access_tuples: + string_names, accesses = zip(*access_tuples) + try: + tree_nodes = fake.get_faked_tree_nodes(evaluator.latest_grammar, string_names) + except fake.FakeDoesNotExist: + pass + else: + for access2, tree_node in zip(accesses, tree_nodes): + parent_context = CompiledObject(evaluator, access2, parent_context, tree_node) + print('foo', obj, tree_nodes, parent_context) + + # TODO this if is ugly. Please remove, it may make certain + # properties of that function unusable. + if tree_node.type == 'function': + from jedi.evaluate.context.function import FunctionContext + return FunctionContext(evaluator, parent_context.parent_context, tree_node) + return parent_context + if parent_context is None: + parent_context = create(evaluator, _builtins) + + return CompiledObject(evaluator, access, parent_context, faked) + + +def _create_from_access(evaluator, access, parent_context=None, faked=None): + if parent_context is None: + parent_context = create(evaluator, _builtins) + return CompiledObject(evaluator, access, parent_context, faked) diff --git a/jedi/evaluate/compiled/access.py b/jedi/evaluate/compiled/access.py new file mode 100644 index 00000000..bf884c9f --- /dev/null +++ b/jedi/evaluate/compiled/access.py @@ -0,0 +1,253 @@ +import inspect +import types +import operator as op + +from jedi._compatibility import unicode, is_py3, is_py34, builtins +from jedi.evaluate.compiled.getattr_static import getattr_static + + +MethodDescriptorType = type(str.replace) +# These are not considered classes and access is granted even though they have +# a __class__ attribute. +NOT_CLASS_TYPES = ( + types.BuiltinFunctionType, + types.CodeType, + types.FrameType, + types.FunctionType, + types.GeneratorType, + types.GetSetDescriptorType, + types.LambdaType, + types.MemberDescriptorType, + types.MethodType, + types.ModuleType, + types.TracebackType, + MethodDescriptorType +) + +if is_py3: + NOT_CLASS_TYPES += ( + types.MappingProxyType, + types.SimpleNamespace + ) + if is_py34: + NOT_CLASS_TYPES += (types.DynamicClassAttribute,) + + +# 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, +) + +_sentinel = object() + +# Maps Python syntax to the operator module. +COMPARISON_OPERATORS = { + '==': op.eq, + '!=': op.ne, + 'is': op.is_, + 'is not': op.is_not, + '<': op.lt, + '<=': op.le, + '>': op.gt, + '>=': op.ge, +} + +_OPERATORS = { + '+': op.add, + '-': op.sub, +} +_OPERATORS.update(COMPARISON_OPERATORS) + + + + +class DirectObjectAccess(object): + def __init__(self, obj): + self._obj = obj + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, self._obj) + + def _create_access(self, obj): + return DirectObjectAccess(obj) + + def py__bool__(self): + return bool(self._obj) + + def py__file__(self): + try: + return self._obj.__file__ + except AttributeError: + return None + + def py__doc__(self, include_call_signature=False): + return inspect.getdoc(self._obj) or '' + + def py__name__(self): + if not is_class_instance(self._obj) or \ + inspect.ismethoddescriptor(self._obj): # slots + cls = self._obj + else: + try: + cls = self._obj.__class__ + except AttributeError: + # happens with numpy.core.umath._UFUNC_API (you get it + # automatically by doing `import numpy`. + return None + + try: + return cls.__name__ + except AttributeError: + return None + + def py__mro__accesses(self): + return tuple(self._create_access(cls) for cls in self._obj.__mro__[1:]) + + def py__getitem__(self, index): + if type(self._obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): + # Get rid of side effects, we won't call custom `__getitem__`s. + return None + + return self._create_access(self._obj[index]) + + def py__iter__list(self): + if type(self._obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): + # Get rid of side effects, we won't call custom `__getitem__`s. + return [] + + lst = [] + for i, part in enumerate(self._obj): + if i > 20: + # Should not go crazy with large iterators + break + lst.append(self._create_access(part)) + return lst + + def get_repr(self): + return repr(self._obj) + + def is_class(self): + return inspect.isclass(self._obj) + + def dir(self): + return dir(self._obj) + + def has_iter(self): + try: + iter(self._obj) + return True + except TypeError: + return False + + def is_allowed_getattr(self, name): + try: + attr, is_get_descriptor = getattr_static(self._obj, name) + except AttributeError: + return [] + 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 False + return True + + def getattr(self, name, default=_sentinel): + try: + return self._create_access(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 None + + def get_safe_value(self, default=_sentinel): + if type(self._obj) in (float, int, str, unicode, slice, type(Ellipsis)): + return self._obj + if default == _sentinel: + raise ValueError + + def get_api_type(self): + obj = self._obj + if self.is_class(): + return 'class' + elif inspect.ismodule(obj): + return 'module' + elif inspect.isbuiltin(obj) or inspect.ismethod(obj) \ + or inspect.ismethoddescriptor(obj) or inspect.isfunction(obj): + return 'function' + # Everything else... + return 'instance' + + def get_access_path_tuples(self): + path = self._get_objects_path() + try: + # Just provoke an AttributeError. + result = [(o.__name__, o) for o in path] + except AttributeError: + return [] + else: + return [(name, self._create_access(o)) for name, o in result] + + def _get_objects_path(self): + def get(): + obj = self._obj + yield obj + try: + obj = obj.__objclass__ + except AttributeError: + pass + else: + yield obj + + try: + # Returns a dotted string path. + imp_plz = obj.__module__ + except AttributeError: + # Unfortunately in some cases like `int` there's no __module__ + if not inspect.ismodule(obj): + yield builtins + else: + if imp_plz is None: + # Happens for example in `(_ for _ in []).send.__module__`. + yield builtins + else: + try: + yield __import__(imp_plz) + except ImportError: + # __module__ can be something arbitrary that doesn't exist. + yield builtins + + return list(reversed(list(get()))) + + def execute_operation(self, other, operator): + op = _OPERATORS[operator] + return self._create_access(op(self._obj, other._obj)) + + def needs_type_completions(self): + return inspect.isclass(self._obj) and self._obj != type + + +def is_class_instance(obj): + """Like inspect.* methods.""" + try: + cls = obj.__class__ + except AttributeError: + return False + else: + return cls != type and not issubclass(cls, NOT_CLASS_TYPES) diff --git a/jedi/evaluate/compiled/fake.py b/jedi/evaluate/compiled/fake.py index f56c919d..dfddef14 100644 --- a/jedi/evaluate/compiled/fake.py +++ b/jedi/evaluate/compiled/fake.py @@ -6,72 +6,60 @@ mixing in Python code, the autocompletion should work much better for builtins. import os import inspect -import types from itertools import chain from parso.python import tree -from jedi._compatibility import is_py3, builtins, unicode, is_py34 +from jedi._compatibility import is_py3, builtins, unicode -modules = {} +fake_modules = {} -MethodDescriptorType = type(str.replace) -# These are not considered classes and access is granted even though they have -# a __class__ attribute. -NOT_CLASS_TYPES = ( - types.BuiltinFunctionType, - types.CodeType, - types.FrameType, - types.FunctionType, - types.GeneratorType, - types.GetSetDescriptorType, - types.LambdaType, - types.MemberDescriptorType, - types.MethodType, - types.ModuleType, - types.TracebackType, - MethodDescriptorType -) +def _get_path_dict(): + path = os.path.dirname(os.path.abspath(__file__)) + base_path = os.path.join(path, 'fake') + dct = {} + for file_name in os.listdir(base_path): + if file_name.endswith('.pym'): + dct[file_name[:-4]] = os.path.join(base_path, file_name) + return dct -if is_py3: - NOT_CLASS_TYPES += ( - types.MappingProxyType, - types.SimpleNamespace - ) - if is_py34: - NOT_CLASS_TYPES += (types.DynamicClassAttribute,) + +_path_dict = _get_path_dict() class FakeDoesNotExist(Exception): pass -def _load_faked_module(grammar, module): - module_name = module.__name__ +def _load_faked_module(grammar, module_name): if module_name == '__builtin__' and not is_py3: module_name = 'builtins' try: - return modules[module_name] + return fake_modules[module_name] except KeyError: - path = os.path.dirname(os.path.abspath(__file__)) - try: - with open(os.path.join(path, 'fake', module_name) + '.pym') as f: - source = f.read() - except IOError: - modules[module_name] = None - return - modules[module_name] = m = grammar.parse(unicode(source)) + pass - if module_name == 'builtins' and not is_py3: - # There are two implementations of `open` for either python 2/3. - # -> Rename the python2 version (`look at fake/builtins.pym`). - open_func = _search_scope(m, 'open') - open_func.children[1].value = 'open_python3' - open_func = _search_scope(m, 'open_python2') - open_func.children[1].value = 'open' - return m + try: + path = _path_dict[module_name] + except KeyError: + fake_modules[module_name] = None + return + + with open(path) as f: + source = f.read() + + fake_modules[module_name] = m = grammar.parse(unicode(source)) + + if module_name == 'builtins' and not is_py3: + # There are two implementations of `open` for either python 2/3. + # -> Rename the python2 version (`look at fake/builtins.pym`). + open_func = _search_scope(m, 'open') + open_func.children[1].value = 'open_python3' + open_func = _search_scope(m, 'open_python2') + open_func.children[1].value = 'open' + return m def _search_scope(scope, obj_name): @@ -80,31 +68,6 @@ def _search_scope(scope, obj_name): return s -def _get_module(obj): - if inspect.ismodule(obj): - return obj - try: - obj = obj.__objclass__ - except AttributeError: - pass - - try: - imp_plz = obj.__module__ - except AttributeError: - # Unfortunately in some cases like `int` there's no __module__ - return builtins - else: - if imp_plz is None: - # Happens for example in `(_ for _ in []).send.__module__`. - return builtins - else: - try: - return __import__(imp_plz) - except ImportError: - # __module__ can be something arbitrary that doesn't exist. - return builtins - - def _faked(grammar, module, obj, name): # Crazy underscore actions to try to escape all the internal madness. if module is None: @@ -188,26 +151,24 @@ def _get_faked(grammar, module, obj, name=None): return result, fake_module -def get_faked(evaluator, module, obj, name=None, parent_context=None): - if parent_context and parent_context.tree_node is not None: +def get_faked_with_parent_context(parent_context, name): + if parent_context.tree_node is not None: # Try to search in already clearly defined stuff. found = _search_scope(parent_context.tree_node, name) if found is not None: return found - else: + raise FakeDoesNotExist + + +def get_faked_tree_nodes(grammar, string_names): + module = base = _load_faked_module(grammar, string_names[0]) + if module is None: + raise FakeDoesNotExist + + tree_nodes = [module] + for name in string_names[1:]: + base = _search_scope(base, name) + if base is None: raise FakeDoesNotExist - - faked, fake_module = _get_faked(evaluator.latest_grammar, module and module.obj, obj, name) - if module is not None: - module.get_used_names = fake_module.get_used_names - return faked - - -def is_class_instance(obj): - """Like inspect.* methods.""" - try: - cls = obj.__class__ - except AttributeError: - return False - else: - return cls != type and not issubclass(cls, NOT_CLASS_TYPES) + tree_nodes.append(base) + return tree_nodes diff --git a/jedi/evaluate/context/instance.py b/jedi/evaluate/context/instance.py index 2c8d796c..b6015350 100644 --- a/jedi/evaluate/context/instance.py +++ b/jedi/evaluate/context/instance.py @@ -306,7 +306,7 @@ class BoundMethod(FunctionContext): class CompiledBoundMethod(compiled.CompiledObject): def __init__(self, func): super(CompiledBoundMethod, self).__init__( - func.evaluator, func.obj, func.parent_context, func.tree_node) + func.evaluator, func.access, func.parent_context, func.tree_node) def get_param_names(self): return list(super(CompiledBoundMethod, self).get_param_names())[1:] diff --git a/jedi/evaluate/context/iterable.py b/jedi/evaluate/context/iterable.py index d0f468e4..9b6c644f 100644 --- a/jedi/evaluate/context/iterable.py +++ b/jedi/evaluate/context/iterable.py @@ -300,7 +300,7 @@ class SequenceLiteralContext(ArrayMixin, AbstractIterable): for key, value in self._items(): for k in self._defining_context.eval_node(key): if isinstance(k, compiled.CompiledObject) \ - and index == k.obj: + and index == k.get_safe_value(default=None): return self._defining_context.eval_node(value) raise KeyError('No key found in dictionary %s.' % self) diff --git a/jedi/evaluate/helpers.py b/jedi/evaluate/helpers.py index 6652287b..44a6f6b4 100644 --- a/jedi/evaluate/helpers.py +++ b/jedi/evaluate/helpers.py @@ -183,7 +183,7 @@ def is_compiled(context): def is_string(context): - return is_compiled(context) and isinstance(context.obj, (str, unicode)) + return is_compiled(context) and isinstance(context.get_safe_value(default=None), (str, unicode)) def is_literal(context): @@ -191,4 +191,4 @@ def is_literal(context): def is_number(context): - return is_compiled(context) and isinstance(context.obj, (int, float)) + return is_compiled(context) and isinstance(context.get_safe_value(default=None), (int, float)) diff --git a/jedi/evaluate/syntax_tree.py b/jedi/evaluate/syntax_tree.py index 1d847a49..ddfdf4c4 100644 --- a/jedi/evaluate/syntax_tree.py +++ b/jedi/evaluate/syntax_tree.py @@ -22,6 +22,7 @@ from jedi.evaluate.context import iterable from jedi.evaluate.context import TreeInstance, CompiledInstance from jedi.evaluate.finder import NameFinder from jedi.evaluate.helpers import is_string, is_literal, is_number, is_compiled +from jedi.evaluate.compiled.access import COMPARISON_OPERATORS def _limit_context_infers(func): @@ -318,19 +319,6 @@ def eval_factor(context_set, operator): yield context -# Maps Python syntax to the operator module. -COMPARISON_OPERATORS = { - '==': op.eq, - '!=': op.ne, - 'is': op.is_, - 'is not': op.is_not, - '<': op.lt, - '<=': op.le, - '>': op.gt, - '>=': op.ge, -} - - def _literals_to_types(evaluator, result): # Changes literals ('a', 1, 1.0, etc) to its type instances (str(), # int(), float(), etc). @@ -384,30 +372,29 @@ def _eval_comparison_part(evaluator, context, left, operator, right): return ContextSet(right) elif operator == '+': if l_is_num and r_is_num or is_string(left) and is_string(right): - return ContextSet(compiled.create(evaluator, left.obj + right.obj)) + return ContextSet(left.execute_operation(right, operator)) elif _is_tuple(left) and _is_tuple(right) or _is_list(left) and _is_list(right): return ContextSet(iterable.MergedArray(evaluator, (left, right))) elif operator == '-': if l_is_num and r_is_num: - return ContextSet(compiled.create(evaluator, left.obj - right.obj)) + return ContextSet(left.execute_operation(right, operator)) elif operator == '%': # With strings and numbers the left type typically remains. Except for # `int() % float()`. return ContextSet(left) elif operator in COMPARISON_OPERATORS: - operation = COMPARISON_OPERATORS[operator] if is_compiled(left) and is_compiled(right): # Possible, because the return is not an option. Just compare. - left = left.obj - right = right.obj - - try: - result = operation(left, right) - except TypeError: - # Could be True or False. - return ContextSet(compiled.create(evaluator, True), compiled.create(evaluator, False)) + try: + return ContextSet(left.execute_operation(right, operator)) + except TypeError: + # Could be True or False. + pass else: + raise NotImplementedError return ContextSet(compiled.create(evaluator, result)) + + return ContextSet(compiled.create(evaluator, True), compiled.create(evaluator, False)) elif operator == 'in': return NO_CONTEXTS diff --git a/test/test_api/test_environment.py b/test/test_api/test_environment.py index efb0045c..736089ce 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -11,7 +11,8 @@ def test_sys_path(): @pytest.mark.parametrize( 'version', - ['2.6', '2.7', '3.3', '3.4', '3.5', '3.6', '3.7'] + # TODO add '2.6', '2.7', + ['3.3', '3.4', '3.5', '3.6', '3.7'] ) def test_versions(version): executable = 'python' + version