| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- from enum import Enum
- import importlib
- 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``.
- Each backend has a name, a module name containing the backend code, and an
- optional GUI framework that must be running if the backend is interactive.
- There are three sources of backends: built-in (source code is within the
- Matplotlib repository), explicit ``module://some.backend`` syntax (backend is
- obtained by loading the module), or via an entry point (self-registering
- backend in an external package).
- .. versionadded:: 3.9
- """
- # Mapping of built-in backend name to GUI framework, or "headless" for no
- # GUI framework. 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()}"
- _BUILTIN_BACKEND_TO_GUI_FRAMEWORK = {
- "gtk3agg": "gtk3",
- "gtk3cairo": "gtk3",
- "gtk4agg": "gtk4",
- "gtk4cairo": "gtk4",
- "macosx": "macosx",
- "nbagg": "nbagg",
- "notebook": "nbagg",
- "qtagg": "qt",
- "qtcairo": "qt",
- "qt5agg": "qt5",
- "qt5cairo": "qt5",
- "tkagg": "tk",
- "tkcairo": "tk",
- "webagg": "webagg",
- "wx": "wx",
- "wxagg": "wx",
- "wxcairo": "wx",
- "agg": "headless",
- "cairo": "headless",
- "pdf": "headless",
- "pgf": "headless",
- "ps": "headless",
- "svg": "headless",
- "template": "headless",
- }
- # Reverse mapping of gui framework to preferred built-in backend.
- _GUI_FRAMEWORK_TO_BACKEND = {
- "gtk3": "gtk3agg",
- "gtk4": "gtk4agg",
- "headless": "agg",
- "macosx": "macosx",
- "qt": "qtagg",
- "qt5": "qt5agg",
- "qt6": "qtagg",
- "tk": "tkagg",
- "wx": "wxagg",
- }
- def __init__(self):
- # Only load entry points when first needed.
- self._loaded_entry_points = False
- # Mapping of non-built-in backend to GUI framework, added dynamically from
- # entry points and from matplotlib.use("module://some.backend") format.
- # New entries have an "unknown" GUI framework that is determined when first
- # needed by calling _get_gui_framework_by_loading.
- self._backend_to_gui_framework = {}
- # Mapping of backend name to module name, where different from
- # f"matplotlib.backends.backend_{backend_name.lower()}". These are either
- # hardcoded for backward compatibility, or loaded from entry points or
- # "module://some.backend" syntax.
- self._name_to_module = {
- "notebook": "nbagg",
- }
- def _backend_module_name(self, backend):
- if backend.startswith("module://"):
- return backend[9:]
- # Return name of module containing the specified backend.
- # Does not check if the backend is valid, use is_valid_backend for that.
- backend = backend.lower()
- # Check if have specific name to module mapping.
- backend = self._name_to_module.get(backend, backend)
- return (backend[9:] if backend.startswith("module://")
- else f"matplotlib.backends.backend_{backend}")
- def _clear(self):
- # Clear all dynamically-added data, used for testing only.
- self.__init__()
- def _ensure_entry_points_loaded(self):
- # Load entry points, if they have not already been loaded.
- if not self._loaded_entry_points:
- entries = self._read_entry_points()
- self._validate_and_store_entry_points(entries)
- self._loaded_entry_points = True
- def _get_gui_framework_by_loading(self, backend):
- # Determine GUI framework for a backend by loading its module and reading the
- # FigureCanvas.required_interactive_framework attribute.
- # Returns "headless" if there is no GUI framework.
- module = self.load_backend_module(backend)
- canvas_class = module.FigureCanvas
- return canvas_class.required_interactive_framework or "headless"
- def _read_entry_points(self):
- # Read entry points of modules that self-advertise as Matplotlib backends.
- # Expects entry points like this one from matplotlib-inline (in pyproject.toml
- # format):
- # [project.entry-points."matplotlib.backend"]
- # inline = "matplotlib_inline.backend_inline"
- import importlib.metadata as im
- entry_points = im.entry_points(group="matplotlib.backend")
- entries = [(entry.name, entry.value) for entry in entry_points]
- # For backward compatibility, if matplotlib-inline and/or ipympl are installed
- # but too old to include entry points, create them. Do not import ipympl
- # directly as this calls matplotlib.use() whilst in this function.
- def backward_compatible_entry_points(
- entries, module_name, threshold_version, names, target):
- from matplotlib import _parse_to_version_info
- try:
- module_version = im.version(module_name)
- if _parse_to_version_info(module_version) < threshold_version:
- for name in names:
- entries.append((name, target))
- except im.PackageNotFoundError:
- pass
- names = [entry[0] for entry in entries]
- if "inline" not in names:
- backward_compatible_entry_points(
- entries, "matplotlib_inline", (0, 1, 7), ["inline"],
- "matplotlib_inline.backend_inline")
- if "ipympl" not in names:
- backward_compatible_entry_points(
- entries, "ipympl", (0, 9, 4), ["ipympl", "widget"],
- "ipympl.backend_nbagg")
- return entries
- def _validate_and_store_entry_points(self, entries):
- # Validate and store entry points so that they can be used via matplotlib.use()
- # in the normal manner. Entry point names cannot be of module:// format, cannot
- # shadow a built-in backend name, and there cannot be multiple entry points
- # with the same name but different modules. Multiple entry points with the same
- # name and value are permitted (it can sometimes happen outside of our control,
- # see https://github.com/matplotlib/matplotlib/issues/28367).
- for name, module in set(entries):
- name = name.lower()
- if name.startswith("module://"):
- raise RuntimeError(
- f"Entry point name '{name}' cannot start with 'module://'")
- if name in self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK:
- raise RuntimeError(f"Entry point name '{name}' is a built-in backend")
- if name in self._backend_to_gui_framework:
- raise RuntimeError(f"Entry point name '{name}' duplicated")
- self._name_to_module[name] = "module://" + module
- # Do not yet know backend GUI framework, determine it only when necessary.
- self._backend_to_gui_framework[name] = "unknown"
- 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 or None
- Backend name or None if GUI framework not recognised.
- """
- return self._GUI_FRAMEWORK_TO_BACKEND.get(framework.lower())
- def is_valid_backend(self, backend):
- """
- Return True if the backend name is valid, False otherwise.
- A backend name is valid if it is one of the built-in backends or has been
- dynamically added via an entry point. Those beginning with ``module://`` are
- always considered valid and are added to the current list of all backends
- within this function.
- Even if a name is valid, it may not be importable or usable. This can only be
- determined by loading and using the backend module.
- Parameters
- ----------
- backend : str
- Name of backend.
- Returns
- -------
- bool
- True if backend is valid, False otherwise.
- """
- if not backend.startswith("module://"):
- backend = backend.lower()
- # For backward compatibility, convert ipympl and matplotlib-inline long
- # module:// names to their shortened forms.
- backwards_compat = {
- "module://ipympl.backend_nbagg": "widget",
- "module://matplotlib_inline.backend_inline": "inline",
- }
- backend = backwards_compat.get(backend, backend)
- if (backend in self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK or
- backend in self._backend_to_gui_framework):
- return True
- if backend.startswith("module://"):
- self._backend_to_gui_framework[backend] = "unknown"
- return True
- # Only load entry points if really need to and not already done so.
- self._ensure_entry_points_loaded()
- if backend in self._backend_to_gui_framework:
- return True
- return False
- def list_all(self):
- """
- Return list of all known backends.
- These include built-in backends and those obtained at runtime either from entry
- points or explicit ``module://some.backend`` syntax.
- Entry points will be loaded if they haven't been already.
- Returns
- -------
- list of str
- Backend names.
- """
- self._ensure_entry_points_loaded()
- return [*self.list_builtin(), *self._backend_to_gui_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 [k for k, v in self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK.items()
- if v != "headless"]
- elif filter_ == BackendFilter.NON_INTERACTIVE:
- return [k for k, v in self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK.items()
- if v == "headless"]
- return [*self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK]
- def list_gui_frameworks(self):
- """
- Return list of GUI frameworks used by Matplotlib backends.
- Returns
- -------
- list of str
- GUI framework names.
- """
- return [k for k in self._GUI_FRAMEWORK_TO_BACKEND if k != "headless"]
- def load_backend_module(self, backend):
- """
- Load and return the module containing the specified backend.
- Parameters
- ----------
- backend : str
- Name of backend to load.
- Returns
- -------
- Module
- Module containing backend.
- """
- module_name = self._backend_module_name(backend)
- return importlib.import_module(module_name)
- def resolve_backend(self, backend):
- """
- Return the backend and GUI framework for the specified backend name.
- If the GUI framework is not yet known then it will be determined by loading the
- backend module and checking the ``FigureCanvas.required_interactive_framework``
- attribute.
- This function only loads entry points if they have not already been loaded and
- the backend is not built-in and not of ``module://some.backend`` format.
- Parameters
- ----------
- backend : str or None
- Name of backend, or None to use the default backend.
- Returns
- -------
- backend : str
- The backend name.
- framework : str or None
- The GUI framework, which will be None for a backend that is non-interactive.
- """
- if isinstance(backend, str):
- if not backend.startswith("module://"):
- backend = backend.lower()
- else: # Might be _auto_backend_sentinel or None
- # Use whatever is already running...
- from matplotlib import get_backend
- backend = get_backend()
- # Is backend already known (built-in or dynamically loaded)?
- gui = (self._BUILTIN_BACKEND_TO_GUI_FRAMEWORK.get(backend) or
- self._backend_to_gui_framework.get(backend))
- # Is backend "module://something"?
- if gui is None and isinstance(backend, str) and backend.startswith("module://"):
- gui = "unknown"
- # Is backend a possible entry point?
- if gui is None and not self._loaded_entry_points:
- self._ensure_entry_points_loaded()
- gui = self._backend_to_gui_framework.get(backend)
- # Backend known but not its gui framework.
- if gui == "unknown":
- gui = self._get_gui_framework_by_loading(backend)
- self._backend_to_gui_framework[backend] = gui
- if gui is None:
- raise RuntimeError(f"'{backend}' is not a recognised backend name")
- return backend, gui if gui != "headless" else None
- def resolve_gui_or_backend(self, gui_or_backend):
- """
- Return the backend and GUI framework for the specified string that may be
- either a GUI framework or a backend name, tested in that order.
- This is for use with the IPython %matplotlib magic command which may be a GUI
- framework such as ``%matplotlib qt`` or a backend name such as
- ``%matplotlib qtagg``.
- This function only loads entry points if they have not already been loaded and
- the backend is not built-in and not of ``module://some.backend`` format.
- Parameters
- ----------
- gui_or_backend : str or None
- Name of GUI framework or backend, or None to use the default backend.
- Returns
- -------
- backend : str
- The backend name.
- framework : str or None
- The GUI framework, which will be None for a backend that is non-interactive.
- """
- if not gui_or_backend.startswith("module://"):
- gui_or_backend = gui_or_backend.lower()
- # First check if it is a gui loop name.
- backend = self.backend_for_gui_framework(gui_or_backend)
- if backend is not None:
- return backend, gui_or_backend if gui_or_backend != "headless" else None
- # Then check if it is a backend name.
- try:
- return self.resolve_backend(gui_or_backend)
- except Exception: # KeyError ?
- raise RuntimeError(
- f"'{gui_or_backend}' is not a recognised GUI loop or backend name")
- # Singleton
- backend_registry = BackendRegistry()
|