Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 12f65d1

Browse files
committed
Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
Also fixes issue #13581: `help(type)` wouldn't display anything.
2 parents 501da61 + 86a8a9a commit 12f65d1

4 files changed

Lines changed: 151 additions & 38 deletions

File tree

Lib/inspect.py

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ def ismethoddescriptor(object):
9999
tests return false from the ismethoddescriptor() test, simply because
100100
the other tests promise more -- you can, e.g., count on having the
101101
__func__ attribute (etc) when an object passes ismethod()."""
102-
return (hasattr(object, "__get__")
103-
and not hasattr(object, "__set__") # else it's a data descriptor
104-
and not ismethod(object) # mutual exclusion
105-
and not isfunction(object)
106-
and not isclass(object))
102+
if isclass(object) or ismethod(object) or isfunction(object):
103+
# mutual exclusion
104+
return False
105+
tp = type(object)
106+
return hasattr(tp, "__get__") and not hasattr(tp, "__set__")
107107

108108
def isdatadescriptor(object):
109109
"""Return true if the object is a data descriptor.
@@ -113,7 +113,11 @@ def isdatadescriptor(object):
113113
Typically, data descriptors will also have __name__ and __doc__ attributes
114114
(properties, getsets, and members have both of these attributes), but this
115115
is not guaranteed."""
116-
return (hasattr(object, "__set__") and hasattr(object, "__get__"))
116+
if isclass(object) or ismethod(object) or isfunction(object):
117+
# mutual exclusion
118+
return False
119+
tp = type(object)
120+
return hasattr(tp, "__set__") and hasattr(tp, "__get__")
117121

118122
if hasattr(types, 'MemberDescriptorType'):
119123
# CPython and equivalent
@@ -253,12 +257,23 @@ def isabstract(object):
253257
def getmembers(object, predicate=None):
254258
"""Return all members of an object as (name, value) pairs sorted by name.
255259
Optionally, only return members that satisfy a given predicate."""
260+
if isclass(object):
261+
mro = (object,) + getmro(object)
262+
else:
263+
mro = ()
256264
results = []
257265
for key in dir(object):
258-
try:
259-
value = getattr(object, key)
260-
except AttributeError:
261-
continue
266+
# First try to get the value via __dict__. Some descriptors don't
267+
# like calling their __get__ (see bug #1785).
268+
for base in mro:
269+
if key in base.__dict__:
270+
value = base.__dict__[key]
271+
break
272+
else:
273+
try:
274+
value = getattr(object, key)
275+
except AttributeError:
276+
continue
262277
if not predicate or predicate(value):
263278
results.append((key, value))
264279
results.sort()
@@ -294,30 +309,21 @@ def classify_class_attrs(cls):
294309
names = dir(cls)
295310
result = []
296311
for name in names:
297-
# Get the object associated with the name.
312+
# Get the object associated with the name, and where it was defined.
298313
# Getting an obj from the __dict__ sometimes reveals more than
299314
# using getattr. Static and class methods are dramatic examples.
300-
if name in cls.__dict__:
301-
obj = cls.__dict__[name]
315+
# Furthermore, some objects may raise an Exception when fetched with
316+
# getattr(). This is the case with some descriptors (bug #1785).
317+
# Thus, we only use getattr() as a last resort.
318+
homecls = None
319+
for base in (cls,) + mro:
320+
if name in base.__dict__:
321+
obj = base.__dict__[name]
322+
homecls = base
323+
break
302324
else:
303325
obj = getattr(cls, name)
304-
305-
# Figure out where it was defined.
306-
homecls = getattr(obj, "__objclass__", None)
307-
if homecls is None:
308-
# search the dicts.
309-
for base in mro:
310-
if name in base.__dict__:
311-
homecls = base
312-
break
313-
314-
# Get the object again, in order to get it from the defining
315-
# __dict__ instead of via getattr (if possible).
316-
if homecls is not None and name in homecls.__dict__:
317-
obj = homecls.__dict__[name]
318-
319-
# Also get the object via getattr.
320-
obj_via_getattr = getattr(cls, name)
326+
homecls = getattr(obj, "__objclass__", homecls)
321327

322328
# Classify the object.
323329
if isinstance(obj, staticmethod):
@@ -326,11 +332,18 @@ def classify_class_attrs(cls):
326332
kind = "class method"
327333
elif isinstance(obj, property):
328334
kind = "property"
329-
elif (isfunction(obj_via_getattr) or
330-
ismethoddescriptor(obj_via_getattr)):
335+
elif ismethoddescriptor(obj):
331336
kind = "method"
332-
else:
337+
elif isdatadescriptor(obj):
333338
kind = "data"
339+
else:
340+
obj_via_getattr = getattr(cls, name)
341+
if (isfunction(obj_via_getattr) or
342+
ismethoddescriptor(obj_via_getattr)):
343+
kind = "method"
344+
else:
345+
kind = "data"
346+
obj = obj_via_getattr
334347

335348
result.append(Attribute(name, kind, homecls, obj))
336349

Lib/pydoc.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -748,8 +748,15 @@ def spill(msg, attrs, predicate):
748748
hr.maybe()
749749
push(msg)
750750
for name, kind, homecls, value in ok:
751-
push(self.document(getattr(object, name), name, mod,
752-
funcs, classes, mdict, object))
751+
try:
752+
value = getattr(object, name)
753+
except Exception:
754+
# Some descriptors may meet a failure in their __get__.
755+
# (bug #1785)
756+
push(self._docdescriptor(name, value, mod))
757+
else:
758+
push(self.document(value, name, mod,
759+
funcs, classes, mdict, object))
753760
push('\n')
754761
return attrs
755762

@@ -790,7 +797,12 @@ def spilldata(msg, attrs, predicate):
790797
mdict = {}
791798
for key, kind, homecls, value in attrs:
792799
mdict[key] = anchor = '#' + name + '-' + key
793-
value = getattr(object, key)
800+
try:
801+
value = getattr(object, name)
802+
except Exception:
803+
# Some descriptors may meet a failure in their __get__.
804+
# (bug #1785)
805+
pass
794806
try:
795807
# The value may not be hashable (e.g., a data attr with
796808
# a dict or list value).
@@ -1177,8 +1189,15 @@ def spill(msg, attrs, predicate):
11771189
hr.maybe()
11781190
push(msg)
11791191
for name, kind, homecls, value in ok:
1180-
push(self.document(getattr(object, name),
1181-
name, mod, object))
1192+
try:
1193+
value = getattr(object, name)
1194+
except Exception:
1195+
# Some descriptors may meet a failure in their __get__.
1196+
# (bug #1785)
1197+
push(self._docdescriptor(name, value, mod))
1198+
else:
1199+
push(self.document(value,
1200+
name, mod, object))
11821201
return attrs
11831202

11841203
def spilldescriptors(msg, attrs, predicate):

Lib/test/test_inspect.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,10 +425,37 @@ def tearDown(self):
425425
def test_class(self):
426426
self.assertSourceEqual(self.fodderModule.X, 1, 2)
427427

428+
429+
class _BrokenDataDescriptor(object):
430+
"""
431+
A broken data descriptor. See bug #1785.
432+
"""
433+
def __get__(*args):
434+
raise AssertionError("should not __get__ data descriptors")
435+
436+
def __set__(*args):
437+
raise RuntimeError
438+
439+
def __getattr__(*args):
440+
raise AssertionError("should not __getattr__ data descriptors")
441+
442+
443+
class _BrokenMethodDescriptor(object):
444+
"""
445+
A broken method descriptor. See bug #1785.
446+
"""
447+
def __get__(*args):
448+
raise AssertionError("should not __get__ method descriptors")
449+
450+
def __getattr__(*args):
451+
raise AssertionError("should not __getattr__ method descriptors")
452+
453+
428454
# Helper for testing classify_class_attrs.
429455
def attrs_wo_objs(cls):
430456
return [t[:3] for t in inspect.classify_class_attrs(cls)]
431457

458+
432459
class TestClassesAndFunctions(unittest.TestCase):
433460
def test_newstyle_mro(self):
434461
# The same w/ new-class MRO.
@@ -525,6 +552,9 @@ def m1(self): pass
525552

526553
datablob = '1'
527554

555+
dd = _BrokenDataDescriptor()
556+
md = _BrokenMethodDescriptor()
557+
528558
attrs = attrs_wo_objs(A)
529559
self.assertIn(('s', 'static method', A), attrs, 'missing static method')
530560
self.assertIn(('c', 'class method', A), attrs, 'missing class method')
@@ -533,6 +563,8 @@ def m1(self): pass
533563
'missing plain method: %r' % attrs)
534564
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
535565
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
566+
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
567+
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
536568

537569
class B(A):
538570

@@ -545,6 +577,8 @@ def m(self): pass
545577
self.assertIn(('m', 'method', B), attrs, 'missing plain method')
546578
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
547579
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
580+
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
581+
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
548582

549583

550584
class C(A):
@@ -559,6 +593,8 @@ def c(self): pass
559593
self.assertIn(('m', 'method', C), attrs, 'missing plain method')
560594
self.assertIn(('m1', 'method', A), attrs, 'missing plain method')
561595
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
596+
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
597+
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
562598

563599
class D(B, C):
564600

@@ -571,6 +607,49 @@ def m1(self): pass
571607
self.assertIn(('m', 'method', B), attrs, 'missing plain method')
572608
self.assertIn(('m1', 'method', D), attrs, 'missing plain method')
573609
self.assertIn(('datablob', 'data', A), attrs, 'missing data')
610+
self.assertIn(('md', 'method', A), attrs, 'missing method descriptor')
611+
self.assertIn(('dd', 'data', A), attrs, 'missing data descriptor')
612+
613+
def test_classify_builtin_types(self):
614+
# Simple sanity check that all built-in types can have their
615+
# attributes classified.
616+
for name in dir(__builtins__):
617+
builtin = getattr(__builtins__, name)
618+
if isinstance(builtin, type):
619+
inspect.classify_class_attrs(builtin)
620+
621+
def test_getmembers_descriptors(self):
622+
class A(object):
623+
dd = _BrokenDataDescriptor()
624+
md = _BrokenMethodDescriptor()
625+
626+
def pred_wrapper(pred):
627+
# A quick'n'dirty way to discard standard attributes of new-style
628+
# classes.
629+
class Empty(object):
630+
pass
631+
def wrapped(x):
632+
if '__name__' in dir(x) and hasattr(Empty, x.__name__):
633+
return False
634+
return pred(x)
635+
return wrapped
636+
637+
ismethoddescriptor = pred_wrapper(inspect.ismethoddescriptor)
638+
isdatadescriptor = pred_wrapper(inspect.isdatadescriptor)
639+
640+
self.assertEqual(inspect.getmembers(A, ismethoddescriptor),
641+
[('md', A.__dict__['md'])])
642+
self.assertEqual(inspect.getmembers(A, isdatadescriptor),
643+
[('dd', A.__dict__['dd'])])
644+
645+
class B(A):
646+
pass
647+
648+
self.assertEqual(inspect.getmembers(B, ismethoddescriptor),
649+
[('md', A.__dict__['md'])])
650+
self.assertEqual(inspect.getmembers(B, isdatadescriptor),
651+
[('dd', A.__dict__['dd'])])
652+
574653

575654
class TestGetcallargsFunctions(unittest.TestCase):
576655

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,8 @@ Core and Builtins
419419
Library
420420
-------
421421

422+
- Issue #1785: Fix inspect and pydoc with misbehaving descriptors.
423+
422424
- Issue #13637: "a2b" functions in the binascii module now accept ASCII-only
423425
unicode strings.
424426

0 commit comments

Comments
 (0)