diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 1dbe761..c3114a7 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -36,6 +36,7 @@ from IPython.utils import PyColorize 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 index 818baad..bffa1d0 100644 --- 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 diff --git a/IPython/utils/tests/test_dir2.py b/IPython/utils/tests/test_dir2.py index 3d0ba61..fc5dca1 100644 --- a/IPython/utils/tests/test_dir2.py +++ b/IPython/utils/tests/test_dir2.py @@ -50,3 +50,31 @@ def test_SubClass_with_trait_names_attr(): 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)