Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add BackendRegistry singleton class #27719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/api/backend_registry_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
********************************
``matplotlib.backends.registry``
********************************

.. automodule:: matplotlib.backends.registry
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions doc/api/index_backend_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions doc/api/next_api_changes/deprecations/27719-IT.rst
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions doc/users/next_whats_new/backend_registry.rst
Original file line number Diff line number Diff line change
@@ -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``.
26 changes: 0 additions & 26 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/matplotlib/backends/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ python_sources = [
'backend_wxagg.py',
'backend_wxcairo.py',
'qt_compat.py',
'registry.py',
]

typing_sources = [
Expand Down
93 changes: 93 additions & 0 deletions lib/matplotlib/backends/registry.py
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we have matplotlib.backends.registry.backend_registry, which seems a bit redundant. Would it make sense to move this to _registry.py (to keep files small), and then import into matplotlib/backends/__init__.py so we have matplotlib.backends.registry as the instance?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not. There is value in matching the the singleton instance name backend_registry to the class name BackendRegistry. Also the internal usage is from matplotlib.backends.registry import backend_registry, so the name is free standing. Only registry would be quite ambiguous (one could change to fully qualified usages only, but 🤷 ). And finally, the backend registry is used really rarely so that verbosity is not much of an issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean towards the explicit and verbose approach, but maybe there is some middle ground. If I keep the files named as they are but import into matplotlib.backends.__init__ using from .registry import BackendFilter, backend_registry then the standard usage becomes

from matplotlib.backends import backend_registry

rather than

from matplotlib.backends.registry import backend_registry

removing some verbosity.

This has the added advantage that you cannot import the BackendRegistry class in the same way which is good as nobody should need the class, only the singleton instance.

I have implemented this in commit 8f10a01.

14 changes: 14 additions & 0 deletions lib/matplotlib/backends/registry.pyi
Original file line number Diff line number Diff line change
@@ -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
19 changes: 8 additions & 11 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 += [
Expand Down Expand Up @@ -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)

Expand Down
41 changes: 26 additions & 15 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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()


Expand Down
67 changes: 67 additions & 0 deletions lib/matplotlib/tests/test_backend_registry.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 4 additions & 2 deletions lib/matplotlib/tests/test_matplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down