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

Skip to content

Commit efcf82f

Browse files
bpo-35619: Improve support of custom data descriptors in help() and pydoc. (pythonGH-11366)
1 parent 6fe9c44 commit efcf82f

File tree

3 files changed

+182
-46
lines changed

3 files changed

+182
-46
lines changed

Lib/pydoc.py

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,6 @@ def stripid(text):
137137
# The behaviour of %p is implementation-dependent in terms of case.
138138
return _re_stripid.sub(r'\1', text)
139139

140-
def _is_some_method(obj):
141-
return (inspect.isfunction(obj) or
142-
inspect.ismethod(obj) or
143-
inspect.isbuiltin(obj) or
144-
inspect.ismethoddescriptor(obj))
145-
146140
def _is_bound_method(fn):
147141
"""
148142
Returns True if fn is a bound method, regardless of whether
@@ -158,7 +152,7 @@ def _is_bound_method(fn):
158152

159153
def allmethods(cl):
160154
methods = {}
161-
for key, value in inspect.getmembers(cl, _is_some_method):
155+
for key, value in inspect.getmembers(cl, inspect.isroutine):
162156
methods[key] = 1
163157
for base in cl.__bases__:
164158
methods.update(allmethods(base)) # all your base are belong to us
@@ -379,15 +373,13 @@ def document(self, object, name=None, *args):
379373
# identifies something in a way that pydoc itself has issues handling;
380374
# think 'super' and how it is a descriptor (which raises the exception
381375
# by lacking a __name__ attribute) and an instance.
382-
if inspect.isgetsetdescriptor(object): return self.docdata(*args)
383-
if inspect.ismemberdescriptor(object): return self.docdata(*args)
384376
try:
385377
if inspect.ismodule(object): return self.docmodule(*args)
386378
if inspect.isclass(object): return self.docclass(*args)
387379
if inspect.isroutine(object): return self.docroutine(*args)
388380
except AttributeError:
389381
pass
390-
if isinstance(object, property): return self.docproperty(*args)
382+
if inspect.isdatadescriptor(object): return self.docdata(*args)
391383
return self.docother(*args)
392384

393385
def fail(self, object, name=None, *args):
@@ -809,7 +801,7 @@ def spill(msg, attrs, predicate):
809801
except Exception:
810802
# Some descriptors may meet a failure in their __get__.
811803
# (bug #1785)
812-
push(self._docdescriptor(name, value, mod))
804+
push(self.docdata(value, name, mod))
813805
else:
814806
push(self.document(value, name, mod,
815807
funcs, classes, mdict, object))
@@ -822,7 +814,7 @@ def spilldescriptors(msg, attrs, predicate):
822814
hr.maybe()
823815
push(msg)
824816
for name, kind, homecls, value in ok:
825-
push(self._docdescriptor(name, value, mod))
817+
push(self.docdata(value, name, mod))
826818
return attrs
827819

828820
def spilldata(msg, attrs, predicate):
@@ -994,32 +986,27 @@ def docroutine(self, object, name=None, mod=None,
994986
doc = doc and '<dd><tt>%s</tt></dd>' % doc
995987
return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
996988

997-
def _docdescriptor(self, name, value, mod):
989+
def docdata(self, object, name=None, mod=None, cl=None):
990+
"""Produce html documentation for a data descriptor."""
998991
results = []
999992
push = results.append
1000993

1001994
if name:
1002995
push('<dl><dt><strong>%s</strong></dt>\n' % name)
1003-
if value.__doc__ is not None:
1004-
doc = self.markup(getdoc(value), self.preformat)
996+
if object.__doc__ is not None:
997+
doc = self.markup(getdoc(object), self.preformat)
1005998
push('<dd><tt>%s</tt></dd>\n' % doc)
1006999
push('</dl>\n')
10071000

10081001
return ''.join(results)
10091002

1010-
def docproperty(self, object, name=None, mod=None, cl=None):
1011-
"""Produce html documentation for a property."""
1012-
return self._docdescriptor(name, object, mod)
1003+
docproperty = docdata
10131004

10141005
def docother(self, object, name=None, mod=None, *ignored):
10151006
"""Produce HTML documentation for a data object."""
10161007
lhs = name and '<strong>%s</strong> = ' % name or ''
10171008
return lhs + self.repr(object)
10181009

1019-
def docdata(self, object, name=None, mod=None, cl=None):
1020-
"""Produce html documentation for a data descriptor."""
1021-
return self._docdescriptor(name, object, mod)
1022-
10231010
def index(self, dir, shadowed=None):
10241011
"""Generate an HTML index for a directory of modules."""
10251012
modpkgs = []
@@ -1292,7 +1279,7 @@ def spill(msg, attrs, predicate):
12921279
except Exception:
12931280
# Some descriptors may meet a failure in their __get__.
12941281
# (bug #1785)
1295-
push(self._docdescriptor(name, value, mod))
1282+
push(self.docdata(value, name, mod))
12961283
else:
12971284
push(self.document(value,
12981285
name, mod, object))
@@ -1304,7 +1291,7 @@ def spilldescriptors(msg, attrs, predicate):
13041291
hr.maybe()
13051292
push(msg)
13061293
for name, kind, homecls, value in ok:
1307-
push(self._docdescriptor(name, value, mod))
1294+
push(self.docdata(value, name, mod))
13081295
return attrs
13091296

13101297
def spilldata(msg, attrs, predicate):
@@ -1420,26 +1407,21 @@ def docroutine(self, object, name=None, mod=None, cl=None):
14201407
doc = getdoc(object) or ''
14211408
return decl + '\n' + (doc and self.indent(doc).rstrip() + '\n')
14221409

1423-
def _docdescriptor(self, name, value, mod):
1410+
def docdata(self, object, name=None, mod=None, cl=None):
1411+
"""Produce text documentation for a data descriptor."""
14241412
results = []
14251413
push = results.append
14261414

14271415
if name:
14281416
push(self.bold(name))
14291417
push('\n')
1430-
doc = getdoc(value) or ''
1418+
doc = getdoc(object) or ''
14311419
if doc:
14321420
push(self.indent(doc))
14331421
push('\n')
14341422
return ''.join(results)
14351423

1436-
def docproperty(self, object, name=None, mod=None, cl=None):
1437-
"""Produce text documentation for a property."""
1438-
return self._docdescriptor(name, object, mod)
1439-
1440-
def docdata(self, object, name=None, mod=None, cl=None):
1441-
"""Produce text documentation for a data descriptor."""
1442-
return self._docdescriptor(name, object, mod)
1424+
docproperty = docdata
14431425

14441426
def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None):
14451427
"""Produce text documentation for a data object."""
@@ -1673,9 +1655,7 @@ def render_doc(thing, title='Python Library Documentation: %s', forceload=0,
16731655
if not (inspect.ismodule(object) or
16741656
inspect.isclass(object) or
16751657
inspect.isroutine(object) or
1676-
inspect.isgetsetdescriptor(object) or
1677-
inspect.ismemberdescriptor(object) or
1678-
isinstance(object, property)):
1658+
inspect.isdatadescriptor(object)):
16791659
# If the passed object is a piece of data or an instance,
16801660
# document its available methods instead of its value.
16811661
object = type(object)

Lib/test/test_pydoc.py

Lines changed: 164 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -743,15 +743,6 @@ def test_splitdoc_with_description(self):
743743
self.assertEqual(pydoc.splitdoc(example_string),
744744
('I Am A Doc', '\nHere is my description'))
745745

746-
def test_is_object_or_method(self):
747-
doc = pydoc.Doc()
748-
# Bound Method
749-
self.assertTrue(pydoc._is_some_method(doc.fail))
750-
# Method Descriptor
751-
self.assertTrue(pydoc._is_some_method(int.__add__))
752-
# String
753-
self.assertFalse(pydoc._is_some_method("I am not a method"))
754-
755746
def test_is_package_when_not_package(self):
756747
with test.support.temp_cwd() as test_dir:
757748
self.assertFalse(pydoc.ispackage(test_dir))
@@ -1093,6 +1084,12 @@ def _get_summary_line(o):
10931084
assert len(lines) >= 2
10941085
return lines[2]
10951086

1087+
@staticmethod
1088+
def _get_summary_lines(o):
1089+
text = pydoc.plain(pydoc.render_doc(o))
1090+
lines = text.split('\n')
1091+
return '\n'.join(lines[2:])
1092+
10961093
# these should include "self"
10971094
def test_unbound_python_method(self):
10981095
self.assertEqual(self._get_summary_line(textwrap.TextWrapper.wrap),
@@ -1108,7 +1105,6 @@ def test_bound_python_method(self):
11081105
t = textwrap.TextWrapper()
11091106
self.assertEqual(self._get_summary_line(t.wrap),
11101107
"wrap(text) method of textwrap.TextWrapper instance")
1111-
11121108
def test_field_order_for_named_tuples(self):
11131109
Person = namedtuple('Person', ['nickname', 'firstname', 'agegroup'])
11141110
s = pydoc.render_doc(Person)
@@ -1138,6 +1134,164 @@ def test_module_level_callable(self):
11381134
self.assertEqual(self._get_summary_line(os.stat),
11391135
"stat(path, *, dir_fd=None, follow_symlinks=True)")
11401136

1137+
@requires_docstrings
1138+
def test_staticmethod(self):
1139+
class X:
1140+
@staticmethod
1141+
def sm(x, y):
1142+
'''A static method'''
1143+
...
1144+
self.assertEqual(self._get_summary_lines(X.__dict__['sm']),
1145+
"<staticmethod object>")
1146+
self.assertEqual(self._get_summary_lines(X.sm), """\
1147+
sm(x, y)
1148+
A static method
1149+
""")
1150+
self.assertIn("""
1151+
| Static methods defined here:
1152+
|\x20\x20
1153+
| sm(x, y)
1154+
| A static method
1155+
""", pydoc.plain(pydoc.render_doc(X)))
1156+
1157+
@requires_docstrings
1158+
def test_classmethod(self):
1159+
class X:
1160+
@classmethod
1161+
def cm(cls, x):
1162+
'''A class method'''
1163+
...
1164+
self.assertEqual(self._get_summary_lines(X.__dict__['cm']),
1165+
"<classmethod object>")
1166+
self.assertEqual(self._get_summary_lines(X.cm), """\
1167+
cm(x) method of builtins.type instance
1168+
A class method
1169+
""")
1170+
self.assertIn("""
1171+
| Class methods defined here:
1172+
|\x20\x20
1173+
| cm(x) from builtins.type
1174+
| A class method
1175+
""", pydoc.plain(pydoc.render_doc(X)))
1176+
1177+
@requires_docstrings
1178+
def test_getset_descriptor(self):
1179+
# Currently these attributes are implemented as getset descriptors
1180+
# in CPython.
1181+
self.assertEqual(self._get_summary_line(int.numerator), "numerator")
1182+
self.assertEqual(self._get_summary_line(float.real), "real")
1183+
self.assertEqual(self._get_summary_line(Exception.args), "args")
1184+
self.assertEqual(self._get_summary_line(memoryview.obj), "obj")
1185+
1186+
@requires_docstrings
1187+
def test_member_descriptor(self):
1188+
# Currently these attributes are implemented as member descriptors
1189+
# in CPython.
1190+
self.assertEqual(self._get_summary_line(complex.real), "real")
1191+
self.assertEqual(self._get_summary_line(range.start), "start")
1192+
self.assertEqual(self._get_summary_line(slice.start), "start")
1193+
self.assertEqual(self._get_summary_line(property.fget), "fget")
1194+
self.assertEqual(self._get_summary_line(StopIteration.value), "value")
1195+
1196+
@requires_docstrings
1197+
def test_slot_descriptor(self):
1198+
class Point:
1199+
__slots__ = 'x', 'y'
1200+
self.assertEqual(self._get_summary_line(Point.x), "x")
1201+
1202+
@requires_docstrings
1203+
def test_dict_attr_descriptor(self):
1204+
class NS:
1205+
pass
1206+
self.assertEqual(self._get_summary_line(NS.__dict__['__dict__']),
1207+
"__dict__")
1208+
1209+
@requires_docstrings
1210+
def test_structseq_member_descriptor(self):
1211+
self.assertEqual(self._get_summary_line(type(sys.hash_info).width),
1212+
"width")
1213+
self.assertEqual(self._get_summary_line(type(sys.flags).debug),
1214+
"debug")
1215+
self.assertEqual(self._get_summary_line(type(sys.version_info).major),
1216+
"major")
1217+
self.assertEqual(self._get_summary_line(type(sys.float_info).max),
1218+
"max")
1219+
1220+
@requires_docstrings
1221+
def test_namedtuple_field_descriptor(self):
1222+
Box = namedtuple('Box', ('width', 'height'))
1223+
self.assertEqual(self._get_summary_lines(Box.width), """\
1224+
Alias for field number 0
1225+
""")
1226+
1227+
@requires_docstrings
1228+
def test_property(self):
1229+
class Rect:
1230+
@property
1231+
def area(self):
1232+
'''Area of the rect'''
1233+
return self.w * self.h
1234+
1235+
self.assertEqual(self._get_summary_lines(Rect.area), """\
1236+
Area of the rect
1237+
""")
1238+
self.assertIn("""
1239+
| area
1240+
| Area of the rect
1241+
""", pydoc.plain(pydoc.render_doc(Rect)))
1242+
1243+
@requires_docstrings
1244+
def test_custom_non_data_descriptor(self):
1245+
class Descr:
1246+
def __get__(self, obj, cls):
1247+
if obj is None:
1248+
return self
1249+
return 42
1250+
class X:
1251+
attr = Descr()
1252+
1253+
text = pydoc.plain(pydoc.render_doc(X.attr))
1254+
self.assertEqual(self._get_summary_lines(X.attr), """\
1255+
<test.test_pydoc.TestDescriptions.test_custom_non_data_descriptor.<locals>.Descr object>""")
1256+
1257+
X.attr.__doc__ = 'Custom descriptor'
1258+
self.assertEqual(self._get_summary_lines(X.attr), """\
1259+
<test.test_pydoc.TestDescriptions.test_custom_non_data_descriptor.<locals>.Descr object>""")
1260+
1261+
X.attr.__name__ = 'foo'
1262+
self.assertEqual(self._get_summary_lines(X.attr), """\
1263+
foo(...)
1264+
Custom descriptor
1265+
""")
1266+
1267+
@requires_docstrings
1268+
def test_custom_data_descriptor(self):
1269+
class Descr:
1270+
def __get__(self, obj, cls):
1271+
if obj is None:
1272+
return self
1273+
return 42
1274+
def __set__(self, obj, cls):
1275+
1/0
1276+
class X:
1277+
attr = Descr()
1278+
1279+
text = pydoc.plain(pydoc.render_doc(X.attr))
1280+
self.assertEqual(self._get_summary_lines(X.attr), "")
1281+
1282+
X.attr.__doc__ = 'Custom descriptor'
1283+
text = pydoc.plain(pydoc.render_doc(X.attr))
1284+
self.assertEqual(self._get_summary_lines(X.attr), """\
1285+
Custom descriptor
1286+
""")
1287+
1288+
X.attr.__name__ = 'foo'
1289+
text = pydoc.plain(pydoc.render_doc(X.attr))
1290+
self.assertEqual(self._get_summary_lines(X.attr), """\
1291+
foo
1292+
Custom descriptor
1293+
""")
1294+
11411295

11421296
class PydocServerTest(unittest.TestCase):
11431297
"""Tests for pydoc._start_server"""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improved support of custom data descriptors in :func:`help` and
2+
:mod:`pydoc`.

0 commit comments

Comments
 (0)