[Python-checkins] bpo-35619: Improve support of custom data descriptors in help() and pydoc. (GH-11366)

Serhiy Storchaka webhook-mailer at python.org
Tue Jan 15 03:53:22 EST 2019


https://github.com/python/cpython/commit/efcf82f94572abcdbd70336e0b2c3d0f4df280bc
commit: efcf82f94572abcdbd70336e0b2c3d0f4df280bc
branch: master
author: Serhiy Storchaka <storchaka at gmail.com>
committer: GitHub <noreply at github.com>
date: 2019-01-15T10:53:18+02:00
summary:

bpo-35619: Improve support of custom data descriptors in help() and pydoc. (GH-11366)

files:
A Misc/NEWS.d/next/Library/2018-12-30-19-50-36.bpo-35619.ZRXdhy.rst
M Lib/pydoc.py
M Lib/test/test_pydoc.py

diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 59f6e3935135..daa7205bd74e 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 @@ def document(self, object, name=None, *args):
         # 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 @@ def spill(msg, attrs, predicate):
                     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 @@ def spilldescriptors(msg, attrs, predicate):
                 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 @@ def docroutine(self, object, name=None, mod=None,
             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 @@ def spill(msg, attrs, predicate):
                     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 @@ def spilldescriptors(msg, attrs, predicate):
                 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 @@ def docroutine(self, object, name=None, mod=None, cl=None):
             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 ffe80fc06fc6..c2bd9f3012c1 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -743,15 +743,6 @@ def test_splitdoc_with_description(self):
         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 @@ def _get_summary_line(o):
         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 @@ def test_bound_python_method(self):
         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 @@ def test_module_level_callable(self):
         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 000000000000..fe278e63dd86
--- /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`.



More information about the Python-checkins mailing list