Skip to content

Commit 85159ce

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 85159ce

4 files changed

Lines changed: 53 additions & 27 deletions

File tree

lib/matplotlib/__init__.py

Lines changed: 37 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. None of the three
698+
# RcParams instances (rcParams, rcParamsDefault, rcParamsOrig) stores
699+
# "backend" as a dict key; reads/writes are all redirected here.
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):
@@ -752,25 +763,27 @@ def _update_raw(self, other_params):
752763
The input mapping from which to update.
753764
"""
754765
if isinstance(other_params, RcParams):
766+
# RcParams never stores "backend" in its dict; skip it here too.
755767
other_params = dict.items(other_params)
768+
else:
769+
if "backend" in other_params:
770+
raise ValueError(
771+
"'backend' should not be passed anymore in internal calls "
772+
"to _update_raw"
773+
)
756774
dict.update(self, other_params)
757775

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-
770776
def __setitem__(self, key, val):
771-
if (key == "backend"
772-
and val is rcsetup._auto_backend_sentinel
773-
and "backend" in self):
777+
if (key == "backend" and val is rcsetup._auto_backend_sentinel):
778+
# Don't let caller silently overwrite a real backend with the auto-sentinel
779+
# (only internal code via _set() may do that).
780+
#
781+
# The primary reason for existence was covering internal rcParams logic
782+
# since end-users do not have direct access to
783+
# rcsetup._auto_backend_sentinel. It is likely that this is not needed
784+
# anymore due to removind "backend" from the dict and making it
785+
# a class attribute RcParams._backend`. But to be on the safe side, we
786+
# keep this as long as "backend" is a valid key for rcParams.
774787
return
775788
valid_key = _api.getitem_checked(
776789
self.validate, rcParam=key, _error_cls=KeyError
@@ -784,18 +797,19 @@ def __setitem__(self, key, val):
784797
self._set(key, cval)
785798

786799
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:
800+
if key == "backend":
801+
# In theory, this should only ever be used after the global rcParams
802+
# has been set up, but better be safe e.g. in presence of breakpoints.
803+
if (self is globals().get("rcParams")
804+
and RcParams._backend is rcsetup._auto_backend_sentinel):
792805
from matplotlib import pyplot as plt
793806
plt.switch_backend(rcsetup._auto_backend_sentinel)
794-
return self._get(key)
807+
return RcParams._backend
808+
return dict.__getitem__(self, key)
795809

796810
def _get_backend_or_none(self):
797811
"""Get the requested backend, if any, without triggering resolution."""
798-
backend = self._get("backend")
812+
backend = RcParams._backend
799813
return None if backend is rcsetup._auto_backend_sentinel else backend
800814

801815
def __repr__(self):
@@ -992,7 +1006,6 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
9921006
transform=lambda line: line[1:] if line.startswith("#") else line,
9931007
fail_on_error=True)
9941008
rcParamsDefault._update_raw(rcsetup._hardcoded_defaults)
995-
rcParamsDefault._ensure_has_backend()
9961009

9971010
rcParams = RcParams() # The global instance.
9981011
rcParams._update_raw(rcParamsDefault)
@@ -1201,8 +1214,7 @@ def rc_context(rc=None, fname=None):
12011214
plt.plot(x, y)
12021215
12031216
"""
1204-
orig = dict(rcParams.copy())
1205-
del orig['backend']
1217+
orig = rcParams.copy()
12061218
try:
12071219
if fname:
12081220
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)