[Python-checkins] cpython: Close #19030: inspect.getmembers and inspect.classify_class_attrs

ethan.furman python-checkins at python.org
Fri Oct 18 09:27:45 CEST 2013


http://hg.python.org/cpython/rev/39b06c3fbe2e
changeset:   86419:39b06c3fbe2e
user:        Ethan Furman <ethan at stoneleaf.us>
date:        Fri Oct 18 00:27:39 2013 -0700
summary:
  Close #19030: inspect.getmembers and inspect.classify_class_attrs

Order of search is now:
  1. Try getattr
  2. If that throws an exception, check __dict__ directly
  3. If still not found, walk the mro looking for the eldest class that has
     the attribute (e.g. things returned by __getattr__)
  4. If none of that works (e.g. due to a buggy __dir__, __getattr__, etc.
     method or missing __slot__ attribute), ignore the attribute entirely.

files:
  Doc/library/inspect.rst  |    6 +-
  Lib/inspect.py           |   57 ++++++++------
  Lib/test/test_inspect.py |  100 ++++++++++++++++++++++++--
  Lib/types.py             |    2 +-
  Misc/NEWS                |    5 +
  5 files changed, 131 insertions(+), 39 deletions(-)


diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -173,9 +173,9 @@
 
    .. note::
 
-      :func:`getmembers` will only return metaclass attributes when the
-      argument is a class and those attributes have been listed in a custom
-      :meth:`__dir__`.
+      :func:`getmembers` will only return class attributes defined in the
+      metaclass when the argument is a class and those attributes have been
+      listed in the metaclass' custom :meth:`__dir__`.
 
 
 .. function:: getmoduleinfo(path)
diff --git a/Lib/inspect.py b/Lib/inspect.py
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -280,18 +280,22 @@
     except AttributeError:
         pass
     for key in names:
-        # 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__ and key not in processed:
-                # handle the normal case first; if duplicate entries exist
-                # they will be handled second
-                value = base.__dict__[key]
-                break
-        else:
-            try:
-                value = getattr(object, key)
-            except AttributeError:
+        # First try to get the value via getattr.  Some descriptors don't
+        # like calling their __get__ (see bug #1785), so fall back to
+        # looking in the __dict__.
+        try:
+            value = getattr(object, key)
+            # handle the duplicate key
+            if key in processed:
+                raise AttributeError
+        except AttributeError:
+            for base in mro:
+                if key in base.__dict__:
+                    value = base.__dict__[key]
+                    break
+            else:
+                # could be a (currently) missing slot member, or a buggy
+                # __dir__; discard and move on
                 continue
         if not predicate or predicate(value):
             results.append((key, value))
@@ -336,7 +340,7 @@
     # add any virtual attributes to the list of names
     # this may result in duplicate entries if, for example, a virtual
     # attribute with the same name as a member property exists
-    for base in cls.__bases__:
+    for base in mro:
         for k, v in base.__dict__.items():
             if isinstance(v, types.DynamicClassAttribute):
                 names.append(k)
@@ -356,36 +360,43 @@
         homecls = None
         get_obj = sentinel
         dict_obj = sentinel
-
-
         if name not in processed:
             try:
                 get_obj = getattr(cls, name)
             except Exception as exc:
                 pass
             else:
-                homecls = getattr(get_obj, "__class__")
                 homecls = getattr(get_obj, "__objclass__", homecls)
                 if homecls not in possible_bases:
                     # if the resulting object does not live somewhere in the
-                    # mro, drop it and go with the dict_obj version only
+                    # mro, drop it and search the mro manually
                     homecls = None
-                    get_obj = sentinel
-
+                    last_cls = None
+                    last_obj = None
+                    for srch_cls in ((cls,) + mro):
+                        srch_obj = getattr(srch_cls, name, None)
+                        if srch_obj is get_obj:
+                            last_cls = srch_cls
+                            last_obj = srch_obj
+                    if last_cls is not None:
+                        homecls = last_cls
         for base in possible_bases:
             if name in base.__dict__:
                 dict_obj = base.__dict__[name]
                 homecls = homecls or base
                 break
-
+        if homecls is None:
+            # unable to locate the attribute anywhere, most likely due to
+            # buggy custom __dir__; discard and move on
+            continue
         # Classify the object or its descriptor.
         if get_obj is not sentinel:
             obj = get_obj
         else:
             obj = dict_obj
-        if isinstance(obj, staticmethod):
+        if isinstance(dict_obj, staticmethod):
             kind = "static method"
-        elif isinstance(obj, classmethod):
+        elif isinstance(dict_obj, classmethod):
             kind = "class method"
         elif isinstance(obj, property):
             kind = "property"
@@ -393,10 +404,8 @@
             kind = "method"
         else:
             kind = "data"
-
         result.append(Attribute(name, kind, homecls, obj))
         processed.add(name)
-
     return result
 
 # ----------------------------------------------------------- class helpers
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -126,7 +126,6 @@
     def test_get_slot_members(self):
         class C(object):
             __slots__ = ("a", "b")
-
         x = C()
         x.a = 42
         members = dict(inspect.getmembers(x))
@@ -469,13 +468,13 @@
     A broken data descriptor. See bug #1785.
     """
     def __get__(*args):
-        raise AssertionError("should not __get__ data descriptors")
+        raise AttributeError("broken data descriptor")
 
     def __set__(*args):
         raise RuntimeError
 
     def __getattr__(*args):
-        raise AssertionError("should not __getattr__ data descriptors")
+        raise AttributeError("broken data descriptor")
 
 
 class _BrokenMethodDescriptor(object):
@@ -483,10 +482,10 @@
     A broken method descriptor. See bug #1785.
     """
     def __get__(*args):
-        raise AssertionError("should not __get__ method descriptors")
+        raise AttributeError("broken method descriptor")
 
     def __getattr__(*args):
-        raise AssertionError("should not __getattr__ method descriptors")
+        raise AttributeError("broken method descriptor")
 
 
 # Helper for testing classify_class_attrs.
@@ -656,13 +655,77 @@
             if isinstance(builtin, type):
                 inspect.classify_class_attrs(builtin)
 
-    def test_classify_VirtualAttribute(self):
-        class VA:
+    def test_classify_DynamicClassAttribute(self):
+        class Meta(type):
+            def __getattr__(self, name):
+                if name == 'ham':
+                    return 'spam'
+                return super().__getattr__(name)
+        class VA(metaclass=Meta):
             @types.DynamicClassAttribute
             def ham(self):
                 return 'eggs'
-        should_find = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
-        self.assertIn(should_find, inspect.classify_class_attrs(VA))
+        should_find_dca = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
+        self.assertIn(should_find_dca, inspect.classify_class_attrs(VA))
+        should_find_ga = inspect.Attribute('ham', 'data', VA, 'spam')
+        self.assertIn(should_find_ga, inspect.classify_class_attrs(VA))
+
+    def test_classify_VirtualAttribute(self):
+        class Meta(type):
+            def __dir__(cls):
+                return ['__class__', '__module__', '__name__', 'BOOM']
+            def __getattr__(self, name):
+                if name =='BOOM':
+                    return 42
+                return super().__getattr(name)
+        class Class(metaclass=Meta):
+            pass
+        should_find = inspect.Attribute('BOOM', 'data', Class, 42)
+        self.assertIn(should_find, inspect.classify_class_attrs(Class))
+
+    def test_classify_VirtualAttribute_multi_classes(self):
+        class Meta1(type):
+            def __dir__(cls):
+                return ['__class__', '__module__', '__name__', 'one']
+            def __getattr__(self, name):
+                if name =='one':
+                    return 1
+                return super().__getattr__(name)
+        class Meta2(type):
+            def __dir__(cls):
+                return ['__class__', '__module__', '__name__', 'two']
+            def __getattr__(self, name):
+                if name =='two':
+                    return 2
+                return super().__getattr__(name)
+        class Meta3(Meta1, Meta2):
+            def __dir__(cls):
+                return list(sorted(set(['__class__', '__module__', '__name__', 'three'] +
+                    Meta1.__dir__(cls) + Meta2.__dir__(cls))))
+            def __getattr__(self, name):
+                if name =='three':
+                    return 3
+                return super().__getattr__(name)
+        class Class1(metaclass=Meta1):
+            pass
+        class Class2(Class1, metaclass=Meta3):
+            pass
+
+        should_find1 = inspect.Attribute('one', 'data', Class1, 1)
+        should_find2 = inspect.Attribute('two', 'data', Class2, 2)
+        should_find3 = inspect.Attribute('three', 'data', Class2, 3)
+        cca = inspect.classify_class_attrs(Class2)
+        for sf in (should_find1, should_find2, should_find3):
+            self.assertIn(sf, cca)
+
+    def test_classify_class_attrs_with_buggy_dir(self):
+        class M(type):
+            def __dir__(cls):
+                return ['__class__', '__name__', 'missing']
+        class C(metaclass=M):
+            pass
+        attrs = [a[0] for a in inspect.classify_class_attrs(C)]
+        self.assertNotIn('missing', attrs)
 
     def test_getmembers_descriptors(self):
         class A(object):
@@ -708,11 +771,26 @@
         self.assertIn(('f', b.f), inspect.getmembers(b, inspect.ismethod))
 
     def test_getmembers_VirtualAttribute(self):
-        class A:
+        class M(type):
+            def __getattr__(cls, name):
+                if name == 'eggs':
+                    return 'scrambled'
+                return super().__getattr__(name)
+        class A(metaclass=M):
             @types.DynamicClassAttribute
             def eggs(self):
                 return 'spam'
-        self.assertIn(('eggs', A.__dict__['eggs']), inspect.getmembers(A))
+        self.assertIn(('eggs', 'scrambled'), inspect.getmembers(A))
+        self.assertIn(('eggs', 'spam'), inspect.getmembers(A()))
+
+    def test_getmembers_with_buggy_dir(self):
+        class M(type):
+            def __dir__(cls):
+                return ['__class__', '__name__', 'missing']
+        class C(metaclass=M):
+            pass
+        attrs = [a[0] for a in inspect.getmembers(C)]
+        self.assertNotIn('missing', attrs)
 
 
 _global_ref = object()
diff --git a/Lib/types.py b/Lib/types.py
--- a/Lib/types.py
+++ b/Lib/types.py
@@ -117,7 +117,7 @@
         self.fset = fset
         self.fdel = fdel
         # next two lines make DynamicClassAttribute act the same as property
-        self.__doc__ = doc or fget.__doc__ or self.__doc__
+        self.__doc__ = doc or fget.__doc__
         self.overwrite_doc = doc is None
         # support for abstract methods
         self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False))
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -135,6 +135,11 @@
 - Issue #4366: Fix building extensions on all platforms when --enable-shared
   is used.
 
+- Issue #19030: Fixed `inspect.getmembers` and `inspect.classify_class_attrs`
+  to attempt activating descriptors before falling back to a __dict__ search
+  for faulty descriptors.  `inspect.classify_class_attrs` no longer returns
+  Attributes whose home class is None.
+
 C API
 -----
 

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list