diff --git a/doc/api/backend_registry_api.rst b/doc/api/backend_registry_api.rst new file mode 100644 index 000000000000..ca184c67d0a2 --- /dev/null +++ b/doc/api/backend_registry_api.rst @@ -0,0 +1,8 @@ +******************************** +``matplotlib.backends.registry`` +******************************** + +.. automodule:: matplotlib.backends.registry + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index 6012f71c52a4..66009d86825d 100644 --- a/doc/api/index_backend_api.rst +++ b/doc/api/index_backend_api.rst @@ -17,6 +17,7 @@ backend_pdf_api.rst backend_pgf_api.rst backend_ps_api.rst + backend_registry_api.rst backend_qt_api.rst backend_svg_api.rst backend_tk_api.rst diff --git a/doc/api/next_api_changes/deprecations/27719-IT.rst b/doc/api/next_api_changes/deprecations/27719-IT.rst new file mode 100644 index 000000000000..c41e9d2c396f --- /dev/null +++ b/doc/api/next_api_changes/deprecations/27719-IT.rst @@ -0,0 +1,11 @@ +``rcsetup.interactive_bk``, ``rcsetup.non_interactive_bk`` and ``rcsetup.all_backends`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... are deprecated and replaced by ``matplotlib.backends.backend_registry.list_builtin`` +with the following arguments + +- ``matplotlib.backends.BackendFilter.INTERACTIVE`` +- ``matplotlib.backends.BackendFilter.NON_INTERACTIVE`` +- ``None`` + +respectively. diff --git a/doc/users/next_whats_new/backend_registry.rst b/doc/users/next_whats_new/backend_registry.rst new file mode 100644 index 000000000000..61b65a9d6470 --- /dev/null +++ b/doc/users/next_whats_new/backend_registry.rst @@ -0,0 +1,6 @@ +BackendRegistry +~~~~~~~~~~~~~~~ + +New :class:`~matplotlib.backends.registry.BackendRegistry` class is the single +source of truth for available backends. The singleton instance is +``matplotlib.backends.backend_registry``. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index a353500f8725..a1e7eb4f3ffc 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -93,32 +93,6 @@ } -def _safe_pyplot_import(): - """ - Import and return ``pyplot``, correctly setting the backend if one is - already forced. - """ - try: - import matplotlib.pyplot as plt - except ImportError: # Likely due to a framework mismatch. - current_framework = cbook._get_running_interactive_framework() - if current_framework is None: - raise # No, something else went wrong, likely with the install... - backend_mapping = { - 'qt': 'qtagg', - 'gtk3': 'gtk3agg', - 'gtk4': 'gtk4agg', - 'wx': 'wxagg', - 'tk': 'tkagg', - 'macosx': 'macosx', - 'headless': 'agg', - } - backend = backend_mapping[current_framework] - rcParams["backend"] = mpl.rcParamsOrig["backend"] = backend - import matplotlib.pyplot as plt # Now this should succeed. - return plt - - def register_backend(format, backend, description=None): """ Register a backend for saving to a given file format. diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index 3e687f85b0be..cf0f682d5d4b 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -1,3 +1,5 @@ +from .registry import BackendFilter, backend_registry # noqa: F401 + # NOTE: plt.switch_backend() (called at import time) will add a "backend" # attribute here for backcompat. _QT_FORCE_QT5_BINDING = False diff --git a/lib/matplotlib/backends/meson.build b/lib/matplotlib/backends/meson.build index 050cc616b42c..1e3e47c0a915 100644 --- a/lib/matplotlib/backends/meson.build +++ b/lib/matplotlib/backends/meson.build @@ -33,6 +33,7 @@ python_sources = [ 'backend_wxagg.py', 'backend_wxcairo.py', 'qt_compat.py', + 'registry.py', ] typing_sources = [ diff --git a/lib/matplotlib/backends/registry.py b/lib/matplotlib/backends/registry.py new file mode 100644 index 000000000000..484d6ed5f26d --- /dev/null +++ b/lib/matplotlib/backends/registry.py @@ -0,0 +1,93 @@ +from enum import Enum + + +class BackendFilter(Enum): + """ + Filter used with :meth:`~matplotlib.backends.registry.BackendRegistry.list_builtin` + + .. versionadded:: 3.9 + """ + INTERACTIVE = 0 + NON_INTERACTIVE = 1 + + +class BackendRegistry: + """ + Registry of backends available within Matplotlib. + + This is the single source of truth for available backends. + + All use of ``BackendRegistry`` should be via the singleton instance + ``backend_registry`` which can be imported from ``matplotlib.backends``. + + .. versionadded:: 3.9 + """ + # Built-in backends are those which are included in the Matplotlib repo. + # A backend with name 'name' is located in the module + # f'matplotlib.backends.backend_{name.lower()}' + + # The capitalized forms are needed for ipython at present; this may + # change for later versions. + _BUILTIN_INTERACTIVE = [ + "GTK3Agg", "GTK3Cairo", "GTK4Agg", "GTK4Cairo", + "MacOSX", + "nbAgg", + "QtAgg", "QtCairo", "Qt5Agg", "Qt5Cairo", + "TkAgg", "TkCairo", + "WebAgg", + "WX", "WXAgg", "WXCairo", + ] + _BUILTIN_NOT_INTERACTIVE = [ + "agg", "cairo", "pdf", "pgf", "ps", "svg", "template", + ] + _GUI_FRAMEWORK_TO_BACKEND_MAPPING = { + "qt": "qtagg", + "gtk3": "gtk3agg", + "gtk4": "gtk4agg", + "wx": "wxagg", + "tk": "tkagg", + "macosx": "macosx", + "headless": "agg", + } + + def backend_for_gui_framework(self, framework): + """ + Return the name of the backend corresponding to the specified GUI framework. + + Parameters + ---------- + framework : str + GUI framework such as "qt". + + Returns + ------- + str + Backend name. + """ + return self._GUI_FRAMEWORK_TO_BACKEND_MAPPING.get(framework) + + def list_builtin(self, filter_=None): + """ + Return list of backends that are built into Matplotlib. + + Parameters + ---------- + filter_ : `~.BackendFilter`, optional + Filter to apply to returned backends. For example, to return only + non-interactive backends use `.BackendFilter.NON_INTERACTIVE`. + + Returns + ------- + list of str + Backend names. + """ + if filter_ == BackendFilter.INTERACTIVE: + return self._BUILTIN_INTERACTIVE + elif filter_ == BackendFilter.NON_INTERACTIVE: + return self._BUILTIN_NOT_INTERACTIVE + + return self._BUILTIN_INTERACTIVE + self._BUILTIN_NOT_INTERACTIVE + + +# Singleton +backend_registry = BackendRegistry() diff --git a/lib/matplotlib/backends/registry.pyi b/lib/matplotlib/backends/registry.pyi new file mode 100644 index 000000000000..e48531be471d --- /dev/null +++ b/lib/matplotlib/backends/registry.pyi @@ -0,0 +1,14 @@ +from enum import Enum + + +class BackendFilter(Enum): + INTERACTIVE: int + NON_INTERACTIVE: int + + +class BackendRegistry: + def backend_for_gui_framework(self, interactive_framework: str) -> str | None: ... + def list_builtin(self, filter_: BackendFilter | None) -> list[str]: ... + + +backend_registry: BackendRegistry diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 2cf0d5325a63..778a9e132d43 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -69,6 +69,7 @@ from matplotlib.artist import Artist from matplotlib.axes import Axes from matplotlib.axes import Subplot # noqa: F401 +from matplotlib.backends import BackendFilter, backend_registry from matplotlib.projections import PolarAxes from matplotlib import mlab # for detrend_none, window_hanning from matplotlib.scale import get_scale_names # noqa: F401 @@ -301,16 +302,11 @@ def switch_backend(newbackend: str) -> None: if newbackend is rcsetup._auto_backend_sentinel: current_framework = cbook._get_running_interactive_framework() - mapping = {'qt': 'qtagg', - 'gtk3': 'gtk3agg', - 'gtk4': 'gtk4agg', - 'wx': 'wxagg', - 'tk': 'tkagg', - 'macosx': 'macosx', - 'headless': 'agg'} - - if current_framework in mapping: - candidates = [mapping[current_framework]] + + if (current_framework and + (backend := backend_registry.backend_for_gui_framework( + current_framework))): + candidates = [backend] else: candidates = [] candidates += [ @@ -2510,7 +2506,8 @@ def polar(*args, **kwargs) -> list[Line2D]: # is compatible with the current running interactive framework. if (rcParams["backend_fallback"] and rcParams._get_backend_or_none() in ( # type: ignore[attr-defined] - set(rcsetup.interactive_bk) - {'WebAgg', 'nbAgg'}) + set(backend_registry.list_builtin(BackendFilter.INTERACTIVE)) - + {'WebAgg', 'nbAgg'}) and cbook._get_running_interactive_framework()): rcParams._set("backend", rcsetup._auto_backend_sentinel) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index f730db0dee3b..6abc8372222d 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -23,6 +23,7 @@ import numpy as np from matplotlib import _api, cbook +from matplotlib.backends import BackendFilter, backend_registry from matplotlib.cbook import ls_mapper from matplotlib.colors import Colormap, is_color_like from matplotlib._fontconfig_pattern import parse_fontconfig_pattern @@ -32,20 +33,30 @@ from cycler import Cycler, cycler as ccycler -# The capitalized forms are needed for ipython at present; this may -# change for later versions. -interactive_bk = [ - 'GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', - 'MacOSX', - 'nbAgg', - 'QtAgg', 'QtCairo', 'Qt5Agg', 'Qt5Cairo', - 'TkAgg', 'TkCairo', - 'WebAgg', - 'WX', 'WXAgg', 'WXCairo', -] -non_interactive_bk = ['agg', 'cairo', - 'pdf', 'pgf', 'ps', 'svg', 'template'] -all_backends = interactive_bk + non_interactive_bk +@_api.caching_module_getattr +class __getattr__: + @_api.deprecated( + "3.9", + alternative="``matplotlib.backends.backend_registry.list_builtin" + "(matplotlib.backends.BackendFilter.INTERACTIVE)``") + @property + def interactive_bk(self): + return backend_registry.list_builtin(BackendFilter.INTERACTIVE) + + @_api.deprecated( + "3.9", + alternative="``matplotlib.backends.backend_registry.list_builtin" + "(matplotlib.backends.BackendFilter.NON_INTERACTIVE)``") + @property + def non_interactive_bk(self): + return backend_registry.list_builtin(BackendFilter.NON_INTERACTIVE) + + @_api.deprecated( + "3.9", + alternative="``matplotlib.backends.backend_registry.list_builtin()``") + @property + def all_backends(self): + return backend_registry.list_builtin() class ValidateInStrings: @@ -256,7 +267,7 @@ def validate_fonttype(s): _validate_standard_backends = ValidateInStrings( - 'backend', all_backends, ignorecase=True) + 'backend', backend_registry.list_builtin(), ignorecase=True) _auto_backend_sentinel = object() diff --git a/lib/matplotlib/tests/test_backend_registry.py b/lib/matplotlib/tests/test_backend_registry.py new file mode 100644 index 000000000000..aed258f36413 --- /dev/null +++ b/lib/matplotlib/tests/test_backend_registry.py @@ -0,0 +1,67 @@ +from collections.abc import Sequence +from typing import Any + +import pytest + +import matplotlib as mpl +from matplotlib.backends import BackendFilter, backend_registry + + +def has_duplicates(seq: Sequence[Any]) -> bool: + return len(seq) > len(set(seq)) + + +@pytest.mark.parametrize( + 'framework,expected', + [ + ('qt', 'qtagg'), + ('gtk3', 'gtk3agg'), + ('gtk4', 'gtk4agg'), + ('wx', 'wxagg'), + ('tk', 'tkagg'), + ('macosx', 'macosx'), + ('headless', 'agg'), + ('does not exist', None), + ] +) +def test_backend_for_gui_framework(framework, expected): + assert backend_registry.backend_for_gui_framework(framework) == expected + + +def test_list_builtin(): + backends = backend_registry.list_builtin() + assert not has_duplicates(backends) + # Compare using sets as order is not important + assert {*backends} == { + 'GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', 'MacOSX', 'nbAgg', 'QtAgg', + 'QtCairo', 'Qt5Agg', 'Qt5Cairo', 'TkAgg', 'TkCairo', 'WebAgg', 'WX', 'WXAgg', + 'WXCairo', 'agg', 'cairo', 'pdf', 'pgf', 'ps', 'svg', 'template', + } + + +@pytest.mark.parametrize( + 'filter,expected', + [ + (BackendFilter.INTERACTIVE, + ['GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', 'MacOSX', 'nbAgg', 'QtAgg', + 'QtCairo', 'Qt5Agg', 'Qt5Cairo', 'TkAgg', 'TkCairo', 'WebAgg', 'WX', 'WXAgg', + 'WXCairo']), + (BackendFilter.NON_INTERACTIVE, + ['agg', 'cairo', 'pdf', 'pgf', 'ps', 'svg', 'template']), + ] +) +def test_list_builtin_with_filter(filter, expected): + backends = backend_registry.list_builtin(filter) + assert not has_duplicates(backends) + # Compare using sets as order is not important + assert {*backends} == {*expected} + + +def test_deprecated_rcsetup_attributes(): + match = "was deprecated in Matplotlib 3.9" + with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match): + mpl.rcsetup.interactive_bk + with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match): + mpl.rcsetup.non_interactive_bk + with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match): + mpl.rcsetup.all_backends diff --git a/lib/matplotlib/tests/test_matplotlib.py b/lib/matplotlib/tests/test_matplotlib.py index a1aa4ec212d6..a2f467ac48de 100644 --- a/lib/matplotlib/tests/test_matplotlib.py +++ b/lib/matplotlib/tests/test_matplotlib.py @@ -57,10 +57,12 @@ def parse(key): backends += [e.strip() for e in line.split(',') if e] return backends + from matplotlib.backends import BackendFilter, backend_registry + assert (set(parse('- interactive backends:\n')) == - set(matplotlib.rcsetup.interactive_bk)) + set(backend_registry.list_builtin(BackendFilter.INTERACTIVE))) assert (set(parse('- non-interactive backends:\n')) == - set(matplotlib.rcsetup.non_interactive_bk)) + set(backend_registry.list_builtin(BackendFilter.NON_INTERACTIVE))) def test_importable_with__OO():
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: