mirror of
https://github.com/python/cpython.git
synced 2025-10-09 16:34:44 +00:00
Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
Also fixes issue #13581: `help(type)` wouldn't display anything.
This commit is contained in:
commit
12f65d1fef
4 changed files with 151 additions and 38 deletions
|
@ -99,11 +99,11 @@ def ismethoddescriptor(object):
|
||||||
tests return false from the ismethoddescriptor() test, simply because
|
tests return false from the ismethoddescriptor() test, simply because
|
||||||
the other tests promise more -- you can, e.g., count on having the
|
the other tests promise more -- you can, e.g., count on having the
|
||||||
__func__ attribute (etc) when an object passes ismethod()."""
|
__func__ attribute (etc) when an object passes ismethod()."""
|
||||||
return (hasattr(object, "__get__")
|
if isclass(object) or ismethod(object) or isfunction(object):
|
||||||
and not hasattr(object, "__set__") # else it's a data descriptor
|
# mutual exclusion
|
||||||
and not ismethod(object) # mutual exclusion
|
return False
|
||||||
and not isfunction(object)
|
tp = type(object)
|
||||||
and not isclass(object))
|
return hasattr(tp, "__get__") and not hasattr(tp, "__set__")
|
||||||
|
|
||||||
def isdatadescriptor(object):
|
def isdatadescriptor(object):
|
||||||
"""Return true if the object is a data descriptor.
|
"""Return true if the object is a data descriptor.
|
||||||
|
@ -113,7 +113,11 @@ def isdatadescriptor(object):
|
||||||
Typically, data descriptors will also have __name__ and __doc__ attributes
|
Typically, data descriptors will also have __name__ and __doc__ attributes
|
||||||
(properties, getsets, and members have both of these attributes), but this
|
(properties, getsets, and members have both of these attributes), but this
|
||||||
is not guaranteed."""
|
is not guaranteed."""
|
||||||
return (hasattr(object, "__set__") and hasattr(object, "__get__"))
|
if isclass(object) or ismethod(object) or isfunction(object):
|
||||||
|
# mutual exclusion
|
||||||
|
return False
|
||||||
|
tp = type(object)
|
||||||
|
return hasattr(tp, "__set__") and hasattr(tp, "__get__")
|
||||||
|
|
||||||
if hasattr(types, 'MemberDescriptorType'):
|
if hasattr(types, 'MemberDescriptorType'):
|
||||||
# CPython and equivalent
|
# CPython and equivalent
|
||||||
|
@ -253,12 +257,23 @@ def isabstract(object):
|
||||||
def getmembers(object, predicate=None):
|
def getmembers(object, predicate=None):
|
||||||
"""Return all members of an object as (name, value) pairs sorted by name.
|
"""Return all members of an object as (name, value) pairs sorted by name.
|
||||||
Optionally, only return members that satisfy a given predicate."""
|
Optionally, only return members that satisfy a given predicate."""
|
||||||
|
if isclass(object):
|
||||||
|
mro = (object,) + getmro(object)
|
||||||
|
else:
|
||||||
|
mro = ()
|
||||||
results = []
|
results = []
|
||||||
for key in dir(object):
|
for key in dir(object):
|
||||||
try:
|
# First try to get the value via __dict__. Some descriptors don't
|
||||||
value = getattr(object, key)
|
# like calling their __get__ (see bug #1785).
|
||||||
except AttributeError:
|
for base in mro:
|
||||||
continue
|
if key in base.__dict__:
|
||||||
|
value = base.__dict__[key]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = getattr(object, key)
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
if not predicate or predicate(value):
|
if not predicate or predicate(value):
|
||||||
results.append((key, value))
|
results.append((key, value))
|
||||||
results.sort()
|
results.sort()
|
||||||
|
@ -294,30 +309,21 @@ def classify_class_attrs(cls):
|
||||||
names = dir(cls)
|
names = dir(cls)
|
||||||
result = []
|
result = []
|
||||||
for name in names:
|
for name in names:
|
||||||
# Get the object associated with the name.
|
# Get the object associated with the name, and where it was defined.
|
||||||
# Getting an obj from the __dict__ sometimes reveals more than
|
# Getting an obj from the __dict__ sometimes reveals more than
|
||||||
# using getattr. Static and class methods are dramatic examples.
|
# using getattr. Static and class methods are dramatic examples.
|
||||||
if name in cls.__dict__:
|
# Furthermore, some objects may raise an Exception when fetched with
|
||||||
obj = cls.__dict__[name]
|
# getattr(). This is the case with some descriptors (bug #1785).
|
||||||
|
# Thus, we only use getattr() as a last resort.
|
||||||
|
homecls = None
|
||||||
|
for base in (cls,) + mro:
|
||||||
|
if name in base.__dict__:
|
||||||
|
obj = base.__dict__[name]
|
||||||
|
homecls = base
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
obj = getattr(cls, name)
|
obj = getattr(cls, name)
|
||||||
|
homecls = getattr(obj, "__objclass__", homecls)
|
||||||
# Figure out where it was defined.
|
|
||||||
homecls = getattr(obj, "__objclass__", None)
|
|
||||||
if homecls is None:
|
|
||||||
# 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.
|
# Classify the object.
|
||||||
if isinstance(obj, staticmethod):
|
if isinstance(obj, staticmethod):
|
||||||
|
@ -326,11 +332,18 @@ def classify_class_attrs(cls):
|
||||||
kind = "class method"
|
kind = "class method"
|
||||||
elif isinstance(obj, property):
|
elif isinstance(obj, property):
|
||||||
kind = "property"
|
kind = "property"
|
||||||
elif (isfunction(obj_via_getattr) or
|
elif ismethoddescriptor(obj):
|
||||||
ismethoddescriptor(obj_via_getattr)):
|
|
||||||
kind = "method"
|
kind = "method"
|
||||||
else:
|
elif isdatadescriptor(obj):
|
||||||
kind = "data"
|
kind = "data"
|
||||||
|
else:
|
||||||
|
obj_via_getattr = getattr(cls, name)
|
||||||
|
if (isfunction(obj_via_getattr) or
|
||||||
|
ismethoddescriptor(obj_via_getattr)):
|
||||||
|
kind = "method"
|
||||||
|
else:
|
||||||
|
kind = "data"
|
||||||
|
obj = obj_via_getattr
|
||||||
|
|
||||||
result.append(Attribute(name, kind, homecls, obj))
|
result.append(Attribute(name, kind, homecls, obj))
|
||||||
|
|
||||||
|
|
29
Lib/pydoc.py
29
Lib/pydoc.py
|
@ -748,8 +748,15 @@ class HTMLDoc(Doc):
|
||||||
hr.maybe()
|
hr.maybe()
|
||||||
push(msg)
|
push(msg)
|
||||||
for name, kind, homecls, value in ok:
|
for name, kind, homecls, value in ok:
|
||||||
push(self.document(getattr(object, name), name, mod,
|
try:
|
||||||
funcs, classes, mdict, object))
|
value = getattr(object, name)
|
||||||
|
except Exception:
|
||||||
|
# Some descriptors may meet a failure in their __get__.
|
||||||
|
# (bug #1785)
|
||||||
|
push(self._docdescriptor(name, value, mod))
|
||||||
|
else:
|
||||||
|
push(self.document(value, name, mod,
|
||||||
|
funcs, classes, mdict, object))
|
||||||
push('\n')
|
push('\n')
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -790,7 +797,12 @@ class HTMLDoc(Doc):
|
||||||
mdict = {}
|
mdict = {}
|
||||||
for key, kind, homecls, value in attrs:
|
for key, kind, homecls, value in attrs:
|
||||||
mdict[key] = anchor = '#' + name + '-' + key
|
mdict[key] = anchor = '#' + name + '-' + key
|
||||||
value = getattr(object, key)
|
try:
|
||||||
|
value = getattr(object, name)
|
||||||
|
except Exception:
|
||||||
|
# Some descriptors may meet a failure in their __get__.
|
||||||
|
# (bug #1785)
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
# The value may not be hashable (e.g., a data attr with
|
# The value may not be hashable (e.g., a data attr with
|
||||||
# a dict or list value).
|
# a dict or list value).
|
||||||
|
@ -1177,8 +1189,15 @@ location listed above.
|
||||||
hr.maybe()
|
hr.maybe()
|
||||||
push(msg)
|
push(msg)
|
||||||
for name, kind, homecls, value in ok:
|
for name, kind, homecls, value in ok:
|
||||||
push(self.document(getattr(object, name),
|
try:
|
||||||
name, mod, object))
|
value = getattr(object, name)
|
||||||
|
except Exception:
|
||||||
|
# Some descriptors may meet a failure in their __get__.
|
||||||
|
# (bug #1785)
|
||||||
|
push(self._docdescriptor(name, value, mod))
|
||||||
|
else:
|
||||||
|
push(self.document(value,
|
||||||
|
name, mod, object))
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def spilldescriptors(msg, attrs, predicate):
|
def spilldescriptors(msg, attrs, predicate):
|
||||||
|
|
|
@ -425,10 +425,37 @@ class TestNoEOL(GetSourceBase):
|
||||||
def test_class(self):
|
def test_class(self):
|
||||||
self.assertSourceEqual(self.fodderModule.X, 1, 2)
|
self.assertSourceEqual(self.fodderModule.X, 1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
class _BrokenDataDescriptor(object):
|
||||||
|
"""
|
||||||
|
A broken data descriptor. See bug #1785.
|
||||||
|
"""
|
||||||
|
def __get__(*args):
|
||||||
|
raise AssertionError("should not __get__ data descriptors")
|
||||||
|
|
||||||
|
def __set__(*args):
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
def __getattr__(*args):
|
||||||
|
raise AssertionError("should not __getattr__ data descriptors")
|
||||||
|
|
||||||
|
|
||||||
|
class _BrokenMethodDescriptor(object):
|
||||||
|
"""
|
||||||
|
A broken method descriptor. See bug #1785.
|
||||||
|
"""
|
||||||
|
def __get__(*args):
|
||||||
|
raise AssertionError("should not __get__ method descriptors")
|
||||||
|
|
||||||
|
def __getattr__(*args):
|
||||||
|
raise AssertionError("should not __getattr__ method descriptors")
|
||||||
|
|
||||||
|
|
||||||
# Helper for testing classify_class_attrs.
|
# Helper for testing classify_class_attrs.
|
||||||
def attrs_wo_objs(cls):
|
def attrs_wo_objs(cls):
|
||||||
return [t[:3] for t in inspect.classify_class_attrs(cls)]
|
return [t[:3] for t in inspect.classify_class_attrs(cls)]
|
||||||
|
|
||||||
|
|
||||||
class TestClassesAndFunctions(unittest.TestCase):
|
class TestClassesAndFunctions(unittest.TestCase):
|
||||||
def test_newstyle_mro(self):
|
def test_newstyle_mro(self):
|
||||||
# The same w/ new-class MRO.
|
# The same w/ new-class MRO.
|
||||||
|
@ -525,6 +552,9 @@ class TestClassesAndFunctions(unittest.TestCase):
|
||||||
|
|
||||||
datablob = '1'
|
datablob = '1'
|
||||||
|
|
||||||
|
dd = _BrokenDataDescriptor()
|
||||||
|
md = _BrokenMethodDescriptor()
|
||||||
|
|
||||||
attrs = attrs_wo_objs(A)
|
attrs = attrs_wo_objs(A)
|
||||||
self.assertIn(('s', 'static method', A), attrs, 'missing static method')
|
self.assertIn(('s', 'static method', A), attrs, 'missing static method')
|
||||||
self.assertIn(('c', 'class method', A), attrs, 'missing class method')
|
self.assertIn(('c', 'class method', A), attrs, 'missing class method')
|
||||||
|
@ -533,6 +563,8 @@ class TestClassesAndFunctions(unittest.TestCase):
|
||||||
'missing plain method: %r' % attrs)
|
'missing plain method: %r' % attrs)
|
||||||
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
|
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
|
||||||
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
|
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
|
||||||
|
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
|
||||||
|
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
|
||||||
|
|
||||||
class B(A):
|
class B(A):
|
||||||
|
|
||||||
|
@ -545,6 +577,8 @@ class TestClassesAndFunctions(unittest.TestCase):
|
||||||
self.assertIn(('m', 'method', B), attrs, 'missing plain method')
|
self.assertIn(('m', 'method', B), attrs, 'missing plain method')
|
||||||
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
|
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
|
||||||
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
|
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
|
||||||
|
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
|
||||||
|
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
|
||||||
|
|
||||||
|
|
||||||
class C(A):
|
class C(A):
|
||||||
|
@ -559,6 +593,8 @@ class TestClassesAndFunctions(unittest.TestCase):
|
||||||
self.assertIn(('m', 'method', C), attrs, 'missing plain method')
|
self.assertIn(('m', 'method', C), attrs, 'missing plain method')
|
||||||
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
|
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
|
||||||
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
|
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
|
||||||
|
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
|
||||||
|
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
|
||||||
|
|
||||||
class D(B, C):
|
class D(B, C):
|
||||||
|
|
||||||
|
@ -571,6 +607,49 @@ class TestClassesAndFunctions(unittest.TestCase):
|
||||||
self.assertIn(('m', 'method', B), attrs, 'missing plain method')
|
self.assertIn(('m', 'method', B), attrs, 'missing plain method')
|
||||||
self.assertIn(('m1', 'method', D), attrs, 'missing plain method')
|
self.assertIn(('m1', 'method', D), attrs, 'missing plain method')
|
||||||
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
|
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
|
||||||
|
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
|
||||||
|
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_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
|
||||||
|
def wrapped(x):
|
||||||
|
if '__name__' in dir(x) and hasattr(Empty, x.__name__):
|
||||||
|
return False
|
||||||
|
return pred(x)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
ismethoddescriptor = pred_wrapper(inspect.ismethoddescriptor)
|
||||||
|
isdatadescriptor = pred_wrapper(inspect.isdatadescriptor)
|
||||||
|
|
||||||
|
self.assertEqual(inspect.getmembers(A, ismethoddescriptor),
|
||||||
|
[('md', A.__dict__['md'])])
|
||||||
|
self.assertEqual(inspect.getmembers(A, isdatadescriptor),
|
||||||
|
[('dd', A.__dict__['dd'])])
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertEqual(inspect.getmembers(B, ismethoddescriptor),
|
||||||
|
[('md', A.__dict__['md'])])
|
||||||
|
self.assertEqual(inspect.getmembers(B, isdatadescriptor),
|
||||||
|
[('dd', A.__dict__['dd'])])
|
||||||
|
|
||||||
|
|
||||||
class TestGetcallargsFunctions(unittest.TestCase):
|
class TestGetcallargsFunctions(unittest.TestCase):
|
||||||
|
|
||||||
|
|
|
@ -419,6 +419,8 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
|
||||||
|
|
||||||
- Issue #13637: "a2b" functions in the binascii module now accept ASCII-only
|
- Issue #13637: "a2b" functions in the binascii module now accept ASCII-only
|
||||||
unicode strings.
|
unicode strings.
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue