Repository: numpy/numpydoc

Issue:
Anonymous return values have their types populated in the name slot of the tuple. 
I noticed an inconsistency, when using numpydoc version 0.6.0 in python2.7 on Ubuntu. The parsed return section information returns different styles of tuple depending on if the return value is anoymous or not. 

Here is a minimal working example: 

```python

def mwe():
        from numpydoc.docscrape import NumpyDocString
        docstr = (
            'Returns\n'
            '----------\n'
            'int\n'
            '    can return an anoymous integer\n'
            'out : ndarray\n'
            '    can return a named value\n'
        )
        doc = NumpyDocString(docstr)
        returns = doc._parsed_data['Returns']
        print(returns)
```

This results in 

```python
[(u'int', '', [u'can return an anoymous integer']),
 (u'out', u'ndarray', [u'can return a named value'])]
```

However judging by tests (due to lack of docs), I believe it was indented that each value in the returns list should be a tuple of `(arg, arg_type, arg_desc)`. Therefore we should see this instead: 

```python
[('', u'int', [u'can return an anoymous integer']),
 (u'out', u'ndarray', [u'can return a named value'])]
```

My current workaround is this:

```python
        for p_name, p_type, p_descr in returns:
            if not p_type:
                p_name = ''
                p_type = p_name
```


Discussion:
timhoffm: Note: CI currently fails because of pip changes. Should be fixed by #174.
jnothman: if you've not had to modify any tests, how do we know this affects output?

timhoffm: As said above, I didn't run any tests so far myself. It's apparent that one parser for "Parameters" and "Returns" cannot get both right. I'm confident that the proposed code change itself is correct. What has to be still shown is that the calling code and tests work with that (it might be that they partly compensate for the original bug). I thought I'd use the tests in CI for that, but CI is already prematurely failing for different reasons.


timhoffm: Rebased onto master.

*Note:* The tests do currently fail. Waiting for #176 before doing any further changes to fix tests.
timhoffm: PR updated.

Single element returns params such as:
~~~
Returns
-------
int
    The return value.
~~~
were detected as names. I.e. `int` was considered a name. This logical error has been fixed such that `int` is now a type and the name is empty.

As a consequence, `int` is not formatted bold anymore. This is consistent with the formatting of types in patterns like `x : int` and a prerequisite for type references like ``:class:`MyClass` `` to work in this position.
larsoner: @timhoffm can you rebase? Then I can take a look and hopefully merge
rgommers: I've taken the liberty of fixing the merge conflict. The only nontrivial change was deciding where the new heading `Receives` goes; I added it to `'Returns', 'Yields', 'Raises', 'Warns'`.

Patch:
diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py
index 02afd88..32245a9 100644
--- a/numpydoc/docscrape.py
+++ b/numpydoc/docscrape.py
@@ -220,7 +220,7 @@ class NumpyDocString(Mapping):
             else:
                 yield name, self._strip(data[2:])
 
-    def _parse_param_list(self, content):
+    def _parse_param_list(self, content, single_element_is_type=False):
         r = Reader(content)
         params = []
         while not r.eof():
@@ -228,7 +228,10 @@ class NumpyDocString(Mapping):
             if ' : ' in header:
                 arg_name, arg_type = header.split(' : ')[:2]
             else:
-                arg_name, arg_type = header, ''
+                if single_element_is_type:
+                    arg_name, arg_type = '', header
+                else:
+                    arg_name, arg_type = header, ''
 
             desc = r.read_to_next_unindented_line()
             desc = dedent_lines(desc)
@@ -393,10 +396,12 @@ class NumpyDocString(Mapping):
                     self._error_location("The section %s appears twice"
                                          % section)
 
-            if section in ('Parameters', 'Returns', 'Yields', 'Receives',
-                           'Raises', 'Warns', 'Other Parameters', 'Attributes',
+            if section in ('Parameters', 'Other Parameters', 'Attributes',
                            'Methods'):
                 self[section] = self._parse_param_list(content)
+            elif section in ('Returns', 'Yields', 'Raises', 'Warns', 'Receives'):
+                self[section] = self._parse_param_list(
+                    content, single_element_is_type=True)
             elif section.startswith('.. index::'):
                 self['index'] = self._parse_index(section, content)
             elif section == 'See Also':
@@ -452,10 +457,12 @@ class NumpyDocString(Mapping):
         if self[name]:
             out += self._str_header(name)
             for param in self[name]:
+                parts = []
+                if param.name:
+                    parts.append(param.name)
                 if param.type:
-                    out += ['%s : %s' % (param.name, param.type)]
-                else:
-                    out += [param.name]
+                    parts.append(param.type)
+                out += [' : '.join(parts)]
                 if param.desc and ''.join(param.desc).strip():
                     out += self._str_indent(param.desc)
             out += ['']
@@ -637,7 +644,7 @@ class ClassDoc(NumpyDocString):
         if _members is ALL:
             _members = None
         _exclude = config.get('exclude-members', [])
-        
+
         if config.get('show_class_members', True) and _exclude is not ALL:
             def splitlines_x(s):
                 if not s:
@@ -649,7 +656,7 @@ class ClassDoc(NumpyDocString):
                 if not self[field]:
                     doc_list = []
                     for name in sorted(items):
-                        if (name in _exclude or 
+                        if (name in _exclude or
                                 (_members and name not in _members)):
                             continue
                         try:
diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py
index 9b23235..aad64c7 100644
--- a/numpydoc/docscrape_sphinx.py
+++ b/numpydoc/docscrape_sphinx.py
@@ -70,19 +70,19 @@ class SphinxDocString(NumpyDocString):
         return self['Extended Summary'] + ['']
 
     def _str_returns(self, name='Returns'):
-        typed_fmt = '**%s** : %s'
-        untyped_fmt = '**%s**'
+        named_fmt = '**%s** : %s'
+        unnamed_fmt = '%s'
 
         out = []
         if self[name]:
             out += self._str_field_list(name)
             out += ['']
             for param in self[name]:
-                if param.type:
-                    out += self._str_indent([typed_fmt % (param.name.strip(),
+                if param.name:
+                    out += self._str_indent([named_fmt % (param.name.strip(),
                                                           param.type)])
                 else:
-                    out += self._str_indent([untyped_fmt % param.name.strip()])
+                    out += self._str_indent([unnamed_fmt % param.type.strip()])
                 if not param.desc:
                     out += self._str_indent(['..'], 8)
                 else:
@@ -209,12 +209,13 @@ class SphinxDocString(NumpyDocString):
                 display_param, desc = self._process_param(param.name,
                                                           param.desc,
                                                           fake_autosummary)
-
+                parts = []
+                if display_param:
+                    parts.append(display_param)
                 if param.type:
-                    out += self._str_indent(['%s : %s' % (display_param,
-                                                          param.type)])
-                else:
-                    out += self._str_indent([display_param])
+                    parts.append(param.type)
+                out += self._str_indent([' : '.join(parts)])
+
                 if desc and self.use_blockquotes:
                     out += ['']
                 elif not desc:
@@ -376,8 +377,8 @@ class SphinxDocString(NumpyDocString):
             'yields': self._str_returns('Yields'),
             'receives': self._str_returns('Receives'),
             'other_parameters': self._str_param_list('Other Parameters'),
-            'raises': self._str_param_list('Raises'),
-            'warns': self._str_param_list('Warns'),
+            'raises': self._str_returns('Raises'),
+            'warns': self._str_returns('Warns'),
             'warnings': self._str_warnings(),
             'see_also': self._str_see_also(func_role),
             'notes': self._str_section('Notes'),


Test patch:
diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py
index b4b7e03..e5e3f1f 100644
--- a/numpydoc/tests/test_docscrape.py
+++ b/numpydoc/tests/test_docscrape.py
@@ -211,14 +211,14 @@ def test_returns():
     assert desc[-1].endswith('distribution.')
 
     arg, arg_type, desc = doc['Returns'][1]
-    assert arg == 'list of str'
-    assert arg_type == ''
+    assert arg == ''
+    assert arg_type == 'list of str'
     assert desc[0].startswith('This is not a real')
     assert desc[-1].endswith('anonymous return values.')
 
     arg, arg_type, desc = doc['Returns'][2]
-    assert arg == 'no_description'
-    assert arg_type == ''
+    assert arg == ''
+    assert arg_type == 'no_description'
     assert not ''.join(desc).strip()
 
 
@@ -227,7 +227,7 @@ def test_yields():
     assert len(section) == 3
     truth = [('a', 'int', 'apples.'),
              ('b', 'int', 'bananas.'),
-             ('int', '', 'unknowns.')]
+             ('', 'int', 'unknowns.')]
     for (arg, arg_type, desc), (arg_, arg_type_, end) in zip(section, truth):
         assert arg == arg_
         assert arg_type == arg_type_
@@ -594,11 +594,11 @@ of the one-dimensional normal distribution to higher dimensions.
         In other words, each entry ``out[i,j,...,:]`` is an N-dimensional
         value drawn from the distribution.
 
-    **list of str**
+    list of str
         This is not a real return value.  It exists to test
         anonymous return values.
 
-    **no_description**
+    no_description
         ..
 
 :Other Parameters:
@@ -608,12 +608,12 @@ of the one-dimensional normal distribution to higher dimensions.
 
 :Raises:
 
-    **RuntimeError**
+    RuntimeError
         Some error
 
 :Warns:
 
-    **RuntimeWarning**
+    RuntimeWarning
         Some warning
 
 .. warning::
@@ -687,7 +687,7 @@ def test_sphinx_yields_str():
     **b** : int
         The number of bananas.
 
-    **int**
+    int
         The number of unknowns.
 """)
 
@@ -754,16 +754,18 @@ doc5 = NumpyDocString(
 
 def test_raises():
     assert len(doc5['Raises']) == 1
-    name, _, desc = doc5['Raises'][0]
-    assert name == 'LinAlgException'
-    assert desc == ['If array is singular.']
+    param = doc5['Raises'][0]
+    assert param.name == ''
+    assert param.type == 'LinAlgException'
+    assert param.desc == ['If array is singular.']
 
 
 def test_warns():
     assert len(doc5['Warns']) == 1
-    name, _, desc = doc5['Warns'][0]
-    assert name == 'SomeWarning'
-    assert desc == ['If needed']
+    param = doc5['Warns'][0]
+    assert param.name == ''
+    assert param.type == 'SomeWarning'
+    assert param.desc == ['If needed']
 
 
 def test_see_also():
@@ -995,7 +997,7 @@ def test_use_blockquotes():
 
             GHI
 
-        **JKL**
+        JKL
 
             MNO
     ''')