10000 Add BackendRegistry singleton class · ianthomas23/matplotlib@bffe0be · GitHub
[go: up one dir, main page]

Skip to content

Commit bffe0be

Browse files
committed
Add BackendRegistry singleton class
1 parent 5e34777 commit bffe0be

File tree

8 files changed

+186
-41
lines changed

8 files changed

+186
-41
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,12 @@ def _safe_pyplot_import():
104104
current_framework = cbook._get_running_interactive_framework()
105105
if current_framework is None:
106106
raise # No, something else went wrong, likely with the install...
107-
backend_mapping = {
108-
'qt': 'qtagg',
109-
'gtk3': 'gtk3agg',
110-
'gtk4': 'gtk4agg',
111-
'wx': 'wxagg',
112-
'tk': 'tkagg',
113-
'macosx': 'macosx',
114-
'headless': 'agg',
115-
}
116-
backend = backend_mapping[current_framework]
107+
108+
from matplotlib.backends.registry import backendRegistry
109+
backend = backendRegistry.framework_to_backend(current_framework)
110+
if backend is None:
111+
raise KeyError(backend)
112+
117113
rcParams["backend"] = mpl.rcParamsOrig["backend"] = backend
118114
import matplotlib.pyplot as plt # Now this should succeed.
119115
return plt

lib/matplotlib/backends/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ python_sources = [
3333
'backend_wxagg.py',
3434
'backend_wxcairo.py',
3535
'qt_compat.py',
36+
'registry.py',
3637
]
3738

3839
typing_sources = [

lib/matplotlib/backends/registry.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from enum import Enum
2+
3+
4+
class BackendFilter(Enum):
5+
INTERACTIVE = 0
6+
INTERACTIVE_NON_WEB = 1
7+
NON_INTERACTIVE = 2
8+
9+
10+
class BackendRegistry:
11+
"""
12+
Registry of backends available within Matplotlib.
13+
14+
This is the single source of truth for available backends.
15+
"""
16+
def __init__(self):
17+
# Built-in backends are those which are included in the Matplotlib repo.
18+
# A backend with name 'name' is located in the module
19+
# f'matplotlib.backends.backend_{name.lower()}'
20+
21+
# The capitalized forms are needed for ipython at present; this may
22+
# change for later versions.
23+
self._builtin_interactive = [
24+
"GTK3Agg", "GTK3Cairo", "GTK4Agg", "GTK4Cairo",
25+
"MacOSX",
26+
"nbAgg",
27+
"QtAgg", "QtCairo", "Qt5Agg", "Qt5Cairo",
28+
"TkAgg", "TkCairo",
29+
"WebAgg",
30+
"WX", "WXAgg", "WXCairo",
31+
]
32+
self._builtin_not_interactive = [
33+
"agg", "cairo", "pdf", "pgf", "ps", "svg", "template",
34+
]
35+
self._framework_to_backend_mapping = {
36+
"qt": "qtagg",
37+
"gtk3": "gtk3agg",
38+
"gtk4": "gtk4agg",
39+
"wx": "wxagg",
40+
"tk": "tkagg",
41+
"macosx": "macosx",
42+
"headless": "agg",
43+
}
44+
45+
def framework_to_backend(self, interactive_framework):
46+
return self._framework_to_backend_mapping.get(interactive_framework)
47+
48+
def list_builtin(self, filter_=None):
49+
if filter_ == BackendFilter.INTERACTIVE:
50+
return self._builtin_interactive
51+
elif filter_ == BackendFilter.INTERACTIVE_NON_WEB:
52+
return list(filter(lambda x: x.lower() not in ("webagg", "nbagg"),
53+
self._builtin_interactive))
54+
elif filter_ == BackendFilter.NON_INTERACTIVE:
55+
return self._builtin_not_interactive
56+
57+
return self._builtin_interactive + self._builtin_not_interactive
58+
59+
60+
# Singleton
61+
backendRegistry = BackendRegistry()

lib/matplotlib/backends/registry.pyi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from enum import Enum
2+
3+
4+
class BackendFilter(Enum):
5+
INTERACTIVE: int
6+
INTERACTIVE_NON_WEB: int
7+
NON_INTERACTIVE: int
8+
9+
10+
class BackendRegistry:
11+
def __init__(self) -> None: ...
12+
def framework_to_backend(self, interactive_framework: str) -> str | None: ...
13+
def list_builtin(self, filter_: BackendFilter | None) -> list[str]: ...
14+
15+
16+
backendRegistry: BackendRegistry

lib/matplotlib/pyplot.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
from matplotlib.artist import Artist
7070
from matplotlib.axes import Axes
7171
from matplotlib.axes import Subplot # noqa: F401
72+
from matplotlib.backends.registry import BackendFilter, backendRegistry
7273
from matplotlib.projections import PolarAxes
7374
from matplotlib import mlab # for detrend_none, window_hanning
7475
from matplotlib.scale import get_scale_names # noqa: F401
@@ -301,16 +302,10 @@ def switch_backend(newbackend: str) -> None:
301302

302303
if newbackend is rcsetup._auto_backend_sentinel:
303304
current_framework = cbook._get_running_interactive_framework()
304-
mapping = {'qt': 'qtagg',
305-
'gtk3': 'gtk3agg',
306-
'gtk4': 'gtk4agg',
307-
'wx': 'wxagg',
308-
'tk': 'tkagg',
309-
'macosx': 'macosx',
310-
'headless': 'agg'}
311-
312-
if current_framework in mapping:
313-
candidates = [mapping[current_framework]]
305+
306+
if (current_framework and
307+
(backend := backendRegistry.framework_to_backend(current_framework))):
308+
candidates = [backend]
314309
else:
315310
candidates = []
316311
candidates += [
@@ -2509,10 +2504,10 @@ def polar(*args, **kwargs) -> list[Line2D]:
25092504
# requested, ignore rcParams['backend'] and force selection of a backend that
25102505
# is compatible with the current running interactive framework.
25112506
if (rcParams["backend_fallback"]
2512-
and rcParams._get_backend_or_none() in ( # type: ignore[attr-defined]
2513-
set(rcsetup.interactive_bk) - {'WebAgg', 'nbAgg'})
2514-
and cbook._get_running_interactive_framework()):
2515-
rcParams._set("backend", rcsetup._auto_backend_sentinel)
2507+
and rcParams._get_backend_or_none() in ( # type: ignore
2508+
set(backendRegistry.list_builtin(BackendFilter.INTERACTIVE_NON_WEB)))
2509+
and cbook._get_running_interactive_framework()): # type: ignore
2510+
rcParams._set("backend", rcsetup._auto_backend_sentinel) # type: ignore
25162511

25172512
# fmt: on
25182513

lib/matplotlib/rcsetup.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import numpy as np
2424

2525
from matplotlib import _api, cbook
26+
from matplotlib.backends.registry import BackendFilter, backendRegistry
2627
from matplotlib.cbook import ls_mapper
2728
from matplotlib.colors import Colormap, is_color_like
2829
from matplotlib._fontconfig_pattern import parse_fontconfig_pattern
@@ -32,20 +33,34 @@
3233
from cycler import Cycler, cycler as ccycler
3334

3435

35-
# The capitalized forms are needed for ipython at present; this may
36-
# change for later versions.
37-
interactive_bk = [
38-
'GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo',
39-
'MacOSX',
40-
'nbAgg',
41-
'QtAgg', 'QtCairo', 'Qt5Agg', 'Qt5Cairo',
42-
'TkAgg', 'TkCairo',
43-
'WebAgg',
44-
'WX', 'WXAgg', 'WXCairo',
45-
]
46-
non_interactive_bk = ['agg', 'cairo',
47-
'pdf', 'pgf', 'ps', 'svg', 'template']
48-
all_backends = interactive_bk + non_interactive_bk
36+
# Deprecation of module-level attributes using PEP 562
37+
_deprecated_interactive_bk = backendRegistry.list_builtin(BackendFilter.INTERACTIVE)
38+
_deprecated_non_interactive_bk = backendRegistry.list_builtin(
39+
BackendFilter.NON_INTERACTIVE)
40+
_deprecated_all_backends = backendRegistry.list_builtin()
41+
42+
_deprecated_names_and_args = {
43+
"interactive_bk": "matplotlib.backends.registry.BackendFilter.INTERACTIVE",
44+
"non_interactive_bk": "matplotlib.backends.registry.BackendFilter.NON_INTERACTIVE",
45+
"all_backends": "",
46+
}
47+
48+
49+
def __getattr__(name):
50+
if name in _deprecated_names_and_args:
51+
arg = _deprecated_names_and_args[name]
52+
_api.warn_deprecated(
53+
"3.9.0",
54+
name=name,
55+
alternative="``matplotlib.backends.registry.backendRegistry"
56+
f".list_builtin({arg})``",
57+
)
58+
return globals()[f"_deprecated_{name}"]
59+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
60+
61+
62+
def __dir__():
63+
return sorted(globals().keys() | _deprecated_names_and_args.keys())
4964

5065

5166
class ValidateInStrings:
@@ -256,7 +271,7 @@ def validate_fonttype(s):
256271

257272

258273
_validate_standard_backends = ValidateInStrings(
259-
'backend', all_backends, ignorecase=True)
274+
'backend', backendRegistry.list_builtin(), ignorecase=True)
260275
_auto_backend_sentinel = object()
261276

262277

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from collections.abc import Sequence
2+
from typing import Any
3+
4+
import pytest
5+
6+
from matplotlib.backends.registry import BackendFilter, backendRegistry
7+
8+
9+
def has_duplicates(seq: Sequence[Any]) -> bool:
10+
return len(seq) > len(set(seq))
11+
12+
13+
@pytest.mark.parametrize(
14+
'framework,expected',
15+
[
16+
('qt', 'qtagg'),
17+
('gtk3', 'gtk3agg'),
18+
('gtk4', 'gtk4agg'),
19+
('wx', 'wxagg'),
20+
('tk', 'tkagg'),
21+
('macosx', 'macosx'),
22+
('headless', 'agg'),
23+
('does not exist', None),
24+
]
25+
)
26+
def test_framework_to_backend(framework, expected):
27+
assert backendRegistry.framework_to_backend(framework) == expected
28+
29+
30+
def test_list_builtin():
31+
backends = backendRegistry.list_builtin()
32+
assert not has_duplicates(backends)
33+
# Compare using sets as order is not important
34+
assert set(backends) == set((
35+
'GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', 'MacOSX', 'nbAgg', 'QtAgg',
36+
'QtCairo', 'Qt5Agg', 'Qt5Cairo', 'TkAgg', 'TkCairo', 'WebAgg', 'WX', 'WXAgg',
37+
'WXCairo', 'agg', 'cairo', 'pdf', 'pgf', 'ps', 'svg', 'template',
38+
))
39+
40+
41+
@pytest.mark.parametrize(
42+
'filter,expected',
43+
[
44+
(BackendFilter.INTERACTIVE,
45+
['GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', 'MacOSX', 'nbAgg', 'QtAgg',
46+
'QtCairo', 'Qt5Agg', 'Qt5Cairo', 'TkAgg', 'TkCairo', 'WebAgg', 'WX', 'WXAgg',
47+
'WXCairo']),
48+
(BackendFilter.INTERACTIVE_NON_WEB,
49+
['GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', 'MacOSX', 'QtAgg', 'QtCairo',
50+
'Qt5Agg', 'Qt5Cairo', 'TkAgg', 'TkCairo', 'WX', 'WXAgg', 'WXCairo']),
51+
(BackendFilter.NON_INTERACTIVE,
52+
['agg', 'cairo', 'pdf', 'pgf', 'ps', 'svg', 'template']),
53+
]
54+
)
55+
def test_list_builtin_with_filter(filter, expected):
56+
backends = backendRegistry.list_builtin(filter)
57+
assert not has_duplicates(backends)
58+
# Compare using sets as order is not important
59+
assert set(backends) == set(expected)

lib/matplotlib/tests/test_matplotlib.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ def parse(key):
5757
backends += [e.strip() for e in line.split(',') if e]
5858
return backends
5959

60+
from matplotlib.backends.registry import BackendFilter, backendRegistry
61+
6062
assert (set(parse('- interactive backends:\n')) ==
61-
set(matplotlib.rcsetup.interactive_bk))
63+
set(backendRegistry.list_builtin(BackendFilter.INTERACTIVE)))
6264
assert (set(parse('- non-interactive backends:\n')) ==
63-
set(matplotlib.rcsetup.non_interactive_bk))
65+
set(backendRegistry.list_builtin(BackendFilter.NON_INTERACTIVE)))
6466

6567

6668
def test_importable_with__OO():

0 commit comments

Comments
 (0)
0