]> granicus.if.org Git - python/commitdiff
Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
authorAntoine Pitrou <solipsis@pitrou.net>
Wed, 21 Dec 2011 09:16:14 +0000 (10:16 +0100)
committerAntoine Pitrou <solipsis@pitrou.net>
Wed, 21 Dec 2011 09:16:14 +0000 (10:16 +0100)
Also fixes issue #13581: `help(type)` wouldn't display anything.

Lib/inspect.py
Lib/pydoc.py
Lib/test/test_inspect.py
Misc/NEWS

index c727530f0ea0dd74e5cd82a6c9bc7b5aafb59b12..cb07b9d9ab1ef11e8bec1787116107bea3e1abd0 100644 (file)
@@ -247,12 +247,23 @@ def isabstract(object):
 def getmembers(object, 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(object):
+        mro = (object,) + getmro(object)
+    else:
+        mro = ()
     results = []
     for key in dir(object):
-        try:
-            value = getattr(object, key)
-        except AttributeError:
-            continue
+        # 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:
+            try:
+                value = getattr(object, key)
+            except AttributeError:
+                continue
         if not predicate or predicate(value):
             results.append((key, value))
     results.sort()
@@ -288,30 +299,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):
@@ -320,11 +322,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"
-        else:
+        elif isdatadescriptor(obj):
             kind = "data"
+        else:
+            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(Attribute(name, kind, homecls, obj))
 
index d55779022f2d6b1b4dcdf0018ede649f3abcfa6e..19a71d83045d12b2083f75d7a9ba400c362d5267 100755 (executable)
@@ -740,8 +740,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
 
@@ -781,7 +788,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).
@@ -1161,8 +1173,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):
index b8c9a1803c96a97ae5319f91c01f92d8d003df4f..97de7a07286dc3a487f5b07b9a72c2a94e039961 100644 (file)
@@ -404,10 +404,37 @@ class TestBuggyCases(GetSourceBase):
         self.assertEqual(inspect.findsource(co), (lines,0))
         self.assertEqual(inspect.getsource(co), lines[0])
 
+
+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):
         # Test classic-class method resolution order.
@@ -494,6 +521,9 @@ class TestClassesAndFunctions(unittest.TestCase):
 
             datablob = '1'
 
+            dd = _BrokenDataDescriptor()
+            md = _BrokenMethodDescriptor()
+
         attrs = attrs_wo_objs(A)
         self.assertIn(('s', 'static method', A), attrs, 'missing static method')
         self.assertIn(('c', 'class method', A), attrs, 'missing class method')
@@ -501,6 +531,8 @@ class TestClassesAndFunctions(unittest.TestCase):
         self.assertIn(('m', 'method', A), attrs, 'missing plain method')
         self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
         self.assertIn(('datablob', 'data', A), attrs, 'missing data')
+        self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
+        self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
 
         class B(A):
             def m(self): pass
@@ -512,6 +544,8 @@ class TestClassesAndFunctions(unittest.TestCase):
         self.assertIn(('m', 'method', B), attrs, 'missing plain method')
         self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
         self.assertIn(('datablob', 'data', A), attrs, 'missing data')
+        self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
+        self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
 
 
         class C(A):
@@ -525,6 +559,8 @@ class TestClassesAndFunctions(unittest.TestCase):
         self.assertIn(('m', 'method', C), attrs, 'missing plain method')
         self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
         self.assertIn(('datablob', 'data', A), attrs, 'missing data')
+        self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
+        self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
 
         class D(B, C):
             def m1(self): pass
@@ -539,6 +575,8 @@ class TestClassesAndFunctions(unittest.TestCase):
         self.assertIn(('m', 'method', B), attrs, 'missing plain method')
         self.assertIn(('m1', 'method', D), attrs, 'missing plain method')
         self.assertIn(('datablob', 'data', A), attrs, 'missing data')
+        self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
+        self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
 
 
     def test_classify_oldstyle(self):
@@ -554,6 +592,64 @@ class TestClassesAndFunctions(unittest.TestCase):
         """
         self._classify_test(True)
 
+    def test_classify_builtin_types(self):
+        # Simple sanity check that all built-in types can have their
+        # attributes classified.
+        for name in dir(__builtin__):
+            builtin = getattr(__builtin__, name)
+            if isinstance(builtin, type):
+                inspect.classify_class_attrs(builtin)
+
+    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'])])
 
 
 class TestGetcallargsFunctions(unittest.TestCase):
index 7d05311b857d01ac41aa6497d37c4ee1dd62c09a..d2082a6568f81917bd8adb5b7da03859d28e034c 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -86,6 +86,8 @@ Core and Builtins
 Library
 -------
 
+- Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
+
 - Issue #7502: Fix equality comparison for DocTestCase instances.  Patch by
   Cédric Krier.