mirror of
https://github.com/python/cpython.git
synced 2025-10-09 16:34:44 +00:00
bpo-35619: Improve support of custom data descriptors in help() and pydoc. (GH-11366)
This commit is contained in:
parent
6fe9c446f8
commit
efcf82f945
3 changed files with 182 additions and 46 deletions
52
Lib/pydoc.py
52
Lib/pydoc.py
|
@ -137,12 +137,6 @@ def stripid(text):
|
||||||
# The behaviour of %p is implementation-dependent in terms of case.
|
# The behaviour of %p is implementation-dependent in terms of case.
|
||||||
return _re_stripid.sub(r'\1', text)
|
return _re_stripid.sub(r'\1', text)
|
||||||
|
|
||||||
def _is_some_method(obj):
|
|
||||||
return (inspect.isfunction(obj) or
|
|
||||||
inspect.ismethod(obj) or
|
|
||||||
inspect.isbuiltin(obj) or
|
|
||||||
inspect.ismethoddescriptor(obj))
|
|
||||||
|
|
||||||
def _is_bound_method(fn):
|
def _is_bound_method(fn):
|
||||||
"""
|
"""
|
||||||
Returns True if fn is a bound method, regardless of whether
|
Returns True if fn is a bound method, regardless of whether
|
||||||
|
@ -158,7 +152,7 @@ def _is_bound_method(fn):
|
||||||
|
|
||||||
def allmethods(cl):
|
def allmethods(cl):
|
||||||
methods = {}
|
methods = {}
|
||||||
for key, value in inspect.getmembers(cl, _is_some_method):
|
for key, value in inspect.getmembers(cl, inspect.isroutine):
|
||||||
methods[key] = 1
|
methods[key] = 1
|
||||||
for base in cl.__bases__:
|
for base in cl.__bases__:
|
||||||
methods.update(allmethods(base)) # all your base are belong to us
|
methods.update(allmethods(base)) # all your base are belong to us
|
||||||
|
@ -379,15 +373,13 @@ class Doc:
|
||||||
# identifies something in a way that pydoc itself has issues handling;
|
# identifies something in a way that pydoc itself has issues handling;
|
||||||
# think 'super' and how it is a descriptor (which raises the exception
|
# think 'super' and how it is a descriptor (which raises the exception
|
||||||
# by lacking a __name__ attribute) and an instance.
|
# by lacking a __name__ attribute) and an instance.
|
||||||
if inspect.isgetsetdescriptor(object): return self.docdata(*args)
|
|
||||||
if inspect.ismemberdescriptor(object): return self.docdata(*args)
|
|
||||||
try:
|
try:
|
||||||
if inspect.ismodule(object): return self.docmodule(*args)
|
if inspect.ismodule(object): return self.docmodule(*args)
|
||||||
if inspect.isclass(object): return self.docclass(*args)
|
if inspect.isclass(object): return self.docclass(*args)
|
||||||
if inspect.isroutine(object): return self.docroutine(*args)
|
if inspect.isroutine(object): return self.docroutine(*args)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
if isinstance(object, property): return self.docproperty(*args)
|
if inspect.isdatadescriptor(object): return self.docdata(*args)
|
||||||
return self.docother(*args)
|
return self.docother(*args)
|
||||||
|
|
||||||
def fail(self, object, name=None, *args):
|
def fail(self, object, name=None, *args):
|
||||||
|
@ -809,7 +801,7 @@ class HTMLDoc(Doc):
|
||||||
except Exception:
|
except Exception:
|
||||||
# Some descriptors may meet a failure in their __get__.
|
# Some descriptors may meet a failure in their __get__.
|
||||||
# (bug #1785)
|
# (bug #1785)
|
||||||
push(self._docdescriptor(name, value, mod))
|
push(self.docdata(value, name, mod))
|
||||||
else:
|
else:
|
||||||
push(self.document(value, name, mod,
|
push(self.document(value, name, mod,
|
||||||
funcs, classes, mdict, object))
|
funcs, classes, mdict, object))
|
||||||
|
@ -822,7 +814,7 @@ 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._docdescriptor(name, value, mod))
|
push(self.docdata(value, name, mod))
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def spilldata(msg, attrs, predicate):
|
def spilldata(msg, attrs, predicate):
|
||||||
|
@ -994,32 +986,27 @@ class HTMLDoc(Doc):
|
||||||
doc = doc and '<dd><tt>%s</tt></dd>' % doc
|
doc = doc and '<dd><tt>%s</tt></dd>' % doc
|
||||||
return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
|
return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
|
||||||
|
|
||||||
def _docdescriptor(self, name, value, mod):
|
def docdata(self, object, name=None, mod=None, cl=None):
|
||||||
|
"""Produce html documentation for a data descriptor."""
|
||||||
results = []
|
results = []
|
||||||
push = results.append
|
push = results.append
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
push('<dl><dt><strong>%s</strong></dt>\n' % name)
|
push('<dl><dt><strong>%s</strong></dt>\n' % name)
|
||||||
if value.__doc__ is not None:
|
if object.__doc__ is not None:
|
||||||
doc = self.markup(getdoc(value), self.preformat)
|
doc = self.markup(getdoc(object), self.preformat)
|
||||||
push('<dd><tt>%s</tt></dd>\n' % doc)
|
push('<dd><tt>%s</tt></dd>\n' % doc)
|
||||||
push('</dl>\n')
|
push('</dl>\n')
|
||||||
|
|
||||||
return ''.join(results)
|
return ''.join(results)
|
||||||
|
|
||||||
def docproperty(self, object, name=None, mod=None, cl=None):
|
docproperty = docdata
|
||||||
"""Produce html documentation for a property."""
|
|
||||||
return self._docdescriptor(name, object, mod)
|
|
||||||
|
|
||||||
def docother(self, object, name=None, mod=None, *ignored):
|
def docother(self, object, name=None, mod=None, *ignored):
|
||||||
"""Produce HTML documentation for a data object."""
|
"""Produce HTML documentation for a data object."""
|
||||||
lhs = name and '<strong>%s</strong> = ' % name or ''
|
lhs = name and '<strong>%s</strong> = ' % name or ''
|
||||||
return lhs + self.repr(object)
|
return lhs + self.repr(object)
|
||||||
|
|
||||||
def docdata(self, object, name=None, mod=None, cl=None):
|
|
||||||
"""Produce html documentation for a data descriptor."""
|
|
||||||
return self._docdescriptor(name, object, mod)
|
|
||||||
|
|
||||||
def index(self, dir, shadowed=None):
|
def index(self, dir, shadowed=None):
|
||||||
"""Generate an HTML index for a directory of modules."""
|
"""Generate an HTML index for a directory of modules."""
|
||||||
modpkgs = []
|
modpkgs = []
|
||||||
|
@ -1292,7 +1279,7 @@ location listed above.
|
||||||
except Exception:
|
except Exception:
|
||||||
# Some descriptors may meet a failure in their __get__.
|
# Some descriptors may meet a failure in their __get__.
|
||||||
# (bug #1785)
|
# (bug #1785)
|
||||||
push(self._docdescriptor(name, value, mod))
|
push(self.docdata(value, name, mod))
|
||||||
else:
|
else:
|
||||||
push(self.document(value,
|
push(self.document(value,
|
||||||
name, mod, object))
|
name, mod, object))
|
||||||
|
@ -1304,7 +1291,7 @@ 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._docdescriptor(name, value, mod))
|
push(self.docdata(value, name, mod))
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def spilldata(msg, attrs, predicate):
|
def spilldata(msg, attrs, predicate):
|
||||||
|
@ -1420,26 +1407,21 @@ location listed above.
|
||||||
doc = getdoc(object) or ''
|
doc = getdoc(object) or ''
|
||||||
return decl + '\n' + (doc and self.indent(doc).rstrip() + '\n')
|
return decl + '\n' + (doc and self.indent(doc).rstrip() + '\n')
|
||||||
|
|
||||||
def _docdescriptor(self, name, value, mod):
|
def docdata(self, object, name=None, mod=None, cl=None):
|
||||||
|
"""Produce text documentation for a data descriptor."""
|
||||||
results = []
|
results = []
|
||||||
push = results.append
|
push = results.append
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
push(self.bold(name))
|
push(self.bold(name))
|
||||||
push('\n')
|
push('\n')
|
||||||
doc = getdoc(value) or ''
|
doc = getdoc(object) or ''
|
||||||
if doc:
|
if doc:
|
||||||
push(self.indent(doc))
|
push(self.indent(doc))
|
||||||
push('\n')
|
push('\n')
|
||||||
return ''.join(results)
|
return ''.join(results)
|
||||||
|
|
||||||
def docproperty(self, object, name=None, mod=None, cl=None):
|
docproperty = docdata
|
||||||
"""Produce text documentation for a property."""
|
|
||||||
return self._docdescriptor(name, object, mod)
|
|
||||||
|
|
||||||
def docdata(self, object, name=None, mod=None, cl=None):
|
|
||||||
"""Produce text documentation for a data descriptor."""
|
|
||||||
return self._docdescriptor(name, object, mod)
|
|
||||||
|
|
||||||
def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None):
|
def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None):
|
||||||
"""Produce text documentation for a data object."""
|
"""Produce text documentation for a data object."""
|
||||||
|
@ -1673,9 +1655,7 @@ def render_doc(thing, title='Python Library Documentation: %s', forceload=0,
|
||||||
if not (inspect.ismodule(object) or
|
if not (inspect.ismodule(object) or
|
||||||
inspect.isclass(object) or
|
inspect.isclass(object) or
|
||||||
inspect.isroutine(object) or
|
inspect.isroutine(object) or
|
||||||
inspect.isgetsetdescriptor(object) or
|
inspect.isdatadescriptor(object)):
|
||||||
inspect.ismemberdescriptor(object) or
|
|
||||||
isinstance(object, property)):
|
|
||||||
# If the passed object is a piece of data or an instance,
|
# If the passed object is a piece of data or an instance,
|
||||||
# document its available methods instead of its value.
|
# document its available methods instead of its value.
|
||||||
object = type(object)
|
object = type(object)
|
||||||
|
|
|
@ -743,15 +743,6 @@ class PydocDocTest(unittest.TestCase):
|
||||||
self.assertEqual(pydoc.splitdoc(example_string),
|
self.assertEqual(pydoc.splitdoc(example_string),
|
||||||
('I Am A Doc', '\nHere is my description'))
|
('I Am A Doc', '\nHere is my description'))
|
||||||
|
|
||||||
def test_is_object_or_method(self):
|
|
||||||
doc = pydoc.Doc()
|
|
||||||
# Bound Method
|
|
||||||
self.assertTrue(pydoc._is_some_method(doc.fail))
|
|
||||||
# Method Descriptor
|
|
||||||
self.assertTrue(pydoc._is_some_method(int.__add__))
|
|
||||||
# String
|
|
||||||
self.assertFalse(pydoc._is_some_method("I am not a method"))
|
|
||||||
|
|
||||||
def test_is_package_when_not_package(self):
|
def test_is_package_when_not_package(self):
|
||||||
with test.support.temp_cwd() as test_dir:
|
with test.support.temp_cwd() as test_dir:
|
||||||
self.assertFalse(pydoc.ispackage(test_dir))
|
self.assertFalse(pydoc.ispackage(test_dir))
|
||||||
|
@ -1093,6 +1084,12 @@ class TestDescriptions(unittest.TestCase):
|
||||||
assert len(lines) >= 2
|
assert len(lines) >= 2
|
||||||
return lines[2]
|
return lines[2]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_summary_lines(o):
|
||||||
|
text = pydoc.plain(pydoc.render_doc(o))
|
||||||
|
lines = text.split('\n')
|
||||||
|
return '\n'.join(lines[2:])
|
||||||
|
|
||||||
# these should include "self"
|
# these should include "self"
|
||||||
def test_unbound_python_method(self):
|
def test_unbound_python_method(self):
|
||||||
self.assertEqual(self._get_summary_line(textwrap.TextWrapper.wrap),
|
self.assertEqual(self._get_summary_line(textwrap.TextWrapper.wrap),
|
||||||
|
@ -1108,7 +1105,6 @@ class TestDescriptions(unittest.TestCase):
|
||||||
t = textwrap.TextWrapper()
|
t = textwrap.TextWrapper()
|
||||||
self.assertEqual(self._get_summary_line(t.wrap),
|
self.assertEqual(self._get_summary_line(t.wrap),
|
||||||
"wrap(text) method of textwrap.TextWrapper instance")
|
"wrap(text) method of textwrap.TextWrapper instance")
|
||||||
|
|
||||||
def test_field_order_for_named_tuples(self):
|
def test_field_order_for_named_tuples(self):
|
||||||
Person = namedtuple('Person', ['nickname', 'firstname', 'agegroup'])
|
Person = namedtuple('Person', ['nickname', 'firstname', 'agegroup'])
|
||||||
s = pydoc.render_doc(Person)
|
s = pydoc.render_doc(Person)
|
||||||
|
@ -1138,6 +1134,164 @@ class TestDescriptions(unittest.TestCase):
|
||||||
self.assertEqual(self._get_summary_line(os.stat),
|
self.assertEqual(self._get_summary_line(os.stat),
|
||||||
"stat(path, *, dir_fd=None, follow_symlinks=True)")
|
"stat(path, *, dir_fd=None, follow_symlinks=True)")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_staticmethod(self):
|
||||||
|
class X:
|
||||||
|
@staticmethod
|
||||||
|
def sm(x, y):
|
||||||
|
'''A static method'''
|
||||||
|
...
|
||||||
|
self.assertEqual(self._get_summary_lines(X.__dict__['sm']),
|
||||||
|
"<staticmethod object>")
|
||||||
|
self.assertEqual(self._get_summary_lines(X.sm), """\
|
||||||
|
sm(x, y)
|
||||||
|
A static method
|
||||||
|
""")
|
||||||
|
self.assertIn("""
|
||||||
|
| Static methods defined here:
|
||||||
|
|\x20\x20
|
||||||
|
| sm(x, y)
|
||||||
|
| A static method
|
||||||
|
""", pydoc.plain(pydoc.render_doc(X)))
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_classmethod(self):
|
||||||
|
class X:
|
||||||
|
@classmethod
|
||||||
|
def cm(cls, x):
|
||||||
|
'''A class method'''
|
||||||
|
...
|
||||||
|
self.assertEqual(self._get_summary_lines(X.__dict__['cm']),
|
||||||
|
"<classmethod object>")
|
||||||
|
self.assertEqual(self._get_summary_lines(X.cm), """\
|
||||||
|
cm(x) method of builtins.type instance
|
||||||
|
A class method
|
||||||
|
""")
|
||||||
|
self.assertIn("""
|
||||||
|
| Class methods defined here:
|
||||||
|
|\x20\x20
|
||||||
|
| cm(x) from builtins.type
|
||||||
|
| A class method
|
||||||
|
""", pydoc.plain(pydoc.render_doc(X)))
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_getset_descriptor(self):
|
||||||
|
# Currently these attributes are implemented as getset descriptors
|
||||||
|
# in CPython.
|
||||||
|
self.assertEqual(self._get_summary_line(int.numerator), "numerator")
|
||||||
|
self.assertEqual(self._get_summary_line(float.real), "real")
|
||||||
|
self.assertEqual(self._get_summary_line(Exception.args), "args")
|
||||||
|
self.assertEqual(self._get_summary_line(memoryview.obj), "obj")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_member_descriptor(self):
|
||||||
|
# Currently these attributes are implemented as member descriptors
|
||||||
|
# in CPython.
|
||||||
|
self.assertEqual(self._get_summary_line(complex.real), "real")
|
||||||
|
self.assertEqual(self._get_summary_line(range.start), "start")
|
||||||
|
self.assertEqual(self._get_summary_line(slice.start), "start")
|
||||||
|
self.assertEqual(self._get_summary_line(property.fget), "fget")
|
||||||
|
self.assertEqual(self._get_summary_line(StopIteration.value), "value")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_slot_descriptor(self):
|
||||||
|
class Point:
|
||||||
|
__slots__ = 'x', 'y'
|
||||||
|
self.assertEqual(self._get_summary_line(Point.x), "x")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_dict_attr_descriptor(self):
|
||||||
|
class NS:
|
||||||
|
pass
|
||||||
|
self.assertEqual(self._get_summary_line(NS.__dict__['__dict__']),
|
||||||
|
"__dict__")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_structseq_member_descriptor(self):
|
||||||
|
self.assertEqual(self._get_summary_line(type(sys.hash_info).width),
|
||||||
|
"width")
|
||||||
|
self.assertEqual(self._get_summary_line(type(sys.flags).debug),
|
||||||
|
"debug")
|
||||||
|
self.assertEqual(self._get_summary_line(type(sys.version_info).major),
|
||||||
|
"major")
|
||||||
|
self.assertEqual(self._get_summary_line(type(sys.float_info).max),
|
||||||
|
"max")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_namedtuple_field_descriptor(self):
|
||||||
|
Box = namedtuple('Box', ('width', 'height'))
|
||||||
|
self.assertEqual(self._get_summary_lines(Box.width), """\
|
||||||
|
Alias for field number 0
|
||||||
|
""")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_property(self):
|
||||||
|
class Rect:
|
||||||
|
@property
|
||||||
|
def area(self):
|
||||||
|
'''Area of the rect'''
|
||||||
|
return self.w * self.h
|
||||||
|
|
||||||
|
self.assertEqual(self._get_summary_lines(Rect.area), """\
|
||||||
|
Area of the rect
|
||||||
|
""")
|
||||||
|
self.assertIn("""
|
||||||
|
| area
|
||||||
|
| Area of the rect
|
||||||
|
""", pydoc.plain(pydoc.render_doc(Rect)))
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_custom_non_data_descriptor(self):
|
||||||
|
class Descr:
|
||||||
|
def __get__(self, obj, cls):
|
||||||
|
if obj is None:
|
||||||
|
return self
|
||||||
|
return 42
|
||||||
|
class X:
|
||||||
|
attr = Descr()
|
||||||
|
|
||||||
|
text = pydoc.plain(pydoc.render_doc(X.attr))
|
||||||
|
self.assertEqual(self._get_summary_lines(X.attr), """\
|
||||||
|
<test.test_pydoc.TestDescriptions.test_custom_non_data_descriptor.<locals>.Descr object>""")
|
||||||
|
|
||||||
|
X.attr.__doc__ = 'Custom descriptor'
|
||||||
|
self.assertEqual(self._get_summary_lines(X.attr), """\
|
||||||
|
<test.test_pydoc.TestDescriptions.test_custom_non_data_descriptor.<locals>.Descr object>""")
|
||||||
|
|
||||||
|
X.attr.__name__ = 'foo'
|
||||||
|
self.assertEqual(self._get_summary_lines(X.attr), """\
|
||||||
|
foo(...)
|
||||||
|
Custom descriptor
|
||||||
|
""")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_custom_data_descriptor(self):
|
||||||
|
class Descr:
|
||||||
|
def __get__(self, obj, cls):
|
||||||
|
if obj is None:
|
||||||
|
return self
|
||||||
|
return 42
|
||||||
|
def __set__(self, obj, cls):
|
||||||
|
1/0
|
||||||
|
class X:
|
||||||
|
attr = Descr()
|
||||||
|
|
||||||
|
text = pydoc.plain(pydoc.render_doc(X.attr))
|
||||||
|
self.assertEqual(self._get_summary_lines(X.attr), "")
|
||||||
|
|
||||||
|
X.attr.__doc__ = 'Custom descriptor'
|
||||||
|
text = pydoc.plain(pydoc.render_doc(X.attr))
|
||||||
|
self.assertEqual(self._get_summary_lines(X.attr), """\
|
||||||
|
Custom descriptor
|
||||||
|
""")
|
||||||
|
|
||||||
|
X.attr.__name__ = 'foo'
|
||||||
|
text = pydoc.plain(pydoc.render_doc(X.attr))
|
||||||
|
self.assertEqual(self._get_summary_lines(X.attr), """\
|
||||||
|
foo
|
||||||
|
Custom descriptor
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
class PydocServerTest(unittest.TestCase):
|
class PydocServerTest(unittest.TestCase):
|
||||||
"""Tests for pydoc._start_server"""
|
"""Tests for pydoc._start_server"""
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Improved support of custom data descriptors in :func:`help` and
|
||||||
|
:mod:`pydoc`.
|
Loading…
Add table
Add a link
Reference in a new issue