diff --git a/jedi/evaluate/__init__.py b/jedi/evaluate/__init__.py index 0d6e8a3a..a2b3ad81 100644 --- a/jedi/evaluate/__init__.py +++ b/jedi/evaluate/__init__.py @@ -101,6 +101,7 @@ class Evaluator(object): self.is_analysis = False self.python_version = sys.version_info[:2] self.project = project + self.access_cache = {} project.add_evaluator(self) if compiled_sub_process is None: diff --git a/jedi/evaluate/analysis.py b/jedi/evaluate/analysis.py index c825e5fe..95c93515 100644 --- a/jedi/evaluate/analysis.py +++ b/jedi/evaluate/analysis.py @@ -4,6 +4,7 @@ Module for statical analysis. from jedi import debug from parso.python import tree from jedi.evaluate.compiled import CompiledObject +from jedi.evaluate.helpers import is_string CODES = { @@ -117,6 +118,7 @@ def add_attribute_error(name_context, lookup_context, name): slot_names = lookup_context.get_function_slot_names('__getattr__') + \ lookup_context.get_function_slot_names('__getattribute__') for n in slot_names: + # TODO do we even get here? if isinstance(name, CompiledInstanceName) and \ n.parent_context.obj == object: typ = Warning @@ -139,7 +141,7 @@ def _check_for_exception_catch(node_context, jedi_name, exception, payload=None) """ def check_match(cls, exception): try: - return isinstance(cls, CompiledObject) and issubclass(exception, cls.obj) + return isinstance(cls, CompiledObject) and cls.is_super_class(exception) except TypeError: return False @@ -189,8 +191,8 @@ def _check_for_exception_catch(node_context, jedi_name, exception, payload=None) # Check name key, lazy_context = args[1] names = list(lazy_context.infer()) - assert len(names) == 1 and isinstance(names[0], CompiledObject) - assert names[0].obj == payload[1].value + assert len(names) == 1 and is_string(names[0]) + assert names[0].get_safe_value() == payload[1].value # Check objects key, lazy_context = args[0] diff --git a/jedi/evaluate/compiled/__init__.py b/jedi/evaluate/compiled/__init__.py index 9cb49d71..d210060a 100644 --- a/jedi/evaluate/compiled/__init__.py +++ b/jedi/evaluate/compiled/__init__.py @@ -8,15 +8,13 @@ import os import types from functools import partial -from jedi._compatibility import builtins as _builtins, unicode, py_version +from jedi._compatibility import builtins as _builtins from jedi import debug from jedi.cache import underscore_memoization, memoize_method from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition, \ ContextNameMixin 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 jedi.evaluate.compiled.access import DirectObjectAccess, _sentinel, create_access from . import fake @@ -75,7 +73,7 @@ class CompiledObject(Context): @CheckAttribute def py__class__(self): - return create(self.evaluator, self.obj.__class__) + return create(self.evaluator, self.access.py__class__()) @CheckAttribute def py__mro__(self): @@ -85,6 +83,7 @@ class CompiledObject(Context): @CheckAttribute def py__bases__(self): + raise NotImplementedError return tuple(create(self.evaluator, cls) for cls in self.obj.__bases__) def py__bool__(self): @@ -111,7 +110,7 @@ class CompiledObject(Context): parts = p.strip().split('=') yield UnresolvableParamName(self, parts[0]) else: - for signature_param in signature_params.values(): + for signature_param in signature_params: yield SignatureParamName(self, signature_param) def __repr__(self): @@ -201,7 +200,7 @@ class CompiledObject(Context): def dict_values(self): return ContextSet.from_iterable( - create(self.evaluator, v) for v in self.obj.values() + create(self.evaluator, access) for access in self.access.dict_values() ) def get_safe_value(self, default=_sentinel): @@ -213,6 +212,12 @@ class CompiledObject(Context): self.access.execute_operation(other.access, operator) ) + def negate(self): + return create(self.evaluator, self.access.negate()) + + def is_super_class(self, exception): + return self.access.is_super_class(exception) + class CompiledName(AbstractNameDefinition): def __init__(self, evaluator, parent_context, name): @@ -304,8 +309,11 @@ class CompiledObjectFilter(AbstractFilter): @memoize_method def get(self, name): name = str(name) - if not self._compiled_object.access.is_allowed_getattr(name): - return [EmptyCompiledName(self._evaluator, name)] + try: + if not self._compiled_object.access.is_allowed_getattr(name): + return [EmptyCompiledName(self._evaluator, name)] + except AttributeError: + return [] if self._is_instance and name not in self._compiled_object.access.dir(): return [] @@ -463,7 +471,6 @@ def _parse_function_doc(doc): def _create_from_name(evaluator, compiled_object, name): faked = None - print(compiled_object.tree_node) try: faked = fake.get_faked_with_parent_context(compiled_object, name) if faked.type == 'funcdef': @@ -472,8 +479,8 @@ def _create_from_name(evaluator, compiled_object, name): except fake.FakeDoesNotExist: pass - obj = compiled_object.access.getattr(name, default=None) - return create(evaluator, obj, parent_context=compiled_object, faked=faked) + access = compiled_object.access.getattr(name, default=None) + return create(evaluator, access, parent_context=compiled_object, faked=faked) def builtin_from_name(evaluator, string): @@ -498,7 +505,11 @@ _SPECIAL_OBJECTS = { def get_special_object(evaluator, identifier): obj = _SPECIAL_OBJECTS[identifier] - return create(evaluator, obj, parent_context=create(evaluator, _builtins)) + if identifier == 'BUILTINS': + parent_context = None + else: + parent_context = create(evaluator, _builtins) + return create(evaluator, obj, parent_context=parent_context) def compiled_objects_cache(attribute_name): @@ -508,7 +519,7 @@ def compiled_objects_cache(attribute_name): Caching the id has the advantage that an object doesn't need to be hashable. """ - def wrapper(evaluator, obj, parent_context=None, module=None, faked=None): + def wrapper(evaluator, obj, parent_context=None, faked=None): cache = getattr(evaluator, attribute_name) # Do a very cheap form of caching here. key = id(obj), id(parent_context) @@ -516,9 +527,9 @@ def compiled_objects_cache(attribute_name): return cache[key][0] except KeyError: # TODO this whole decorator is way too ugly - result = func(evaluator, obj, parent_context, module, faked) + result = func(evaluator, obj, parent_context, faked) # Need to cache all of them, otherwise the id could be overwritten. - cache[key] = result, obj, parent_context, module, faked + cache[key] = result, obj, parent_context, faked return result return wrapper @@ -526,16 +537,17 @@ def compiled_objects_cache(attribute_name): @compiled_objects_cache('compiled_cache') -def create(evaluator, obj, parent_context=None, module=None, faked=None): +def create(evaluator, obj, parent_context=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) + print('xxx', obj) + return create(evaluator, create_access(evaluator, obj), parent_context, faked) + if inspect.ismodule(obj): if parent_context is not None: # Modules don't have parents, be careful with caching: recurse. @@ -554,8 +566,7 @@ def create(evaluator, obj, parent_context=None, module=None, faked=None): 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) + parent_context = create(evaluator, access2, parent_context, faked=tree_node) # TODO this if is ugly. Please remove, it may make certain # properties of that function unusable. @@ -563,13 +574,18 @@ def create(evaluator, obj, parent_context=None, module=None, faked=None): from jedi.evaluate.context.function import FunctionContext return FunctionContext(evaluator, parent_context.parent_context, tree_node) return parent_context - if parent_context is None: + # TODO wow this is a mess.... + if parent_context is None and not faked: parent_context = create(evaluator, _builtins) + return create(evaluator, obj, parent_context) + print('OOOOOOOOOO', obj) + if access._obj == _builtins and parent_context is not None: + raise 1 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) + return create(evaluator, access, parent_context, faked=faked) diff --git a/jedi/evaluate/compiled/access.py b/jedi/evaluate/compiled/access.py index f3e87ec4..28e6a592 100644 --- a/jedi/evaluate/compiled/access.py +++ b/jedi/evaluate/compiled/access.py @@ -5,6 +5,7 @@ from collections import namedtuple from jedi._compatibility import unicode, is_py3, is_py34, builtins, py_version from jedi.evaluate.compiled.getattr_static import getattr_static +from jedi.evaluate.cache import evaluator_function_cache MethodDescriptorType = type(str.replace) @@ -76,15 +77,22 @@ _OPERATORS.update(COMPARISON_OPERATORS) SignatureParam = namedtuple('SignatureParam', 'name default empty annotation') +@evaluator_function_cache() +def create_access(evaluator, obj): + print('create', obj) + return DirectObjectAccess(evaluator, obj) + + class DirectObjectAccess(object): - def __init__(self, obj): + def __init__(self, evaluator, obj): + self._evaluator = evaluator self._obj = obj def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self._obj) def _create_access(self, obj): - return DirectObjectAccess(obj) + return create_access(self._evaluator, obj) def py__bool__(self): return bool(self._obj) @@ -99,7 +107,7 @@ class DirectObjectAccess(object): return inspect.getdoc(self._obj) or '' def py__name__(self): - if not is_class_instance(self._obj) or \ + if not _is_class_instance(self._obj) or \ inspect.ismethoddescriptor(self._obj): # slots cls = self._obj else: @@ -138,6 +146,9 @@ class DirectObjectAccess(object): lst.append(self._create_access(part)) return lst + def py__class__(self): + return self._create_access(self._obj.__class__) + def get_repr(self): return repr(self._obj) @@ -161,7 +172,7 @@ class DirectObjectAccess(object): try: attr, is_get_descriptor = getattr_static(self._obj, name) except AttributeError: - return [] + raise else: if is_get_descriptor \ and not type(attr) in ALLOWED_DESCRIPTOR_ACCESS: @@ -274,11 +285,20 @@ class DirectObjectAccess(object): default=p.default, empty=p.empty, annotation=p.annotation, - ) for p in signature.parameters + ) for p in signature.parameters.values() ] + def negate(self): + return self._create_access(-self._obj) -def is_class_instance(obj): + def dict_values(self): + return [self._create_access(v) for v in self._obj.values()] + + def is_super_class(self, exception): + return issubclass(exception, self._obj) + + +def _is_class_instance(obj): """Like inspect.* methods.""" try: cls = obj.__class__ diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index ac0f6dd6..c0845374 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -37,7 +37,7 @@ class MixedObject(object): self.parent_context = parent_context self.compiled_object = compiled_object self._context = tree_context - self.obj = compiled_object.obj + self.access = compiled_object.access # We have to overwrite everything that has to do with trailers, name # lookups and filters to make it possible to route name lookups towards @@ -49,7 +49,7 @@ class MixedObject(object): yield MixedObjectFilter(self.evaluator, self) def __repr__(self): - return '<%s: %s>' % (type(self).__name__, repr(self.obj)) + return '<%s: %s>' % (type(self).__name__, repr(self.access)) def __getattr__(self, name): return getattr(self._context, name) @@ -74,17 +74,11 @@ class MixedName(compiled.CompiledName): @underscore_memoization def infer(self): - obj = self.parent_context.obj - try: - # TODO use logic from compiled.CompiledObjectFilter - obj = getattr(obj, self.string_name) - except AttributeError: - # Happens e.g. in properties of - # PyQt4.QtGui.QStyleOptionComboBox.currentText - # -> just set it to None - obj = None + access = self.parent_context.access + # TODO use logic from compiled.CompiledObjectFilter + access = access.getattr(self.string_name, default=None) return ContextSet( - _create(self._evaluator, obj, parent_context=self.parent_context) + _create(self._evaluator, access, parent_context=self.parent_context) ) @property @@ -135,7 +129,10 @@ def _get_object_to_check(python_object): raise TypeError # Prevents computation of `repr` within inspect. -def find_syntax_node_name(evaluator, python_object): +def _find_syntax_node_name(evaluator, access): + # TODO accessing this is bad, but it probably doesn't matter that much, + # because we're working with interpreteters only here. + python_object = access._obj try: python_object = _get_object_to_check(python_object) path = inspect.getsourcefile(python_object) @@ -195,11 +192,11 @@ def find_syntax_node_name(evaluator, python_object): @compiled.compiled_objects_cache('mixed_cache') -def _create(evaluator, obj, parent_context=None, *args): - tree_node, path = find_syntax_node_name(evaluator, obj) +def _create(evaluator, access, parent_context=None, *args): + tree_node, path = _find_syntax_node_name(evaluator, access) compiled_object = compiled.create( - evaluator, obj, parent_context=parent_context.compiled_object) + evaluator, access, parent_context=parent_context.compiled_object) if tree_node is None: return compiled_object @@ -218,7 +215,7 @@ def _create(evaluator, obj, parent_context=None, *args): node_is_object=True ) if tree_node.type == 'classdef': - if not inspect.isclass(obj): + if not access.is_class(): # Is an instance, not a class. tree_context, = tree_context.execute_evaluated() @@ -228,4 +225,3 @@ def _create(evaluator, obj, parent_context=None, *args): compiled_object, tree_context=tree_context ) - diff --git a/jedi/evaluate/context/iterable.py b/jedi/evaluate/context/iterable.py index 9b6c644f..02c6ea76 100644 --- a/jedi/evaluate/context/iterable.py +++ b/jedi/evaluate/context/iterable.py @@ -373,7 +373,7 @@ class SequenceLiteralContext(ArrayMixin, AbstractIterable): for key_node, value in self._items(): for key in self._defining_context.eval_node(key_node): if is_string(key): - yield key.obj, LazyTreeContext(self._defining_context, value) + yield key.get_safe_value(), LazyTreeContext(self._defining_context, value) def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.atom) diff --git a/jedi/evaluate/param.py b/jedi/evaluate/param.py index a46394ce..c5c28353 100644 --- a/jedi/evaluate/param.py +++ b/jedi/evaluate/param.py @@ -192,4 +192,3 @@ def _create_default_param(execution_context, param): def create_default_params(execution_context, funcdef): return [_create_default_param(execution_context, p) for p in funcdef.get_params()] - diff --git a/jedi/evaluate/pep0484.py b/jedi/evaluate/pep0484.py index 820f112c..4d575308 100644 --- a/jedi/evaluate/pep0484.py +++ b/jedi/evaluate/pep0484.py @@ -30,6 +30,7 @@ from jedi.evaluate import compiled from jedi.evaluate.base_context import NO_CONTEXTS, ContextSet from jedi.evaluate.lazy_context import LazyTreeContext from jedi.evaluate.context import ModuleContext +from jedi.evaluate.helpers import is_string from jedi import debug from jedi import _compatibility from jedi import parser_utils @@ -60,16 +61,15 @@ def _fix_forward_reference(context, node): " not %s" % (node, evaled_nodes)) return node evaled_node = list(evaled_nodes)[0] - if isinstance(evaled_node, compiled.CompiledObject) and \ - isinstance(evaled_node.obj, str): + if is_string(evaled_node): try: new_node = context.evaluator.grammar.parse( - _compatibility.unicode(evaled_node.obj), + _compatibility.unicode(evaled_node.get_safe_value()), start_symbol='eval_input', error_recovery=False ) except ParserSyntaxError: - debug.warning('Annotation not parsed: %s' % evaled_node.obj) + debug.warning('Annotation not parsed: %s' % evaled_node) return node else: module = node.get_root_node() diff --git a/jedi/evaluate/stdlib.py b/jedi/evaluate/stdlib.py index 259d6927..07bb393c 100644 --- a/jedi/evaluate/stdlib.py +++ b/jedi/evaluate/stdlib.py @@ -157,7 +157,7 @@ def builtins_getattr(evaluator, objects, names, defaults=None): for obj in objects: for name in names: if is_string(name): - return obj.py__getattribute__(name.obj) + return obj.py__getattribute__(name.get_safe_value()) else: debug.warning('getattr called without str') continue @@ -216,18 +216,21 @@ def builtins_reversed(evaluator, sequences, obj, arguments): def builtins_isinstance(evaluator, objects, types, arguments): bool_results = set() for o in objects: + cls = o.py__class__() try: - mro_func = o.py__class__().py__mro__ + mro_func = cls.py__mro__ except AttributeError: # This is temporary. Everything should have a class attribute in # Python?! Maybe we'll leave it here, because some numpy objects or # whatever might not. - return ContextSet(compiled.create(True), compiled.create(False)) + return ContextSet(compiled.create(evaluator, True), compiled.create(evaluator, False)) mro = mro_func() + print(mro, types) for cls_or_tup in types: if cls_or_tup.is_class(): + print(id(mro[0]), mro[0], id(cls_or_tup), cls_or_tup) bool_results.add(cls_or_tup in mro) elif cls_or_tup.name.string_name == 'tuple' \ and cls_or_tup.get_root_context() == evaluator.BUILTINS: @@ -246,6 +249,7 @@ def builtins_isinstance(evaluator, objects, types, arguments): 'not %s.' % cls_or_tup analysis.add(lazy_context._context, 'type-error-isinstance', node, message) + print(objects, types, bool_results) return ContextSet.from_iterable(compiled.create(evaluator, x) for x in bool_results) @@ -265,15 +269,16 @@ def collections_namedtuple(evaluator, obj, arguments): # Process arguments # TODO here we only use one of the types, we should use all. - name = list(_follow_param(evaluator, arguments, 0))[0].obj + # TODO this is buggy, doesn't need to be a string + name = list(_follow_param(evaluator, arguments, 0))[0].get_safe_value() _fields = list(_follow_param(evaluator, arguments, 1))[0] if isinstance(_fields, compiled.CompiledObject): - fields = _fields.obj.replace(',', ' ').split() + fields = _fields.get_safe_value().replace(',', ' ').split() elif isinstance(_fields, iterable.AbstractIterable): fields = [ - v.obj + v.get_safe_value() for lazy_context in _fields.py__iter__() - for v in lazy_context.infer() if hasattr(v, 'obj') + for v in lazy_context.infer() if is_string(v) ] else: return NO_CONTEXTS diff --git a/jedi/evaluate/syntax_tree.py b/jedi/evaluate/syntax_tree.py index ddfdf4c4..8c777db6 100644 --- a/jedi/evaluate/syntax_tree.py +++ b/jedi/evaluate/syntax_tree.py @@ -2,7 +2,6 @@ Functions evaluating the syntax tree. """ import copy -import operator as op from parso.python import tree @@ -309,7 +308,7 @@ def eval_factor(context_set, operator): for context in context_set: if operator == '-': if is_number(context): - yield compiled.create(context.evaluator, -context.obj) + yield context.negate() elif operator == 'not': value = context.py__bool__() if value is None: # Uncertainty. @@ -383,6 +382,7 @@ def _eval_comparison_part(evaluator, context, left, operator, right): # `int() % float()`. return ContextSet(left) elif operator in COMPARISON_OPERATORS: + print(operator, left, right) if is_compiled(left) and is_compiled(right): # Possible, because the return is not an option. Just compare. try: @@ -391,8 +391,10 @@ def _eval_comparison_part(evaluator, context, left, operator, right): # Could be True or False. pass else: - raise NotImplementedError - return ContextSet(compiled.create(evaluator, result)) + if operator in ('is', '!=', '==', 'is not'): + operation = COMPARISON_OPERATORS[operator] + bool_ = operation(left, right) + return ContextSet(compiled.create(evaluator, bool_)) return ContextSet(compiled.create(evaluator, True), compiled.create(evaluator, False)) elif operator == 'in': diff --git a/jedi/evaluate/sys_path.py b/jedi/evaluate/sys_path.py index 82e5e9df..8110c33d 100644 --- a/jedi/evaluate/sys_path.py +++ b/jedi/evaluate/sys_path.py @@ -121,7 +121,7 @@ def _paths_from_assignment(module_context, expr_stmt): for lazy_context in cn.infer().iterate(cn): for context in lazy_context.infer(): if is_string(context): - abs_path = _abs_path(module_context, context.obj) + abs_path = _abs_path(module_context, context.get_safe_value()) if abs_path is not None: yield abs_path @@ -144,7 +144,7 @@ def _paths_from_list_modifications(module_context, trailer1, trailer2): for context in module_context.create_context(arg).eval_node(arg): if is_string(context): - abs_path = _abs_path(module_context, context.obj) + abs_path = _abs_path(module_context, context.get_safe_value()) if abs_path is not None: yield abs_path diff --git a/test/conftest.py b/test/conftest.py index db2f8260..4fedab6e 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -125,3 +125,8 @@ class StaticAnalysisCase(object): def cwd_tmpdir(monkeypatch, tmpdir): with helpers.set_cwd(tmpdir.dirpath): yield tmpdir + + +@pytest.fixture +def evaluator(): + return jedi.Script('')._evaluator diff --git a/test/test_api/test_environment.py b/test/test_api/test_environment.py index 736089ce..8300837a 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -28,11 +28,6 @@ def test_versions(version): assert any(executable in p for p in sys_path) -@pytest.fixture -def evaluator(): - return jedi.Script('')._evaluator - - def test_import_module(evaluator): compiled_obj = evaluator.compiled_subprocess.import_module(name='math') assert compiled_obj.py__bool__() is True diff --git a/test/test_evaluate/test_compiled.py b/test/test_evaluate/test_compiled.py index 44aef93b..bf193652 100644 --- a/test/test_evaluate/test_compiled.py +++ b/test/test_evaluate/test_compiled.py @@ -50,12 +50,12 @@ def test_parse_function_doc_illegal_docstr(): assert ('', '') == compiled._parse_function_doc(docstr) -def test_doc(): +def test_doc(evaluator): """ Even CompiledObject docs always return empty docstrings - not None, that's just a Jedi API definition. """ - obj = compiled.CompiledObject(_evaluator(), ''.__getnewargs__) + obj = compiled.create(evaluator, ''.__getnewargs__) assert obj.py__doc__() == '' diff --git a/test/test_evaluate/test_literals.py b/test/test_evaluate/test_literals.py index 403020ac..f75d755d 100644 --- a/test/test_evaluate/test_literals.py +++ b/test/test_evaluate/test_literals.py @@ -6,7 +6,7 @@ from jedi._compatibility import py_version, unicode def _eval_literal(code): def_, = jedi.Script(code).goto_definitions() - return def_._name._context.obj + return def_._name._context.access._obj @pytest.mark.skipif('sys.version_info[:2] < (3, 6)') diff --git a/test/test_evaluate/test_precedence.py b/test/test_evaluate/test_precedence.py index f5e07702..1324f56c 100644 --- a/test/test_evaluate/test_precedence.py +++ b/test/test_evaluate/test_precedence.py @@ -14,4 +14,4 @@ def test_equals(source): script = Script(source) node = script._get_module_node().children[0] first, = script._get_module().eval_node(node) - assert isinstance(first, CompiledObject) and first.obj is True + assert isinstance(first, CompiledObject) and first.access._obj is True diff --git a/test/test_parso_integration/test_basic.py b/test/test_parso_integration/test_basic.py index 4eebaa81..d6013baf 100644 --- a/test/test_parso_integration/test_basic.py +++ b/test/test_parso_integration/test_basic.py @@ -83,7 +83,7 @@ def test_add_to_end(): def test_tokenizer_with_string_literal_backslash(): c = jedi.Script("statement = u'foo\\\n'; statement").goto_definitions() - assert c[0]._name._context.obj == 'foo' + assert c[0]._name._context.get_safe_value() == 'foo' def test_ellipsis():