diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 2a83cc1f1091..8f2d77e0e413 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -693,6 +693,12 @@ class RcParams(MutableMapping, dict): validate = rcsetup._validators + # Class-level backend state: shared across all RcParams instances because + # there can only be one active backend at a time. + # rcParams["backend"] remains valid API for now, but stores the values here + # and not as regular key in the underlying dict. + _backend = rcsetup._auto_backend_sentinel + # validate values on the way in def __init__(self, *args, **kwargs): self.update(*args, **kwargs) @@ -715,6 +721,9 @@ def _set(self, key, val): :meta public: """ + if key == "backend": + RcParams._backend = val + return dict.__setitem__(self, key, val) def _get(self, key): @@ -736,6 +745,8 @@ def _get(self, key): :meta public: """ + if key == "backend": + return RcParams._backend return dict.__getitem__(self, key) def _update_raw(self, other_params): @@ -753,24 +764,24 @@ def _update_raw(self, other_params): """ if isinstance(other_params, RcParams): other_params = dict.items(other_params) + else: + if "backend" in other_params: + # should not happen because we aim to not use "backend" as a regular + # key anymore, but keep to ensure we have not overlooked a code path. + raise RuntimeError("'backend' must not be passed to _update_raw()") dict.update(self, other_params) - def _ensure_has_backend(self): - """ - Ensure that a "backend" entry exists. - - Normally, the default matplotlibrc file contains *no* entry for "backend" (the - corresponding line starts with ##, not #; we fill in _auto_backend_sentinel - in that case. However, packagers can set a different default backend - (resulting in a normal `#backend: foo` line) in which case we should *not* - fill in _auto_backend_sentinel. - """ - dict.setdefault(self, "backend", rcsetup._auto_backend_sentinel) - def __setitem__(self, key, val): - if (key == "backend" - and val is rcsetup._auto_backend_sentinel - and "backend" in self): + if (key == "backend" and val is rcsetup._auto_backend_sentinel): + # Don't let caller silently overwrite a real backend with the auto-sentinel + # (only internal code via _set() may do that). + # + # The primary reason for existence was covering internal rcParams logic + # since end-users do not have direct access to + # rcsetup._auto_backend_sentinel. It is likely that this is not needed + # anymore due to removing "backend" from the dict and making it + # a class attribute RcParams._backend`. But to be on the safe side, we + # keep this as long as "backend" is a valid key for rcParams. return valid_key = _api.getitem_checked( self.validate, rcParam=key, _error_cls=KeyError @@ -784,18 +795,19 @@ def __setitem__(self, key, val): self._set(key, cval) def __getitem__(self, key): - # In theory, this should only ever be used after the global rcParams - # has been set up, but better be safe e.g. in presence of breakpoints. - if key == "backend" and self is globals().get("rcParams"): - val = self._get(key) - if val is rcsetup._auto_backend_sentinel: + if key == "backend": + # In theory, this should only ever be used after the global rcParams + # has been set up, but better be safe e.g. in presence of breakpoints. + if (self is globals().get("rcParams") + and RcParams._backend is rcsetup._auto_backend_sentinel): from matplotlib import pyplot as plt plt.switch_backend(rcsetup._auto_backend_sentinel) - return self._get(key) + return RcParams._backend + return dict.__getitem__(self, key) def _get_backend_or_none(self): """Get the requested backend, if any, without triggering resolution.""" - backend = self._get("backend") + backend = RcParams._backend return None if backend is rcsetup._auto_backend_sentinel else backend def __repr__(self): @@ -992,7 +1004,6 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): transform=lambda line: line[1:] if line.startswith("#") else line, fail_on_error=True) rcParamsDefault._update_raw(rcsetup._hardcoded_defaults) -rcParamsDefault._ensure_has_backend() rcParams = RcParams() # The global instance. rcParams._update_raw(rcParamsDefault) @@ -1201,8 +1212,7 @@ def rc_context(rc=None, fname=None): plt.plot(x, y) """ - orig = dict(rcParams.copy()) - del orig['backend'] + orig = rcParams.copy() try: if fname: rc_file(fname) diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 47c9784068fb..fdf70b5aed4b 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -77,7 +77,6 @@ class RcParams(dict[RcKeyType, Any]): def _update_raw(self, other_params: dict[RcKeyType, Any] | RcParams) -> None: ... - def _ensure_has_backend(self) -> None: ... def __setitem__(self, key: RcKeyType, val: Any) -> None: ... def __getitem__(self, key: RcKeyType) -> Any: ... def __iter__(self) -> Generator[RcKeyType, None, None]: ... diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index fb1b2730e93e..ecfa222785a3 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -681,7 +681,13 @@ def test_rc_aliases(group, option, alias, value): def test_all_params_defined_as_code(): - assert set(p.name for p in rcsetup._params_list()) == set(mpl.rcParams.keys()) + params_in_code = {p.name for p in rcsetup._params_list()} + # 'backend' is stored in RcParams._backend (a class variable) rather than + # in the underlying dict, so it does not appear in rcParams.keys() / + # __iter__. It is still accessible via rcParams['backend'] and + # 'backend' in rcParams; it just doesn't show up during iteration. + params_in_code.remove("backend") + assert params_in_code == set(mpl.rcParams.keys()) def test_validators_defined_as_code(): diff --git a/lib/matplotlib/tests/test_typing.py b/lib/matplotlib/tests/test_typing.py index 5d219f91ffa9..902592bcee42 100644 --- a/lib/matplotlib/tests/test_typing.py +++ b/lib/matplotlib/tests/test_typing.py @@ -103,6 +103,15 @@ def test_rcparam_stubs(): if not name.startswith('_') } + # backend is not a regular dict key anymore, but we have special logic to ensure + # read and write access to it. + # The _get('backend') is just a smoke test that 'backend' is still supported, and + # it is thus justified to add 'backend' to the keys. This will fail in the future + # when we remove 'backend' as accepted key and will remind us that we have to + # remove it from the stubs. + plt.rcParamsDefault._get('backend') # smoke test + runtime_rc_keys.add('backend') + assert isinstance(RcKeyType, typing.TypeAliasType) assert {*typing.get_args(RcKeyType.__value__)} == runtime_rc_keys