Repository: ipython/ipython

Issue:
Using hasattr for trait_names instead of just looking for it directly/using __dir__?
This issue just came up with IPython 2.0-dev (appears to not be an issue for IPython 1.0) where our DictWrapper object raised KeyError on bad `__getattr__` (which screws up `hasattr`). It's a badly behaved object, but it's been around for a while. We're going to fix this in 0.13, which we're releasing soonish, but this probably affects a number of versions. https://github.com/pydata/pandas/issues/5182

I'm not clear why defining a `__dir__` method isn't enough, but a simple fix would be to go the EAFP route and just try to do it in try/except rather than doing the `hasattr` check. I'm sure there are other places where this could be an issue 

cc @d10genes who pointed out the problematic interaction with IPython 2.0-dev.



Discussion:
For clarity - I bring this up because I wanted to point out that objects misbehave and it's probably surprising to users; however, but it's not unreasonable to think that most things should work right with `hasattr` ;p

We have a `safe_hasattr()` function in IPython.core.oinspect which can be used for this. 

okay, I can put up a fix for this if you want...

Sure. The function should probably be moved into utils somewhere, because stuff in utils shouldn't rely on things in core.


Resolving patch:
diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py
--- a/IPython/core/oinspect.py
+++ b/IPython/core/oinspect.py
@@ -36,6 +36,7 @@
 from IPython.utils import io
 from IPython.utils import openpy
 from IPython.utils import py3compat
+from IPython.utils.dir2 import safe_hasattr
 from IPython.utils.text import indent
 from IPython.utils.wildcard import list_namespace
 from IPython.utils.coloransi import *
@@ -258,16 +259,6 @@ def call_tip(oinfo, format_call=True):
 
     return call_line, doc
 
-def safe_hasattr(obj, attr):
-    """In recent versions of Python, hasattr() only catches AttributeError.
-    This catches all errors.
-    """
-    try:
-        getattr(obj, attr)
-        return True
-    except:
-        return False
-
 
 def find_file(obj):
     """Find the absolute path to the file where an object was defined.
diff --git a/IPython/utils/dir2.py b/IPython/utils/dir2.py
--- a/IPython/utils/dir2.py
+++ b/IPython/utils/dir2.py
@@ -12,14 +12,25 @@
 #-----------------------------------------------------------------------------
 # Imports
 #-----------------------------------------------------------------------------
-
 #-----------------------------------------------------------------------------
 # Code
 #-----------------------------------------------------------------------------
 
+
+def safe_hasattr(obj, attr):
+    """In recent versions of Python, hasattr() only catches AttributeError.
+    This catches all errors.
+    """
+    try:
+        getattr(obj, attr)
+        return True
+    except:
+        return False
+
+
 def get_class_members(cls):
     ret = dir(cls)
-    if hasattr(cls, '__bases__'):
+    if safe_hasattr(cls, '__bases__'):
         try:
             bases = cls.__bases__
         except AttributeError:
@@ -49,7 +60,7 @@ def dir2(obj):
 
     words = set(dir(obj))
 
-    if hasattr(obj, '__class__'):
+    if safe_hasattr(obj, '__class__'):
         #words.add('__class__')
         words |= set(get_class_members(obj.__class__))
 
@@ -57,14 +68,13 @@ def dir2(obj):
     # for objects with Enthought's traits, add trait_names() list
     # for PyCrust-style, add _getAttributeNames() magic method list
     for attr in ('trait_names', '_getAttributeNames'):
-        if hasattr(obj, attr):
-            try:
-                func = getattr(obj, attr)
-                if callable(func):
-                    words |= set(func())
-            except:
-                # TypeError: obj is class not instance
-                pass
+        try:
+            func = getattr(obj, attr)
+            if callable(func):
+                words |= set(func())
+        except:
+            # TypeError: obj is class not instance
+            pass
 
     # filter out non-string attributes which may be stuffed by dir() calls
     # and poor coding in third-party modules


Test patch:
diff --git a/IPython/utils/tests/test_dir2.py b/IPython/utils/tests/test_dir2.py
--- a/IPython/utils/tests/test_dir2.py
+++ b/IPython/utils/tests/test_dir2.py
@@ -50,3 +50,31 @@ class SubClass(Base):
 
     res = dir2(SubClass())
     assert('trait_names' in res)
+
+
+def test_misbehaving_object_without_trait_names():
+    # dir2 shouldn't raise even when objects are dumb and raise
+    # something other than AttribteErrors on bad getattr.
+
+    class BadTraitNames(object):
+        @property
+        def trait_names(self):
+            raise KeyboardInterrupt("This should be caught")
+
+        def some_method(self):
+            pass
+
+    class MisbehavingGetattr(object):
+        def __getattr__(self):
+            raise KeyError("I should be caught")
+
+        def some_method(self):
+            pass
+
+    class SillierWithDir(MisbehavingGetattr):
+        def __dir__(self):
+            return ['some_method']
+
+    for bad_klass in (BadTraitNames, MisbehavingGetattr, SillierWithDir):
+        res = dir2(bad_klass())
+        assert('some_method' in res)