diff -r 05abbadfe94f Lib/inspect.py --- a/Lib/inspect.py Thu Jan 10 21:37:12 2008 +0100 +++ b/Lib/inspect.py Fri Jan 11 21:14:38 2008 +0100 @@ -198,12 +198,23 @@ def isroutine(object): or ismethod(object) or ismethoddescriptor(object)) -def getmembers(object, predicate=None): +def getmembers(obj, predicate=None): """Return all members of an object as (name, value) pairs sorted by name. Optionally, only return members that satisfy a given predicate.""" + if isclass(obj): + mro = (obj,) + getmro(obj) + else: + mro = (obj,) results = [] - for key in dir(object): - value = getattr(object, key) + for key in dir(obj): + # First try to get the value via __dict__. Some descriptors don't + # like calling their __get__ (see bug #1785). + for base in mro: + if key in base.__dict__: + value = base.__dict__[key] + break + else: + value = getattr(obj, key) if not predicate or predicate(value): results.append((key, value)) results.sort() @@ -237,30 +248,21 @@ def classify_class_attrs(cls): names = dir(cls) result = [] for name in names: - # Get the object associated with the name. + # Get the object associated with the name, and where it was defined. # Getting an obj from the __dict__ sometimes reveals more than # using getattr. Static and class methods are dramatic examples. - if name in cls.__dict__: - obj = cls.__dict__[name] + # Furthermore, some objects may raise an Exception when fetched with + # getattr(). This is the case with some descriptors (bug #1785). + # Thus, we only use getattr() as a last resort. + homecls = None + for base in (cls,) + mro: + if name in base.__dict__: + obj = base.__dict__[name] + homecls = base + break else: obj = getattr(cls, name) - - # Figure out where it was defined. - homecls = getattr(obj, "__objclass__", None) - if homecls is None: - # search the dicts. - for base in mro: - if name in base.__dict__: - homecls = base - break - - # Get the object again, in order to get it from the defining - # __dict__ instead of via getattr (if possible). - if homecls is not None and name in homecls.__dict__: - obj = homecls.__dict__[name] - - # Also get the object via getattr. - obj_via_getattr = getattr(cls, name) + homecls = getattr(obj, "__objclass__", homecls) # Classify the object. if isinstance(obj, staticmethod): @@ -269,11 +271,18 @@ def classify_class_attrs(cls): kind = "class method" elif isinstance(obj, property): kind = "property" - elif (ismethod(obj_via_getattr) or - ismethoddescriptor(obj_via_getattr)): + elif ismethoddescriptor(obj): kind = "method" + elif isdatadescriptor(obj): + kind = "data" else: - kind = "data" + obj_via_getattr = getattr(cls, name) + if (ismethod(obj_via_getattr) or + ismethoddescriptor(obj_via_getattr)): + kind = "method" + else: + kind = "data" + obj = obj_via_getattr result.append((name, kind, homecls, obj)) diff -r 05abbadfe94f Lib/pydoc.py --- a/Lib/pydoc.py Thu Jan 10 21:37:12 2008 +0100 +++ b/Lib/pydoc.py Fri Jan 11 21:14:38 2008 +0100 @@ -734,8 +734,15 @@ class HTMLDoc(Doc): hr.maybe() push(msg) for name, kind, homecls, value in ok: - push(self.document(getattr(object, name), name, mod, - funcs, classes, mdict, object)) + try: + value = getattr(object, name) + except Exception: + # Some descriptors may meet a failure in their __get__. + # (bug #1785) + push(self._docdescriptor(name, value, mod)) + else: + push(self.document(value, name, mod, + funcs, classes, mdict, object)) push('\n') return attrs @@ -774,7 +781,12 @@ class HTMLDoc(Doc): mdict = {} for key, kind, homecls, value in attrs: mdict[key] = anchor = '#' + name + '-' + key - value = getattr(object, key) + try: + value = getattr(object, name) + except Exception: + # Some descriptors may meet a failure in their __get__. + # (bug #1785) + pass try: # The value may not be hashable (e.g., a data attr with # a dict or list value). @@ -1142,8 +1154,15 @@ class TextDoc(Doc): hr.maybe() push(msg) for name, kind, homecls, value in ok: - push(self.document(getattr(object, name), - name, mod, object)) + try: + value = getattr(object, name) + except Exception: + # Some descriptors may meet a failure in their __get__. + # (bug #1785) + push(self._docdescriptor(name, value, mod)) + else: + push(self.document(value, + name, mod, object)) return attrs def spilldescriptors(msg, attrs, predicate): diff -r 05abbadfe94f Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py Thu Jan 10 21:37:12 2008 +0100 +++ b/Lib/test/test_inspect.py Fri Jan 11 21:14:38 2008 +0100 @@ -288,9 +288,36 @@ class TestBuggyCases(GetSourceBase): def test_method_in_dynamic_class(self): self.assertSourceEqual(mod2.method_in_dynamic_class, 95, 97) + +class _BrokenDataDescriptor(object): + """ + A broken data descriptor. See bug #1785. + """ + def __get__(*args): + raise AssertionError("should not __get__ data descriptors") + + def __set__(*args): + raise RuntimeError + + def __getattr__(*args): + raise AssertionError("should not __getattr__ data descriptors") + + +class _BrokenMethodDescriptor(object): + """ + A broken method descriptor. See bug #1785. + """ + def __get__(*args): + raise AssertionError("should not __get__ method descriptors") + + def __getattr__(*args): + raise AssertionError("should not __getattr__ method descriptors") + + # Helper for testing classify_class_attrs. def attrs_wo_objs(cls): return [t[:3] for t in inspect.classify_class_attrs(cls)] + class TestClassesAndFunctions(unittest.TestCase): def test_classic_mro(self): @@ -365,6 +392,9 @@ class TestClassesAndFunctions(unittest.T datablob = '1' + dd = _BrokenDataDescriptor() + md = _BrokenMethodDescriptor() + attrs = attrs_wo_objs(A) self.assert_(('s', 'static method', A) in attrs, 'missing static method') self.assert_(('c', 'class method', A) in attrs, 'missing class method') @@ -372,6 +402,8 @@ class TestClassesAndFunctions(unittest.T self.assert_(('m', 'method', A) in attrs, 'missing plain method') self.assert_(('m1', 'method', A) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') + self.assert_(('md', 'method', A) in attrs, 'missing method descriptor') + self.assert_(('dd', 'data', A) in attrs, 'missing data descriptor') class B(A): def m(self): pass @@ -383,7 +415,8 @@ class TestClassesAndFunctions(unittest.T self.assert_(('m', 'method', B) in attrs, 'missing plain method') self.assert_(('m1', 'method', A) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') - + self.assert_(('md', 'method', A) in attrs, 'missing method descriptor') + self.assert_(('dd', 'data', A) in attrs, 'missing data descriptor') class C(A): def m(self): pass @@ -396,6 +429,8 @@ class TestClassesAndFunctions(unittest.T self.assert_(('m', 'method', C) in attrs, 'missing plain method') self.assert_(('m1', 'method', A) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') + self.assert_(('md', 'method', A) in attrs, 'missing method descriptor') + self.assert_(('dd', 'data', A) in attrs, 'missing data descriptor') class D(B, C): def m1(self): pass @@ -407,6 +442,8 @@ class TestClassesAndFunctions(unittest.T self.assert_(('m', 'method', B) in attrs, 'missing plain method') self.assert_(('m1', 'method', D) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') + self.assert_(('md', 'method', A) in attrs, 'missing method descriptor') + self.assert_(('dd', 'data', A) in attrs, 'missing data descriptor') # Repeat all that, but w/ new-style classes. def test_classify_newstyle(self): @@ -427,6 +464,9 @@ class TestClassesAndFunctions(unittest.T datablob = '1' + dd = _BrokenDataDescriptor() + md = _BrokenMethodDescriptor() + attrs = attrs_wo_objs(A) self.assert_(('s', 'static method', A) in attrs, 'missing static method') self.assert_(('c', 'class method', A) in attrs, 'missing class method') @@ -434,6 +474,8 @@ class TestClassesAndFunctions(unittest.T self.assert_(('m', 'method', A) in attrs, 'missing plain method') self.assert_(('m1', 'method', A) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') + self.assert_(('md', 'method', A) in attrs, 'missing method descriptor') + self.assert_(('dd', 'data', A) in attrs, 'missing data descriptor') class B(A): @@ -446,7 +488,8 @@ class TestClassesAndFunctions(unittest.T self.assert_(('m', 'method', B) in attrs, 'missing plain method') self.assert_(('m1', 'method', A) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') - + self.assert_(('md', 'method', A) in attrs, 'missing method descriptor') + self.assert_(('dd', 'data', A) in attrs, 'missing data descriptor') class C(A): @@ -460,6 +503,8 @@ class TestClassesAndFunctions(unittest.T self.assert_(('m', 'method', C) in attrs, 'missing plain method') self.assert_(('m1', 'method', A) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') + self.assert_(('md', 'method', A) in attrs, 'missing method descriptor') + self.assert_(('dd', 'data', A) in attrs, 'missing data descriptor') class D(B, C): @@ -472,6 +517,61 @@ class TestClassesAndFunctions(unittest.T self.assert_(('m', 'method', B) in attrs, 'missing plain method') self.assert_(('m1', 'method', D) in attrs, 'missing plain method') self.assert_(('datablob', 'data', A) in attrs, 'missing data') + self.assert_(('md', 'method', A) in attrs, 'missing method descriptor') + self.assert_(('dd', 'data', A) in attrs, 'missing data descriptor') + + def test_getmembers_descriptors(self): + # Old-style classes + class A: + dd = _BrokenDataDescriptor() + md = _BrokenMethodDescriptor() + + self.assertEqual(inspect.getmembers(A, inspect.ismethoddescriptor), + [('md', A.__dict__['md'])]) + self.assertEqual(inspect.getmembers(A, inspect.isdatadescriptor), + [('dd', A.__dict__['dd'])]) + + class B(A): + pass + + self.assertEqual(inspect.getmembers(B, inspect.ismethoddescriptor), + [('md', A.__dict__['md'])]) + self.assertEqual(inspect.getmembers(B, inspect.isdatadescriptor), + [('dd', A.__dict__['dd'])]) + + # New-style classes + class A(object): + dd = _BrokenDataDescriptor() + md = _BrokenMethodDescriptor() + + def pred_wrapper(pred): + # A quick'n'dirty way to discard standard attributes of new-style + # classes. + class Empty(object): + pass + def wrapped(x): + if hasattr(x, '__name__') and hasattr(Empty, x.__name__): + return False + return pred(x) + return wrapped + + ismethoddescriptor = pred_wrapper(inspect.ismethoddescriptor) + isdatadescriptor = pred_wrapper(inspect.isdatadescriptor) + + self.assertEqual(inspect.getmembers(A, ismethoddescriptor), + [('md', A.__dict__['md'])]) + self.assertEqual(inspect.getmembers(A, isdatadescriptor), + [('dd', A.__dict__['dd'])]) + + class B(A): + pass + + self.assertEqual(inspect.getmembers(B, ismethoddescriptor), + [('md', A.__dict__['md'])]) + self.assertEqual(inspect.getmembers(B, isdatadescriptor), + [('dd', A.__dict__['dd'])]) + + def test_main(): run_unittest(TestDecorators, TestRetrievingSourceCode, TestOneliners,