diff -r 516576f5f9dc Lib/inspect.py --- a/Lib/inspect.py Sun Oct 06 17:19:54 2013 -0700 +++ b/Lib/inspect.py Sun Oct 06 20:23:35 2013 -0700 @@ -262,23 +262,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 = [] processed = set() names = dir(object) - # add any virtual attributes to the list of names if object is a class - # this may result in duplicate entries if, for example, a virtual - # attribute with the same name as a member property exists + # add any DynamicClassAttributes to the list of names; this may result in + # duplicate entries if, for example, a DynamicClassAttribute with the same + # name as a member property exists try: for base in object.__bases__: for k, v in base.__dict__.items(): if isinstance(v, types.DynamicClassAttribute): names.append(k) 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). @@ -326,23 +326,23 @@ def classify_class_attrs(cls): If one of the items in dir(cls) is stored in the metaclass it will now be discovered and not have None be listed as the class in which it was defined. """ mro = getmro(cls) metamro = getmro(type(cls)) # for attributes stored in the metaclass metamro = tuple([cls for cls in metamro if cls not in (type, object)]) possible_bases = (cls,) + mro + metamro names = dir(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 + # add any DynamicClassAttributes to the list of names; this may result in + # duplicate entries if, for example, a DynamicClassAttribute with the same + # name as a member property exists for base in cls.__bases__: for k, v in base.__dict__.items(): if isinstance(v, types.DynamicClassAttribute): names.append(k) result = [] processed = set() sentinel = object() for name in names: # Get the object associated with the name, and where it was defined. # Normal objects will be looked up with both getattr and directly in @@ -370,20 +370,35 @@ def classify_class_attrs(cls): # if the resulting object does not live somewhere in the # mro, drop it and go with the dict_obj version only homecls = None get_obj = sentinel for base in possible_bases: if name in base.__dict__: dict_obj = base.__dict__[name] homecls = homecls or base break + else: + if homecls is None: + # a virtual attribute, probably returned from a __getattr__ + # method, walk the mro looking for the last class where the + # attribute was returned + last_cls = None + last_obj = None + for srch_cls in ((cls,) + mro): + obj = getattr(srch_cls, name, None) + if obj is not None: + last_cls = srch_cls + last_obj = obj + if last_cls is not None: + get_obj = last_obj + homecls = last_cls # Classify the object or its descriptor. if get_obj is not sentinel: obj = get_obj else: obj = dict_obj if isinstance(obj, staticmethod): kind = "static method" elif isinstance(obj, classmethod): kind = "class method" diff -r 516576f5f9dc Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py Sun Oct 06 17:19:54 2013 -0700 +++ b/Lib/test/test_inspect.py Sun Oct 06 20:23:35 2013 -0700 @@ -649,28 +649,76 @@ class TestClassesAndFunctions(unittest.T self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor') def test_classify_builtin_types(self): # Simple sanity check that all built-in types can have their # attributes classified. for name in dir(__builtins__): builtin = getattr(__builtins__, name) if isinstance(builtin, type): inspect.classify_class_attrs(builtin) - def test_classify_VirtualAttribute(self): + def test_classify_DynamicClassAttribute(self): class VA: @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)) + 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_getmembers_descriptors(self): 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