Skip to content

Commit 1639484

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 1639484

4 files changed

Lines changed: 68 additions & 27 deletions

File tree

lib/matplotlib/__init__.py

Lines changed: 52 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,37 @@ 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+
# Plain dict may contain "backend"; route it through _set so that
770+
# it lands in the class variable instead of the instance dict.
771+
other_params = list(other_params.items()
772+
if hasattr(other_params, 'items')
773+
else other_params)
774+
backend_val = None
775+
filtered = []
776+
for k, v in other_params:
777+
if k == "backend":
778+
backend_val = v
779+
else:
780+
filtered.append((k, v))
781+
if backend_val is not None:
782+
RcParams._backend = backend_val
783+
other_params = filtered
756784
dict.update(self, other_params)
757785

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-
770786
def __setitem__(self, key, val):
771-
if (key == "backend"
772-
and val is rcsetup._auto_backend_sentinel
773-
and "backend" in self):
787+
if (key == "backend" and val is rcsetup._auto_backend_sentinel):
788+
# Don't let caller silently overwrite a real backend with the auto-sentinel
789+
# (only internal code via _set() may do that).
790+
#
791+
# The primary reason for existence was covering internal rcParams logic
792+
# since end-users do not have direct access to
793+
# rcsetup._auto_backend_sentinel. It is likely that this is not needed
794+
# anymore due to removind "backend" from the dict and making it
795+
# a class attribute RcParams._backend`. But to be on the safe side, we
796+
# keep this as long as "backend" is a valid key for rcParams.
774797
return
775798
valid_key = _api.getitem_checked(
776799
self.validate, rcParam=key, _error_cls=KeyError
@@ -784,18 +807,24 @@ def __setitem__(self, key, val):
784807
self._set(key, cval)
785808

786809
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:
810+
if key == "backend":
811+
# In theory, this should only ever be used after the global rcParams
812+
# has been set up, but better be safe e.g. in presence of breakpoints.
813+
if (self is globals().get("rcParams")
814+
and RcParams._backend is rcsetup._auto_backend_sentinel):
792815
from matplotlib import pyplot as plt
793816
plt.switch_backend(rcsetup._auto_backend_sentinel)
794-
return self._get(key)
817+
return RcParams._backend
818+
return dict.__getitem__(self, key)
819+
820+
def __contains__(self, key):
821+
if key == "backend":
822+
return True
823+
return dict.__contains__(self, key)
795824

796825
def _get_backend_or_none(self):
797826
"""Get the requested backend, if any, without triggering resolution."""
798-
backend = self._get("backend")
827+
backend = RcParams._backend
799828
return None if backend is rcsetup._auto_backend_sentinel else backend
800829

801830
def __repr__(self):
@@ -992,7 +1021,6 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
9921021
transform=lambda line: line[1:] if line.startswith("#") else line,
9931022
fail_on_error=True)
9941023
rcParamsDefault._update_raw(rcsetup._hardcoded_defaults)
995-
rcParamsDefault._ensure_has_backend()
9961024

9971025
rcParams = RcParams() # The global instance.
9981026
rcParams._update_raw(rcParamsDefault)
@@ -1201,8 +1229,7 @@ def rc_context(rc=None, fname=None):
12011229
plt.plot(x, y)
12021230
12031231
"""
1204-
orig = dict(rcParams.copy())
1205-
del orig['backend']
1232+
orig = rcParams.copy()
12061233
try:
12071234
if fname:
12081235
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)