diff -r 3752c94368dd Doc/reference/datamodel.rst --- a/Doc/reference/datamodel.rst Sun Oct 13 10:52:10 2013 -0700 +++ b/Doc/reference/datamodel.rst Sun Oct 13 11:03:05 2013 -0700 @@ -495,20 +495,28 @@ Callable types | :attr:`__annotations__` | A dict containing annotations | Writable | | | of parameters. The keys of | | | | the dict are the parameter | | | | names, or ``'return'`` for | | | | the return annotation, if | | | | provided. | | +-------------------------+-------------------------------+-----------+ | :attr:`__kwdefaults__` | A dict containing defaults | Writable | | | for keyword-only parameters. | | +-------------------------+-------------------------------+-----------+ + | :attr:`__objclass__` | 1) indicates this callable | | + | | requires an instance of the | | + | | given type (or a subclass); | | + | | for example CPython sets this | | + | | for unbound methods | | + | | implemented in C rather than | | + | | Python. | | + +-------------------------+-------------------------------+-----------+ Most of the attributes labelled "Writable" check the type of the assigned value. Function objects also support getting and setting arbitrary attributes, which can be used, for example, to attach metadata to functions. Regular attribute dot-notation is used to get and set such attributes. *Note that the current implementation only supports function attributes on user-defined functions. Function attributes on built-in functions may be supported in the future.* Additional information about a function's definition can be retrieved from its diff -r 3752c94368dd Lib/inspect.py --- a/Lib/inspect.py Sun Oct 13 10:52:10 2013 -0700 +++ b/Lib/inspect.py Sun Oct 13 11:03:05 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,70 +326,82 @@ 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 # its class' dict (in case getattr fails [bug #1785], and also to look # for a docstring). # For VirtualAttributes on the second pass we only look in the # class's dict. # # Getting an obj from the __dict__ sometimes reveals more than # using getattr. Static and class methods are dramatic examples. homecls = None - get_obj = sentinel - dict_obj = sentinel + obj = sentinel if name not in processed: try: get_obj = getattr(cls, name) - except Exception as exc: + except Exception: pass else: - homecls = getattr(get_obj, "__class__") - homecls = getattr(get_obj, "__objclass__", homecls) + homecls = getattr(get_obj, "__objclass__", None) 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 so we can continue the search homecls = None - get_obj = sentinel + obj = sentinel - for base in possible_bases: - if name in base.__dict__: - dict_obj = base.__dict__[name] - homecls = homecls or base - break + if homecls is None: + for base in possible_bases: + # this will fail when the attribute is being supplied by + # a metaclass or a custom __getattr__ or __getattribute__ + if name in base.__dict__: + obj = base.__dict__[name] + homecls = 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): + srch_obj = getattr(srch_cls, name, None) + if srch_obj is not None: + last_cls = srch_cls + last_obj = srch_obj + if last_cls is not None: + 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" elif isinstance(obj, property): kind = "property" elif isfunction(obj) or ismethoddescriptor(obj): kind = "method" else: kind = "data" diff -r 3752c94368dd Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py Sun Oct 13 10:52:10 2013 -0700 +++ b/Lib/test/test_inspect.py Sun Oct 13 11:03:05 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