Repository: nilearn/nilearn

Issue:
Improve Issue Templates
As discussed in last meeting:

TODO:

- [x] Fix labels in current issue templates which are not added automatically
- [x] Improve existing templates
- [x] Add new templates? (Documentation Improvement, Question, others...?)


Discussion:
It would be great to have a link on the issue template choice page that goes directly to NeuroStars for usage questions. I think that should be possible with a config file (see [here](https://docs.github.com/en/free-pro-team@latest/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser)). +1 for a documentation improvement template as well.

EDIT: I just added a NeuroStars link to [`NiMARE`](https://github.com/neurostuff/NiMARE/blob/a24b9bc7b9f996f3ce915b02b4e38e2164d6b5c0/.github/ISSUE_TEMPLATE/config.yml) and it looks pretty good:

![image](https://user-images.githubusercontent.com/8228902/103795060-81f74a00-5013-11eb-9940-934f4dd405c6.png)

+1 for a link to Neurostars !
Regarding improving the actual templates- what about making the summary comment at the top into specific sections?

For example, for the bug report, instead of

```
<!--

Include this information:
-------------------------
What version of Nilearn are you using?
What were you trying to do?
What did you expect will happen?
What actually happened?

List the steps you performed that revealed the bug to you.
Include any code samples. Enclose them in triple back-ticks (```)
Like this:

```
<code>
``` 
-->
```

we could have:

```
Nilearn version: 

### Expected behavior


### Actual behavior


### Code to reproduce

\```python

\```
```
I like the idea! :+1: 
I think the template is more clear and this will perhaps encourage user to copy-paste their code to reproduce.

Resolving patch:
diff --git a/nilearn/surface/__init__.py b/nilearn/surface/__init__.py
--- a/nilearn/surface/__init__.py
+++ b/nilearn/surface/__init__.py
@@ -3,7 +3,10 @@
 """
 
 from .surface import (vol_to_surf, load_surf_data,
-                      load_surf_mesh, check_mesh_and_data, Mesh)
+                      load_surf_mesh, load_surface,
+                      check_mesh_and_data, check_surface,
+                      Mesh, Surface)
 
 __all__ = ['vol_to_surf', 'load_surf_data', 'load_surf_mesh',
-           'check_mesh_and_data', 'Mesh']
+           'load_surface', 'check_surface', 'check_mesh_and_data',
+           'Mesh', 'Surface']
diff --git a/nilearn/surface/surface.py b/nilearn/surface/surface.py
--- a/nilearn/surface/surface.py
+++ b/nilearn/surface/surface.py
@@ -33,6 +33,8 @@ class EfficiencyWarning(UserWarning):
 # Create a namedtuple object for meshes
 Mesh = namedtuple("mesh", ["coordinates", "faces"])
 
+# Create a namedtuple object for surfaces
+Surface = namedtuple("surface", ["mesh", "data"])
 
 def _uniform_ball_cloud(n_points=20, dim=3, n_monte_carlo=50000):
     """Get points uniformly spaced in the unit ball."""
@@ -895,6 +897,55 @@ def load_surf_mesh(surf_mesh):
     return mesh
 
 
+def load_surface(surface):
+    """Loads a surface.
+
+    Parameters
+    ----------
+    surface : Surface-like (see description)
+        The surface to be loaded.
+        A surface can be:
+            - a nilearn.surface.Surface
+            - a sequence (mesh, data) where:
+                - mesh can be:
+                    - a nilearn.surface.Mesh
+                    - a path to .gii or .gii.gz etc.
+                    - a sequence of two numpy arrays,
+                    the first containing vertex coordinates
+                    and the second containing triangles.
+                - data can be:
+                    - a path to .gii or .gii.gz etc.
+                    - a numpy array with shape (n_vertices,)
+                    or (n_time_points, n_vertices)
+
+    Returns
+    --------
+    surface : Surface
+        With the fields "mesh" (Mesh object) and "data" (numpy.ndarray).
+
+    """
+    # Handle the case where we received a Surface
+    # object with mesh and data attributes
+    if hasattr(surface, "mesh") and hasattr(surface, "data"):
+        mesh = load_surf_mesh(surface.mesh)
+        data = load_surf_data(surface.data)
+    # Handle the case where we received a sequence
+    # (mesh, data)
+    elif isinstance(surface, (list, tuple, np.ndarray)):
+        if len(surface) == 2:
+            mesh = load_surf_mesh(surface[0])
+            data = load_surf_data(surface[1])
+        else:
+            raise ValueError("`load_surface` accepts iterables "
+                             "of length 2 to define a surface. "
+                             "You provided a {} of length {}.".format(
+                                 type(surface), len(surface)))
+    else:
+        raise ValueError("Wrong parameter `surface` in `load_surface`. "
+                         "Please refer to the documentation for more information.")
+    return Surface(mesh, data)
+
+
 def _check_mesh(mesh):
     """Check that mesh data is either a str, or a dict with sufficient
     entries.
@@ -918,7 +969,32 @@ def _check_mesh(mesh):
 
 
 def check_mesh_and_data(mesh, data):
-    """Load surface mesh and data, check that they have compatible shapes."""
+    """Load surface mesh and data, check that they have compatible shapes.
+
+    Parameters
+    ----------
+    mesh : str or numpy.ndarray or Mesh
+        Either a file containing surface mesh geometry (valid formats
+        are .gii .gii.gz or Freesurfer specific files such as .orig, .pial,
+        .sphere, .white, .inflated) or two Numpy arrays organized in a list,
+        tuple or a namedtuple with the fields "coordinates" and "faces", or a
+        Mesh object with "coordinates" and "faces" attributes.
+
+    data : str or numpy.ndarray
+        Either a file containing surface data (valid format are .gii,
+        .gii.gz, .mgz, .nii, .nii.gz, or Freesurfer specific files such as
+        .thickness, .curv, .sulc, .annot, .label), lists of 1D data files are
+        returned as 2D arrays, or a Numpy array containing surface data.
+
+    Returns
+    -------
+    mesh : Mesh
+        Checked mesh.
+
+    data : numpy.ndarray
+        Checked data.
+
+    """
     mesh = load_surf_mesh(mesh)
     data = load_surf_data(data)
     # Check that mesh coordinates has a number of nodes
@@ -936,3 +1012,38 @@ def check_mesh_and_data(mesh, data):
             "Maximum face index is {} while coordinates array has length {}.".format(
                 mesh.faces.max(), len(mesh.coordinates)))
     return mesh, data
+
+
+def check_surface(surface):
+    """Load a surface as a Surface object.
+    This function will make sure that the surfaces's
+    mesh and data have compatible shapes.
+
+    Parameters
+    ----------
+    surface : Surface-like (see description)
+        The surface to be loaded.
+        A surface can be:
+            - a nilearn.surface.Surface
+            - a sequence (mesh, data) where:
+                - mesh can be:
+                    - a nilearn.surface.Mesh
+                    - a path to .gii or .gii.gz etc.
+                    - a sequence of two numpy arrays,
+                    the first containing vertex coordinates
+                    and the second containing triangles.
+                - data can be:
+                    - a path to .gii or .gii.gz etc.
+                    - a numpy array with shape (n_vertices,)
+                    or (n_time_points, n_vertices)
+
+    Returns
+    -------
+    surface : Surface
+        Checked surface object.
+
+    """
+    surface = load_surface(surface)
+    mesh, data = check_mesh_and_data(surface.mesh,
+                                     surface.data)
+    return Surface(mesh, data)


Test patch:
diff --git a/nilearn/datasets/tests/test_neurovault.py b/nilearn/datasets/tests/test_neurovault.py
--- a/nilearn/datasets/tests/test_neurovault.py
+++ b/nilearn/datasets/tests/test_neurovault.py
@@ -431,7 +431,7 @@ def test_result_filter():
     filter_0 = neurovault.ResultFilter(query_terms={'a': 0},
                                        callable_filter=lambda d: len(d) < 5,
                                        b=1)
-    assert np.unicode(filter_0) == u'ResultFilter'
+    assert str(filter_0) == 'ResultFilter'
     assert filter_0['a'] == 0
     assert filter_0({'a': 0, 'b': 1, 'c': 2})
     assert not filter_0({'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4})
diff --git a/nilearn/decomposition/tests/test_canica.py b/nilearn/decomposition/tests/test_canica.py
--- a/nilearn/decomposition/tests/test_canica.py
+++ b/nilearn/decomposition/tests/test_canica.py
@@ -158,8 +158,13 @@ def test_percentile_range():
     canica = CanICA(n_components=edge_case, threshold=float(edge_case))
     with warnings.catch_warnings(record=True) as warning:
         canica.fit(data)
-        assert len(warning) == 1  # ensure single warning
-        assert "critical threshold" in str(warning[-1].message)
+        # Filter out deprecation warnings
+        not_deprecation_warning = [not issubclass(w.category,
+                                                  DeprecationWarning)
+                                        for w in warning]
+        assert sum(not_deprecation_warning) == 1  # ensure single warning
+        idx_critical_warning = not_deprecation_warning.index(True)
+        assert "critical threshold" in str(warning[idx_critical_warning].message)
 
 
 def test_masker_attributes_with_fit():
diff --git a/nilearn/input_data/tests/test_nifti_maps_masker.py b/nilearn/input_data/tests/test_nifti_maps_masker.py
--- a/nilearn/input_data/tests/test_nifti_maps_masker.py
+++ b/nilearn/input_data/tests/test_nifti_maps_masker.py
@@ -132,7 +132,8 @@ def test_nifti_maps_masker_with_nans():
 
     # nans
     maps_data = get_data(maps_img)
-    mask_data = get_data(mask_img)
+    mask_data = np.array(get_data(mask_img),
+                         dtype=np.float64)
 
     maps_data[:, 9, 9] = np.nan
     maps_data[:, 5, 5] = np.inf
diff --git a/nilearn/surface/tests/test_surface.py b/nilearn/surface/tests/test_surface.py
--- a/nilearn/surface/tests/test_surface.py
+++ b/nilearn/surface/tests/test_surface.py
@@ -4,6 +4,7 @@
 import tempfile
 import warnings
 
+from collections import namedtuple
 from distutils.version import LooseVersion
 
 import nibabel as nb
@@ -20,7 +21,7 @@
 from nilearn import image
 from nilearn.image import resampling
 from nilearn.image.tests.test_resampling import rotation
-from nilearn.surface import Mesh
+from nilearn.surface import Mesh, Surface
 from nilearn.surface import surface
 from nilearn.surface import load_surf_data, load_surf_mesh, vol_to_surf
 from nilearn.surface.surface import (_gifti_img_to_mesh,
@@ -47,6 +48,23 @@ def coordinates(self):
     def faces(self):
         return self._faces
 
+class SurfaceLikeObject(object):
+    """Class with attributes mesh and
+    data to be used for testing purposes.
+    """
+    def __init__(self, mesh, data):
+        self._mesh = mesh
+        self._data = data
+    @classmethod
+    def fromarrays(cls, coordinates, faces, data):
+        return cls(MeshLikeObject(coordinates, faces), data)
+    @property
+    def mesh(self):
+        return self._mesh
+    @property
+    def data(self):
+        return self._data
+
 
 def test_load_surf_data_array():
     # test loading and squeezing data from numpy array
@@ -182,6 +200,39 @@ def test_load_surf_mesh():
     assert_array_equal(mesh_like.faces, loaded_mesh.faces)
 
 
+def test_load_surface():
+    coords, faces = generate_surf()
+    mesh = Mesh(coords, faces)
+    data = mesh[0][:,0]
+    surf = Surface(mesh, data)
+    surf_like_obj = SurfaceLikeObject(mesh, data)
+    # Load the surface from:
+    #   - Surface-like objects having the right attributes
+    #   - a list of length 2 (mesh, data)
+    for loadings in [surf,
+                     surf_like_obj,
+                     [mesh, data]]:
+        s = surface.load_surface(loadings)
+        assert_array_equal(s.data, data)
+        assert_array_equal(s.data, surf.data)
+        assert_array_equal(s.mesh.coordinates, coords)
+        assert_array_equal(s.mesh.coordinates, surf.mesh.coordinates)
+        assert_array_equal(s.mesh.faces, surf.mesh.faces)
+    # Giving an iterable of length other than 2 will raise an error
+    # Length 3
+    with pytest.raises(ValueError,
+                       match="`load_surface` accepts iterables of length 2"):
+        s = surface.load_surface([coords, faces, data])
+    # Length 1
+    with pytest.raises(ValueError,
+                       match="`load_surface` accepts iterables of length 2"):
+        s = surface.load_surface([coords])
+    # Giving other objects will raise an error
+    with pytest.raises(ValueError,
+                       match="Wrong parameter `surface` in `load_surface`"):
+        s = surface.load_surface("foo")
+
+
 def test_load_surf_mesh_list():
     # test if correct list is returned
     mesh = generate_surf()
@@ -661,9 +712,41 @@ def test_check_mesh_and_data():
     wrong_faces = rng.randint(coords.shape[0] + 1, size=(30, 3))
     wrong_mesh = Mesh(coords, wrong_faces)
     # Check that check_mesh_and_data raises an error with the resulting wrong mesh
-    with pytest.raises(ValueError, match="Mismatch between the indices of faces and the number of nodes."):
+    with pytest.raises(ValueError,
+                       match="Mismatch between the indices of faces and the number of nodes."):
         surface.check_mesh_and_data(wrong_mesh, data)
     # Alter the data and check that an error is raised
     data = mesh[0][::2, 0]
-    with pytest.raises(ValueError, match="Mismatch between number of nodes in mesh"):
+    with pytest.raises(ValueError,
+                       match="Mismatch between number of nodes in mesh"):
         surface.check_mesh_and_data(mesh, data)
+
+
+def test_check_surface():
+    coords, faces = generate_surf()
+    mesh = Mesh(coords, faces)
+    data = mesh[0][:,0]
+    surf = Surface(mesh, data)
+    s = surface.check_surface(surf)
+    assert_array_equal(s.data, data)
+    assert_array_equal(s.data, surf.data)
+    assert_array_equal(s.mesh.coordinates, coords)
+    assert_array_equal(s.mesh.coordinates, mesh.coordinates)
+    assert_array_equal(s.mesh.faces, faces)
+    assert_array_equal(s.mesh.faces, mesh.faces)
+    # Generate faces such that max index is larger than
+    # the length of coordinates array.
+    rng = np.random.RandomState(42)
+    wrong_faces = rng.randint(coords.shape[0] + 1, size=(30, 3))
+    wrong_mesh = Mesh(coords, wrong_faces)
+    wrong_surface = Surface(wrong_mesh, data)
+    # Check that check_mesh_and_data raises an error with the resulting wrong mesh
+    with pytest.raises(ValueError,
+                       match="Mismatch between the indices of faces and the number of nodes."):
+        surface.check_surface(wrong_surface)
+    # Alter the data and check that an error is raised
+    wrong_data = mesh[0][::2, 0]
+    wrong_surface = Surface(mesh, wrong_data)
+    with pytest.raises(ValueError,
+                       match="Mismatch between number of nodes in mesh"):
+        surface.check_surface(wrong_surface)