diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 6874b63..427dafc 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1443,10 +1443,16 @@ class InteractiveShell(SingletonConfigurable): continue else: #print 'oname_rest:', oname_rest # dbg - for part in oname_rest: + for idx, part in enumerate(oname_rest): try: parent = obj - obj = getattr(obj,part) + # The last part is looked up in a special way to avoid + # descriptor invocation as it may raise or have side + # effects. + if idx == len(oname_rest) - 1: + obj = self._getattr_property(obj, part) + else: + obj = getattr(obj, part) except: # Blanket except b/c some badly implemented objects # allow __getattr__ to raise exceptions other than @@ -1486,33 +1492,48 @@ class InteractiveShell(SingletonConfigurable): return {'found':found, 'obj':obj, 'namespace':ospace, 'ismagic':ismagic, 'isalias':isalias, 'parent':parent} - def _ofind_property(self, oname, info): - """Second part of object finding, to look for property details.""" - if info.found: - # Get the docstring of the class property if it exists. - path = oname.split('.') - root = '.'.join(path[:-1]) - if info.parent is not None: - try: - target = getattr(info.parent, '__class__') - # The object belongs to a class instance. - try: - target = getattr(target, path[-1]) - # The class defines the object. - if isinstance(target, property): - oname = root + '.__class__.' + path[-1] - info = Struct(self._ofind(oname)) - except AttributeError: pass - except AttributeError: pass - - # We return either the new info or the unmodified input if the object - # hadn't been found - return info + @staticmethod + def _getattr_property(obj, attrname): + """Property-aware getattr to use in object finding. + + If attrname represents a property, return it unevaluated (in case it has + side effects or raises an error. + + """ + if not isinstance(obj, type): + try: + # `getattr(type(obj), attrname)` is not guaranteed to return + # `obj`, but does so for property: + # + # property.__get__(self, None, cls) -> self + # + # The universal alternative is to traverse the mro manually + # searching for attrname in class dicts. + attr = getattr(type(obj), attrname) + except AttributeError: + pass + else: + # This relies on the fact that data descriptors (with both + # __get__ & __set__ magic methods) take precedence over + # instance-level attributes: + # + # class A(object): + # @property + # def foobar(self): return 123 + # a = A() + # a.__dict__['foobar'] = 345 + # a.foobar # == 123 + # + # So, a property may be returned right away. + if isinstance(attr, property): + return attr + + # Nothing helped, fall back. + return getattr(obj, attrname) def _object_find(self, oname, namespaces=None): """Find an object and return a struct with info about it.""" - inf = Struct(self._ofind(oname, namespaces)) - return Struct(self._ofind_property(oname, inf)) + return Struct(self._ofind(oname, namespaces)) def _inspect(self, meth, oname, namespaces=None, **kw): """Generic interface to the inspector system. diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 77e27ff..d029206 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -375,7 +375,62 @@ class InteractiveShellTestCase(unittest.TestCase): namespace = 'IPython internal', obj= cmagic.__wrapped__, parent = None) nt.assert_equal(find, info) - + + def test_ofind_property_with_error(self): + class A(object): + @property + def foo(self): + raise NotImplementedError() + a = A() + + found = ip._ofind('a.foo', [('locals', locals())]) + info = dict(found=True, isalias=False, ismagic=False, + namespace='locals', obj=A.foo, parent=a) + nt.assert_equal(found, info) + + def test_ofind_multiple_attribute_lookups(self): + class A(object): + @property + def foo(self): + raise NotImplementedError() + + a = A() + a.a = A() + a.a.a = A() + + found = ip._ofind('a.a.a.foo', [('locals', locals())]) + info = dict(found=True, isalias=False, ismagic=False, + namespace='locals', obj=A.foo, parent=a.a.a) + nt.assert_equal(found, info) + + def test_ofind_slotted_attributes(self): + class A(object): + __slots__ = ['foo'] + def __init__(self): + self.foo = 'bar' + + a = A() + found = ip._ofind('a.foo', [('locals', locals())]) + info = dict(found=True, isalias=False, ismagic=False, + namespace='locals', obj=a.foo, parent=a) + nt.assert_equal(found, info) + + found = ip._ofind('a.bar', [('locals', locals())]) + info = dict(found=False, isalias=False, ismagic=False, + namespace=None, obj=None, parent=a) + nt.assert_equal(found, info) + + def test_ofind_prefers_property_to_instance_level_attribute(self): + class A(object): + @property + def foo(self): + return 'bar' + a = A() + a.__dict__['foo'] = 'baz' + nt.assert_equal(a.foo, 'bar') + found = ip._ofind('a.foo', [('locals', locals())]) + nt.assert_is(found['obj'], A.foo) + def test_custom_exception(self): called = [] def my_handler(shell, etype, value, tb, tb_offset=None):