New function classify_class_attrs(). As a number of SF bug reports
point out, pydoc doesn't tell you where class attributes were defined, gets several new 2.2 features wrong, and isn't aware of some new features checked in on Thursday <wink>. pydoc is hampered in part because inspect.py has the same limitations. Alas, I can't think of a way to fix this within the current architecture of inspect/pydoc: it's simply not possible in 2.2 to figure out everything needed just from examining the object you get back from class.attr. You also need the class context, and the method resolution order, and tests against various things that simply didn't exist before. OTOH, knowledge of how to do that is getting quite complex, so doesn't belong in pydoc. classify_class_attrs takes a different approach, analyzing all the class attrs "at once", and returning the most interesting stuff for each, all in one gulp. pydoc needs to be reworked to use this for classes (instead of the current "filter dir(class) umpteen times against assorted predicates" approach).
This commit is contained in:
parent
42b6877293
commit
13b49d3374
|
@ -163,6 +163,91 @@ def getmembers(object, predicate=None):
|
|||
results.sort()
|
||||
return results
|
||||
|
||||
def classify_class_attrs(cls):
|
||||
"""Return list of attribute-descriptor tuples.
|
||||
|
||||
For each name in dir(cls), the return list contains a 4-tuple
|
||||
with these elements:
|
||||
|
||||
0. The name (a string).
|
||||
|
||||
1. The kind of attribute this is, one of these strings:
|
||||
'class method' created via classmethod()
|
||||
'static method' created via staticmethod()
|
||||
'property' created via property()
|
||||
'method' any other flavor of method
|
||||
'data' not a method
|
||||
|
||||
2. The class which defined this attribute (a class).
|
||||
|
||||
3. The object as obtained directly from the defining class's
|
||||
__dict__, not via getattr. This is especially important for
|
||||
data attributes: C.data is just a data object, but
|
||||
C.__dict__['data'] may be a data descriptor with additional
|
||||
info, like a __doc__ string.
|
||||
"""
|
||||
|
||||
mro = getmro(cls)
|
||||
names = dir(cls)
|
||||
result = []
|
||||
for name in names:
|
||||
# Get the object associated with the name.
|
||||
# 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]
|
||||
else:
|
||||
obj = getattr(cls, name)
|
||||
|
||||
# Figure out where it was defined.
|
||||
# A complication: static classes in 2.2 copy dict entries from
|
||||
# bases into derived classes, so it's not enough just to look for
|
||||
# "the first" class with the name in its dict. OTOH:
|
||||
# 1. Some-- but not all --methods in 2.2 come with an __objclass__
|
||||
# attr that answers the question directly.
|
||||
# 2. Some-- but not all --classes in 2.2 have a __defined__ dict
|
||||
# saying which names were defined by the class.
|
||||
homecls = getattr(obj, "__objclass__", None)
|
||||
if homecls is None:
|
||||
# Try __defined__.
|
||||
for base in mro:
|
||||
if hasattr(base, "__defined__"):
|
||||
if name in base.__defined__:
|
||||
homecls = base
|
||||
break
|
||||
if homecls is None:
|
||||
# Last chance (and first chance for classic classes): 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)
|
||||
|
||||
# Classify the object.
|
||||
if isinstance(obj, staticmethod):
|
||||
kind = "static method"
|
||||
elif isinstance(obj, classmethod):
|
||||
kind = "class method"
|
||||
elif isinstance(obj, property):
|
||||
kind = "property"
|
||||
elif (ismethod(obj_via_getattr) or
|
||||
ismethoddescriptor(obj_via_getattr)):
|
||||
kind = "method"
|
||||
else:
|
||||
kind = "data"
|
||||
|
||||
result.append((name, kind, homecls, obj))
|
||||
|
||||
return result
|
||||
|
||||
# ----------------------------------------------------------- class helpers
|
||||
def _searchbases(cls, accum):
|
||||
# Simulate the "classic class" search order.
|
||||
|
|
|
@ -233,3 +233,204 @@ class D(B, C): pass
|
|||
expected = (D, B, C, A, object)
|
||||
got = inspect.getmro(D)
|
||||
test(expected == got, "expected %r mro, got %r", expected, got)
|
||||
|
||||
# Test classify_class_attrs.
|
||||
def attrs_wo_objs(cls):
|
||||
return [t[:3] for t in inspect.classify_class_attrs(cls)]
|
||||
|
||||
class A:
|
||||
def s(): pass
|
||||
s = staticmethod(s)
|
||||
|
||||
def c(cls): pass
|
||||
c = classmethod(c)
|
||||
|
||||
def getp(self): pass
|
||||
p = property(getp)
|
||||
|
||||
def m(self): pass
|
||||
|
||||
def m1(self): pass
|
||||
|
||||
datablob = '1'
|
||||
|
||||
attrs = attrs_wo_objs(A)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'class method', A) in attrs, 'missing class method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', A) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
class B(A):
|
||||
def m(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(B)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'class method', A) in attrs, 'missing class method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', B) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
|
||||
class C(A):
|
||||
def m(self): pass
|
||||
def c(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(C)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'method', C) in attrs, 'missing plain method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', C) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
class D(B, C):
|
||||
def m1(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(D)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'class method', A) in attrs, 'missing class method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', B) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', D) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
# Repeat all that, but w/ new-style non-dynamic classes.
|
||||
|
||||
class A(object):
|
||||
__dynamic__ = 0
|
||||
|
||||
def s(): pass
|
||||
s = staticmethod(s)
|
||||
|
||||
def c(cls): pass
|
||||
c = classmethod(c)
|
||||
|
||||
def getp(self): pass
|
||||
p = property(getp)
|
||||
|
||||
def m(self): pass
|
||||
|
||||
def m1(self): pass
|
||||
|
||||
datablob = '1'
|
||||
|
||||
attrs = attrs_wo_objs(A)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'class method', A) in attrs, 'missing class method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', A) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
class B(A):
|
||||
__dynamic__ = 0
|
||||
|
||||
def m(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(B)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'class method', A) in attrs, 'missing class method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', B) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
|
||||
class C(A):
|
||||
__dynamic__ = 0
|
||||
|
||||
def m(self): pass
|
||||
def c(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(C)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'method', C) in attrs, 'missing plain method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', C) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
class D(B, C):
|
||||
__dynamic__ = 0
|
||||
|
||||
def m1(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(D)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'method', C) in attrs, 'missing plain method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', B) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', D) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
# And again, but w/ new-style dynamic classes.
|
||||
|
||||
class A(object):
|
||||
__dynamic__ = 1
|
||||
|
||||
def s(): pass
|
||||
s = staticmethod(s)
|
||||
|
||||
def c(cls): pass
|
||||
c = classmethod(c)
|
||||
|
||||
def getp(self): pass
|
||||
p = property(getp)
|
||||
|
||||
def m(self): pass
|
||||
|
||||
def m1(self): pass
|
||||
|
||||
datablob = '1'
|
||||
|
||||
attrs = attrs_wo_objs(A)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'class method', A) in attrs, 'missing class method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', A) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
class B(A):
|
||||
__dynamic__ = 1
|
||||
|
||||
def m(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(B)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'class method', A) in attrs, 'missing class method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', B) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
|
||||
class C(A):
|
||||
__dynamic__ = 1
|
||||
|
||||
def m(self): pass
|
||||
def c(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(C)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'method', C) in attrs, 'missing plain method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', C) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', A) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
||||
class D(B, C):
|
||||
__dynamic__ = 1
|
||||
|
||||
def m1(self): pass
|
||||
|
||||
attrs = attrs_wo_objs(D)
|
||||
test(('s', 'static method', A) in attrs, 'missing static method')
|
||||
test(('c', 'method', C) in attrs, 'missing plain method')
|
||||
test(('p', 'property', A) in attrs, 'missing property')
|
||||
test(('m', 'method', B) in attrs, 'missing plain method')
|
||||
test(('m1', 'method', D) in attrs, 'missing plain method')
|
||||
test(('datablob', 'data', A) in attrs, 'missing data')
|
||||
|
|
Loading…
Reference in New Issue