Repository: numpy/numpydoc

Issue:
BUG: Reference de-duplication isn't parallel-safe
Currently Numpydoc is marked as a parallel-safe sphinx plugin, but the renaming of duplicate references in ``rename_references`` is not parallel-safe because ``reference_offset`` will be [0] in each process at the start of the function call.

I'm running into conflicts when building the Astropy docs:

```
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/stats/biweight.py:docstring of astropy.stats.biweight_midcorrelation:70: WARNING: duplicate citation R11, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/modeling/index.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/stats/biweight.py:docstring of astropy.stats.biweight_midcovariance:138: WARNING: duplicate citation R33, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.coordinates.BaseCoordinateFrame.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/stats/biweight.py:docstring of astropy.stats.biweight_midvariance:101: WARNING: duplicate citation R56, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.coordinates.EarthLocation.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/stats/biweight.py:docstring of astropy.stats.biweight_midvariance:103: WARNING: duplicate citation R66, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.coordinates.EarthLocation.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/stats/bayesian_blocks.py:docstring of astropy.stats.FitnessFunc:7: WARNING: Unknown target name: "scargle2012".
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/stats/bayesian_blocks.py:docstring of astropy.stats.FitnessFunc:26: WARNING: Unknown target name: "scargle2012".
docstring of astropy.stats.LombScargle.false_alarm_probability:62: WARNING: duplicate citation R1313, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.coordinates.SkyCoord.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/modeling/polynomial.py:docstring of astropy.modeling.polynomial.SIP:55: WARNING: duplicate citation R11, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.stats.biweight_midcorrelation.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/modeling/functional_models.py:docstring of astropy.modeling.functional_models.AiryDisk2D:91: WARNING: duplicate citation R11, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.stats.biweight_midcorrelation.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/modeling/functional_models.py:docstring of astropy.modeling.functional_models.Gaussian2D:133: WARNING: duplicate citation R33, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.stats.biweight_midcovariance.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/modeling/optimizers.py:docstring of astropy.modeling.optimizers.SLSQP:19: WARNING: duplicate citation R99, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.coordinates.Galactic.rst
/Users/tom/Dropbox/Code/Astropy/astropy/astropy/modeling/optimizers.py:docstring of astropy.modeling.optimizers.Simplex:18: WARNING: duplicate citation R1111, other instance in /Users/tom/Dropbox/Code/Astropy/astropy/docs/api/astropy.stats.LombScargle.rst
```

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 7174840..3d680a5 100644
--- a/README.rst
+++ b/README.rst
@@ -11,16 +11,16 @@
 numpydoc -- Numpy's Sphinx extensions
 =====================================
 
-Numpy's documentation uses several custom extensions to Sphinx.  These
-are shipped in this ``numpydoc`` package, in case you want to make use
-of them in third-party projects.
+This package provides the ``numpydoc`` Sphinx extension for handling
+docstrings formatted according to the `NumPy documentation format
+<https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt>`__.
+The extension also adds the code description directives
+``np:function``, ``np-c:function``, etc.
 
-The ``numpydoc`` extension provides support for the Numpy docstring format in
-Sphinx, and adds the code description directives ``np:function``,
-``np-c:function``, etc.  that support the Numpy docstring syntax.
+For usage information, please refer to the `documentation
+<https://numpydoc.readthedocs.io/>`_.
 
-See `numpydoc docstring guide <https://numpydoc.readthedocs.io/en/latest/format.html>`_
-for how to write docs that use this extension, and the `user guide <https://numpydoc.readthedocs.io>`_
-
-Numpydoc inserts a hook into Sphinx's autodoc that converts docstrings
-following the Numpy/Scipy format to a form palatable to Sphinx.
+The `numpydoc docstring guide
+<https://numpydoc.readthedocs.io/en/latest/format.html>`_ explains how
+to write docs formatted for this extension, and the `user guide
+<https://numpydoc.readthedocs.io>`_ explains how to use it with Sphinx.
diff --git a/doc/example.py b/doc/example.py
index 425c892..c55d64d 100644
--- a/doc/example.py
+++ b/doc/example.py
@@ -62,6 +62,7 @@ def foo(var1, var2, long_var_name='hi'):
         Explanation of return value named `describe`.
     out : type
         Explanation of `out`.
+    type_without_description
 
     Other Parameters
     ----------------
diff --git a/doc/format.rst b/doc/format.rst
index d67ddbc..814f301 100644
--- a/doc/format.rst
+++ b/doc/format.rst
@@ -387,6 +387,12 @@ The sections of a function's docstring are:
     should not be required to understand it.  References are numbered, starting
     from one, in the order in which they are cited.
 
+    .. warning:: **References will break tables**
+
+        Where references like [1] appear in a tables within a numpydoc
+        docstring, the table markup will be broken by numpydoc processing.  See
+        `numpydoc issue #130 <https://github.com/numpy/numpydoc/issues/130>`_
+
 14. **Examples**
 
     An optional section for examples, using the `doctest
@@ -443,6 +449,13 @@ The sections of a function's docstring are:
     ``import matplotlib.pyplot as plt``. All other imports, including the
     demonstrated function, must be explicit.
 
+    When matplotlib is imported in the example, the Example code will be
+    wrapped in `matplotlib's Sphinx `plot` directive
+    <http://matplotlib.org/sampledoc/extensions.html>`_.  When matplotlib is
+    not explicitly imported, `.. plot::` can be used directly if
+    `matplotlib.sphinxext.plot_directive` is loaded as a Sphinx extension in
+    ``conf.py``.
+
 
 Documenting classes
 -------------------
diff --git a/doc/install.rst b/doc/install.rst
index ea19389..260b4ee 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -9,8 +9,8 @@ The extension is available from:
 * `numpydoc on GitHub <https://github.com/numpy/numpydoc/>`_
 
 'numpydoc' should be added to the ``extensions`` option in your Sphinx
-``conf.py``.
-
+``conf.py``. (Note that `sphinx.ext.autosummary` will automatically be loaded
+as well.)
 
 Sphinx config options
 =====================
@@ -38,6 +38,10 @@ numpydoc_citation_re : str
   should be mangled to avoid conflicts due to
   duplication across the documentation.  Defaults
   to ``[\w-]+``.
+numpydoc_use_blockquotes : bool
+  Until version 0.8, parameter definitions were shown as blockquotes, rather
+  than in a definition list.  If your styling requires blockquotes, switch
+  this config option to True.  This option will be removed in version 0.10.
 numpydoc_edit_link : bool
   .. deprecated:: edit your HTML template instead
 
diff --git a/numpydoc/__init__.py b/numpydoc/__init__.py
index 5d64ad7..30dba8f 100644
--- a/numpydoc/__init__.py
+++ b/numpydoc/__init__.py
@@ -2,4 +2,7 @@ from __future__ import division, absolute_import, print_function
 
 __version__ = '0.8.0.dev0'
 
-from .numpydoc import setup
+
+def setup(app, *args, **kwargs):
+    from .numpydoc import setup
+    return setup(app, *args, **kwargs)
diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py
index b1bab1d..6ca6292 100644
--- a/numpydoc/docscrape.py
+++ b/numpydoc/docscrape.py
@@ -13,6 +13,15 @@ import copy
 import sys
 
 
+def strip_blank_lines(l):
+    "Remove leading and trailing blank lines from a list of lines"
+    while l and not l[0].strip():
+        del l[0]
+    while l and not l[-1].strip():
+        del l[-1]
+    return l
+
+
 class Reader(object):
     """A line-based string reader.
 
@@ -214,6 +223,7 @@ class NumpyDocString(collections.Mapping):
 
             desc = r.read_to_next_unindented_line()
             desc = dedent_lines(desc)
+            desc = strip_blank_lines(desc)
 
             params.append((arg_name, arg_type, desc))
 
@@ -404,7 +414,8 @@ class NumpyDocString(collections.Mapping):
                     out += ['%s : %s' % (param, param_type)]
                 else:
                     out += [param]
-                out += self._str_indent(desc)
+                if desc and ''.join(desc).strip():
+                    out += self._str_indent(desc)
             out += ['']
         return out
 
diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py
index d2bc923..087ddaf 100644
--- a/numpydoc/docscrape_sphinx.py
+++ b/numpydoc/docscrape_sphinx.py
@@ -31,6 +31,7 @@ class SphinxDocString(NumpyDocString):
 
     def load_config(self, config):
         self.use_plots = config.get('use_plots', False)
+        self.use_blockquotes = config.get('use_blockquotes', False)
         self.class_members_toctree = config.get('class_members_toctree', True)
         self.template = config.get('template', None)
         if self.template is None:
@@ -66,19 +67,28 @@ class SphinxDocString(NumpyDocString):
         return self['Extended Summary'] + ['']
 
     def _str_returns(self, name='Returns'):
+        if self.use_blockquotes:
+            typed_fmt = '**%s** : %s'
+            untyped_fmt = '**%s**'
+        else:
+            typed_fmt = '%s : %s'
+            untyped_fmt = '%s'
+
         out = []
         if self[name]:
             out += self._str_field_list(name)
             out += ['']
             for param, param_type, desc in self[name]:
                 if param_type:
-                    out += self._str_indent(['**%s** : %s' % (param.strip(),
-                                                              param_type)])
+                    out += self._str_indent([typed_fmt % (param.strip(),
+                                                          param_type)])
                 else:
-                    out += self._str_indent([param.strip()])
-                if desc:
+                    out += self._str_indent([untyped_fmt % param.strip()])
+                if desc and self.use_blockquotes:
                     out += ['']
-                    out += self._str_indent(desc, 8)
+                elif not desc:
+                    desc = ['..']
+                out += self._str_indent(desc, 8)
                 out += ['']
         return out
 
@@ -117,7 +127,7 @@ class SphinxDocString(NumpyDocString):
         relies on Sphinx's plugin mechanism.
         """
         param = param.strip()
-        display_param = '**%s**' % param
+        display_param = ('**%s**' if self.use_blockquotes else '%s') % param
 
         if not fake_autosummary:
             return display_param, desc
@@ -191,9 +201,12 @@ class SphinxDocString(NumpyDocString):
                                                           param_type)])
                 else:
                     out += self._str_indent([display_param])
-                if desc:
-                    out += ['']  # produces a blockquote, rather than a dt/dd
-                    out += self._str_indent(desc, 8)
+                if desc and self.use_blockquotes:
+                    out += ['']
+                elif not desc:
+                    # empty definition
+                    desc = ['..']
+                out += self._str_indent(desc, 8)
                 out += ['']
 
         return out
@@ -262,7 +275,6 @@ class SphinxDocString(NumpyDocString):
         out = []
         if self[name]:
             out += self._str_header(name)
-            out += ['']
             content = textwrap.dedent("\n".join(self[name])).split("\n")
             out += content
             out += ['']
@@ -281,6 +293,7 @@ class SphinxDocString(NumpyDocString):
         if self['Warnings']:
             out = ['.. warning::', '']
             out += self._str_indent(self['Warnings'])
+            out += ['']
         return out
 
     def _str_index(self):
@@ -297,6 +310,7 @@ class SphinxDocString(NumpyDocString):
                 out += ['   single: %s' % (', '.join(references))]
             else:
                 out += ['   %s: %s' % (section, ','.join(references))]
+        out += ['']
         return out
 
     def _str_references(self):
diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py
index c14fb0e..2b259af 100644
--- a/numpydoc/numpydoc.py
+++ b/numpydoc/numpydoc.py
@@ -21,14 +21,19 @@ from __future__ import division, absolute_import, print_function
 import sys
 import re
 import pydoc
-import sphinx
 import inspect
 import collections
+import hashlib
+
+from docutils.nodes import citation, Text
+import sphinx
+from sphinx.addnodes import pending_xref, desc_content
 
 if sphinx.__version__ < '1.0.1':
     raise RuntimeError("Sphinx 1.0.1 or newer is required")
 
 from .docscrape_sphinx import get_doc_object, SphinxDocString
+from . import __version__
 
 if sys.version_info[0] >= 3:
     sixu = lambda s: s
@@ -36,9 +41,12 @@ else:
     sixu = lambda s: unicode(s, 'unicode_escape')
 
 
-def rename_references(app, what, name, obj, options, lines,
-                      reference_offset=[0]):
-    # replace reference numbers so that there are no duplicates
+HASH_LEN = 12
+
+
+def rename_references(app, what, name, obj, options, lines):
+    # decorate reference numbers so that there are no duplicates
+    # these are later undecorated in the doctree, in relabel_references
     references = set()
     for line in lines:
         line = line.strip()
@@ -48,19 +56,51 @@ def rename_references(app, what, name, obj, options, lines,
             references.add(m.group(1))
 
     if references:
-        for r in references:
-            if r.isdigit():
-                new_r = sixu("R%d") % (reference_offset[0] + int(r))
-            else:
-                new_r = sixu("%s%d") % (r, reference_offset[0])
+        # we use a hash to mangle the reference name to avoid invalid names
+        sha = hashlib.sha256()
+        sha.update(name.encode('utf8'))
+        prefix = 'R' + sha.hexdigest()[:HASH_LEN]
 
+        for r in references:
+            new_r = prefix + '-' + r
             for i, line in enumerate(lines):
                 lines[i] = lines[i].replace(sixu('[%s]_') % r,
                                             sixu('[%s]_') % new_r)
                 lines[i] = lines[i].replace(sixu('.. [%s]') % r,
                                             sixu('.. [%s]') % new_r)
 
-        reference_offset[0] += len(references)
+
+def _ascend(node, cls):
+    while node and not isinstance(node, cls):
+        node = node.parent
+    return node
+
+
+def relabel_references(app, doc):
+    # Change 'hash-ref' to 'ref' in label text
+    for citation_node in doc.traverse(citation):
+        if _ascend(citation_node, desc_content) is None:
+            # no desc node in ancestry -> not in a docstring
+            # XXX: should we also somehow check it's in a References section?
+            continue
+        label_node = citation_node[0]
+        prefix, _, new_label = label_node[0].astext().partition('-')
+        assert len(prefix) == HASH_LEN + 1
+        new_text = Text(new_label)
+        label_node.replace(label_node[0], new_text)
+
+        for id in citation_node['backrefs']:
+            ref = doc.ids[id]
+            ref_text = ref[0]
+
+            # Sphinx has created pending_xref nodes with [reftext] text.
+            def matching_pending_xref(node):
+                return (isinstance(node, pending_xref) and
+                        node[0].astext() == '[%s]' % ref_text)
+
+            for xref_node in ref.parent.traverse(matching_pending_xref):
+                xref_node.replace(xref_node[0], Text('[%s]' % new_text))
+            ref.replace(ref_text, new_text.copy())
 
 
 DEDUPLICATION_TAG = '    !! processed by numpydoc !!'
@@ -71,6 +111,7 @@ def mangle_docstrings(app, what, name, obj, options, lines):
         return
 
     cfg = {'use_plots': app.config.numpydoc_use_plots,
+           'use_blockquotes': app.config.numpydoc_use_blockquotes,
            'show_class_members': app.config.numpydoc_show_class_members,
            'show_inherited_class_members':
            app.config.numpydoc_show_inherited_class_members,
@@ -137,8 +178,10 @@ def setup(app, get_doc_object_=get_doc_object):
 
     app.connect('autodoc-process-docstring', mangle_docstrings)
     app.connect('autodoc-process-signature', mangle_signature)
+    app.connect('doctree-read', relabel_references)
     app.add_config_value('numpydoc_edit_link', None, False)
     app.add_config_value('numpydoc_use_plots', None, False)
+    app.add_config_value('numpydoc_use_blockquotes', None, False)
     app.add_config_value('numpydoc_show_class_members', True, True)
     app.add_config_value('numpydoc_show_inherited_class_members', True, True)
     app.add_config_value('numpydoc_class_members_toctree', True, True)
@@ -148,7 +191,10 @@ def setup(app, get_doc_object_=get_doc_object):
     app.add_domain(NumpyPythonDomain)
     app.add_domain(NumpyCDomain)
 
-    metadata = {'parallel_read_safe': True}
+    app.setup_extension('sphinx.ext.autosummary')
+
+    metadata = {'version': __version__,
+                'parallel_read_safe': True}
     return metadata
 
 # ------------------------------------------------------------------------------
diff --git a/setup.py b/setup.py
index a2dc46a..ea42ecd 100644
--- a/setup.py
+++ b/setup.py
@@ -3,18 +3,15 @@ from __future__ import division, print_function
 import sys
 import os
 
+import setuptools  # may monkeypatch distutils in some versions. # noqa
 from distutils.command.sdist import sdist
-import setuptools
 from distutils.core import setup
 
+from numpydoc import __version__ as version
+
 if sys.version_info[:2] < (2, 7) or (3, 0) <= sys.version_info[0:2] < (3, 4):
     raise RuntimeError("Python version 2.7 or >= 3.4 required.")
 
-with open('numpydoc/__init__.py') as fid:
-    for line in fid:
-        if line.startswith('__version__'):
-            version = line.strip().split()[-1][1:-1]
-            break
 
 def read(fname):
     """Utility function to get README.rst into long_description.


Test patch:
diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py
index 81f914e..2fb4eb5 100644
--- a/numpydoc/tests/test_docscrape.py
+++ b/numpydoc/tests/test_docscrape.py
@@ -1,6 +1,7 @@
 # -*- encoding:utf-8 -*-
 from __future__ import division, absolute_import, print_function
 
+import re
 import sys
 import textwrap
 import warnings
@@ -55,7 +56,7 @@ doc_txt = '''\
   -------
   out : ndarray
       The drawn samples, arranged according to `shape`.  If the
-      shape given is (m,n,...), then the shape of `out` is is
+      shape given is (m,n,...), then the shape of `out` is
       (m,n,...,N).
 
       In other words, each entry ``out[i,j,...,:]`` is an N-dimensional
@@ -63,6 +64,7 @@ doc_txt = '''\
   list of str
       This is not a real return value.  It exists to test
       anonymous return values.
+  no_description
 
   Other Parameters
   ----------------
@@ -171,7 +173,7 @@ def test_parameters():
     arg, arg_type, desc = doc['Parameters'][1]
     assert_equal(arg_type, '(N, N) ndarray')
     assert desc[0].startswith('Covariance matrix')
-    assert doc['Parameters'][0][-1][-2] == '   (1+2+3)/3'
+    assert doc['Parameters'][0][-1][-1] == '   (1+2+3)/3'
 
 
 def test_other_parameters():
@@ -183,7 +185,7 @@ def test_other_parameters():
 
 
 def test_returns():
-    assert_equal(len(doc['Returns']), 2)
+    assert_equal(len(doc['Returns']), 3)
     arg, arg_type, desc = doc['Returns'][0]
     assert_equal(arg, 'out')
     assert_equal(arg_type, 'ndarray')
@@ -196,6 +198,11 @@ def test_returns():
     assert desc[0].startswith('This is not a real')
     assert desc[-1].endswith('anonymous return values.')
 
+    arg, arg_type, desc = doc['Returns'][2]
+    assert_equal(arg, 'no_description')
+    assert_equal(arg_type, '')
+    assert not ''.join(desc).strip()
+
 
 def test_yields():
     section = doc_yields['Yields']
@@ -316,11 +323,19 @@ def test_index():
     assert_equal(len(doc['index']['refguide']), 2)
 
 
-def non_blank_line_by_line_compare(a, b):
+def _strip_blank_lines(s):
+    "Remove leading, trailing and multiple blank lines"
+    s = re.sub(r'^\s*\n', '', s)
+    s = re.sub(r'\n\s*$', '', s)
+    s = re.sub(r'\n\s*\n', r'\n\n', s)
+    return s
+
+
+def line_by_line_compare(a, b):
     a = textwrap.dedent(a)
     b = textwrap.dedent(b)
-    a = [l.rstrip() for l in a.split('\n') if l.strip()]
-    b = [l.rstrip() for l in b.split('\n') if l.strip()]
+    a = [l.rstrip() for l in _strip_blank_lines(a).split('\n')]
+    b = [l.rstrip() for l in _strip_blank_lines(b).split('\n')]
     assert_list_equal(a, b)
 
 
@@ -328,7 +343,7 @@ def test_str():
     # doc_txt has the order of Notes and See Also sections flipped.
     # This should be handled automatically, and so, one thing this test does
     # is to make sure that See Also precedes Notes in the output.
-    non_blank_line_by_line_compare(str(doc),
+    line_by_line_compare(str(doc),
 """numpy.multivariate_normal(mean, cov, shape=None, spam=None)
 
 Draw values from a multivariate normal distribution with specified
@@ -345,7 +360,6 @@ mean : (N,) ndarray
     .. math::
 
        (1+2+3)/3
-
 cov : (N, N) ndarray
     Covariance matrix of the distribution.
 shape : tuple of ints
@@ -357,7 +371,7 @@ Returns
 -------
 out : ndarray
     The drawn samples, arranged according to `shape`.  If the
-    shape given is (m,n,...), then the shape of `out` is is
+    shape given is (m,n,...), then the shape of `out` is
     (m,n,...,N).
 
     In other words, each entry ``out[i,j,...,:]`` is an N-dimensional
@@ -365,6 +379,7 @@ out : ndarray
 list of str
     This is not a real return value.  It exists to test
     anonymous return values.
+no_description
 
 Other Parameters
 ----------------
@@ -387,6 +402,7 @@ Certain warnings apply.
 
 See Also
 --------
+
 `some`_, `other`_, `funcs`_
 
 `otherfunc`_
@@ -438,7 +454,7 @@ standard deviation:
 
 
 def test_yield_str():
-    non_blank_line_by_line_compare(str(doc_yields),
+    line_by_line_compare(str(doc_yields),
 """Test generator
 
 Yields
@@ -455,7 +471,7 @@ int
 
 def test_sphinx_str():
     sphinx_doc = SphinxDocString(doc_txt)
-    non_blank_line_by_line_compare(str(sphinx_doc),
+    line_by_line_compare(str(sphinx_doc),
 """
 .. index:: random
    single: random;distributions, random;gauss
@@ -468,56 +484,51 @@ of the one-dimensional normal distribution to higher dimensions.
 
 :Parameters:
 
-    **mean** : (N,) ndarray
-
+    mean : (N,) ndarray
         Mean of the N-dimensional distribution.
 
         .. math::
 
            (1+2+3)/3
 
-    **cov** : (N, N) ndarray
-
+    cov : (N, N) ndarray
         Covariance matrix of the distribution.
 
-    **shape** : tuple of ints
-
+    shape : tuple of ints
         Given a shape of, for example, (m,n,k), m*n*k samples are
         generated, and packed in an m-by-n-by-k arrangement.  Because
         each sample is N-dimensional, the output shape is (m,n,k,N).
 
 :Returns:
 
-    **out** : ndarray
-
+    out : ndarray
         The drawn samples, arranged according to `shape`.  If the
-        shape given is (m,n,...), then the shape of `out` is is
+        shape given is (m,n,...), then the shape of `out` is
         (m,n,...,N).
 
         In other words, each entry ``out[i,j,...,:]`` is an N-dimensional
         value drawn from the distribution.
 
     list of str
-
         This is not a real return value.  It exists to test
         anonymous return values.
 
-:Other Parameters:
+    no_description
+        ..
 
-    **spam** : parrot
+:Other Parameters:
 
+    spam : parrot
         A parrot off its mortal coil.
 
 :Raises:
 
-    **RuntimeError**
-
+    RuntimeError
         Some error
 
 :Warns:
 
-    **RuntimeWarning**
-
+    RuntimeWarning
         Some warning
 
 .. warning::
@@ -580,21 +591,18 @@ standard deviation:
 
 def test_sphinx_yields_str():
     sphinx_doc = SphinxDocString(doc_yields_txt)
-    non_blank_line_by_line_compare(str(sphinx_doc),
+    line_by_line_compare(str(sphinx_doc),
 """Test generator
 
 :Yields:
 
-    **a** : int
-
+    a : int
         The number of apples.
 
-    **b** : int
-
+    b : int
         The number of bananas.
 
     int
-
         The number of unknowns.
 """)
 
@@ -855,6 +863,46 @@ def test_plot_examples():
     assert str(doc).count('plot::') == 1, str(doc)
 
 
+def test_use_blockquotes():
+    cfg = dict(use_blockquotes=True)
+    doc = SphinxDocString("""
+    Parameters
+    ----------
+    abc : def
+        ghi
+    jkl
+        mno
+
+    Returns
+    -------
+    ABC : DEF
+        GHI
+    JKL
+        MNO
+    """, config=cfg)
+    line_by_line_compare(str(doc), '''
+    :Parameters:
+
+        **abc** : def
+
+            ghi
+
+        **jkl**
+
+            mno
+
+    :Returns:
+
+        **ABC** : DEF
+
+            GHI
+
+        **JKL**
+
+            MNO
+    ''')
+
+
 def test_class_members():
 
     class Dummy(object):
@@ -960,6 +1008,7 @@ class_doc_txt = """
     f : callable ``f(t, y, *f_args)``
         Aaa.
     jac : callable ``jac(t, y, *jac_args)``
+
         Bbb.
 
     Attributes
@@ -994,7 +1043,7 @@ class_doc_txt = """
 
 def test_class_members_doc():
     doc = ClassDoc(None, class_doc_txt)
-    non_blank_line_by_line_compare(str(doc),
+    line_by_line_compare(str(doc),
     """
     Foo
 
@@ -1030,9 +1079,7 @@ def test_class_members_doc():
     Methods
     -------
     a
-
     b
-
     c
 
     .. index::
@@ -1076,18 +1123,16 @@ def test_class_members_doc_sphinx():
             return None
 
     doc = SphinxClassDoc(Foo, class_doc_txt)
-    non_blank_line_by_line_compare(str(doc),
+    line_by_line_compare(str(doc),
     """
     Foo
 
     :Parameters:
 
-        **f** : callable ``f(t, y, *f_args)``
-
+        f : callable ``f(t, y, *f_args)``
             Aaa.
 
-        **jac** : callable ``jac(t, y, *jac_args)``
-
+        jac : callable ``jac(t, y, *jac_args)``
             Bbb.
 
     .. rubric:: Examples
@@ -1096,22 +1141,30 @@ def test_class_members_doc_sphinx():
 
     :Attributes:
 
-        **t** : float
+        t : float
             Current time.
-        **y** : ndarray
+
+        y : ndarray
             Current variable values.
 
             * hello
             * world
+
         :obj:`an_attribute <an_attribute>` : float
             Test attribute
-        **no_docstring** : str
+
+        no_docstring : str
             But a description
-        **no_docstring2** : str
+
+        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
 
@@ -1128,22 +1181,19 @@ def test_class_members_doc_sphinx():
 
 def test_templated_sections():
     doc = SphinxClassDoc(None, class_doc_txt,
-                         config={'template': jinja2.Template('{{examples}}{{parameters}}')})
-    non_blank_line_by_line_compare(str(doc),
+                         config={'template': jinja2.Template('{{examples}}\n{{parameters}}')})
+    line_by_line_compare(str(doc),
     """
     .. rubric:: Examples
 
     For usage examples, see `ode`.
 
-
     :Parameters:
 
-        **f** : callable ``f(t, y, *f_args)``
-
+        f : callable ``f(t, y, *f_args)``
             Aaa.
 
-        **jac** : callable ``jac(t, y, *jac_args)``
-
+        jac : callable ``jac(t, y, *jac_args)``
             Bbb.
 
     """)