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

1import functools 

2import operator 

3import os 

4import warnings 

5 

6from packaging.version import parse 

7 

8from nilearn._utils.logger import find_stack_level 

9 

10OPTIONAL_MATPLOTLIB_MIN_VERSION = "3.3.0" 

11 

12 

13def set_mpl_backend(message=None): 

14 """Check if matplotlib is installed. 

15 

16 If not installed, raise error and display warning to install necessary 

17 dependencies. 

18 

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. 

22 

23 If current backend is not usable, switch to default "Agg" backend. 

24 

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() 

58 

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() 

66 

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 ) 

73 

74 

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. 

83 

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,...} 

90 

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. 

94 

95 lib_name : str, default='Nilearn' 

96 Name of the library to which the decoratee belongs. 

97 For informational purpose in the warning text. 

98 

99 """ 

100 

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) 

109 

110 return wrapper 

111 

112 return _replace_params 

113 

114 

115def _warn_deprecated_params(replacement_params, end_version, lib_name, kwargs): 

116 """Raise warnings about deprecated parameters, \ 

117 for the decorator replace_parameters(). 

118 

119 Parameters 

120 ---------- 

121 replacement_params : Dict[str, str] 

122 Dictionary of old_parameters as keys with replacement parameters 

123 as their corresponding values. 

124 

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. 

128 

129 lib_name : str 

130 Name of the library. For informational purpose in the warning text. 

131 

132 kwargs : Dict[str, any] 

133 Dictionary of all the keyword args passed on the decorated function. 

134 

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 ) 

149 

150 

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(). 

155 

156 Parameters 

157 ---------- 

158 replacement_params : Dict[str, str] 

159 Dictionary of old_parameters as keys with replacement parameters 

160 as their corresponding values. 

161 

162 kwargs : Dict[str, any] 

163 Dictionary of all the keyword args passed on the decorated function. 

164 

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. 

171 

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 

179 

180 

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. 

184 

185 Parameters 

186 ---------- 

187 removed_params : list[string] 

188 List of old parameters to be removed. 

189 Example: [old_param1, old_param2, ...] 

190 

191 reason : str 

192 Detailed reason of deprecated parameter and alternative solutions. 

193 

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. 

197 

198 """ 

199 

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) 

215 

216 return wrapper 

217 

218 return _remove_params 

219 

220 

221def stringify_path(path): 

222 """Convert path-like objects to string. 

223 

224 This is used to allow functions expecting string filesystem paths to accept 

225 objects using `__fspath__` protocol. 

226 

227 Parameters 

228 ---------- 

229 path : str or path-like object 

230 

231 Returns 

232 ------- 

233 str 

234 

235 """ 

236 return path.__fspath__() if isinstance(path, os.PathLike) else path 

237 

238 

239VERSION_OPERATORS = { 

240 "==": operator.eq, 

241 "!=": operator.ne, 

242 ">": operator.gt, 

243 ">=": operator.ge, 

244 "<": operator.lt, 

245 "<=": operator.le, 

246} 

247 

248 

249def compare_version(version_a, operator, version_b): 

250 """Compare two version strings via a user-specified operator. 

251 

252 .. note:: 

253 

254 This function is inspired from MNE-Python. 

255 See https://github.com/mne-tools/mne-python/blob/main/mne/fixes.py 

256 

257 Parameters 

258 ---------- 

259 version_a : :obj:`str` 

260 First version string. 

261 

262 operator : {'==', '!=','>', '<', '>=', '<='} 

263 Operator to compare ``version_a`` and ``version_b`` in the form of 

264 ``version_a operator version_b``. 

265 

266 version_b : :obj:`str` 

267 Second version string. 

268 

269 Returns 

270 ------- 

271 result : :obj:`bool` 

272 The result of the version comparison. 

273 

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)) 

279 

280 

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 

289 

290 

291def check_matplotlib(): 

292 """Check if matplotlib is installed, raise an error if not. 

293 

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 ) 

303 

304 

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 

312 

313 

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 

321 

322 

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. 

326 

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. 

330 

331 Parameters 

332 ---------- 

333 copy_header : :obj:`bool" 

334 

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 ) 

349 

350 

351# TODO: This can be removed once MPL 3.5 is the min 

352def constrained_layout_kwargs(): 

353 import matplotlib 

354 

355 if compare_version(matplotlib.__version__, ">=", "3.5"): 

356 return {"layout": "constrained"} 

357 else: 

358 return {"constrained_layout": True}