Repository: nilearn/nilearn

Issue:
extract is_plotly_installed function
Wonder if some of this should not be extracted into a separate utility function `is_plotly_installed()`. 

this should probably be done in a different PR though, so don't overthing it for this PR

_Originally posted by @Remi-Gau in https://github.com/nilearn/nilearn/pull/3949#discussion_r1317466053_
            


Discussion:

Resolving patch:
diff --git a/nilearn/_utils/helpers.py b/nilearn/_utils/helpers.py
--- a/nilearn/_utils/helpers.py
+++ b/nilearn/_utils/helpers.py
@@ -203,3 +203,21 @@ def _compare_version(version_a, operator, version_b):
         error_msg = "'_compare_version' received an unexpected operator "
         raise ValueError(error_msg + operator + ".")
     return VERSION_OPERATORS[operator](parse(version_a), parse(version_b))
+
+
+def is_plotly_installed():
+    """Check if plotly is installed."""
+    try:
+        import plotly.graph_objects as go  # noqa: F401
+    except ImportError:
+        return False
+    return True
+
+
+def is_kaleido_installed():
+    """Check if kaleido is installed."""
+    try:
+        import kaleido  # noqa: F401
+    except ImportError:
+        return False
+    return True
diff --git a/nilearn/plotting/displays/_figures.py b/nilearn/plotting/displays/_figures.py
--- a/nilearn/plotting/displays/_figures.py
+++ b/nilearn/plotting/displays/_figures.py
@@ -1,3 +1,6 @@
+from nilearn._utils.helpers import is_kaleido_installed, is_plotly_installed
+
+
 class SurfaceFigure:
     """Abstract class for surface figures.
 
@@ -61,12 +64,12 @@ class PlotlySurfaceFigure(SurfaceFigure):
     """
 
     def __init__(self, figure=None, output_file=None):
-        try:
-            import plotly.graph_objects as go
-        except ImportError:
+        if not is_plotly_installed():
             raise ImportError(
                 "Plotly is required to use `PlotlySurfaceFigure`."
             )
+        import plotly.graph_objects as go
+
         if figure is not None and not isinstance(figure, go.Figure):
             raise TypeError(
                 "`PlotlySurfaceFigure` accepts only plotly figure objects."
@@ -94,9 +97,7 @@ def savefig(self, output_file=None):
         output_file : :obj:`str` or ``None``, optional
             Path to output file.
         """
-        try:
-            import kaleido  # noqa: F401
-        except ImportError:
+        if not is_kaleido_installed():
             raise ImportError(
                 "`kaleido` is required to save plotly figures to disk."
             )
diff --git a/nilearn/plotting/surf_plotting.py b/nilearn/plotting/surf_plotting.py
--- a/nilearn/plotting/surf_plotting.py
+++ b/nilearn/plotting/surf_plotting.py
@@ -16,6 +16,7 @@
 
 from nilearn import image, surface
 from nilearn._utils import _compare_version, check_niimg_3d, fill_doc
+from nilearn._utils.helpers import is_kaleido_installed, is_plotly_installed
 from nilearn.plotting.cm import _mix_colormaps, cold_hot
 from nilearn.plotting.html_surface import _get_vertexcolor
 from nilearn.plotting.img_plotting import _get_colorbar_and_data_ranges
@@ -249,11 +250,11 @@ def _plot_surf_plotly(coords, faces, surf_map=None, bg_map=None,
         bugs that you may encounter.
 
     """
-    try:
+    if is_plotly_installed():
         import plotly.graph_objects as go
 
         from nilearn.plotting.displays import PlotlySurfaceFigure
-    except ImportError:
+    else:
         msg = "Using engine='plotly' requires that ``plotly`` is installed."
         raise ImportError(msg)
 
@@ -308,15 +309,13 @@ def _plot_surf_plotly(coords, faces, surf_map=None, bg_map=None,
                       **LAYOUT)
 
     # save figure
+    plotly_figure = PlotlySurfaceFigure(figure=fig, output_file=output_file)
+
     if output_file is not None:
-        try:
-            import kaleido  # noqa: F401
-        except ImportError:
+        if not is_kaleido_installed():
             msg = ("Saving figures to file with engine='plotly' requires "
                    "that ``kaleido`` is installed.")
             raise ImportError(msg)
-    plotly_figure = PlotlySurfaceFigure(figure=fig, output_file=output_file)
-    if output_file is not None:
         plotly_figure.savefig()
 
     return plotly_figure


Test patch:
diff --git a/nilearn/_utils/tests/test_helpers.py b/nilearn/_utils/tests/test_helpers.py
--- a/nilearn/_utils/tests/test_helpers.py
+++ b/nilearn/_utils/tests/test_helpers.py
@@ -150,3 +150,11 @@ def test_compare_version_error():
         match="'_compare_version' received an unexpected operator <>.",
     ):
         helpers._compare_version("0.1.0", "<>", "1.1.0")
+
+
+def test_is_plotly_installed():
+    helpers.is_plotly_installed()
+
+
+def test_is_kaleido_installed():
+    helpers.is_kaleido_installed()
diff --git a/nilearn/plotting/tests/test_surf_plotting.py b/nilearn/plotting/tests/test_surf_plotting.py
--- a/nilearn/plotting/tests/test_surf_plotting.py
+++ b/nilearn/plotting/tests/test_surf_plotting.py
@@ -9,6 +9,7 @@
 from matplotlib.figure import Figure
 from numpy.testing import assert_array_equal
 
+from nilearn._utils.helpers import is_kaleido_installed, is_plotly_installed
 from nilearn.datasets import fetch_surf_fsaverage
 from nilearn.plotting.displays import PlotlySurfaceFigure
 from nilearn.plotting.surf_plotting import (
@@ -27,20 +28,6 @@
 from nilearn.surface import load_surf_data, load_surf_mesh
 from nilearn.surface.testing_utils import generate_surf
 
-try:
-    import plotly.graph_objects as go
-except ImportError:
-    PLOTLY_INSTALLED = False
-else:
-    PLOTLY_INSTALLED = True
-
-try:
-    import kaleido  # noqa:F401
-except ImportError:
-    KALEIDO_INSTALLED = False
-else:
-    KALEIDO_INSTALLED = True
-
 try:
     import IPython.display  # noqa:F401
 except ImportError:
@@ -257,7 +244,7 @@ def test_surface_figure():
     assert s.output_file == "bar.png"
 
 
-@pytest.mark.skipif(PLOTLY_INSTALLED,
+@pytest.mark.skipif(is_plotly_installed(),
                     reason='Plotly is installed.')
 def test_plotly_surface_figure_import_error():
     """Test that an ImportError is raised when instantiating a
@@ -267,7 +254,7 @@ def test_plotly_surface_figure_import_error():
         PlotlySurfaceFigure()
 
 
-@pytest.mark.skipif(not PLOTLY_INSTALLED or KALEIDO_INSTALLED,
+@pytest.mark.skipif(not is_plotly_installed() or is_kaleido_installed(),
                     reason=("This test only runs if Plotly is "
                             "installed, but not kaleido."))
 def test_plotly_surface_figure_savefig_error():
@@ -278,7 +265,7 @@ def test_plotly_surface_figure_savefig_error():
         PlotlySurfaceFigure().savefig()
 
 
-@pytest.mark.skipif(not PLOTLY_INSTALLED or not KALEIDO_INSTALLED,
+@pytest.mark.skipif(not is_plotly_installed() or not is_kaleido_installed(),
                     reason=("Plotly and/or kaleido not installed; "
                             "required for this test."))
 def test_plotly_surface_figure():
@@ -291,11 +278,12 @@ def test_plotly_surface_figure():
     ps.savefig('foo.png')
 
 
-@pytest.mark.skipif(not PLOTLY_INSTALLED or not IPYTHON_INSTALLED,
+@pytest.mark.skipif(not is_plotly_installed() or not IPYTHON_INSTALLED,
                     reason=("Plotly and/or Ipython is not installed; "
                             "required for this test."))
 @pytest.mark.parametrize("renderer", ['png', 'jpeg', 'svg'])
 def test_plotly_show(renderer):
+    import plotly.graph_objects as go
     ps = PlotlySurfaceFigure(go.Figure())
     assert ps.output_file is None
     assert ps.figure is not None
@@ -306,10 +294,11 @@ def test_plotly_show(renderer):
     assert f'image/{key}' in mock_display.call_args.args[0]
 
 
-@pytest.mark.skipif(not PLOTLY_INSTALLED or not KALEIDO_INSTALLED,
+@pytest.mark.skipif(not is_plotly_installed() or not is_kaleido_installed(),
                     reason=("Plotly and/or kaleido not installed; "
                             "required for this test."))
 def test_plotly_savefig(tmp_path):
+    import plotly.graph_objects as go
     ps = PlotlySurfaceFigure(go.Figure(), output_file=tmp_path / "foo.png")
     assert ps.output_file == tmp_path / "foo.png"
     assert ps.figure is not None
@@ -317,7 +306,7 @@ def test_plotly_savefig(tmp_path):
     assert (tmp_path / "foo.png").exists()
 
 
-@pytest.mark.skipif(not PLOTLY_INSTALLED,
+@pytest.mark.skipif(not is_plotly_installed(),
                     reason='Plotly is not installed; required for this test.')
 @pytest.mark.parametrize("input_obj", ["foo", Figure(), ["foo", "bar"]])
 def test_instantiation_error_plotly_surface_figure(input_obj):
@@ -416,7 +405,7 @@ def test_plot_surf_engine_error():
 
 @pytest.mark.parametrize("engine", ["matplotlib", "plotly"])
 def test_plot_surf(engine, tmp_path):
-    if not PLOTLY_INSTALLED and engine == "plotly":
+    if not is_plotly_installed() and engine == "plotly":
         pytest.skip('Plotly is not installed; required for this test.')
     mesh = generate_surf()
     rng = np.random.RandomState(42)
@@ -497,7 +486,7 @@ def custom_avg_function(vertices):
 
 @pytest.mark.parametrize("engine", ["matplotlib", "plotly"])
 def test_plot_surf_error(engine):
-    if not PLOTLY_INSTALLED and engine == "plotly":
+    if not is_plotly_installed() and engine == "plotly":
         pytest.skip('Plotly is not installed; required for this test.')
     mesh = generate_surf()
     rng = np.random.RandomState(42)
@@ -603,7 +592,7 @@ def custom_avg_function(vertices):
 
 @pytest.mark.parametrize("engine", ["matplotlib", "plotly"])
 def test_plot_surf_stat_map(engine):
-    if not PLOTLY_INSTALLED and engine == "plotly":
+    if not is_plotly_installed() and engine == "plotly":
         pytest.skip('Plotly is not installed; required for this test.')
     mesh = generate_surf()
     rng = np.random.RandomState(42)
@@ -731,7 +720,7 @@ def _generate_data_test_surf_roi():
 
 @pytest.mark.parametrize("engine", ["matplotlib", "plotly"])
 def test_plot_surf_roi(engine):
-    if not PLOTLY_INSTALLED and engine == "plotly":
+    if not is_plotly_installed() and engine == "plotly":
         pytest.skip('Plotly is not installed; required for this test.')
     mesh, roi_map, parcellation = _generate_data_test_surf_roi()
     # plot roi
@@ -800,7 +789,7 @@ def test_plot_surf_roi_matplotlib_specific():
 
 @pytest.mark.parametrize("engine", ["matplotlib", "plotly"])
 def test_plot_surf_roi_error(engine):
-    if not PLOTLY_INSTALLED and engine == "plotly":
+    if not is_plotly_installed() and engine == "plotly":
         pytest.skip('Plotly is not installed; required for this test.')
     mesh = generate_surf()
     rng = np.random.RandomState(42)
@@ -811,13 +800,13 @@ def test_plot_surf_roi_error(engine):
         plot_surf_roi(mesh, roi_map=roi_idx, engine=engine)
 
 
+@pytest.mark.skipif(not is_plotly_installed(),
+                    reason=("This test only runs if Plotly is installed."))
 @pytest.mark.parametrize(
     "kwargs", [{"vmin": 2}, {"vmin": 2, "threshold": 5}, {"threshold": 5}]
 )
 def test_plot_surf_roi_colorbar_vmin_equal_across_engines(kwargs):
     """See issue https://github.com/nilearn/nilearn/issues/3944"""
-    if not PLOTLY_INSTALLED:
-        pytest.skip("Plotly is not installed; required for this test.")
     mesh = generate_surf()
     roi_map = np.arange(0, len(mesh[0]))
 
@@ -1138,13 +1127,13 @@ def test_compute_facecolors_matplotlib():
         )
 
 
+@pytest.mark.skipif(not is_plotly_installed(),
+                    reason=("This test only runs if Plotly is installed."))
 @pytest.mark.parametrize("avg_method", ["mean", "median"])
 @pytest.mark.parametrize("symmetric_cmap", [True, False, None])
 @pytest.mark.parametrize("engine", ["matplotlib", "plotly"])
 def test_plot_surf_roi_default_arguments(engine, symmetric_cmap, avg_method):
     """Regression test for https://github.com/nilearn/nilearn/issues/3941"""
-    if not PLOTLY_INSTALLED:
-        pytest.skip('Plotly is not installed; required for this test.')
     mesh, roi_map, _ = _generate_data_test_surf_roi()
     plot_surf_roi(mesh, roi_map=roi_map,
                   engine=engine,