Repository: mne-tools/mne-python

Issue:
broken links to source in docs
Some links from function documentation in the API reference to the function definition in the source code are broken.  For example, on this page: https://mne-tools.github.io/dev/generated/mne.stats.spatio_temporal_cluster_1samp_test.html

the link text `[source]` to the right of the function signature links to `http://github.com/mne-tools/mne-python/blob/master/doc/%3Cstring%3E#L1210-L1339` (note the `<string>` near the end of the URL)

Also occurs for `spatio_temporal_cluster_test`, `permutation_t_test`, `permutation_cluster_1samp_test` (but is not a sitewide problem --- it works fine for e.g., `fdr_correction`; maybe it's only broken for functions in `cluster_level.py`?)


Discussion:
It will be broken for anything that uses the verbose decorator. We need to make the linking function smarter

Resolving patch:
diff --git a/doc/conf.py b/doc/conf.py
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -22,6 +22,7 @@
 from numpydoc import numpydoc, docscrape  # noqa
 import sphinx_fontawesome
 import mne
+from mne.utils import linkcode_resolve  # noqa, analysis:ignore
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
@@ -322,60 +323,3 @@
 }
 
 numpydoc_class_members_toctree = False
-
-
-# -----------------------------------------------------------------------------
-# Source code links (adapted from SciPy (doc/source/conf.py))
-# -----------------------------------------------------------------------------
-
-def linkcode_resolve(domain, info):
-    """
-    Determine the URL corresponding to Python object
-    """
-    if domain != 'py':
-        return None
-
-    modname = info['module']
-    fullname = info['fullname']
-
-    submod = sys.modules.get(modname)
-    if submod is None:
-        return None
-
-    obj = submod
-    for part in fullname.split('.'):
-        try:
-            obj = getattr(obj, part)
-        except:
-            return None
-
-    try:
-        fn = inspect.getsourcefile(obj)
-    except:
-        fn = None
-    if not fn:
-        try:
-            fn = inspect.getsourcefile(sys.modules[obj.__module__])
-        except:
-            fn = None
-    if not fn:
-        return None
-
-    try:
-        source, lineno = inspect.getsourcelines(obj)
-    except:
-        lineno = None
-
-    if lineno:
-        linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1)
-    else:
-        linespec = ""
-
-    fn = relpath(fn, start=dirname(mne.__file__))
-
-    if 'dev' in mne.__version__:
-        kind = 'master'
-    else:
-        kind = 'maint/%s' % ('.'.join(mne.__version__.split('.')[:2]))
-    return "http://github.com/mne-tools/mne-python/blob/%s/mne/%s%s" % (  # noqa
-       kind, fn, linespec)
diff --git a/mne/utils.py b/mne/utils.py
--- a/mne/utils.py
+++ b/mne/utils.py
@@ -2748,3 +2748,76 @@ def _validate_type(item, types=None, item_name=None, type_name=None):
     if not isinstance(item, types):
         raise TypeError(item_name, ' must be an instance of ', type_name,
                         ', got %s instead.' % (type(item),))
+
+
+def linkcode_resolve(domain, info):
+    """Determine the URL corresponding to Python object.
+
+    Parameters
+    ----------
+    domain : str
+        Only useful when 'py'.
+    info : dict
+        With keys "module" and "fullname".
+
+    Returns
+    -------
+    url : str
+        The code URL.
+
+    Notes
+    -----
+    This has been adapted to deal with our "verbose" decorator.
+
+    Adapted from SciPy (doc/source/conf.py).
+    """
+    import mne
+    if domain != 'py':
+        return None
+
+    modname = info['module']
+    fullname = info['fullname']
+
+    submod = sys.modules.get(modname)
+    if submod is None:
+        return None
+
+    obj = submod
+    for part in fullname.split('.'):
+        try:
+            obj = getattr(obj, part)
+        except Exception:
+            return None
+
+    try:
+        fn = inspect.getsourcefile(obj)
+    except Exception:
+        fn = None
+    if not fn:
+        try:
+            fn = inspect.getsourcefile(sys.modules[obj.__module__])
+        except Exception:
+            fn = None
+    if not fn:
+        return None
+    if fn == '<string>':  # verbose decorator
+        fn = inspect.getmodule(obj).__file__
+    fn = op.relpath(fn, start=op.dirname(mne.__file__))
+    fn = '/'.join(op.normpath(fn).split(os.sep))  # in case on Windows
+
+    try:
+        source, lineno = inspect.getsourcelines(obj)
+    except Exception:
+        lineno = None
+
+    if lineno:
+        linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1)
+    else:
+        linespec = ""
+
+    if 'dev' in mne.__version__:
+        kind = 'master'
+    else:
+        kind = 'maint/%s' % ('.'.join(mne.__version__.split('.')[:2]))
+    return "http://github.com/mne-tools/mne-python/blob/%s/mne/%s%s" % (  # noqa
+       kind, fn, linespec)
diff --git a/tutorials/plot_background_filtering.py b/tutorials/plot_background_filtering.py
--- a/tutorials/plot_background_filtering.py
+++ b/tutorials/plot_background_filtering.py
@@ -929,7 +929,7 @@ def baseline_plot(x):
 # ``h_freq + transition_bandwidth/2.``.
 #
 # Filter length (order) and transition bandwidth (roll-off)
-# --------------------------------------------------------
+# ---------------------------------------------------------
 # In the :ref:`tut_filtering_in_python` section, we have already talked about
 # the default filter lengths and transition bandwidths that are used when no
 # custom values are specified using the respective filter function's arguments.


Test patch:
diff --git a/mne/tests/test_utils.py b/mne/tests/test_utils.py
--- a/mne/tests/test_utils.py
+++ b/mne/tests/test_utils.py
@@ -1,6 +1,7 @@
 from copy import deepcopy
 import os.path as op
 import os
+import sys
 import warnings
 import webbrowser
 
@@ -28,7 +29,8 @@
                        _get_call_line, compute_corr, sys_info, verbose,
                        check_fname, get_config_path,
                        object_size, buggy_mkl_svd, _get_inst_data,
-                       copy_doc, copy_function_doc_to_method_doc, ProgressBar)
+                       copy_doc, copy_function_doc_to_method_doc, ProgressBar,
+                       linkcode_resolve)
 
 
 warnings.simplefilter('always')  # enable b/c these tests throw warnings
@@ -803,4 +805,20 @@ def test_open_docs():
         webbrowser.open_new_tab = old_tab
 
 
+def test_linkcode_resolve():
+    """Test linkcode resolving."""
+    ex = '' if sys.version[0] == '2' else '#L'
+    url = linkcode_resolve('py', dict(module='mne', fullname='Epochs'))
+    assert '/mne/epochs.py' + ex in url
+    url = linkcode_resolve('py', dict(module='mne',
+                                      fullname='compute_covariance'))
+    assert '/mne/cov.py' + ex in url
+    url = linkcode_resolve('py', dict(module='mne',
+                                      fullname='convert_forward_solution'))
+    assert '/mne/forward/forward.py' + ex in url
+    url = linkcode_resolve('py', dict(module='mne',
+                                      fullname='datasets.sample.data_path'))
+    assert '/mne/datasets/sample/sample.py' + ex in url
+
+
 run_tests_if_main()