diff options
| author | Serhiy Storchaka <storchaka@gmail.com> | 2019-01-15 10:53:18 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-01-15 10:53:18 +0200 |
| commit | efcf82f94572abcdbd70336e0b2c3d0f4df280bc (patch) | |
| tree | 3af379fca24788ac7196139940ba250148c84114 | |
| parent | 6fe9c446f8302553952f63fc6d96be4dfa48ceba (diff) | |
| download | cpython-git-efcf82f94572abcdbd70336e0b2c3d0f4df280bc.tar.gz | |
bpo-35619: Improve support of custom data descriptors in help() and pydoc. (GH-11366)
| -rw-r--r-- | Lib/pydoc.py | 52 | ||||
| -rw-r--r-- | Lib/test/test_pydoc.py | 174 | ||||
| -rw-r--r-- | Misc/NEWS.d/next/Library/2018-12-30-19-50-36.bpo-35619.ZRXdhy.rst | 2 |
3 files changed, 182 insertions, 46 deletions
diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 59f6e39351..daa7205bd7 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -137,12 +137,6 @@ def stripid(text): # The behaviour of %p is implementation-dependent in terms of case. 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): """ Returns True if fn is a bound method, regardless of whether @@ -158,7 +152,7 @@ def _is_bound_method(fn): def allmethods(cl): methods = {} - for key, value in inspect.getmembers(cl, _is_some_method): + for key, value in inspect.getmembers(cl, inspect.isroutine): methods[key] = 1 for base in cl.__bases__: 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; # think 'super' and how it is a descriptor (which raises the exception # 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: if inspect.ismodule(object): return self.docmodule(*args) if inspect.isclass(object): return self.docclass(*args) if inspect.isroutine(object): return self.docroutine(*args) except AttributeError: pass - if isinstance(object, property): return self.docproperty(*args) + if inspect.isdatadescriptor(object): return self.docdata(*args) return self.docother(*args) def fail(self, object, name=None, *args): @@ -809,7 +801,7 @@ class HTMLDoc(Doc): except Exception: # Some descriptors may meet a failure in their __get__. # (bug #1785) - push(self._docdescriptor(name, value, mod)) + push(self.docdata(value, name, mod)) else: push(self.document(value, name, mod, funcs, classes, mdict, object)) @@ -822,7 +814,7 @@ class HTMLDoc(Doc): hr.maybe() push(msg) for name, kind, homecls, value in ok: - push(self._docdescriptor(name, value, mod)) + push(self.docdata(value, name, mod)) return attrs def spilldata(msg, attrs, predicate): @@ -994,32 +986,27 @@ class HTMLDoc(Doc): doc = doc and '<dd><tt>%s</tt></dd>' % 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 = [] push = results.append if name: push('<dl><dt><strong>%s</strong></dt>\n' % name) - if value.__doc__ is not None: - doc = self.markup(getdoc(value), self.preformat) + if object.__doc__ is not None: + doc = self.markup(getdoc(object), self.preformat) push('<dd><tt>%s</tt></dd>\n' % doc) push('</dl>\n') return ''.join(results) - def docproperty(self, object, name=None, mod=None, cl=None): - """Produce html documentation for a property.""" - return self._docdescriptor(name, object, mod) + docproperty = docdata def docother(self, object, name=None, mod=None, *ignored): """Produce HTML documentation for a data object.""" lhs = name and '<strong>%s</strong> = ' % name or '' 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): """Generate an HTML index for a directory of modules.""" modpkgs = [] @@ -1292,7 +1279,7 @@ location listed above. except Exception: # Some descriptors may meet a failure in their __get__. # (bug #1785) - push(self._docdescriptor(name, value, mod)) + push(self.docdata(value, name, mod)) else: push(self.document(value, name, mod, object)) @@ -1304,7 +1291,7 @@ location listed above. hr.maybe() push(msg) for name, kind, homecls, value in ok: - push(self._docdescriptor(name, value, mod)) + push(self.docdata(value, name, mod)) return attrs def spilldata(msg, attrs, predicate): @@ -1420,26 +1407,21 @@ location listed above. doc = getdoc(object) or '' 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 = [] push = results.append if name: push(self.bold(name)) push('\n') - doc = getdoc(value) or '' + doc = getdoc(object) or '' if doc: push(self.indent(doc)) push('\n') return ''.join(results) - def docproperty(self, object, name=None, mod=None, cl=None): - """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) + docproperty = docdata def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None): """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 inspect.isclass(object) or inspect.isroutine(object) or - inspect.isgetsetdescriptor(object) or - inspect.ismemberdescriptor(object) or - isinstance(object, property)): + inspect.isdatadescriptor(object)): # If the passed object is a piece of data or an instance, # document its available methods instead of its value. object = type(object) diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index ffe80fc06f..c2bd9f3012 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -743,15 +743,6 @@ class PydocDocTest(unittest.TestCase): self.assertEqual(pydoc.splitdoc(example_string), ('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): with test.support.temp_cwd() as test_dir: self.assertFalse(pydoc.ispackage(test_dir)) @@ -1093,6 +1084,12 @@ class TestDescriptions(unittest.TestCase): assert len(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" def test_unbound_python_method(self): self.assertEqual(self._get_summary_line(textwrap.TextWrapper.wrap), @@ -1108,7 +1105,6 @@ class TestDescriptions(unittest.TestCase): t = textwrap.TextWrapper() self.assertEqual(self._get_summary_line(t.wrap), "wrap(text) method of textwrap.TextWrapper instance") - def test_field_order_for_named_tuples(self): Person = namedtuple('Person', ['nickname', 'firstname', 'agegroup']) s = pydoc.render_doc(Person) @@ -1138,6 +1134,164 @@ class TestDescriptions(unittest.TestCase): self.assertEqual(self._get_summary_line(os.stat), "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): """Tests for pydoc._start_server""" diff --git a/Misc/NEWS.d/next/Library/2018-12-30-19-50-36.bpo-35619.ZRXdhy.rst b/Misc/NEWS.d/next/Library/2018-12-30-19-50-36.bpo-35619.ZRXdhy.rst new file mode 100644 index 0000000000..fe278e63dd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-12-30-19-50-36.bpo-35619.ZRXdhy.rst @@ -0,0 +1,2 @@ +Improved support of custom data descriptors in :func:`help` and +:mod:`pydoc`. |
