Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 35 additions & 25 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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)
Comment on lines -758 to -768

@timhoffm timhoffm May 31, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: _ensure_has_backend() is not needed anymore, because the backend is stored in the class attribute RcParams._backend and thus always available.


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
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1201,8 +1212,7 @@ def rc_context(rc=None, fname=None):
plt.plot(x, y)

"""
orig = dict(rcParams.copy())
del orig['backend']
Comment on lines -1204 to -1205

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: The dict conversion was only needed to be able to drop the backend entry. Since that doesn't exist anymore, we don't need the dict conversion.

orig = rcParams.copy()
try:
if fname:
rc_file(fname)
Expand Down
1 change: 0 additions & 1 deletion lib/matplotlib/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]: ...
Expand Down
8 changes: 7 additions & 1 deletion lib/matplotlib/tests/test_rcparams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
9 changes: 9 additions & 0 deletions lib/matplotlib/tests/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading