Skip to content

Commit 8b8563d

Browse files
committed
MNT: Move "backend" from a regular rcParams dict entry to a class attribute
Working towards matplotlib#31791. > rcParams should be only configuration, not state; i.e. wrt. backend, it > can hold the desired backend and backend priorities, but it should not > hold the currently active backend or trigger backend selection as a > side-effect. The latter causes a lot of complexity in rcParams. This PR is the first step in the migration strategy outlined there: > Make the backend a RcParams class attribute and do not hold > "backend" as a regular dict entry. There can only be one backend, > so putting that value in a class attribute `RcParams._backend` makes > sense. This already reduces the complexity as we don't need special > handling to sync/copy the value when rcParams are bulk-replaced > (loading from file or rc_context). Instead add special logic to keep > behavioral backward compatibility with the dict API > (`rcParams["backend"]` / `rcParams._get("backend")` / > `"rcParams._set("backend", val)`). Note on API compatibility: This PR limits itself to the known typical dict-API usages in the context of backend handling. I've refrained from trying to fully replicate dict behavior of a "virtual" "backend" parameter, i.e. `len(rcParams)` / `rcParams.keys()` / `rcParams.values()` / `rcParams.items()` do not contain "backend" anymore. I would be willing to accept this level of API breakage. If deemed critical, we could add compatibility shims for that too.
1 parent 7c377c5 commit 8b8563d

4 files changed

Lines changed: 51 additions & 27 deletions

File tree

lib/matplotlib/__init__.py

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,12 @@ class RcParams(MutableMapping, dict):
693693

694694
validate = rcsetup._validators
695695

696+
# Class-level backend state: shared across all RcParams instances because
697+
# there can only be one active backend at a time.
698+
# rcParams["backend"] remains valid API for now, but stores the values here
699+
# and not as regular key in the underlying dict.
700+
_backend = rcsetup._auto_backend_sentinel
701+
696702
# validate values on the way in
697703
def __init__(self, *args, **kwargs):
698704
self.update(*args, **kwargs)
@@ -715,6 +721,9 @@ def _set(self, key, val):
715721
716722
:meta public:
717723
"""
724+
if key == "backend":
725+
RcParams._backend = val
726+
return
718727
dict.__setitem__(self, key, val)
719728

720729
def _get(self, key):
@@ -736,6 +745,8 @@ def _get(self, key):
736745
737746
:meta public:
738747
"""
748+
if key == "backend":
749+
return RcParams._backend
739750
return dict.__getitem__(self, key)
740751

741752
def _update_raw(self, other_params):
@@ -753,24 +764,24 @@ def _update_raw(self, other_params):
753764
"""
754765
if isinstance(other_params, RcParams):
755766
other_params = dict.items(other_params)
767+
else:
768+
if "backend" in other_params:
769+
# should not happen because we aim to not use "backend" as a regular
770+
# key anymore, but keep to ensure we have not overlooked a code path.
771+
raise RuntimeError("'backend' must not be passed to _update_raw()")
756772
dict.update(self, other_params)
757773

758-
def _ensure_has_backend(self):
759-
"""
760-
Ensure that a "backend" entry exists.
761-
762-
Normally, the default matplotlibrc file contains *no* entry for "backend" (the
763-
corresponding line starts with ##, not #; we fill in _auto_backend_sentinel
764-
in that case. However, packagers can set a different default backend
765-
(resulting in a normal `#backend: foo` line) in which case we should *not*
766-
fill in _auto_backend_sentinel.
767-
"""
768-
dict.setdefault(self, "backend", rcsetup._auto_backend_sentinel)
769-
770774
def __setitem__(self, key, val):
771-
if (key == "backend"
772-
and val is rcsetup._auto_backend_sentinel
773-
and "backend" in self):
775+
if (key == "backend" and val is rcsetup._auto_backend_sentinel):
776+
# Don't let caller silently overwrite a real backend with the auto-sentinel
777+
# (only internal code via _set() may do that).
778+
#
779+
# The primary reason for existence was covering internal rcParams logic
780+
# since end-users do not have direct access to
781+
# rcsetup._auto_backend_sentinel. It is likely that this is not needed
782+
# anymore due to removing "backend" from the dict and making it
783+
# a class attribute RcParams._backend`. But to be on the safe side, we
784+
# keep this as long as "backend" is a valid key for rcParams.
774785
return
775786
valid_key = _api.getitem_checked(
776787
self.validate, rcParam=key, _error_cls=KeyError
@@ -784,18 +795,19 @@ def __setitem__(self, key, val):
784795
self._set(key, cval)
785796

786797
def __getitem__(self, key):
787-
# In theory, this should only ever be used after the global rcParams
788-
# has been set up, but better be safe e.g. in presence of breakpoints.
789-
if key == "backend" and self is globals().get("rcParams"):
790-
val = self._get(key)
791-
if val is rcsetup._auto_backend_sentinel:
798+
if key == "backend":
799+
# In theory, this should only ever be used after the global rcParams
800+
# has been set up, but better be safe e.g. in presence of breakpoints.
801+
if (self is globals().get("rcParams")
802+
and RcParams._backend is rcsetup._auto_backend_sentinel):
792803
from matplotlib import pyplot as plt
793804
plt.switch_backend(rcsetup._auto_backend_sentinel)
794-
return self._get(key)
805+
return RcParams._backend
806+
return dict.__getitem__(self, key)
795807

796808
def _get_backend_or_none(self):
797809
"""Get the requested backend, if any, without triggering resolution."""
798-
backend = self._get("backend")
810+
backend = RcParams._backend
799811
return None if backend is rcsetup._auto_backend_sentinel else backend
800812

801813
def __repr__(self):
@@ -992,7 +1004,6 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
9921004
transform=lambda line: line[1:] if line.startswith("#") else line,
9931005
fail_on_error=True)
9941006
rcParamsDefault._update_raw(rcsetup._hardcoded_defaults)
995-
rcParamsDefault._ensure_has_backend()
9961007

9971008
rcParams = RcParams() # The global instance.
9981009
rcParams._update_raw(rcParamsDefault)
@@ -1201,8 +1212,7 @@ def rc_context(rc=None, fname=None):
12011212
plt.plot(x, y)
12021213
12031214
"""
1204-
orig = dict(rcParams.copy())
1205-
del orig['backend']
1215+
orig = rcParams.copy()
12061216
try:
12071217
if fname:
12081218
rc_file(fname)

lib/matplotlib/__init__.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ class RcParams(dict[RcKeyType, Any]):
7878

7979
def _update_raw(self, other_params: dict | RcParams) -> None: ...
8080

81-
def _ensure_has_backend(self) -> None: ...
8281
def __setitem__(self, key: RcKeyType, val: Any) -> None: ...
8382
def __getitem__(self, key: RcKeyType) -> Any: ...
8483
def __iter__(self) -> Generator[RcKeyType, None, None]: ...

lib/matplotlib/tests/test_rcparams.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,13 @@ def test_rc_aliases(group, option, alias, value):
686686

687687

688688
def test_all_params_defined_as_code():
689-
assert set(p.name for p in rcsetup._params_list()) == set(mpl.rcParams.keys())
689+
params_in_code = {p.name for p in rcsetup._params_list()}
690+
# 'backend' is stored in RcParams._backend (a class variable) rather than
691+
# in the underlying dict, so it does not appear in rcParams.keys() /
692+
# __iter__. It is still accessible via rcParams['backend'] and
693+
# 'backend' in rcParams; it just doesn't show up during iteration.
694+
params_in_code.remove("backend")
695+
assert params_in_code == set(mpl.rcParams.keys())
690696

691697

692698
def test_validators_defined_as_code():

lib/matplotlib/tests/test_typing.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ def test_rcparam_stubs():
104104
if not name.startswith('_')
105105
}
106106

107+
# backend is not a regular dict key anymore, but we have special logic to ensure
108+
# read and write access to it.
109+
# The _get('backend') is just a smoke test that 'backend' is still supported, and
110+
# it is thus justified to add 'backend' to the keys. This will fail in the future
111+
# when we remove 'backend' as accepted key and will remind us that we have to
112+
# remove it from the stubs.
113+
plt.rcParamsDefault._get('backend')
114+
runtime_rc_keys.add('backend')
115+
107116
assert {*typing.get_args(RcKeyType)} == runtime_rc_keys
108117

109118
runtime_rc_group_keys = set()

0 commit comments

Comments
 (0)