From a37201bc1dc6e359d73a8d9a738fbb42d2fe2e62 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 13 Aug 2017 22:24:50 +0200 Subject: [PATCH] Finally fixing the Python 2 issues with static_getattr. --- jedi/api/interpreter.py | 2 +- jedi/evaluate/compiled/getattr_static.py | 77 ++++++++++++++++++++++-- test/test_api/test_interpreter.py | 18 +++++- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/jedi/api/interpreter.py b/jedi/api/interpreter.py index 1c6d085b..e4b5dbb6 100644 --- a/jedi/api/interpreter.py +++ b/jedi/api/interpreter.py @@ -8,7 +8,7 @@ from jedi.evaluate.compiled import mixed from jedi.evaluate.context import Context -class NamespaceObject(): +class NamespaceObject(object): def __init__(self, dct): self.__dict__ = dct diff --git a/jedi/evaluate/compiled/getattr_static.py b/jedi/evaluate/compiled/getattr_static.py index 882c64d6..9f8cd8a8 100644 --- a/jedi/evaluate/compiled/getattr_static.py +++ b/jedi/evaluate/compiled/getattr_static.py @@ -6,10 +6,9 @@ information returned to enable Jedi to make decisions. import types -_sentinel = object() +from jedi._compatibility import py_version -def _static_getmro(klass): - return type.__dict__['__mro__'].__get__(klass) +_sentinel = object() def _check_instance(obj, attr): instance_dict = {} @@ -37,7 +36,7 @@ def _is_type(obj): return True -def _shadowed_dict(klass): +def _shadowed_dict_newstyle(klass): dict_attr = type.__dict__["__dict__"] for entry in _static_getmro(klass): try: @@ -52,8 +51,74 @@ def _shadowed_dict(klass): return _sentinel +def _static_getmro_newstyle(klass): + return type.__dict__['__mro__'].__get__(klass) + + +if py_version >= 30: + _shadowed_dict = _shadowed_dict_newstyle + _get_type = type + _static_getmro = _static_getmro_newstyle +else: + def _shadowed_dict(klass): + """ + In Python 2 __dict__ is not overwritable: + + class Foo(object): pass + setattr(Foo, '__dict__', 4) + + Traceback (most recent call last): + File "", line 1, in + TypeError: __dict__ must be a dictionary object + + It applies to both newstyle and oldstyle classes: + + class Foo(object): pass + setattr(Foo, '__dict__', 4) + Traceback (most recent call last): + File "", line 1, in + AttributeError: attribute '__dict__' of 'type' objects is not writable + + It also applies to instances of those objects. However to keep things + straight forward, newstyle classes always use the complicated way of + accessing it while oldstyle classes just use getattr. + """ + if type(klass) is _oldstyle_class_type: + return getattr(klass, '__dict__', _sentinel) + return _shadowed_dict_newstyle(klass) + + class _OldStyleClass(): + pass + + _oldstyle_instance_type = type(_OldStyleClass()) + _oldstyle_class_type = type(_OldStyleClass) + + def _get_type(obj): + type_ = object.__getattribute__(obj, '__class__') + if type_ is _oldstyle_instance_type: + # Somehow for old style classes we need to access it directly. + return obj.__class__ + return type_ + + def _static_getmro(klass): + if type(klass) is _oldstyle_class_type: + def oldstyle_mro(klass): + """ + Oldstyle mro is a really simplistic way of look up mro: + https://stackoverflow.com/questions/54867/what-is-the-difference-between-old-style-and-new-style-classes-in-python + """ + yield klass + for base in klass.__bases__: + for yield_from in oldstyle_mro(base): + yield yield_from + + return oldstyle_mro(klass) + + return _static_getmro_newstyle(klass) + + def _safe_hasattr(obj, name): - return _check_class(type(obj), name) is not _sentinel + return _check_class(_get_type(obj), name) is not _sentinel def _safe_is_data_descriptor(obj): @@ -76,7 +141,7 @@ def getattr_static(obj, attr, default=_sentinel): """ instance_result = _sentinel if not _is_type(obj): - klass = type(obj) + klass = _get_type(obj) dict_attr = _shadowed_dict(klass) if (dict_attr is _sentinel or type(dict_attr) is types.MemberDescriptorType): diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index df9d04bb..6b170ad4 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -177,7 +177,7 @@ def test_getitem_side_effects(): _assert_interpreter_complete('foo[0].', locals(), []) -def test_property_error(): +def test_property_error_oldstyle(): lst = [] class Foo3(): @property @@ -193,6 +193,22 @@ def test_property_error(): assert lst == [] +def test_property_error_newstyle(): + lst = [] + class Foo3(object): + @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): pass