Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/backends/registry.py: 24%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

122 statements  

1from enum import Enum 

2import importlib 

3 

4 

5class BackendFilter(Enum): 

6 """ 

7 Filter used with :meth:`~matplotlib.backends.registry.BackendRegistry.list_builtin` 

8 

9 .. versionadded:: 3.9 

10 """ 

11 INTERACTIVE = 0 

12 NON_INTERACTIVE = 1 

13 

14 

15class BackendRegistry: 

16 """ 

17 Registry of backends available within Matplotlib. 

18 

19 This is the single source of truth for available backends. 

20 

21 All use of ``BackendRegistry`` should be via the singleton instance 

22 ``backend_registry`` which can be imported from ``matplotlib.backends``. 

23 

24 Each backend has a name, a module name containing the backend code, and an 

25 optional GUI framework that must be running if the backend is interactive. 

26 There are three sources of backends: built-in (source code is within the 

27 Matplotlib repository), explicit ``module://some.backend`` syntax (backend is 

28 obtained by loading the module), or via an entry point (self-registering 

29 backend in an external package). 

30 

31 .. versionadded:: 3.9 

32 """ 

33 # Mapping of built-in backend name to GUI framework, or "headless" for no 

34 # GUI framework. Built-in backends are those which are included in the 

35 # Matplotlib repo. A backend with name 'name' is located in the module 

36 # f"matplotlib.backends.backend_{name.lower()}" 

37 _BUILTIN_BACKEND_TO_GUI_FRAMEWORK = { 

38 "gtk3agg": "gtk3", 

39 "gtk3cairo": "gtk3", 

40 "gtk4agg": "gtk4", 

41 "gtk4cairo": "gtk4", 

42 "macosx": "macosx", 

43 "nbagg": "nbagg", 

44 "notebook": "nbagg", 

45 "qtagg": "qt", 

46 "qtcairo": "qt", 

47 "qt5agg": "qt5", 

48 "qt5cairo": "qt5", 

49 "tkagg": "tk", 

50 "tkcairo": "tk", 

51 "webagg": "webagg", 

52 "wx": "wx", 

53 "wxagg": "wx", 

54 "wxcairo": "wx", 

55 "agg": "headless", 

56 "cairo": "headless", 

57 "pdf": "headless", 

58 "pgf": "headless", 

59 "ps": "headless", 

60 "svg": "headless", 

61 "template": "headless", 

62 } 

63 

64 # Reverse mapping of gui framework to preferred built-in backend. 

65 _GUI_FRAMEWORK_TO_BACKEND = { 

66 "gtk3": "gtk3agg", 

67 "gtk4": "gtk4agg", 

68 "headless": "agg", 

69 "macosx": "macosx", 

70 "qt": "qtagg", 

71 "qt5": "qt5agg", 

72 "qt6": "qtagg", 

73 "tk": "tkagg", 

74 "wx": "wxagg", 

75 } 

76 

77 def __init__(self): 

78 # Only load entry points when first needed. 

79 self._loaded_entry_points = False 

80 

81 # Mapping of non-built-in backend to GUI framework, added dynamically from 

82 # entry points and from matplotlib.use("module://some.backend") format. 

83 # New entries have an "unknown" GUI framework that is determined when first 

84 # needed by calling _get_gui_framework_by_loading. 

85 self._backend_to_gui_framework = {} 

86 

87 # Mapping of backend name to module name, where different from 

88 # f"matplotlib.backends.backend_{backend_name.lower()}". These are either 

89 # hardcoded for backward compatibility, or loaded from entry points or 

90 # "module://some.backend" syntax. 

91 self._name_to_module = { 

92 "notebook": "nbagg", 

93 } 

94 

95 def _backend_module_name(self, backend): 

96 if backend.startswith("module://"): 

97 return backend[9:] 

98 

99 # Return name of module containing the specified backend. 

100 # Does not check if the backend is valid, use is_valid_backend for that. 

101 backend = backend.lower() 

102 

103 # Check if have specific name to module mapping. 

104 backend = self._name_to_module.get(backend, backend) 

105 

106 return (backend[9:] if backend.startswith("module://") 

107 else f"matplotlib.backends.backend_{backend}") 

108 

109 def _clear(self): 

110 # Clear all dynamically-added data, used for testing only. 

111 self.__init__() 

112 

113 def _ensure_entry_points_loaded(self): 

114 # Load entry points, if they have not already been loaded. 

115 if not self._loaded_entry_points: 

116 entries = self._read_entry_points() 

117 self._validate_and_store_entry_points(entries) 

118 self._loaded_entry_points = True 

119 

120 def _get_gui_framework_by_loading(self, backend): 

121 # Determine GUI framework for a backend by loading its module and reading the 

122 # FigureCanvas.required_interactive_framework attribute. 

123 # Returns "headless" if there is no GUI framework. 

124 module = self.load_backend_module(backend) 

125 canvas_class = module.FigureCanvas 

126 return canvas_class.required_interactive_framework or "headless" 

127 

128 def _read_entry_points(self): 

129 # Read entry points of modules that self-advertise as Matplotlib backends. 

130 # Expects entry points like this one from matplotlib-inline (in pyproject.toml 

131 # format): 

132 # [project.entry-points."matplotlib.backend"] 

133 # inline = "matplotlib_inline.backend_inline" 

134 import importlib.metadata as im 

135 import sys 

136 

137 # entry_points group keyword not available before Python 3.10 

138 group = "matplotlib.backend" 

139 if sys.version_info >= (3, 10): 

140 entry_points = im.entry_points(group=group) 

141 else: 

142 entry_points = im.entry_points().get(group, ()) 

143 entries = [(entry.name, entry.value) for entry in entry_points] 

144 

145 # For backward compatibility, if matplotlib-inline and/or ipympl are installed 

146 # but too old to include entry points, create them. Do not import ipympl 

147 # directly as this calls matplotlib.use() whilst in this function. 

148 def backward_compatible_entry_points( 

149 entries, module_name, threshold_version, names, target): 

150 from matplotlib import _parse_to_version_info 

151 try: 

152 module_version = im.version(module_name) 

153 if _parse_to_version_info(module_version) < threshold_version: 

154 for name in names: 

155 entries.append((name, target)) 

156 except im.PackageNotFoundError: 

157 pass 

158 

159 names = [entry[0] for entry in entries] 

160 if "inline" not in names: 

161 backward_compatible_entry_points( 

162 entries, "matplotlib_inline", (0, 1, 7), ["inline"], 

163 "matplotlib_inline.backend_inline") 

164 if "ipympl" not in names: 

165 backward_compatible_entry_points( 

166 entries, "ipympl", (0, 9, 4), ["ipympl", "widget"], 

167 "ipympl.backend_nbagg") 

168 

169 return entries 

170 

171 def _validate_and_store_entry_points(self, entries): 

172 # Validate and store entry points so that they can be used via matplotlib.use() 

173 # in the normal manner. Entry point names cannot be of module:// format, cannot 

174 # shadow a built-in backend name, and there cannot be multiple entry points 

175 # with the same name but different modules. Multiple entry points with the same 

176 # name and value are permitted (it can sometimes happen outside of our control, 

177 # see https://github.com/matplotlib/matplotlib/issues/28367). 

178 for name, module in set(entries): 

179 name = name.lower() 

180 if name.startswith("module://"): 

181 raise RuntimeError( 

182 f"Entry point name '{name}' cannot start with 'module://'") 

183 if name in self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK: 

184 raise RuntimeError(f"Entry point name '{name}' is a built-in backend") 

185 if name in self._backend_to_gui_framework: 

186 raise RuntimeError(f"Entry point name '{name}' duplicated") 

187 

188 self._name_to_module[name] = "module://" + module 

189 # Do not yet know backend GUI framework, determine it only when necessary. 

190 self._backend_to_gui_framework[name] = "unknown" 

191 

192 def backend_for_gui_framework(self, framework): 

193 """ 

194 Return the name of the backend corresponding to the specified GUI framework. 

195 

196 Parameters 

197 ---------- 

198 framework : str 

199 GUI framework such as "qt". 

200 

201 Returns 

202 ------- 

203 str or None 

204 Backend name or None if GUI framework not recognised. 

205 """ 

206 return self._GUI_FRAMEWORK_TO_BACKEND.get(framework.lower()) 

207 

208 def is_valid_backend(self, backend): 

209 """ 

210 Return True if the backend name is valid, False otherwise. 

211 

212 A backend name is valid if it is one of the built-in backends or has been 

213 dynamically added via an entry point. Those beginning with ``module://`` are 

214 always considered valid and are added to the current list of all backends 

215 within this function. 

216 

217 Even if a name is valid, it may not be importable or usable. This can only be 

218 determined by loading and using the backend module. 

219 

220 Parameters 

221 ---------- 

222 backend : str 

223 Name of backend. 

224 

225 Returns 

226 ------- 

227 bool 

228 True if backend is valid, False otherwise. 

229 """ 

230 if not backend.startswith("module://"): 

231 backend = backend.lower() 

232 

233 # For backward compatibility, convert ipympl and matplotlib-inline long 

234 # module:// names to their shortened forms. 

235 backwards_compat = { 

236 "module://ipympl.backend_nbagg": "widget", 

237 "module://matplotlib_inline.backend_inline": "inline", 

238 } 

239 backend = backwards_compat.get(backend, backend) 

240 

241 if (backend in self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK or 

242 backend in self._backend_to_gui_framework): 

243 return True 

244 

245 if backend.startswith("module://"): 

246 self._backend_to_gui_framework[backend] = "unknown" 

247 return True 

248 

249 # Only load entry points if really need to and not already done so. 

250 self._ensure_entry_points_loaded() 

251 if backend in self._backend_to_gui_framework: 

252 return True 

253 

254 return False 

255 

256 def list_all(self): 

257 """ 

258 Return list of all known backends. 

259 

260 These include built-in backends and those obtained at runtime either from entry 

261 points or explicit ``module://some.backend`` syntax. 

262 

263 Entry points will be loaded if they haven't been already. 

264 

265 Returns 

266 ------- 

267 list of str 

268 Backend names. 

269 """ 

270 self._ensure_entry_points_loaded() 

271 return [*self.list_builtin(), *self._backend_to_gui_framework] 

272 

273 def list_builtin(self, filter_=None): 

274 """ 

275 Return list of backends that are built into Matplotlib. 

276 

277 Parameters 

278 ---------- 

279 filter_ : `~.BackendFilter`, optional 

280 Filter to apply to returned backends. For example, to return only 

281 non-interactive backends use `.BackendFilter.NON_INTERACTIVE`. 

282 

283 Returns 

284 ------- 

285 list of str 

286 Backend names. 

287 """ 

288 if filter_ == BackendFilter.INTERACTIVE: 

289 return [k for k, v in self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK.items() 

290 if v != "headless"] 

291 elif filter_ == BackendFilter.NON_INTERACTIVE: 

292 return [k for k, v in self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK.items() 

293 if v == "headless"] 

294 

295 return [*self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK] 

296 

297 def list_gui_frameworks(self): 

298 """ 

299 Return list of GUI frameworks used by Matplotlib backends. 

300 

301 Returns 

302 ------- 

303 list of str 

304 GUI framework names. 

305 """ 

306 return [k for k in self._GUI_FRAMEWORK_TO_BACKEND if k != "headless"] 

307 

308 def load_backend_module(self, backend): 

309 """ 

310 Load and return the module containing the specified backend. 

311 

312 Parameters 

313 ---------- 

314 backend : str 

315 Name of backend to load. 

316 

317 Returns 

318 ------- 

319 Module 

320 Module containing backend. 

321 """ 

322 module_name = self._backend_module_name(backend) 

323 return importlib.import_module(module_name) 

324 

325 def resolve_backend(self, backend): 

326 """ 

327 Return the backend and GUI framework for the specified backend name. 

328 

329 If the GUI framework is not yet known then it will be determined by loading the 

330 backend module and checking the ``FigureCanvas.required_interactive_framework`` 

331 attribute. 

332 

333 This function only loads entry points if they have not already been loaded and 

334 the backend is not built-in and not of ``module://some.backend`` format. 

335 

336 Parameters 

337 ---------- 

338 backend : str or None 

339 Name of backend, or None to use the default backend. 

340 

341 Returns 

342 ------- 

343 backend : str 

344 The backend name. 

345 framework : str or None 

346 The GUI framework, which will be None for a backend that is non-interactive. 

347 """ 

348 if isinstance(backend, str): 

349 if not backend.startswith("module://"): 

350 backend = backend.lower() 

351 else: # Might be _auto_backend_sentinel or None 

352 # Use whatever is already running... 

353 from matplotlib import get_backend 

354 backend = get_backend() 

355 

356 # Is backend already known (built-in or dynamically loaded)? 

357 gui = (self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK.get(backend) or 

358 self._backend_to_gui_framework.get(backend)) 

359 

360 # Is backend "module://something"? 

361 if gui is None and isinstance(backend, str) and backend.startswith("module://"): 

362 gui = "unknown" 

363 

364 # Is backend a possible entry point? 

365 if gui is None and not self._loaded_entry_points: 

366 self._ensure_entry_points_loaded() 

367 gui = self._backend_to_gui_framework.get(backend) 

368 

369 # Backend known but not its gui framework. 

370 if gui == "unknown": 

371 gui = self._get_gui_framework_by_loading(backend) 

372 self._backend_to_gui_framework[backend] = gui 

373 

374 if gui is None: 

375 raise RuntimeError(f"'{backend}' is not a recognised backend name") 

376 

377 return backend, gui if gui != "headless" else None 

378 

379 def resolve_gui_or_backend(self, gui_or_backend): 

380 """ 

381 Return the backend and GUI framework for the specified string that may be 

382 either a GUI framework or a backend name, tested in that order. 

383 

384 This is for use with the IPython %matplotlib magic command which may be a GUI 

385 framework such as ``%matplotlib qt`` or a backend name such as 

386 ``%matplotlib qtagg``. 

387 

388 This function only loads entry points if they have not already been loaded and 

389 the backend is not built-in and not of ``module://some.backend`` format. 

390 

391 Parameters 

392 ---------- 

393 gui_or_backend : str or None 

394 Name of GUI framework or backend, or None to use the default backend. 

395 

396 Returns 

397 ------- 

398 backend : str 

399 The backend name. 

400 framework : str or None 

401 The GUI framework, which will be None for a backend that is non-interactive. 

402 """ 

403 if not gui_or_backend.startswith("module://"): 

404 gui_or_backend = gui_or_backend.lower() 

405 

406 # First check if it is a gui loop name. 

407 backend = self.backend_for_gui_framework(gui_or_backend) 

408 if backend is not None: 

409 return backend, gui_or_backend if gui_or_backend != "headless" else None 

410 

411 # Then check if it is a backend name. 

412 try: 

413 return self.resolve_backend(gui_or_backend) 

414 except Exception: # KeyError ? 

415 raise RuntimeError( 

416 f"'{gui_or_backend} is not a recognised GUI loop or backend name") 

417 

418 

419# Singleton 

420backend_registry = BackendRegistry()