Commit 86a8a9ae authored by Antoine Pitrou's avatar Antoine Pitrou

Issue #1785: Fix inspect and pydoc with misbehaving descriptors.

Also fixes issue #13581: `help(type)` wouldn't display anything.
parent 53aa1d7c
...@@ -100,11 +100,11 @@ def ismethoddescriptor(object): ...@@ -100,11 +100,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.
...@@ -114,7 +114,11 @@ def isdatadescriptor(object): ...@@ -114,7 +114,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
...@@ -254,12 +258,23 @@ def isabstract(object): ...@@ -254,12 +258,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()
...@@ -295,30 +310,21 @@ def classify_class_attrs(cls): ...@@ -295,30 +310,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):
...@@ -327,11 +333,18 @@ def classify_class_attrs(cls): ...@@ -327,11 +333,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))
......
...@@ -754,8 +754,15 @@ class HTMLDoc(Doc): ...@@ -754,8 +754,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
...@@ -796,7 +803,12 @@ class HTMLDoc(Doc): ...@@ -796,7 +803,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).
...@@ -1180,8 +1192,15 @@ location listed above. ...@@ -1180,8 +1192,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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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):
......
...@@ -97,6 +97,8 @@ Core and Builtins ...@@ -97,6 +97,8 @@ Core and Builtins
Library Library
------- -------
- Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
- Issue #11813: Fix inspect.getattr_static for modules. Patch by Andreas - Issue #11813: Fix inspect.getattr_static for modules. Patch by Andreas
Stührk. Stührk.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment