diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 53f27c46314a..623b22852e30 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -134,8 +134,8 @@ import atexit -from collections import namedtuple -from collections.abc import MutableMapping +from collections import namedtuple, ChainMap +from collections.abc import MutableMapping, Mapping, KeysView, ValuesView, ItemsView import contextlib import functools import importlib @@ -155,6 +155,7 @@ import numpy from packaging.version import parse as parse_version +from copy import deepcopy # cbook must import matplotlib only within function # definitions, so it is safe to import from it here. @@ -650,7 +651,7 @@ def gen_candidates(): @_docstring.Substitution( "\n".join(map("- {}".format, sorted(rcsetup._validators, key=str.lower))) ) -class RcParams(MutableMapping, dict): +class RcParams(MutableMapping): """ A dict-like key-value store for config parameters, including validation. @@ -665,12 +666,13 @@ class RcParams(MutableMapping, dict): -------- :ref:`customizing-with-matplotlibrc-files` """ - validate = rcsetup._validators - # validate values on the way in def __init__(self, *args, **kwargs): + self._rcvalues = ChainMap({}) self.update(*args, **kwargs) + self._rcvalues = self._rcvalues.new_child() + self._defaults = self._rcvalues.maps[-1] def _set(self, key, val): """ @@ -690,7 +692,7 @@ def _set(self, key, val): :meta public: """ - dict.__setitem__(self, key, val) + self._rcvalues[key] = val def _get(self, key): """ @@ -711,7 +713,7 @@ def _get(self, key): :meta public: """ - return dict.__getitem__(self, key) + return self._rcvalues[key] def __setitem__(self, key, val): try: @@ -766,30 +768,84 @@ def __getitem__(self, key): return self._get(key) + def get_default(self, key): + """Return default value for the key set during initialization.""" + if key in _deprecated_map: + version, alt_key, alt_val, inverse_alt = _deprecated_map[key] + _api.warn_deprecated( + version, name=key, obj_type="rcparam", alternative=alt_key) + return inverse_alt(self._get(alt_key)) + + elif key in _deprecated_ignore_map: + version, alt_key = _deprecated_ignore_map[key] + _api.warn_deprecated( + version, name=key, obj_type="rcparam", alternative=alt_key) + return self._defaults[alt_key] if alt_key else None + + return self._defaults[key] + + def get_defaults(self): + """Return default values set during initialization.""" + return self._defaults.copy() + def _get_backend_or_none(self): """Get the requested backend, if any, without triggering resolution.""" backend = self._get("backend") return None if backend is rcsetup._auto_backend_sentinel else backend + def __delitem__(self, key): + if key not in self.validate: + raise KeyError( + f"{key} is not a valid rc parameter (see rcParams.keys() for " + f"a list of valid parameters)") + try: + del self._rcvalues[key] + except KeyError as err: + raise KeyError( + f"No custom value set for {key}. Cannot delete default value." + ) from err + + def __contains__(self, key): + return key in self._rcvalues + + def __iter__(self): + """Yield from sorted list of keys""" + yield from sorted(self._rcvalues.keys()) + + def __len__(self): + return len(self._rcvalues) + def __repr__(self): class_name = self.__class__.__name__ indent = len(class_name) + 1 with _api.suppress_matplotlib_deprecation_warning(): - repr_split = pprint.pformat(dict(self), indent=1, + repr_split = pprint.pformat(dict(self._rcvalues.items()), indent=1, width=80 - indent).split('\n') repr_indented = ('\n' + ' ' * indent).join(repr_split) return f'{class_name}({repr_indented})' def __str__(self): - return '\n'.join(map('{0[0]}: {0[1]}'.format, sorted(self.items()))) + return '\n'.join(map('{0[0]}: {0[1]}'.format, sorted(self._rcvalues.items()))) - def __iter__(self): - """Yield sorted list of keys.""" - with _api.suppress_matplotlib_deprecation_warning(): - yield from sorted(dict.__iter__(self)) + @_api.deprecated("3.8") + def clear(self): + pass - def __len__(self): - return dict.__len__(self) + def reset(self): + self._rcvalues.clear() + + def setdefault(self, key, default=None): + """Insert key with a value of default if key is not in the dictionary. + + Return the value for key if key is in the dictionary, else default. + """ + if key in self: + return self[key] + self[key] = default + return default + + def copy(self): + return deepcopy(self) def find_all(self, pattern): """ @@ -807,13 +863,6 @@ def find_all(self, pattern): for key, value in self.items() if pattern_re.search(key)) - def copy(self): - """Copy this RcParams instance.""" - rccopy = RcParams() - for k in self: # Skip deprecations and revalidation. - rccopy._set(k, self._get(k)) - return rccopy - def rc_params(fail_on_error=False): """Construct a `RcParams` instance from the default Matplotlib rc file.""" @@ -894,7 +943,7 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): fname) raise - config = RcParams() + config = dict() for key, (val, line, line_no) in rc_temp.items(): if key in rcsetup._validators: @@ -923,7 +972,7 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): or from the matplotlib source distribution""", dict(key=key, fname=fname, line_no=line_no, line=line.rstrip('\n'), version=version)) - return config + return RcParams(config) def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): @@ -947,7 +996,7 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): return config_from_file with _api.suppress_matplotlib_deprecation_warning(): - config = RcParams({**rcParamsDefault, **config_from_file}) + config = RcParams({**rcParams.get_defaults(), **config_from_file}) if "".join(config['text.latex.preamble']): _log.info(""" @@ -962,24 +1011,21 @@ 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( +rcParams = _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) +rcParams._rcvalues = rcParams._rcvalues.parents +rcParams.update(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) -rcParams = RcParams() # The global instance. -dict.update(rcParams, dict.items(rcParamsDefault)) -dict.update(rcParams, _rc_params_in_file(matplotlib_fname())) +rcParams.update(_rc_params_in_file(matplotlib_fname())) +rcParams.setdefault("backend", rcsetup._auto_backend_sentinel) +rcParams._rcvalues = rcParams._rcvalues.new_child() rcParamsOrig = rcParams.copy() with _api.suppress_matplotlib_deprecation_warning(): # This also checks that all rcParams are indeed listed in the template. @@ -987,7 +1033,7 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): defaultParams = rcsetup.defaultParams = { # We want to resolve deprecated rcParams, but not backend... key: [(rcsetup._auto_backend_sentinel if key == "backend" else - rcParamsDefault[key]), + rcParams.get_default(key)), validator] for key, validator in rcsetup._validators.items()} if rcParams['axes.formatter.use_locale']: @@ -1086,13 +1132,10 @@ def rcdefaults(): Use a specific style file. Call ``style.use('default')`` to restore the default style. """ - # Deprecation warnings were already handled when creating rcParamsDefault, - # no need to reemit them here. - with _api.suppress_matplotlib_deprecation_warning(): - from .style.core import STYLE_BLACKLIST - rcParams.clear() - rcParams.update({k: v for k, v in rcParamsDefault.items() - if k not in STYLE_BLACKLIST}) + # # Deprecation warnings were already handled when creating rcParamsDefault, + # # no need to reemit them here. + from .style import core + core.use('default') def rc_file_defaults(): @@ -1133,7 +1176,7 @@ def rc_file(fname, *, use_default_template=True): from .style.core import STYLE_BLACKLIST rc_from_file = rc_params_from_file( fname, use_default_template=use_default_template) - rcParams.update({k: rc_from_file[k] for k in rc_from_file + rcParams.update({k: rc_from_file[k] for k in rc_from_file.keys() if k not in STYLE_BLACKLIST}) @@ -1182,16 +1225,18 @@ def rc_context(rc=None, fname=None): plt.plot(x, y) """ - orig = dict(rcParams.copy()) - del orig['backend'] try: + rcParams._rcvalues = rcParams._rcvalues.new_child() if fname: rc_file(fname) if rc: rcParams.update(rc) yield finally: - dict.update(rcParams, orig) # Revert to the original rcs. + # Revert to the original rcs. + backend = rcParams["backend"] + rcParams._rcvalues = rcParams._rcvalues.parents + rcParams["backend"] = backend def use(backend, *, force=True): diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 8ef23a3dc4c2..d37ac6469770 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -32,12 +32,12 @@ __all__ = [ import os from pathlib import Path -from collections.abc import Callable, Generator +from collections.abc import Callable, Generator, MutableMapping import contextlib from packaging.version import Version from matplotlib._api import MatplotlibDeprecationWarning -from typing import Any, NamedTuple +from typing import Any, NamedTuple, Self class _VersionInfo(NamedTuple): major: int @@ -65,15 +65,22 @@ def get_cachedir() -> str: ... def get_data_path() -> str: ... def matplotlib_fname() -> str: ... -class RcParams(dict[str, Any]): +class RcParams(MutableMapping[str, Any]): validate: dict[str, Callable] + namespaces: tuple + single_key_set: set def __init__(self, *args, **kwargs) -> None: ... + @staticmethod + def _split_key(key: str, sep: str = ...) -> tuple[list, int]: ... + def _set(self, key: str, val: Any) -> None: ... + def _get(self, key: str) -> Any: ... def __setitem__(self, key: str, val: Any) -> None: ... def __getitem__(self, key: str) -> Any: ... + def __delitem__(self, key: str) -> None: ... def __iter__(self) -> Generator[str, None, None]: ... def __len__(self) -> int: ... - def find_all(self, pattern: str) -> RcParams: ... - def copy(self) -> RcParams: ... + def find_all(self, pattern: str) -> Self: ... + def copy(self) -> Self: ... def rc_params(fail_on_error: bool = ...) -> RcParams: ... def rc_params_from_file( diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 7f4aa12c9ed6..5669ee82f2f3 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -65,7 +65,9 @@ FigureCanvasBase, FigureManagerBase, MouseButton) from matplotlib.figure import Figure, FigureBase, figaspect from matplotlib.gridspec import GridSpec, SubplotSpec -from matplotlib import rcsetup, rcParamsDefault, rcParamsOrig +from matplotlib import rcParams, get_backend, rcParamsOrig +from matplotlib.rcsetup import interactive_bk as _interactive_bk +from matplotlib.rcsetup import _auto_backend_sentinel from matplotlib.artist import Artist from matplotlib.axes import Axes, Subplot # type: ignore from matplotlib.projections import PolarAxes # type: ignore @@ -301,7 +303,7 @@ def switch_backend(newbackend: str) -> None: # make sure the init is pulled up so we can assign to it later import matplotlib.backends - if newbackend is rcsetup._auto_backend_sentinel: + if newbackend is _auto_backend_sentinel: current_framework = cbook._get_running_interactive_framework() mapping = {'qt': 'qtagg', 'gtk3': 'gtk3agg', @@ -336,7 +338,7 @@ def switch_backend(newbackend: str) -> None: rcParamsOrig["backend"] = "agg" return # have to escape the switch on access logic - old_backend = dict.__getitem__(rcParams, 'backend') + old_backend = rcParams._get('backend') module = importlib.import_module(cbook._backend_module_name(newbackend)) canvas_class = module.FigureCanvas @@ -413,7 +415,7 @@ def draw_if_interactive() -> None: _log.debug("Loaded backend %s version %s.", newbackend, backend_mod.backend_version) - rcParams['backend'] = rcParamsDefault['backend'] = newbackend + rcParams['backend'] = newbackend _backend_mod = backend_mod for func_name in ["new_figure_manager", "draw_if_interactive", "show"]: globals()[func_name].__signature__ = inspect.signature( @@ -745,7 +747,7 @@ def xkcd( "xkcd mode is not compatible with text.usetex = True") stack = ExitStack() - stack.callback(dict.update, rcParams, rcParams.copy()) # type: ignore + stack.callback(rcParams.update, rcParams.copy()) # type: ignore from matplotlib import patheffects rcParams.update({ @@ -2474,9 +2476,9 @@ def polar(*args, **kwargs) -> list[Line2D]: # is compatible with the current running interactive framework. if (rcParams["backend_fallback"] and rcParams._get_backend_or_none() in ( # type: ignore - set(rcsetup.interactive_bk) - {'WebAgg', 'nbAgg'}) + set(_interactive_bk) - {'WebAgg', 'nbAgg'}) and cbook._get_running_interactive_framework()): # type: ignore - rcParams._set("backend", rcsetup._auto_backend_sentinel) # type: ignore + rcParams._set("backend", _auto_backend_sentinel) # type: ignore # fmt: on diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 7e9008c56165..57b3a60a0a54 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -26,7 +26,7 @@ import importlib_resources import matplotlib as mpl -from matplotlib import _api, _docstring, _rc_params_in_file, rcParamsDefault +from matplotlib import _api, _docstring, _rc_params_in_file _log = logging.getLogger(__name__) @@ -114,7 +114,7 @@ def use(style): # rcParamsDefault, no need to reemit them here. with _api.suppress_matplotlib_deprecation_warning(): # don't trigger RcParams.__getitem__('backend') - style = {k: rcParamsDefault[k] for k in rcParamsDefault + style = {k: mpl.rcParams.get_default(k) for k in mpl.rcParams if k not in STYLE_BLACKLIST} elif style in library: style = library[style] diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 65cd823f13a9..dc4ff1f8cff5 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -652,3 +652,39 @@ def test_rcparams_path_sketch_from_file(tmpdir, value): rc_path.write(f"path.sketch: {value}") with mpl.rc_context(fname=rc_path): assert mpl.rcParams["path.sketch"] == (1, 2, 3) + + +def test_rcparams_getdefault(): + with mpl.rc_context({"image.lut": 128}): + assert mpl.rcParams.get_default("image.lut") == 256 + + +def test_rcparams_getdefaults(): + mpl.rc("image", lut=128) + defaults = mpl.rcParams.get_defaults() + assert defaults == mpl.rcParams._defaults + + +def test_rcdefaults(): + # webagg.port is a style blacklisted key that shouldn't be + # updated when resetting rcParams to default values. + mpl.rcParams["webagg.port"] = 9000 + # lines.linewidth is not a style blacklisted key and should be + # reset to the default value. + # breakpoint() + lw = mpl.rcParams.get_default("lines.linewidth") + mpl.rcParams["lines.linewidth"] = lw + 1 + mpl.rcdefaults() + assert mpl.rcParams["webagg.port"] == 9000 + assert mpl.rcParams["lines.linewidth"] == lw + + +def test_rcparams_reset(): + mpl.rcParams["image.lut"] = 128 + mpl.rcParams.reset() + assert mpl.rcParams["image.lut"] == 256 + + +def test_rcparams_clear(): + with pytest.raises(mpl.MatplotlibDeprecationWarning): + mpl.rcParams.clear() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy