Repository: numpy/numpydoc

Issue:
syntax to document default values
The `numpydoc` format guide says this about default values:

> Optional keyword parameters have default values, which are
>    displayed as part of the function signature.  They can also be
>    detailed in the description::
>
>      Description of parameter `x` (the default is -1, which implies summation
>      over all axes).
>
>    When a parameter can only assume one of a fixed set of values,
>    those values can be listed in braces, with the default appearing first::
>
>      order : {'C', 'F', 'A'}
>          Description of `order`.

so default values can be documented as
* part of the signature
* mentioned in the parameter description
* for a fixed set of values, the first value of that set

`pandas` and later `xarray` decided to instead document default values as part of the type specification, but with a slightly different syntax:

`pandas` does something like
```
Parameters
----------
a : int, default 0
```

while `xarray` uses a additional colon:
```
Parameters
----------
a : int, default: 0
```

(also, the example set for the `numpydoc_xref_ignore` contains `'default'`)

In order to fix the numerous broken references when building the `xarray` documentation using `napoleon` I have been working on sphinx-doc/sphinx#7690 to preprocess the parameter type specification. However, including support for `xarray`'s default value syntax was rejected because the `sphinx` maintainer didn't want to include a unofficial extension to the format (the PR is still pending).

So here's my question: is any of those two formats "official"? If not, could we make one of them (or both?) official?

Discussion:

Patch:
diff --git a/.coveragerc b/.coveragerc
index 92a2a25..efab5a6 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -4,3 +4,5 @@ source = numpydoc
 include = */numpydoc/*
 omit =
     */setup.py
+    numpydoc/tests/*
+    numpydoc/templates/*
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 895a0b9..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-# After changing this file, check it on:
-#   http://lint.travis-ci.org/
-language: python
-group: travis_latest
-os: linux
-dist: bionic
-
-addons:
-  apt:
-    packages:
-      - texlive
-      - texlive-latex-extra
-      - latexmk
-      - dvipng
-
-cache:
-  directories:
-    - $HOME/.cache/pip
-
-jobs:
-  include:
-    - python: 3.7
-    - python: 3.6
-      env: SPHINX_SPEC="==2.1.0" SPHINXOPTS=""
-    - python: 3.5
-      env: SPHINX_SPEC="==1.6.5" SPHINXOPTS=""
-
-before_install:
-  - pip install --upgrade pip setuptools  # Ensure there is `wheel` support
-  - pip install pytest pytest-cov pydocstyle numpy matplotlib sphinx${SPHINX_SPEC} codecov check-manifest
-
-script:
-  - check-manifest
-  - |
-    python setup.py sdist
-    cd dist
-    pip install numpydoc* -v
-  - pytest -v --pyargs numpydoc
-  # Making sure the command line options work
-  - python -m numpydoc numpydoc.tests.test_main._capture_stdout
-  - echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash
-  - python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout
-  - echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash
-  - pydocstyle --convention numpy ../doc/example.py
-  # Build documentation
-  - |
-    cd ../doc
-    make SPHINXOPTS=$SPHINXOPTS html
-    make SPHINXOPTS=$SPHINXOPTS latexpdf
-
-after_script:
-  - |
-    cd ../dist
-    codecov
diff --git a/doc/example.py b/doc/example.py
index 2652283..b3ba686 100644
--- a/doc/example.py
+++ b/doc/example.py
@@ -71,9 +71,9 @@ def foo(var1, var2, *args, long_var_name='hi', **kwargs):
     Other Parameters
     ----------------
     only_seldom_used_keywords : type
-        Explanation
+        Explanation.
     common_parameters_listed_above : type
-        Explanation
+        Explanation.
 
     Raises
     ------
diff --git a/doc/format.rst b/doc/format.rst
index 9f9744b..173dd5c 100644
--- a/doc/format.rst
+++ b/doc/format.rst
@@ -166,7 +166,7 @@ The sections of a function's docstring are:
      x : type
          Description of parameter `x`.
      y
-         Description of parameter `y` (with type not specified)
+         Description of parameter `y` (with type not specified).
 
    Enclose variables in single backticks.  The colon must be preceded
    by a space, or omitted if the type is absent.
@@ -197,6 +197,13 @@ The sections of a function's docstring are:
      Description of parameter `x` (the default is -1, which implies summation
      over all axes).
 
+   or as part of the type, instead of ``optional``. If the default value would not be
+   used as a value, ``optional`` is preferred. These are all equivalent::
+
+     copy : bool, default True
+     copy : bool, default=True
+     copy : bool, default: True
+
    When a parameter can only assume one of a fixed set of values,
    those values can be listed in braces, with the default appearing first::
 
@@ -303,7 +310,7 @@ The sections of a function's docstring are:
 
       See Also
       --------
-      average : Weighted average
+      average : Weighted average.
 
     When referring to functions in the same sub-module, no prefix is
     needed, and the tree is searched upwards for a match.
@@ -314,7 +321,7 @@ The sections of a function's docstring are:
 
     ::
 
-      fft.fft2 : 2-D fast discrete Fourier transform
+      fft.fft2 : 2-D fast discrete Fourier transform.
 
     When referring to an entirely different module::
 
@@ -329,6 +336,16 @@ The sections of a function's docstring are:
       func_b, func_c_, func_d
       func_e
 
+    If the combination of the function name and the description creates
+    a line that is too long, the entry may be written as two lines, with
+    the function name and colon on the first line, and the description
+    on the next line, indented four spaces::
+
+      See Also
+      --------
+      package.module.submodule.func_a :
+          A somewhat long description of the function.
+
 13. **Notes**
 
     An optional section that provides additional information about the
@@ -426,7 +443,7 @@ The sections of a function's docstring are:
       >>> np.add(1, 2)
       3
 
-      Comment explaining the second example
+      Comment explaining the second example.
 
       >>> np.add([1, 2], [3, 4])
       array([4, 6])
@@ -512,9 +529,9 @@ simply listed by name::
   real
   imag
   x : float
-      The X coordinate
+      The X coordinate.
   y : float
-      The Y coordinate
+      The Y coordinate.
 
 In general, it is not necessary to list class methods.  Those that are
 not part of the public API have names that start with an underscore.
diff --git a/doc/install.rst b/doc/install.rst
index ba799e3..d80e7a7 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -63,15 +63,6 @@ numpydoc_xref_aliases : dict
   aliases/shortcuts used when specifying the types of parameters.
   The keys should not have any spaces. Together with the ``intersphinx``
   extension, you can map to links in any documentation.
-  The default is an empty ``dict``.
-
-  If you have the following ``intersphinx`` namespace configuration::
-
-      intersphinx_mapping = {
-          'python': ('https://docs.python.org/3/', None),
-          'numpy': ('https://docs.scipy.org/doc/numpy', None),
-          ...
-      }
 
   The default ``numpydoc_xref_aliases`` will supply some common ``Python``
   standard library and ``NumPy`` names for you. Then for your module, a useful
@@ -85,14 +76,26 @@ numpydoc_xref_aliases : dict
 
   This option depends on the ``numpydoc_xref_param_type`` option
   being ``True``.
-numpydoc_xref_ignore : set
-    Words not to cross-reference. Most likely, these are common words
+numpydoc_xref_ignore : set or ``"all"``
+    How to handle terms not in ``numpydoc_xref_aliases`` when
+    ``numpydoc_xref_aliases=True``. The value can either be a ``set`` 
+    containing terms to ignore, or ``"all"``. In the former case, the set
+    contains words not to cross-reference. Most likely, these are common words
     used in parameter type descriptions that may be confused for
     classes of the same name. For example::
 
         numpydoc_xref_ignore = {'type', 'optional', 'default'}
 
     The default is an empty set.
+
+    If the ``numpydoc_xref_ignore="all"``, then all unrecognized terms are
+    ignored, i.e. terms not in ``numpydoc_xref_aliases`` are *not* wrapped in
+    ``:obj:`` roles.
+    This configuration parameter may be useful if you only want create
+    cross references for a small number of terms. In this case, including the
+    desired cross reference mappings in ``numpydoc_xref_aliases`` and setting
+    ``numpydoc_xref_ignore="all"`` is more convenient than explicitly listing
+    terms to ignore in a set.
 numpydoc_edit_link : bool
   .. deprecated:: edit your HTML template instead
 
diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py
index d79992c..15b4e10 100644
--- a/numpydoc/docscrape.py
+++ b/numpydoc/docscrape.py
@@ -219,12 +219,14 @@ class NumpyDocString(Mapping):
                 yield name, self._strip(data[2:])
 
     def _parse_param_list(self, content, single_element_is_type=False):
+        content = dedent_lines(content)
         r = Reader(content)
         params = []
         while not r.eof():
             header = r.read().strip()
-            if ' : ' in header:
-                arg_name, arg_type = header.split(' : ', maxsplit=1)
+            if ' :' in header:
+                arg_name, arg_type = header.split(' :', maxsplit=1)
+                arg_name, arg_type = arg_name.strip(), arg_type.strip()
             else:
                 if single_element_is_type:
                     arg_name, arg_type = '', header
@@ -292,7 +294,7 @@ class NumpyDocString(Mapping):
             """Match ':role:`name`' or 'name'."""
             m = self._func_rgx.match(text)
             if not m:
-                raise ParseError("%s is not a item name" % text)
+                self._error_location(f"Error parsing See Also entry {line!r}")
             role = m.group('role')
             name = m.group('name') if role else m.group('name2')
             return name, role, m.end()
@@ -327,7 +329,7 @@ class NumpyDocString(Mapping):
                 rest = list(filter(None, [description]))
                 items.append((funcs, rest))
             else:
-                raise ParseError("%s is not a item name" % line)
+                self._error_location(f"Error parsing See Also entry {line!r}")
         return items
 
     def _parse_index(self, section, content):
@@ -410,14 +412,14 @@ class NumpyDocString(Mapping):
                 self[section] = content
 
     def _error_location(self, msg, error=True):
-        if hasattr(self, '_obj'):
+        if hasattr(self, '_obj') and self._obj is not None:
             # 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))
+            msg += f" in the docstring of {self._obj.__name__}"
+            msg += f" in {filename}." if filename else ""
         if error:
             raise ValueError(msg)
         else:
@@ -429,28 +431,22 @@ class NumpyDocString(Mapping):
         return [name, len(name)*symbol]
 
     def _str_indent(self, doc, indent=4):
-        out = []
-        for line in doc:
-            out += [' '*indent + line]
-        return out
+        return [' '*indent + line for line in doc]
 
     def _str_signature(self):
         if self['Signature']:
             return [self['Signature'].replace('*', r'\*')] + ['']
-        else:
-            return ['']
+        return ['']
 
     def _str_summary(self):
         if self['Summary']:
             return self['Summary'] + ['']
-        else:
-            return []
+        return []
 
     def _str_extended_summary(self):
         if self['Extended Summary']:
             return self['Extended Summary'] + ['']
-        else:
-            return []
+        return []
 
     def _str_param_list(self, name):
         out = []
@@ -523,8 +519,7 @@ class NumpyDocString(Mapping):
             out += ['   :%s: %s' % (section, ', '.join(references))]
         if output_index:
             return out
-        else:
-            return ''
+        return ''
 
     def __str__(self, func_role=''):
         out = []
@@ -544,23 +539,11 @@ class NumpyDocString(Mapping):
         return '\n'.join(out)
 
 
-def indent(str, indent=4):
-    indent_str = ' '*indent
-    if str is None:
-        return indent_str
-    lines = str.split('\n')
-    return '\n'.join(indent_str + l for l in lines)
-
-
 def dedent_lines(lines):
     """Deindent a list of lines maximally"""
     return textwrap.dedent("\n".join(lines)).split("\n")
 
 
-def header(text, style='-'):
-    return text + '\n' + style*len(text) + '\n'
-
-
 class FunctionDoc(NumpyDocString):
     def __init__(self, func, role='func', doc=None, config={}):
         self._f = func
diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py
index ebc828f..38a49ba 100644
--- a/numpydoc/docscrape_sphinx.py
+++ b/numpydoc/docscrape_sphinx.py
@@ -52,10 +52,6 @@ class SphinxDocString(NumpyDocString):
 
     def _str_signature(self):
         return ['']
-        if self['Signature']:
-            return ['``%s``' % self['Signature']] + ['']
-        else:
-            return ['']
 
     def _str_summary(self):
         return self['Summary'] + ['']
@@ -77,7 +73,8 @@ class SphinxDocString(NumpyDocString):
                     param_type = make_xref(
                         param_type,
                         self.xref_aliases,
-                        self.xref_ignore)
+                        self.xref_ignore
+                    )
                 if param.name:
                     out += self._str_indent([named_fmt % (param.name.strip(),
                                                           param_type)])
@@ -217,7 +214,8 @@ class SphinxDocString(NumpyDocString):
                         param_type = make_xref(
                             param_type,
                             self.xref_aliases,
-                            self.xref_ignore)
+                            self.xref_ignore
+                        )
                     parts.append(param_type)
                 out += self._str_indent([' : '.join(parts)])
 
diff --git a/numpydoc/validate.py b/numpydoc/validate.py
index 1fd80e1..be50885 100644
--- a/numpydoc/validate.py
+++ b/numpydoc/validate.py
@@ -425,14 +425,17 @@ def _check_desc(desc, code_no_desc, code_no_upper, code_no_period, **kwargs):
     return errs
 
 
-def validate(func_name):
+def validate(obj_name):
     """
     Validate the docstring.
 
     Parameters
     ----------
-    func_name : function
-        Function whose docstring will be evaluated (e.g. pandas.read_csv).
+    obj_name : str
+        The name of the object whose docstring will be evaluated, e.g.
+        'pandas.read_csv'. The string must include the full, unabbreviated
+        package/module names, i.e. 'pandas.read_csv', not 'pd.read_csv' or
+        'read_csv'.
 
     Returns
     -------
@@ -465,7 +468,7 @@ def validate(func_name):
     they are validated, are not documented more than in the source code of this
     function.
     """
-    doc = Docstring(func_name)
+    doc = Docstring(obj_name)
 
     errs = []
     if not doc.raw_doc:
diff --git a/numpydoc/xref.py b/numpydoc/xref.py
index a4ceaf9..7e98b96 100644
--- a/numpydoc/xref.py
+++ b/numpydoc/xref.py
@@ -95,7 +95,9 @@ DEFAULT_LINKS = {
 
 
 def make_xref(param_type, xref_aliases, xref_ignore):
-    """Enclose str in a :obj: role.
+    """Parse and apply appropriate sphinx role(s) to `param_type`.
+
+    The :obj: role is the default.
 
     Parameters
     ----------
@@ -104,26 +106,41 @@ def make_xref(param_type, xref_aliases, xref_ignore):
     xref_aliases : dict
         Mapping used to resolve common abbreviations and aliases
         to fully qualified names that can be cross-referenced.
-    xref_ignore : set
-        Words not to cross-reference.
+    xref_ignore : set or "all"
+        A set containing words not to cross-reference. Instead of a set, the
+        string 'all' can be given to ignore all unrecognized terms. 
+        Unrecognized terms include those that are not in `xref_aliases` and
+        are not already wrapped in a reST role.
 
     Returns
     -------
     out : str
-        Text with parts that may be wrapped in a
+        Text with fully-qualified names and terms that may be wrapped in a
         ``:obj:`` role.
     """
+    ignore_set = xref_ignore
+    wrap_unknown = True
+    if isinstance(xref_ignore, str):
+        if xref_ignore.lower() == "all":
+            wrap_unknown = False
+            ignore_set = set()
+        else:
+            raise TypeError(
+                "xref_ignore must be a set or 'all', got {}".format(xref_ignore)
+            )
+
     if param_type in xref_aliases:
         link, title = xref_aliases[param_type], param_type
         param_type = link
     else:
         link = title = param_type
 
-    if QUALIFIED_NAME_RE.match(link) and link not in xref_ignore:
+    if QUALIFIED_NAME_RE.match(link) and link not in ignore_set:
         if link != title:
             return ':obj:`%s <%s>`' % (title, link)
-        else:
+        if wrap_unknown:
             return ':obj:`%s`' % link
+        return link
 
     def _split_and_apply_re(s, pattern):
         """
@@ -139,8 +156,7 @@ def make_xref(param_type, xref_aliases, xref_ignore):
                 if pattern.match(tok):
                     results.append(tok)
                 else:
-                    res = make_xref(
-                        tok, xref_aliases, xref_ignore)
+                    res = make_xref(tok, xref_aliases, xref_ignore)
                     # Opening brackets immediately after a role is
                     # bad markup. Detect that and add backslash.
                     # :role:`type`( to :role:`type`\(
diff --git a/setup.cfg b/setup.cfg
index 420fc78..1e9d2d6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,7 +1,7 @@
 [tool:pytest]
 addopts =
     --showlocals --doctest-modules -ra --cov-report= --cov=numpydoc
-    --junit-xml=junit-results.xml --ignore=doc/conf.py
+    --junit-xml=junit-results.xml --ignore=doc/
 junit_family = xunit2
 filterwarnings =
     ignore:'U' mode is deprecated:DeprecationWarning


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..c6eafc5
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,63 @@
+name: numpydoc tests
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+jobs:
+  test:
+    runs-on: ${{ matrix.os }}-latest
+    strategy:
+      matrix:
+        os: [Ubuntu]
+        python-version: [3.6, 3.7, 3.8, 3.9]
+        sphinx-version: ["sphinx==1.6.5", "sphinx==2.1", "sphinx>3.0"]
+    steps:
+    - uses: actions/checkout@v2
+    
+    - name: Python setup
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Setup environment
+      run: |
+        python -m venv venv
+        source venv/bin/activate
+
+    - name: Install
+      run: |
+        python -m pip install --upgrade pip wheel setuptools
+        python -m pip install -r test_requirements.txt -r doc/requirements.txt
+        python -m pip install codecov
+        python -m pip install ${{ matrix.sphinx-version }}
+        python -m pip install .
+        pip list
+
+    - name: Run test suite
+      run: |
+        pytest -v --pyargs .
+
+    - name: Test coverage
+      run: |
+        codecov
+
+    - name: Make sure CLI works
+      run: |
+        python -m numpydoc numpydoc.tests.test_main._capture_stdout
+        echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash
+        python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout
+        echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash
+
+    - name: Setup for doc build
+      run: |
+        # Get scipy-sphinx-theme
+        git submodule update --init
+        sudo apt install texlive texlive-latex-extra latexmk dvipng
+
+    - name: Build documentation
+      run: |
+        make -C doc html SPHINXOPTS="-nT"
+        make -C doc latexpdf SPHINXOPTS="-nT"
diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py
index 2ab0218..080e8ce 100644
--- a/numpydoc/tests/test_docscrape.py
+++ b/numpydoc/tests/test_docscrape.py
@@ -133,7 +133,11 @@ doc_txt = '''\
      :refguide: random;distributions, random;gauss
 
   '''
-doc = NumpyDocString(doc_txt)
+
+@pytest.fixture(params=['','\n    '], ids=["flush", "newline_indented"])
+def doc(request):
+    return NumpyDocString(request.param+doc_txt)
+
 
 doc_yields_txt = """
 Test generator
@@ -169,21 +173,21 @@ c : int
 doc_sent = NumpyDocString(doc_sent_txt)
 
 
-def test_signature():
+def test_signature(doc):
     assert doc['Signature'].startswith('numpy.multivariate_normal(')
     assert doc['Signature'].endswith('spam=None)')
 
 
-def test_summary():
+def test_summary(doc):
     assert doc['Summary'][0].startswith('Draw values')
     assert doc['Summary'][-1].endswith('covariance.')
 
 
-def test_extended_summary():
+def test_extended_summary(doc):
     assert doc['Extended Summary'][0].startswith('The multivariate normal')
 
 
-def test_parameters():
+def test_parameters(doc):
     assert len(doc['Parameters']) == 4
     names = [n for n, _, _ in doc['Parameters']]
     assert all(a == b for a, b in zip(names, ['mean', 'cov', 'shape']))
@@ -205,7 +209,7 @@ def test_parameters():
     assert desc[0].startswith('The type and size')
 
 
-def test_other_parameters():
+def test_other_parameters(doc):
     assert len(doc['Other Parameters']) == 1
     assert [n for n, _, _ in doc['Other Parameters']] == ['spam']
     arg, arg_type, desc = doc['Other Parameters'][0]
@@ -213,7 +217,8 @@ def test_other_parameters():
     assert desc[0].startswith('A parrot off its mortal coil')
 
 
-def test_returns():
+
+def test_returns(doc):
     assert len(doc['Returns']) == 3
     arg, arg_type, desc = doc['Returns'][0]
     assert arg == 'out'
@@ -290,7 +295,8 @@ Notes
 -----
 That should break...
 """
-    assert_raises(ValueError, NumpyDocString, doc_text)
+    with pytest.raises(ValueError, match="The section Notes appears twice"):
+        NumpyDocString(doc_text)
 
     # if we have a numpydoc object, we know where the error came from
     class Dummy:
@@ -327,38 +333,30 @@ That should break...
         Second note.
         """
 
-    try:
+    with pytest.raises(ValueError, match="Dummy class"):
         SphinxClassDoc(Dummy)
-    except ValueError as e:
-        # python 3 version or python 2 version
-        assert ("test_section_twice.<locals>.Dummy" in str(e)
-                or 'test_docscrape.Dummy' in str(e))
 
-    try:
+    with pytest.raises(ValueError, match="dummy_func"):
         SphinxFunctionDoc(dummy_func)
-    except ValueError as e:
-        # python 3 version or python 2 version
-        assert ("test_section_twice.<locals>.dummy_func" in str(e)
-                or 'function dummy_func' in str(e))
 
 
-def test_notes():
+def test_notes(doc):
     assert doc['Notes'][0].startswith('Instead')
     assert doc['Notes'][-1].endswith('definite.')
     assert len(doc['Notes']) == 17
 
 
-def test_references():
+def test_references(doc):
     assert doc['References'][0].startswith('..')
     assert doc['References'][-1].endswith('2001.')
 
 
-def test_examples():
+def test_examples(doc):
     assert doc['Examples'][0].startswith('>>>')
     assert doc['Examples'][-1].endswith('True]')
 
 
-def test_index():
+def test_index(doc):
     assert doc['index']['default'] == 'random'
     assert len(doc['index']) == 2
     assert len(doc['index']['refguide']) == 2
@@ -382,7 +380,7 @@ def line_by_line_compare(a, b, n_lines=None):
         assert aa == bb
 
 
-def test_str():
+def test_str(doc):
     # 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.
@@ -837,13 +835,9 @@ def test_see_also_parse_error():
     --------
     :func:`~foo`
     """)
-    with assert_raises(ParseError) as err:
+    with pytest.raises(ValueError, match="See Also entry ':func:`~foo`'"):
         NumpyDocString(text)
 
-    s1 = str(r":func:`~foo` is not a item name in '\n    z(x,theta)\n\n    See Also\n    --------\n    :func:`~foo`\n    '")
-    s2 = str(err.value)
-    assert s1 == s2
-
 
 def test_see_also_print():
     class Dummy:
@@ -896,19 +890,16 @@ This should be ignored and warned about
         """
         pass
 
-    with warnings.catch_warnings(record=True) as w:
-        warnings.filterwarnings('always', '', UserWarning)
+    with pytest.warns(UserWarning, match="Unknown section Mope") as record:
         NumpyDocString(doc_text)
-        assert len(w) == 1
-        assert "Unknown section Mope" == str(w[0].message)
+    assert len(record) == 1
 
-    with warnings.catch_warnings(record=True) as w:
-        warnings.filterwarnings('always', '', UserWarning)
+    # SphinxClassDoc has _obj.__name__ == "BadSection". Test that this is
+    # included in the message
+    msg_match = "Unknown section Nope in the docstring of BadSection"
+    with pytest.warns(UserWarning, match=msg_match) as record:
         SphinxClassDoc(BadSection)
-        assert len(w) == 1
-        assert('test_docscrape.test_unknown_section.<locals>.BadSection'
-               in str(w[0].message)
-               or 'test_docscrape.BadSection' in str(w[0].message))
+    assert len(record) == 1
 
 
 doc7 = NumpyDocString("""
@@ -921,6 +912,21 @@ doc7 = NumpyDocString("""
 def test_empty_first_line():
     assert doc7['Summary'][0].startswith('Doc starts')
 
+doc8 = NumpyDocString("""
+
+        Parameters with colon and no types:
+
+        Parameters
+        ----------
+
+        data :
+            some stuff, technically invalid
+        """)
+
+
+def test_trailing_colon():
+    assert doc8['Parameters'][0].name == 'data'
+
 
 def test_no_summary():
     str(SphinxDocString("""
diff --git a/numpydoc/tests/test_main.py b/numpydoc/tests/test_main.py
index d7f6657..1ba2549 100644
--- a/numpydoc/tests/test_main.py
+++ b/numpydoc/tests/test_main.py
@@ -87,7 +87,7 @@ def test_render_object_returns_correct_exit_status():
         'numpydoc.tests.test_main._capture_stdout')
     assert exit_status == 0
 
-    with pytest.raises(numpydoc.docscrape.ParseError):
+    with pytest.raises(ValueError):
         numpydoc.__main__.render_object(
             'numpydoc.tests.test_main._invalid_docstring')
 
diff --git a/numpydoc/tests/test_xref.py b/numpydoc/tests/test_xref.py
index 31dfacd..aa14970 100644
--- a/numpydoc/tests/test_xref.py
+++ b/numpydoc/tests/test_xref.py
@@ -1,25 +1,15 @@
 # -*- encoding:utf-8 -*-
 import pytest
-from numpydoc.xref import make_xref
-
-xref_aliases = {
-    # python
-    'sequence': ':term:`python:sequence`',
-    'iterable': ':term:`python:iterable`',
-    'string': 'str',
-    # numpy
-    'array': 'numpy.ndarray',
-    'dtype': 'numpy.dtype',
-    'ndarray': 'numpy.ndarray',
-    'matrix': 'numpy.matrix',
-    'array-like': ':term:`numpy:array_like`',
-    'array_like': ':term:`numpy:array_like`',
-}
+from numpydoc.xref import make_xref, DEFAULT_LINKS
+
+# Use the default numpydoc link mapping
+xref_aliases = DEFAULT_LINKS
+
 
 # Comes mainly from numpy
 data = r"""
 (...) array_like, float, optional
-(...) :term:`numpy:array_like`, :obj:`float`, optional
+(...) :term:`numpy:array_like`, :class:`python:float`, optional
 
 (2,) ndarray
 (2,) :obj:`ndarray <numpy.ndarray>`
@@ -31,37 +21,37 @@ data = r"""
 (..., :obj:`M`, :obj:`N`) :term:`numpy:array_like`
 
 (float, float), optional
-(:obj:`float`, :obj:`float`), optional
+(:class:`python:float`, :class:`python:float`), optional
 
 1-D array or sequence
 1-D :obj:`array <numpy.ndarray>` or :term:`python:sequence`
 
 array of str or unicode-like
-:obj:`array <numpy.ndarray>` of :obj:`str` or unicode-like
+:obj:`array <numpy.ndarray>` of :class:`python:str` or unicode-like
 
 array_like of float
-:term:`numpy:array_like` of :obj:`float`
+:term:`numpy:array_like` of :class:`python:float`
 
 bool or callable
-:obj:`bool` or :obj:`callable`
+:ref:`bool <python:bltin-boolean-values>` or :func:`python:callable`
 
 int in [0, 255]
-:obj:`int` in [0, 255]
+:class:`python:int` in [0, 255]
 
 int or None, optional
-:obj:`int` or :obj:`None`, optional
+:class:`python:int` or :data:`python:None`, optional
 
 list of str or array_like
-:obj:`list` of :obj:`str` or :term:`numpy:array_like`
+:class:`python:list` of :class:`python:str` or :term:`numpy:array_like`
 
 sequence of array_like
 :term:`python:sequence` of :term:`numpy:array_like`
 
 str or pathlib.Path
-:obj:`str` or :obj:`pathlib.Path`
+:class:`python:str` or :obj:`pathlib.Path`
 
 {'', string}, optional
-{'', :obj:`string <str>`}, optional
+{'', :class:`python:str`}, optional
 
 {'C', 'F', 'A', or 'K'}, optional
 {'C', 'F', 'A', or 'K'}, optional
@@ -70,16 +60,16 @@ str or pathlib.Path
 {'linear', 'lower', 'higher', 'midpoint', 'nearest'}
 
 {False, True, 'greedy', 'optimal'}
-{:obj:`False`, :obj:`True`, 'greedy', 'optimal'}
+{:data:`python:False`, :data:`python:True`, 'greedy', 'optimal'}
 
 {{'begin', 1}, {'end', 0}}, {string, int}
-{{'begin', 1}, {'end', 0}}, {:obj:`string <str>`, :obj:`int`}
+{{'begin', 1}, {'end', 0}}, {:class:`python:str`, :class:`python:int`}
 
 callable f'(x,*args)
-:obj:`callable` f'(x,*args)
+:func:`python:callable` f'(x,*args)
 
 callable ``fhess(x, *args)``, optional
-:obj:`callable` ``fhess(x, *args)``, optional
+:func:`python:callable` ``fhess(x, *args)``, optional
 
 spmatrix (format: ``csr``, ``bsr``, ``dia`` or coo``)
 :obj:`spmatrix` (format: ``csr``, ``bsr``, ``dia`` or coo``)
@@ -88,28 +78,123 @@ spmatrix (format: ``csr``, ``bsr``, ``dia`` or coo``)
 :ref:`strftime <strftime-strptime-behavior>`
 
 callable or :ref:`strftime <strftime-strptime-behavior>`
-:obj:`callable` or :ref:`strftime <strftime-strptime-behavior>`
+:func:`python:callable` or :ref:`strftime <strftime-strptime-behavior>`
+
+callable or :ref:`strftime behavior <strftime-strptime-behavior>`
+:func:`python:callable` or :ref:`strftime behavior <strftime-strptime-behavior>`
+
+list(int)
+:class:`python:list`\(:class:`python:int`)
+
+list[int]
+:class:`python:list`\[:class:`python:int`]
+
+dict(str, int)
+:class:`python:dict`\(:class:`python:str`, :class:`python:int`)
+
+dict[str,  int]
+:class:`python:dict`\[:class:`python:str`,  :class:`python:int`]
+
+tuple(float, float)
+:class:`python:tuple`\(:class:`python:float`, :class:`python:float`)
+
+dict[tuple(str, str), int]
+:class:`python:dict`\[:class:`python:tuple`\(:class:`python:str`, :class:`python:str`), :class:`python:int`]
+"""  # noqa: E501
+
+data_ignore_obj = r"""
+(...) array_like, float, optional
+(...) :term:`numpy:array_like`, :class:`python:float`, optional
+
+(2,) ndarray
+(2,) :obj:`ndarray <numpy.ndarray>`
+
+(...,M,N) array_like
+(...,M,N) :term:`numpy:array_like`
+
+(..., M, N) array_like
+(..., M, N) :term:`numpy:array_like`
+
+(float, float), optional
+(:class:`python:float`, :class:`python:float`), optional
+
+1-D array or sequence
+1-D :obj:`array <numpy.ndarray>` or :term:`python:sequence`
+
+array of str or unicode-like
+:obj:`array <numpy.ndarray>` of :class:`python:str` or unicode-like
+
+array_like of float
+:term:`numpy:array_like` of :class:`python:float`
+
+bool or callable
+:ref:`bool <python:bltin-boolean-values>` or :func:`python:callable`
+
+int in [0, 255]
+:class:`python:int` in [0, 255]
+
+int or None, optional
+:class:`python:int` or :data:`python:None`, optional
+
+list of str or array_like
+:class:`python:list` of :class:`python:str` or :term:`numpy:array_like`
+
+sequence of array_like
+:term:`python:sequence` of :term:`numpy:array_like`
+
+str or pathlib.Path
+:class:`python:str` or pathlib.Path
+
+{'', string}, optional
+{'', :class:`python:str`}, optional
+
+{'C', 'F', 'A', or 'K'}, optional
+{'C', 'F', 'A', or 'K'}, optional
+
+{'linear', 'lower', 'higher', 'midpoint', 'nearest'}
+{'linear', 'lower', 'higher', 'midpoint', 'nearest'}
+
+{False, True, 'greedy', 'optimal'}
+{:data:`python:False`, :data:`python:True`, 'greedy', 'optimal'}
+
+{{'begin', 1}, {'end', 0}}, {string, int}
+{{'begin', 1}, {'end', 0}}, {:class:`python:str`, :class:`python:int`}
+
+callable f'(x,*args)
+:func:`python:callable` f'(x,*args)
+
+callable ``fhess(x, *args)``, optional
+:func:`python:callable` ``fhess(x, *args)``, optional
+
+spmatrix (format: ``csr``, ``bsr``, ``dia`` or coo``)
+spmatrix (format: ``csr``, ``bsr``, ``dia`` or coo``)
+
+:ref:`strftime <strftime-strptime-behavior>`
+:ref:`strftime <strftime-strptime-behavior>`
+
+callable or :ref:`strftime <strftime-strptime-behavior>`
+:func:`python:callable` or :ref:`strftime <strftime-strptime-behavior>`
 
 callable or :ref:`strftime behavior <strftime-strptime-behavior>`
-:obj:`callable` or :ref:`strftime behavior <strftime-strptime-behavior>`
+:func:`python:callable` or :ref:`strftime behavior <strftime-strptime-behavior>`
 
 list(int)
-:obj:`list`\(:obj:`int`)
+:class:`python:list`\(:class:`python:int`)
 
 list[int]
-:obj:`list`\[:obj:`int`]
+:class:`python:list`\[:class:`python:int`]
 
 dict(str, int)
-:obj:`dict`\(:obj:`str`, :obj:`int`)
+:class:`python:dict`\(:class:`python:str`, :class:`python:int`)
 
 dict[str,  int]
-:obj:`dict`\[:obj:`str`,  :obj:`int`]
+:class:`python:dict`\[:class:`python:str`,  :class:`python:int`]
 
 tuple(float, float)
-:obj:`tuple`\(:obj:`float`, :obj:`float`)
+:class:`python:tuple`\(:class:`python:float`, :class:`python:float`)
 
 dict[tuple(str, str), int]
-:obj:`dict`\[:obj:`tuple`\(:obj:`str`, :obj:`str`), :obj:`int`]
+:class:`python:dict`\[:class:`python:tuple`\(:class:`python:str`, :class:`python:str`), :class:`python:int`]
 """  # noqa: E501
 
 xref_ignore = {'or', 'in', 'of', 'default', 'optional'}
@@ -121,3 +206,14 @@ xref_ignore = {'or', 'in', 'of', 'default', 'optional'}
 )
 def test_make_xref(param_type, expected_result):
     assert make_xref(param_type, xref_aliases, xref_ignore) == expected_result
+
+@pytest.mark.parametrize(
+    ('param_type', 'expected_result'),
+    [tuple(s.split('\n')) for s in data_ignore_obj.strip().split('\n\n')]
+)
+def test_make_xref_ignore_unknown(param_type, expected_result):
+    assert make_xref(param_type, xref_aliases, xref_ignore="all") == expected_result
+
+def test_xref_ignore_is_all():
+    with pytest.raises(TypeError, match="must be a set or 'all'"):
+        make_xref("array_like", xref_aliases, xref_ignore="foo")