Coverage for nilearn/_utils/helpers.py: 43%
97 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1import functools
2import operator
3import os
4import warnings
6from packaging.version import parse
8from nilearn._utils.logger import find_stack_level
10OPTIONAL_MATPLOTLIB_MIN_VERSION = "3.3.0"
13def set_mpl_backend(message=None):
14 """Check if matplotlib is installed.
16 If not installed, raise error and display warning to install necessary
17 dependencies.
19 If installed, check if the installed version complies with the minimum
20 supported matplotlib version. If it does not, raise error; otherwise set
21 the matplotlib backend.
23 If current backend is not usable, switch to default "Agg" backend.
25 Parameters
26 ----------
27 message: str, default=None
28 Message to be prepended to standard warning when matplotlib is not
29 installed.
30 """
31 # We are doing local imports here to avoid polluting our namespace
32 try:
33 import matplotlib
34 except ImportError:
35 warning = (
36 "Some dependencies of nilearn.plotting package seem to be missing."
37 "\nThey can be installed with:\n"
38 " pip install 'nilearn[plotting]'"
39 )
40 if message is not None: 40 ↛ 42line 40 didn't jump to line 42 because the condition on line 40 was always true
41 warning = f"{message}\n{warning}"
42 warnings.warn(warning, stacklevel=find_stack_level())
43 raise
44 else:
45 # When matplotlib was successfully imported we need to check
46 # that the version is greater that the minimum required one
47 mpl_version = getattr(matplotlib, "__version__", "0.0.0")
48 if not compare_version(
49 mpl_version, ">=", OPTIONAL_MATPLOTLIB_MIN_VERSION
50 ):
51 raise ImportError(
52 f"A matplotlib version of at least "
53 f"{OPTIONAL_MATPLOTLIB_MIN_VERSION} "
54 f"is required to use nilearn. {mpl_version} was found. "
55 f"Please upgrade matplotlib."
56 )
57 current_backend = matplotlib.get_backend().lower()
59 try:
60 # Making sure the current backend is usable by matplotlib
61 matplotlib.use(current_backend)
62 except Exception:
63 # If not, switching to default agg backend
64 matplotlib.use("Agg")
65 new_backend = matplotlib.get_backend().lower()
67 if new_backend != current_backend:
68 # Matplotlib backend has been changed, let's warn the user
69 warnings.warn(
70 f"Backend changed to {new_backend}...",
71 stacklevel=find_stack_level(),
72 )
75def rename_parameters(
76 replacement_params,
77 end_version="future",
78 lib_name="Nilearn",
79):
80 """Use this decorator to deprecate & replace specified parameters \
81 in the decorated functions and methods without changing \
82 function definition or signature.
84 Parameters
85 ----------
86 replacement_params : Dict[string, string]
87 Dict where the key-value pairs represent the old parameters
88 and their corresponding new parameters.
89 Example: {old_param1: new_param1, old_param2: new_param2,...}
91 end_version : str {'future' | 'next' | <version>}, default='future'
92 Version when using the deprecated parameters will raise an error.
93 For informational purpose in the warning text.
95 lib_name : str, default='Nilearn'
96 Name of the library to which the decoratee belongs.
97 For informational purpose in the warning text.
99 """
101 def _replace_params(func):
102 @functools.wraps(func)
103 def wrapper(*args, **kwargs):
104 _warn_deprecated_params(
105 replacement_params, end_version, lib_name, kwargs
106 )
107 kwargs = transfer_deprecated_param_vals(replacement_params, kwargs)
108 return func(*args, **kwargs)
110 return wrapper
112 return _replace_params
115def _warn_deprecated_params(replacement_params, end_version, lib_name, kwargs):
116 """Raise warnings about deprecated parameters, \
117 for the decorator replace_parameters().
119 Parameters
120 ----------
121 replacement_params : Dict[str, str]
122 Dictionary of old_parameters as keys with replacement parameters
123 as their corresponding values.
125 end_version : str
126 The version where use of the deprecated parameters will raise an error.
127 For informational purpose in the warning text.
129 lib_name : str
130 Name of the library. For informational purpose in the warning text.
132 kwargs : Dict[str, any]
133 Dictionary of all the keyword args passed on the decorated function.
135 """
136 used_deprecated_params = set(kwargs).intersection(replacement_params)
137 for deprecated_param_ in used_deprecated_params:
138 replacement_param = replacement_params[deprecated_param_]
139 param_deprecation_msg = (
140 f'The parameter "{deprecated_param_}" '
141 f"will be removed in {end_version} release of {lib_name}. "
142 f'Please use the parameter "{replacement_param}" instead.'
143 )
144 warnings.warn(
145 category=DeprecationWarning,
146 message=param_deprecation_msg,
147 stacklevel=find_stack_level(),
148 )
151def transfer_deprecated_param_vals(replacement_params, kwargs):
152 """Reassigns new parameters \
153 the values passed to their corresponding deprecated parameters \
154 for the decorator replace_parameters().
156 Parameters
157 ----------
158 replacement_params : Dict[str, str]
159 Dictionary of old_parameters as keys with replacement parameters
160 as their corresponding values.
162 kwargs : Dict[str, any]
163 Dictionary of all the keyword args passed on the decorated function.
165 Returns
166 -------
167 kwargs : Dict[str, any]
168 Dictionary of all the keyword args to be passed on
169 to the decorated function, with old parameter names
170 replaced by new parameters, with their values intact.
172 """
173 for old_param, new_param in replacement_params.items():
174 old_param_val = kwargs.setdefault(old_param, None)
175 if old_param_val is not None:
176 kwargs[new_param] = old_param_val
177 kwargs.pop(old_param)
178 return kwargs
181def remove_parameters(removed_params, reason, end_version="future"):
182 """Use this decorator to deprecate \
183 but not renamed parameters in the decorated functions and methods.
185 Parameters
186 ----------
187 removed_params : list[string]
188 List of old parameters to be removed.
189 Example: [old_param1, old_param2, ...]
191 reason : str
192 Detailed reason of deprecated parameter and alternative solutions.
194 end_version : str {'future' | 'next' | <version>}, default='future'
195 Version when using the deprecated parameters will raise an error.
196 For informational purpose in the warning text.
198 """
200 def _remove_params(func):
201 @functools.wraps(func)
202 def wrapper(*args, **kwargs):
203 if found := set(removed_params).intersection(kwargs):
204 message = (
205 f"Parameter(s) {', '.join(found)} "
206 f"will be removed in version {end_version}; "
207 f"{reason}"
208 )
209 warnings.warn(
210 category=DeprecationWarning,
211 message=message,
212 stacklevel=find_stack_level(),
213 )
214 return func(*args, **kwargs)
216 return wrapper
218 return _remove_params
221def stringify_path(path):
222 """Convert path-like objects to string.
224 This is used to allow functions expecting string filesystem paths to accept
225 objects using `__fspath__` protocol.
227 Parameters
228 ----------
229 path : str or path-like object
231 Returns
232 -------
233 str
235 """
236 return path.__fspath__() if isinstance(path, os.PathLike) else path
239VERSION_OPERATORS = {
240 "==": operator.eq,
241 "!=": operator.ne,
242 ">": operator.gt,
243 ">=": operator.ge,
244 "<": operator.lt,
245 "<=": operator.le,
246}
249def compare_version(version_a, operator, version_b):
250 """Compare two version strings via a user-specified operator.
252 .. note::
254 This function is inspired from MNE-Python.
255 See https://github.com/mne-tools/mne-python/blob/main/mne/fixes.py
257 Parameters
258 ----------
259 version_a : :obj:`str`
260 First version string.
262 operator : {'==', '!=','>', '<', '>=', '<='}
263 Operator to compare ``version_a`` and ``version_b`` in the form of
264 ``version_a operator version_b``.
266 version_b : :obj:`str`
267 Second version string.
269 Returns
270 -------
271 result : :obj:`bool`
272 The result of the version comparison.
274 """
275 if operator not in VERSION_OPERATORS: 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true
276 error_msg = "'compare_version' received an unexpected operator "
277 raise ValueError(error_msg + operator + ".")
278 return VERSION_OPERATORS[operator](parse(version_a), parse(version_b))
281def is_matplotlib_installed():
282 """Check if matplotlib is installed."""
283 try:
284 import matplotlib # noqa: F401
285 except ImportError:
286 return False
287 else:
288 return True
291def check_matplotlib():
292 """Check if matplotlib is installed, raise an error if not.
294 Used in examples that require matplolib.
295 """
296 if not is_matplotlib_installed():
297 raise RuntimeError(
298 "This script needs the matplotlib library.\n"
299 "You can install Nilearn "
300 "and all its plotting dependencies with:\n"
301 "pip install 'nilearn[plotting]'"
302 )
305def is_plotly_installed():
306 """Check if plotly is installed."""
307 try:
308 import plotly.graph_objects as go # noqa: F401
309 except ImportError:
310 return False
311 return True
314def is_kaleido_installed():
315 """Check if kaleido is installed."""
316 try:
317 import kaleido # noqa: F401
318 except ImportError:
319 return False
320 return True
323# TODO: remove this function after release 0.13.0
324def check_copy_header(copy_header):
325 """Check the value of the `copy_header` parameter.
327 Only being used with `nilearn.image` and resampling functions to warn
328 users that `copy_header` will default to `True` from release 0.13.0
329 onwards.
331 Parameters
332 ----------
333 copy_header : :obj:`bool"
335 """
336 if not copy_header:
337 copy_header_default = (
338 "From release 0.13.0 onwards, this function will, by default, "
339 "copy the header of the input image to the output. "
340 "Currently, the header is reset to the default Nifti1Header. "
341 "To suppress this warning and use the new behavior, set "
342 "`copy_header=True`."
343 )
344 warnings.warn(
345 category=FutureWarning,
346 message=copy_header_default,
347 stacklevel=find_stack_level(),
348 )
351# TODO: This can be removed once MPL 3.5 is the min
352def constrained_layout_kwargs():
353 import matplotlib
355 if compare_version(matplotlib.__version__, ">=", "3.5"):
356 return {"layout": "constrained"}
357 else:
358 return {"constrained_layout": True}