Repository: ipython/ipython

Issue:
sh.py and ipython for python 3.3
Someone using sh.py with ipython ran into the following issue, and I was hoping someone with better working knowledge of ipython's internals could shed some light on what's happening: amoffat/sh/issues/97

sh.py has some magic going on internally, where any attribute access on the module can resolve dynamically to a specific object.  This issue only seems to be in ipython for python 3.3, python 2.7 works fine.  Anyone have an idea of what could be happening?

Thanks



Discussion:
It seems Python 3.2 changed `hasattr()` to only catch `AttributeError`, rather than any exception. Here's the [mailing list thread](http://mail.python.org/pipermail/python-dev/2010-August/103178.html) and the [bug report](http://bugs.python.org/issue9666) for it.

I think we should catch this in IPython, but I consider that 'protecting against misbehaving code'. To be a good Python citizen, I think `sh` should:
- Catch any special attribute lookups (`__x__`), and, if there isn't a sensible return value, just raise `AttributeError` without trying to look them up as program names. This would deal with the first case reported.
- If attribute access fails because a command can't be found, ideally the error raised should be a subclass of `AttributeError`.

That would help other tools that try to do introspection as well.

Thanks for the feedback @takluyver, I'll take care of it in sh

Reopening, because we'll fix this in IPython as well. Even if code is doing bizarre stuff, it shouldn't be able to crash IPython. And if there's one bit of code in the wild doing this, chances are there are others.


Resolving patch:
diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py
--- a/IPython/core/oinspect.py
+++ b/IPython/core/oinspect.py
@@ -261,6 +261,16 @@ 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.
@@ -279,7 +289,7 @@ def find_file(obj):
       The absolute path to the file where the object was defined.
     """
     # get source if obj was decorated with @decorator
-    if hasattr(obj, '__wrapped__'):
+    if safe_hasattr(obj, '__wrapped__'):
         obj = obj.__wrapped__
 
     fname = None
@@ -316,7 +326,7 @@ def find_source_lines(obj):
       The line number where the object definition starts.
     """
     # get source if obj was decorated with @decorator
-    if hasattr(obj, '__wrapped__'):
+    if safe_hasattr(obj, '__wrapped__'):
         obj = obj.__wrapped__
     
     try:
@@ -779,7 +789,7 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
                 out['init_docstring'] = init_ds
 
             # Call form docstring for callable instances
-            if hasattr(obj, '__call__'):
+            if safe_hasattr(obj, '__call__'):
                 call_def = self._getdef(obj.__call__, oname)
                 if call_def is not None:
                     out['call_def'] = self.format(call_def)


Test patch:
diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py
--- a/IPython/core/tests/test_oinspect.py
+++ b/IPython/core/tests/test_oinspect.py
@@ -158,6 +158,11 @@ def Clcmagic(self, cline, ccell=None):
         "A class-based line/cell magic"
 
 
+class Awkward(object):
+    def __getattr__(self, name):
+        raise Exception(name)
+
+
 def check_calltip(obj, name, call, docstring):
     """Generic check pattern all calltip tests will use"""
     info = inspector.info(obj, name)
@@ -261,6 +266,10 @@ def test_info():
         nt.assert_equal(i['type_name'], 'instance')
         nt.assert_equal(i['docstring'], OldStyle.__doc__)
 
+def test_info_awkward():
+    # Just test that this doesn't throw an error.
+    i = inspector.info(Awkward())
+
 def test_getdoc():
     class A(object):
         """standard docstring"""