From 01ceeef0b28ea7bdd4e7e32bef1d3eabb8aa23b2 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:45:02 +0200 Subject: [PATCH] MNT: Don't rely on RcParams being a dict subclass in internal code Eventually, we want to be able to remove the dict subclassing from RcParams, which will allow better initialization and handling. We've publically announced that people should not rely on dict in https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.7.0.html#rcparams-type This is an internal cleanup step to remove the dict-assumption and a preparation for further refactoring. --- lib/matplotlib/__init__.py | 48 +++++++++++++++++++++++++++---------- lib/matplotlib/__init__.pyi | 4 ++++ lib/matplotlib/pyplot.py | 5 ++-- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 8a77e5601d8c..9f9e7cbceddc 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -712,6 +712,35 @@ def _get(self, key): """ return dict.__getitem__(self, key) + def _update_raw(self, other_params): + """ + Directly update the data from *other_params*, bypassing deprecation, + backend and validation logic on both sides. + + This ``rcParams._update_raw(params)`` replaces the previous pattern + ``dict.update(rcParams, params)``. + + Parameters + ---------- + other_params : dict or `.RcParams` + The input mapping from which to update. + """ + if isinstance(other_params, RcParams): + other_params = dict.items(other_params) + 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): try: if key in _deprecated_map: @@ -961,24 +990,17 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): return config -# When constructing the global instances, we need to perform certain updates -# by explicitly calling the superclass (dict.update, dict.items) to avoid -# triggering resolution of _auto_backend_sentinel. rcParamsDefault = _rc_params_in_file( cbook._get_data_path("matplotlibrc"), # Strip leading comment. transform=lambda line: line[1:] if line.startswith("#") else line, fail_on_error=True) -dict.update(rcParamsDefault, rcsetup._hardcoded_defaults) -# Normally, the default matplotlibrc file contains *no* entry for backend (the -# corresponding line starts with ##, not #; we fill on _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(rcParamsDefault, "backend", rcsetup._auto_backend_sentinel) +rcParamsDefault._update_raw(rcsetup._hardcoded_defaults) +rcParamsDefault._ensure_has_backend() + rcParams = RcParams() # The global instance. -dict.update(rcParams, dict.items(rcParamsDefault)) -dict.update(rcParams, _rc_params_in_file(matplotlib_fname())) +rcParams._update_raw(rcParamsDefault) +rcParams._update_raw(_rc_params_in_file(matplotlib_fname())) rcParamsOrig = rcParams.copy() with _api.suppress_matplotlib_deprecation_warning(): # This also checks that all rcParams are indeed listed in the template. @@ -1190,7 +1212,7 @@ def rc_context(rc=None, fname=None): rcParams.update(rc) yield finally: - dict.update(rcParams, orig) # Revert to the original rcs. + rcParams._update_raw(orig) # Revert to the original rcs. def use(backend, *, force=True): diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index e7208a17c99f..05dc927dc6c9 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -70,6 +70,10 @@ class RcParams(dict[str, Any]): def __init__(self, *args, **kwargs) -> None: ... def _set(self, key: str, val: Any) -> None: ... def _get(self, key: str) -> Any: ... + + def _update_raw(self, other_params: dict | RcParams) -> None: ... + + def _ensure_has_backend(self) -> None: ... def __setitem__(self, key: str, val: Any) -> None: ... def __getitem__(self, key: str) -> Any: ... def __iter__(self) -> Generator[str, None, None]: ... diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index d54a25056175..6e917ac9b53b 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -410,8 +410,7 @@ def switch_backend(newbackend: str) -> None: switch_backend("agg") rcParamsOrig["backend"] = "agg" return - # have to escape the switch on access logic - old_backend = dict.__getitem__(rcParams, 'backend') + old_backend = rcParams._get('backend') # get without triggering backend resolution module = backend_registry.load_backend_module(newbackend) canvas_class = module.FigureCanvas @@ -841,7 +840,7 @@ def xkcd( "xkcd mode is not compatible with text.usetex = True") stack = ExitStack() - stack.callback(dict.update, rcParams, rcParams.copy()) # type: ignore[arg-type] + stack.callback(rcParams._update_raw, rcParams.copy()) # type: ignore[arg-type] from matplotlib import patheffects rcParams.update({