Repository: numpy/numpydoc

Issue:
Unknown Section error has no context.
Similar to #95 the "Unknown section: X" error has no context, making it really hard to find and fix :-/

Discussion:

Patch:
diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py
index 074a7f7..45883d0 100644
--- a/numpydoc/docscrape.py
+++ b/numpydoc/docscrape.py
@@ -136,7 +136,7 @@ class NumpyDocString(collections.Mapping):
 
     def __setitem__(self, key, val):
         if key not in self._parsed_data:
-            warn("Unknown section %s" % key)
+            self._error_location("Unknown section %s" % key, error=False)
         else:
             self._parsed_data[key] = val
 
@@ -331,19 +331,8 @@ class NumpyDocString(collections.Mapping):
                 section = (s.capitalize() for s in section.split(' '))
                 section = ' '.join(section)
                 if self.get(section):
-                    if hasattr(self, '_obj'):
-                        # we know where the docs came from:
-                        try:
-                            filename = inspect.getsourcefile(self._obj)
-                        except TypeError:
-                            filename = None
-                        msg = ("The section %s appears twice in "
-                               "the docstring of %s in %s." %
-                               (section, self._obj, filename))
-                        raise ValueError(msg)
-                    else:
-                        msg = ("The section %s appears twice" % section)
-                        raise ValueError(msg)
+                    self._error_location("The section %s appears twice"
+                                         % section)
 
             if section in ('Parameters', 'Returns', 'Yields', 'Raises',
                            'Warns', 'Other Parameters', 'Attributes',
@@ -356,6 +345,20 @@ class NumpyDocString(collections.Mapping):
             else:
                 self[section] = content
 
+    def _error_location(self, msg, error=True):
+        if hasattr(self, '_obj'):
+            # we know where the docs came from:
+            try:
+                filename = inspect.getsourcefile(self._obj)
+            except TypeError:
+                filename = None
+            msg = msg + (" in the docstring of %s in %s."
+                         % (self._obj, filename))
+        if error:
+            raise ValueError(msg)
+        else:
+            warn(msg)
+
     # string conversion routines
 
     def _str_header(self, name, symbol='-'):


Test patch:
diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py
index 297a0ac..2dc45e3 100644
--- a/numpydoc/tests/test_docscrape.py
+++ b/numpydoc/tests/test_docscrape.py
@@ -3,6 +3,7 @@ from __future__ import division, absolute_import, print_function
 
 import sys
 import textwrap
+import warnings
 
 import jinja2
 
@@ -151,13 +152,16 @@ def test_signature():
     assert doc['Signature'].startswith('numpy.multivariate_normal(')
     assert doc['Signature'].endswith('spam=None)')
 
+
 def test_summary():
     assert doc['Summary'][0].startswith('Draw values')
     assert doc['Summary'][-1].endswith('covariance.')
 
+
 def test_extended_summary():
     assert doc['Extended Summary'][0].startswith('The multivariate normal')
 
+
 def test_parameters():
     assert_equal(len(doc['Parameters']), 3)
     assert_equal([n for n,_,_ in doc['Parameters']], ['mean','cov','shape'])
@@ -167,6 +171,7 @@ def test_parameters():
     assert desc[0].startswith('Covariance matrix')
     assert doc['Parameters'][0][-1][-2] == '   (1+2+3)/3'
 
+
 def test_other_parameters():
     assert_equal(len(doc['Other Parameters']), 1)
     assert_equal([n for n,_,_ in doc['Other Parameters']], ['spam'])
@@ -174,6 +179,7 @@ def test_other_parameters():
     assert_equal(arg_type, 'parrot')
     assert desc[0].startswith('A parrot off its mortal coil')
 
+
 def test_returns():
     assert_equal(len(doc['Returns']), 2)
     arg, arg_type, desc = doc['Returns'][0]
@@ -188,6 +194,7 @@ def test_returns():
     assert desc[0].startswith('This is not a real')
     assert desc[-1].endswith('anonymous return values.')
 
+
 def test_yields():
     section = doc_yields['Yields']
     assert_equal(len(section), 3)
@@ -200,6 +207,7 @@ def test_yields():
         assert desc[0].startswith('The number of')
         assert desc[0].endswith(end)
 
+
 def test_returnyield():
     doc_text = """
 Test having returns and yields.
@@ -289,26 +297,31 @@ def test_notes():
     assert doc['Notes'][-1].endswith('definite.')
     assert_equal(len(doc['Notes']), 17)
 
+
 def test_references():
     assert doc['References'][0].startswith('..')
     assert doc['References'][-1].endswith('2001.')
 
+
 def test_examples():
     assert doc['Examples'][0].startswith('>>>')
     assert doc['Examples'][-1].endswith('True]')
 
+
 def test_index():
     assert_equal(doc['index']['default'], 'random')
     assert_equal(len(doc['index']), 2)
     assert_equal(len(doc['index']['refguide']), 2)
 
-def non_blank_line_by_line_compare(a,b):
+
+def non_blank_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()]
     assert_list_equal(a, b)
 
+
 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
@@ -595,15 +608,18 @@ doc2 = NumpyDocString("""
         If None, the index is into the flattened array, otherwise along
         the specified axis""")
 
+
 def test_parameters_without_extended_description():
     assert_equal(len(doc2['Parameters']), 2)
 
+
 doc3 = NumpyDocString("""
     my_signature(*params, **kwds)
 
     Return this and that.
     """)
 
+
 def test_escape_stars():
     signature = str(doc3).split('\n')[0]
     assert_equal(signature, 'my_signature(\*params, \*\*kwds)')
@@ -614,14 +630,17 @@ def test_escape_stars():
     fdoc = FunctionDoc(func=my_func)
     assert_equal(fdoc['Signature'], 'my_func(a, b, \*\*kwargs)')
 
+
 doc4 = NumpyDocString(
     """a.conj()
 
     Return an array with all complex-valued elements conjugated.""")
 
+
 def test_empty_extended_summary():
     assert_equal(doc4['Extended Summary'], [])
 
+
 doc5 = NumpyDocString(
     """
     a.something()
@@ -637,18 +656,21 @@ doc5 = NumpyDocString(
         If needed
     """)
 
+
 def test_raises():
     assert_equal(len(doc5['Raises']), 1)
     name,_,desc = doc5['Raises'][0]
     assert_equal(name,'LinAlgException')
     assert_equal(desc,['If array is singular.'])
 
+
 def test_warns():
     assert_equal(len(doc5['Warns']), 1)
     name,_,desc = doc5['Warns'][0]
     assert_equal(name,'SomeWarning')
     assert_equal(desc,['If needed'])
 
+
 def test_see_also():
     doc6 = NumpyDocString(
     """
@@ -726,12 +748,45 @@ def test_see_also_print():
     assert('    some relationship' in s)
     assert(':func:`func_d`' in s)
 
+
+def test_unknown_section():
+    doc_text = """
+Test having an unknown section
+
+Mope
+----
+This should be ignored and warned about
+"""
+
+    class BadSection(object):
+        """Class with bad section.
+
+        Nope
+        ----
+        This class has a nope section.
+        """
+        pass
+
+    with warnings.catch_warnings(record=True) as w:
+        NumpyDocString(doc_text)
+        assert len(w) == 1
+        assert "Unknown section Mope" == str(w[0].message)
+
+    with warnings.catch_warnings(record=True) as w:
+        SphinxClassDoc(BadSection)
+        assert len(w) == 1
+        assert_true('test_docscrape.test_unknown_section.<locals>.BadSection'
+                    in str(w[0].message)
+                    or 'test_docscrape.BadSection' in str(w[0].message))
+
+
 doc7 = NumpyDocString("""
 
         Doc starts on second line.
 
         """)
 
+
 def test_empty_first_line():
     assert doc7['Summary'][0].startswith('Doc starts')
 
@@ -762,6 +817,7 @@ def test_unicode():
     assert isinstance(doc['Summary'][0], str)
     assert doc['Summary'][0] == 'öäöäöäöäöåååå'
 
+
 def test_plot_examples():
     cfg = dict(use_plots=True)
 
@@ -785,6 +841,7 @@ def test_plot_examples():
     """, config=cfg)
     assert str(doc).count('plot::') == 1, str(doc)
 
+
 def test_class_members():
 
     class Dummy(object):
@@ -866,6 +923,7 @@ def test_class_members():
         else:
             assert 'Spammity index' in str(doc), str(doc)
 
+
 def test_duplicate_signature():
     # Duplicate function signatures occur e.g. in ufuncs, when the
     # automatic mechanism adds one, and a more detailed comes from the
@@ -911,6 +969,7 @@ class_doc_txt = """
     For usage examples, see `ode`.
 """
 
+
 def test_class_members_doc():
     doc = ClassDoc(None, class_doc_txt)
     non_blank_line_by_line_compare(str(doc),
@@ -949,6 +1008,7 @@ def test_class_members_doc():
 
     """)
 
+
 def test_class_members_doc_sphinx():
     class Foo:
         @property
@@ -997,6 +1057,7 @@ def test_class_members_doc_sphinx():
 
     """)
 
+
 def test_templated_sections():
     doc = SphinxClassDoc(None, class_doc_txt,
                          config={'template': jinja2.Template('{{examples}}{{parameters}}')})
@@ -1020,8 +1081,6 @@ def test_templated_sections():
     """)
 
 
-
-
 if __name__ == "__main__":
     import nose
     nose.run()