From 63c141cacd0d655647430fe4b6a10c22d355aef2 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Fri, 18 Oct 2013 00:27:39 -0700 Subject: [PATCH] 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. --- 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 index de5a0b3995d..0da4056f370 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -173,9 +173,9 @@ attributes: .. 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 index d03edd95665..7cd70115c62 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -280,18 +280,22 @@ def getmembers(object, predicate=None): 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 @@ def classify_class_attrs(cls): # 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 @@ def classify_class_attrs(cls): 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 @@ def classify_class_attrs(cls): 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 index fb5fe17e22c..fb6aa6a8aee 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -126,7 +126,6 @@ class TestPredicates(IsTestBase): 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 @@ class _BrokenDataDescriptor(object): 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 @@ class _BrokenMethodDescriptor(object): 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 @@ class TestClassesAndFunctions(unittest.TestCase): 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 @@ class TestClassesAndFunctions(unittest.TestCase): 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 index b0bbfc1b4a0..7e4fec2d25d 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -117,7 +117,7 @@ class DynamicClassAttribute: 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 index 90b2e4583b9..ad57f53aebb 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -135,6 +135,11 @@ Library - 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 -----