Repository: numpy/numpydoc

Issue:
Need Attributes displayable like param list
I've not investigated when/whether there was a change in numpydoc, but scikit-learn has long relied on Attributes (as per docstring, rather than than descriptors on the class) being listed like Parameters. See old docs at http://scikit-learn.org/0.18/modules/generated/sklearn.linear_model.LogisticRegression.html for instance.

We've merged a recent numpydoc into scikit-learn master, which uses `_str_member_list` to render Attributes, rather than `_str_param_list`. See new docs at http://scikit-learn.org/dev/modules/generated/sklearn.linear_model.LogisticRegression.html. The listing looks more like methods, and hence like what you get out of autosummary for descriptors, but it altogether (and particularly the type spec) looks wrong. I'm not sure how those type specs work well for any users of Attributes.

To fix scikit-learn's needs, I propose that we extend the Jinja templating of numpy_docstring.rst such that instead of just rendering strings with e.g. `{{ attributes }}`, we have Jinja statements like:

```
{% members_list Attributes %}
```
or perhaps Jinja filters like:
```
{% raw_attributes | members_list %}
```
but unless we want to compose filters, I'm not sure the benefit of this.

Would this be acceptable?

Discussion:

Patch:
diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py
index d8a495e..a555ed0 100644
--- a/numpydoc/docscrape_sphinx.py
+++ b/numpydoc/docscrape_sphinx.py
@@ -79,21 +79,136 @@ class SphinxDocString(NumpyDocString):
                 out += ['']
         return out
 
-    def _str_param_list(self, name):
+    def _process_param(self, param, desc, autosum):
+        """Determine how to display a parameter
+
+        Emulates autosummary behavior if autosum is not None.
+
+        Parameters
+        ----------
+        param : str
+            The name of the parameter
+        desc : list of str
+            The parameter description as given in the docstring. This is
+            ignored when autosummary logic applies.
+        autosum : list or None
+            If a list, autosummary-style behaviour will apply for params
+            that are attributes of the class and have a docstring.
+            Names for autosummary generation will be appended to this list.
+
+            If None, autosummary is disabled.
+
+        Returns
+        -------
+        display_param : str
+            The marked up parameter name for display. This may include a link
+            to the corresponding attribute's own documentation.
+        desc : list of str
+            A list of description lines. This may be identical to the input
+            ``desc``, if ``autosum is None`` or ``param`` is not a class
+            attribute, or it will be a summary of the class attribute's
+            docstring.
+
+        Notes
+        -----
+        This does not have the autosummary functionality to display a method's
+        signature, and hence is not used to format methods.  It may be
+        complicated to incorporate autosummary's signature mangling, as it
+        relies on Sphinx's plugin mechanism.
+        """
+        param = param.strip()
+        display_param = '**%s**' % param
+
+        if autosum is None:
+            return display_param, desc
+
+        param_obj = getattr(self._obj, param, None)
+        if not (callable(param_obj)
+                or isinstance(param_obj, property)
+                or inspect.isgetsetdescriptor(param_obj)):
+            param_obj = None
+        obj_doc = pydoc.getdoc(param_obj)
+
+        if not (param_obj and obj_doc):
+            return display_param, desc
+
+        prefix = getattr(self, '_name', '')
+        if prefix:
+            autosum_prefix = '~%s.' % prefix
+            link_prefix = '%s.' % prefix
+        else:
+            autosum_prefix = ''
+            link_prefix = ''
+
+        # Referenced object has a docstring
+        autosum.append("    %s%s" % (autosum_prefix, param))
+        display_param = ':obj:`%s <%s%s>`' % (param,
+                                              link_prefix,
+                                              param)
+        if obj_doc:
+            # Overwrite desc. Take summary logic of autosummary
+            desc = re.split('\n\s*\n', obj_doc.strip(), 1)[0]
+            # XXX: Should this have DOTALL?
+            #      It does not in autosummary
+            m = re.search(r"^([A-Z].*?\.)(?:\s|$)",
+                          ' '.join(desc.split()))
+            if m:
+                desc = m.group(1).strip()
+            else:
+                desc = desc.partition('\n')[0]
+            desc = desc.split('\n')
+        return display_param, desc
+
+    def _str_param_list(self, name, fake_autosummary=False):
+        """Generate RST for a listing of parameters or similar
+
+        Parameter names are displayed as bold text, and descriptions
+        are in blockquotes.  Descriptions may therefore contain block
+        markup as well.
+
+        Parameters
+        ----------
+        name : str
+            Section name (e.g. Parameters)
+        fake_autosummary : bool
+            When True, the parameter names may correspond to attributes of the
+            object beign documented, usually ``property`` instances on a class.
+            In this case, names will be linked to fuller descriptions.
+
+        Returns
+        -------
+        rst : list of str
+        """
         out = []
         if self[name]:
+            if fake_autosummary:
+                autosum = []
+            else:
+                autosum = None
+
             out += self._str_field_list(name)
             out += ['']
             for param, param_type, desc in self[name]:
+                display_param, desc = self._process_param(param, desc, autosum)
+
                 if param_type:
-                    out += self._str_indent(['**%s** : %s' % (param.strip(),
-                                                              param_type)])
+                    out += self._str_indent(['%s : %s' % (display_param,
+                                                          param_type)])
                 else:
-                    out += self._str_indent(['**%s**' % param.strip()])
+                    out += self._str_indent([display_param])
                 if desc:
-                    out += ['']
+                    out += ['']  # produces a blockquote, rather than a dt/dd
                     out += self._str_indent(desc, 8)
                 out += ['']
+
+            if fake_autosummary and autosum:
+                if self.class_members_toctree:
+                    autosum.insert(0, '    :toctree:')
+                autosum.insert(0, '.. autosummary::')
+                out += ['..', '    HACK to make autogen generate docs:']
+                out += self._str_indent(autosum, 4)
+                out += ['']
+
         return out
 
     @property
@@ -130,7 +245,7 @@ class SphinxDocString(NumpyDocString):
                         or inspect.isgetsetdescriptor(param_obj)):
                     param_obj = None
 
-                if param_obj and (pydoc.getdoc(param_obj) or not desc):
+                if param_obj and pydoc.getdoc(param_obj):
                     # Referenced object has a docstring
                     autosum += ["   %s%s" % (prefix, param)]
                 else:
@@ -250,7 +365,8 @@ class SphinxDocString(NumpyDocString):
             'notes': self._str_section('Notes'),
             'references': self._str_references(),
             'examples': self._str_examples(),
-            'attributes': self._str_member_list('Attributes'),
+            'attributes': self._str_param_list('Attributes',
+                                               fake_autosummary=True),
             'methods': self._str_member_list('Methods'),
         }
         ns = dict((k, '\n'.join(v)) for k, v in ns.items())


Test patch:
diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py
index 297a0ac..2ae51c6 100644
--- a/numpydoc/tests/test_docscrape.py
+++ b/numpydoc/tests/test_docscrape.py
@@ -17,6 +17,8 @@ from numpydoc.docscrape_sphinx import (SphinxDocString, SphinxClassDoc,
 from nose.tools import (assert_equal, assert_raises, assert_list_equal,
                         assert_true)
 
+assert_list_equal.__self__.maxDiff = None
+
 if sys.version_info[0] >= 3:
     sixu = lambda s: s
 else:
@@ -897,8 +899,17 @@ class_doc_txt = """
         Current time.
     y : ndarray
         Current variable values.
-    x : float
-        Some parameter
+
+        * hello
+        * world
+    an_attribute : float
+        The docstring is printed instead
+    no_docstring : str
+        But a description
+    no_docstring2 : str
+    multiline_sentence
+    midword_period
+    no_period
 
     Methods
     -------
@@ -934,8 +945,17 @@ def test_class_members_doc():
         Current time.
     y : ndarray
         Current variable values.
-    x : float
-        Some parameter
+
+        * hello
+        * world
+    an_attribute : float
+        The docstring is printed instead
+    no_docstring : str
+        But a description
+    no_docstring2 : str
+    multiline_sentence
+    midword_period
+    no_period
 
     Methods
     -------
@@ -952,10 +972,38 @@ def test_class_members_doc():
 def test_class_members_doc_sphinx():
     class Foo:
         @property
-        def x(self):
+        def an_attribute(self):
             """Test attribute"""
             return None
 
+        @property
+        def no_docstring(self):
+            return None
+
+        @property
+        def no_docstring2(self):
+            return None
+
+        @property
+        def multiline_sentence(self):
+            """This is a
+            sentence. It spans multiple lines."""
+            return None
+
+        @property
+        def midword_period(self):
+            """The sentence for numpy.org."""
+            return None
+
+        @property
+        def no_period(self):
+            """This does not have a period
+            so we truncate its summary to the first linebreak
+
+            Apparently.
+            """
+            return None
+
     doc = SphinxClassDoc(Foo, class_doc_txt)
     non_blank_line_by_line_compare(str(doc),
     """
@@ -975,17 +1023,36 @@ def test_class_members_doc_sphinx():
 
     For usage examples, see `ode`.
 
-    .. rubric:: Attributes
-
-    .. autosummary::
-       :toctree:
-
-       x
-
-    =====  ==========
-    **t**  (float) Current time.
-    **y**  (ndarray) Current variable values.
-    =====  ==========
+    :Attributes:
+
+        **t** : float
+            Current time.
+        **y** : ndarray
+            Current variable values.
+
+            * hello
+            * world
+        :obj:`an_attribute <an_attribute>` : float
+            Test attribute
+        **no_docstring** : str
+            But a description
+        **no_docstring2** : str
+        :obj:`multiline_sentence <multiline_sentence>`
+            This is a sentence.
+        :obj:`midword_period <midword_period>`
+            The sentence for numpy.org.
+        :obj:`no_period <no_period>`
+            This does not have a period
+
+    ..
+        HACK to make autogen generate docs:
+        .. autosummary::
+            :toctree:
+
+            an_attribute
+            multiline_sentence
+            midword_period
+            no_period
 
     .. rubric:: Methods