Skip to content

Commit b8572a1

Browse files
committed
Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
Also fixes issue #13581: `help(type)` wouldn't display anything.
1 parent 587c738 commit b8572a1

File tree

4 files changed

+158
-32
lines changed

4 files changed

+158
-32
lines changed

Lib/inspect.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,23 @@ def isabstract(object):
247247
def getmembers(object, predicate=None):
248248
"""Return all members of an object as (name, value) pairs sorted by name.
249249
Optionally, only return members that satisfy a given predicate."""
250+
if isclass(object):
251+
mro = (object,) + getmro(object)
252+
else:
253+
mro = ()
250254
results = []
251255
for key in dir(object):
252-
try:
253-
value = getattr(object, key)
254-
except AttributeError:
255-
continue
256+
# First try to get the value via __dict__. Some descriptors don't
257+
# like calling their __get__ (see bug #1785).
258+
for base in mro:
259+
if key in base.__dict__:
260+
value = base.__dict__[key]
261+
break
262+
else:
263+
try:
264+
value = getattr(object, key)
265+
except AttributeError:
266+
continue
256267
if not predicate or predicate(value):
257268
results.append((key, value))
258269
results.sort()
@@ -288,30 +299,21 @@ def classify_class_attrs(cls):
288299
names = dir(cls)
289300
result = []
290301
for name in names:
291-
# Get the object associated with the name.
302+
# Get the object associated with the name, and where it was defined.
292303
# Getting an obj from the __dict__ sometimes reveals more than
293304
# using getattr. Static and class methods are dramatic examples.
294-
if name in cls.__dict__:
295-
obj = cls.__dict__[name]
305+
# Furthermore, some objects may raise an Exception when fetched with
306+
# getattr(). This is the case with some descriptors (bug #1785).
307+
# Thus, we only use getattr() as a last resort.
308+
homecls = None
309+
for base in (cls,) + mro:
310+
if name in base.__dict__:
311+
obj = base.__dict__[name]
312+
homecls = base
313+
break
296314
else:
297315
obj = getattr(cls, name)
298-
299-
# Figure out where it was defined.
300-
homecls = getattr(obj, "__objclass__", None)
301-
if homecls is None:
302-
# search the dicts.
303-
for base in mro:
304-
if name in base.__dict__:
305-
homecls = base
306-
break
307-
308-
# Get the object again, in order to get it from the defining
309-
# __dict__ instead of via getattr (if possible).
310-
if homecls is not None and name in homecls.__dict__:
311-
obj = homecls.__dict__[name]
312-
313-
# Also get the object via getattr.
314-
obj_via_getattr = getattr(cls, name)
316+
homecls = getattr(obj, "__objclass__", homecls)
315317

316318
# Classify the object.
317319
if isinstance(obj, staticmethod):
@@ -320,11 +322,18 @@ def classify_class_attrs(cls):
320322
kind = "class method"
321323
elif isinstance(obj, property):
322324
kind = "property"
323-
elif (ismethod(obj_via_getattr) or
324-
ismethoddescriptor(obj_via_getattr)):
325+
elif ismethoddescriptor(obj):
325326
kind = "method"
326-
else:
327+
elif isdatadescriptor(obj):
327328
kind = "data"
329+
else:
330+
obj_via_getattr = getattr(cls, name)
331+
if (ismethod(obj_via_getattr) or
332+
ismethoddescriptor(obj_via_getattr)):
333+
kind = "method"
334+
else:
335+
kind = "data"
336+
obj = obj_via_getattr
328337

329338
result.append(Attribute(name, kind, homecls, obj))
330339

Lib/pydoc.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -740,8 +740,15 @@ def spill(msg, attrs, predicate):
740740
hr.maybe()
741741
push(msg)
742742
for name, kind, homecls, value in ok:
743-
push(self.document(getattr(object, name), name, mod,
744-
funcs, classes, mdict, object))
743+
try:
744+
value = getattr(object, name)
745+
except Exception:
746+
# Some descriptors may meet a failure in their __get__.
747+
# (bug #1785)
748+
push(self._docdescriptor(name, value, mod))
749+
else:
750+
push(self.document(value, name, mod,
751+
funcs, classes, mdict, object))
745752
push('\n')
746753
return attrs
747754

@@ -781,7 +788,12 @@ def spilldata(msg, attrs, predicate):
781788
mdict = {}
782789
for key, kind, homecls, value in attrs:
783790
mdict[key] = anchor = '#' + name + '-' + key
784-
value = getattr(object, key)
791+
try:
792+
value = getattr(object, name)
793+
except Exception:
794+
# Some descriptors may meet a failure in their __get__.
795+
# (bug #1785)
796+
pass
785797
try:
786798
# The value may not be hashable (e.g., a data attr with
787799
# a dict or list value).
@@ -1161,8 +1173,15 @@ def spill(msg, attrs, predicate):
11611173
hr.maybe()
11621174
push(msg)
11631175
for name, kind, homecls, value in ok:
1164-
push(self.document(getattr(object, name),
1165-
name, mod, object))
1176+
try:
1177+
value = getattr(object, name)
1178+
except Exception:
1179+
# Some descriptors may meet a failure in their __get__.
1180+
# (bug #1785)
1181+
push(self._docdescriptor(name, value, mod))
1182+
else:
1183+
push(self.document(value,
1184+
name, mod, object))
11661185
return attrs
11671186

11681187
def spilldescriptors(msg, attrs, predicate):

Lib/test/test_inspect.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,10 +404,37 @@ def test_findsource_code_in_linecache(self):
404404
self.assertEqual(inspect.findsource(co), (lines,0))
405405
self.assertEqual(inspect.getsource(co), lines[0])
406406

407+
408+
class _BrokenDataDescriptor(object):
409+
"""
410+
A broken data descriptor. See bug #1785.
411+
"""
412+
def __get__(*args):
413+
raise AssertionError("should not __get__ data descriptors")
414+
415+
def __set__(*args):
416+
raise RuntimeError
417+
418+
def __getattr__(*args):
419+
raise AssertionError("should not __getattr__ data descriptors")
420+
421+
422+
class _BrokenMethodDescriptor(object):
423+
"""
424+
A broken method descriptor. See bug #1785.
425+
"""
426+
def __get__(*args):
427+
raise AssertionError("should not __get__ method descriptors")
428+
429+
def __getattr__(*args):
430+
raise AssertionError("should not __getattr__ method descriptors")
431+
432+
407433
# Helper for testing classify_class_attrs.
408434
def attrs_wo_objs(cls):
409435
return [t[:3] for t in inspect.classify_class_attrs(cls)]
410436

437+
411438
class TestClassesAndFunctions(unittest.TestCase):
412439
def test_classic_mro(self):
413440
# Test classic-class method resolution order.
@@ -494,13 +521,18 @@ def m1(self): pass
494521

495522
datablob = '1'
496523

524+
dd = _BrokenDataDescriptor()
525+
md = _BrokenMethodDescriptor()
526+
497527
attrs = attrs_wo_objs(A)
498528
self.assertIn(('s', 'static method', A), attrs, 'missing static method')
499529
self.assertIn(('c', 'class method', A), attrs, 'missing class method')
500530
self.assertIn(('p', 'property', A), attrs, 'missing property')
501531
self.assertIn(('m', 'method', A), attrs, 'missing plain method')
502532
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
503533
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
534+
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
535+
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
504536

505537
class B(A):
506538
def m(self): pass
@@ -512,6 +544,8 @@ def m(self): pass
512544
self.assertIn(('m', 'method', B), attrs, 'missing plain method')
513545
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
514546
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
547+
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
548+
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
515549

516550

517551
class C(A):
@@ -525,6 +559,8 @@ def c(self): pass
525559
self.assertIn(('m', 'method', C), attrs, 'missing plain method')
526560
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
527561
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
562+
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
563+
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
528564

529565
class D(B, C):
530566
def m1(self): pass
@@ -539,6 +575,8 @@ def m1(self): pass
539575
self.assertIn(('m', 'method', B), attrs, 'missing plain method')
540576
self.assertIn(('m1', 'method', D), attrs, 'missing plain method')
541577
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
578+
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
579+
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
542580

543581

544582
def test_classify_oldstyle(self):
@@ -554,6 +592,64 @@ def test_classify_newstyle(self):
554592
"""
555593
self._classify_test(True)
556594

595+
def test_classify_builtin_types(self):
596+
# Simple sanity check that all built-in types can have their
597+
# attributes classified.
598+
for name in dir(__builtin__):
599+
builtin = getattr(__builtin__, name)
600+
if isinstance(builtin, type):
601+
inspect.classify_class_attrs(builtin)
602+
603+
def test_getmembers_descriptors(self):
604+
# Old-style classes
605+
class A:
606+
dd = _BrokenDataDescriptor()
607+
md = _BrokenMethodDescriptor()
608+
609+
self.assertEqual(inspect.getmembers(A, inspect.ismethoddescriptor),
610+
[('md', A.__dict__['md'])])
611+
self.assertEqual(inspect.getmembers(A, inspect.isdatadescriptor),
612+
[('dd', A.__dict__['dd'])])
613+
614+
class B(A):
615+
pass
616+
617+
self.assertEqual(inspect.getmembers(B, inspect.ismethoddescriptor),
618+
[('md', A.__dict__['md'])])
619+
self.assertEqual(inspect.getmembers(B, inspect.isdatadescriptor),
620+
[('dd', A.__dict__['dd'])])
621+
622+
# New-style classes
623+
class A(object):
624+
dd = _BrokenDataDescriptor()
625+
md = _BrokenMethodDescriptor()
626+
627+
def pred_wrapper(pred):
628+
# A quick'n'dirty way to discard standard attributes of new-style
629+
# classes.
630+
class Empty(object):
631+
pass
632+
def wrapped(x):
633+
if hasattr(x, '__name__') and hasattr(Empty, x.__name__):
634+
return False
635+
return pred(x)
636+
return wrapped
637+
638+
ismethoddescriptor = pred_wrapper(inspect.ismethoddescriptor)
639+
isdatadescriptor = pred_wrapper(inspect.isdatadescriptor)
640+
641+
self.assertEqual(inspect.getmembers(A, ismethoddescriptor),
642+
[('md', A.__dict__['md'])])
643+
self.assertEqual(inspect.getmembers(A, isdatadescriptor),
644+
[('dd', A.__dict__['dd'])])
645+
646+
class B(A):
647+
pass
648+
649+
self.assertEqual(inspect.getmembers(B, ismethoddescriptor),
650+
[('md', A.__dict__['md'])])
651+
self.assertEqual(inspect.getmembers(B, isdatadescriptor),
652+
[('dd', A.__dict__['dd'])])
557653

558654

559655
class TestGetcallargsFunctions(unittest.TestCase):

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ Core and Builtins
8686
Library
8787
-------
8888

89+
- Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
90+
8991
- Issue #7502: Fix equality comparison for DocTestCase instances. Patch by
9092
Cédric Krier.
9193

0 commit comments

Comments
 (0)