From ab1ea99718adf4eb5df73979acdafa86e2cf77f9 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Tue, 4 Apr 2023 16:36:04 -0500 Subject: [PATCH 01/22] Implement RcParams using ChainMaps. --- lib/matplotlib/__init__.py | 251 +++++++++++++++++++------- lib/matplotlib/mpl-data/matplotlibrc | 10 +- lib/matplotlib/pyplot.py | 14 +- lib/matplotlib/rcsetup.py | 10 +- lib/matplotlib/style/core.py | 6 +- lib/matplotlib/tests/test_rcparams.py | 2 +- lib/matplotlib/tests/test_widgets.py | 2 +- 7 files changed, 210 insertions(+), 85 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 53f27c46314a..a55deeb912de 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, defaultdict +from collections.abc import MutableMapping, Mapping 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,14 +666,71 @@ class RcParams(MutableMapping, dict): -------- :ref:`customizing-with-matplotlibrc-files` """ - validate = rcsetup._validators + namespaces = ( + "backends", + "lines", + "patches", + "hatches", + "boxplot", + "font", + "text", + "latex", + "axes", + "date", + "xtick", + "ytick", + "grid", + "legend", + "figure", + "image", + "contour", + "errorbar", + "hist", + "scatter", + "agg", + "path", + "savefig", + "tk", + "ps", + "pdf", + "svg", + "pgf", + "docstring", + "keymap", + "animation", + "_internal", + "webagg", + "markers", + "pcolor", + "pcolormesh", + "patch", + "hatch", + "mathtext", + "polaraxes", + "axes3d", + "xaxis", + "yaxis", + "default" + ) + + single_key_set = {"backend", "toolbar", "interactive", + "timezone", "backend_fallback"} - # validate values on the way in def __init__(self, *args, **kwargs): + self._namespace_maps = {name: ChainMap({}) for name in self.namespaces} self.update(*args, **kwargs) + self._namespace_maps = { + name: mapping.new_child() + for name, mapping in self._namespace_maps.items() + } + self._mapping = ChainMap(*self._namespace_maps.values()) - def _set(self, key, val): + def _split_key(self, key, sep="."): + keys = key.split(sep, maxsplit=1) + return keys, len(keys) + + def _set(self, key, value): """ Directly write data bypassing deprecation and validation logic. @@ -690,7 +748,13 @@ def _set(self, key, val): :meta public: """ - dict.__setitem__(self, key, val) + keys, depth = self._split_key(key) + if depth == 1: + if key in self.single_key_set: + self._namespace_maps["default"][key] = value + self._namespace_maps[key] = value + elif depth == 2: + self._namespace_maps[keys[0]][keys[1]] = value def _get(self, key): """ @@ -711,7 +775,13 @@ def _get(self, key): :meta public: """ - return dict.__getitem__(self, key) + keys, depth = self._split_key(key) + if depth == 1: + if key in self.single_key_set: + return self._namespace_maps["default"].get(key) + return self._namespace_maps[key] + elif depth == 2: + return self._namespace_maps[keys[0]].get(keys[1]) def __setitem__(self, key, val): try: @@ -733,10 +803,13 @@ def __setitem__(self, key, val): if val is rcsetup._auto_backend_sentinel: if 'backend' in self: return + if key in self.single_key_set: + key = f"default.{key}" try: cval = self.validate[key](val) except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None + # breakpoint() self._set(key, cval) except KeyError as err: raise KeyError( @@ -768,51 +841,97 @@ def __getitem__(self, key): def _get_backend_or_none(self): """Get the requested backend, if any, without triggering resolution.""" - backend = self._get("backend") + backend = self._get("default.backend") return None if backend is rcsetup._auto_backend_sentinel else backend - 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, - 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()))) + def __delitem__(self, key): + keys, depth = self._split_key(key) + if depth == 1: + del self._namespace_maps[key] + elif depth == 2: + del self._namespace_maps[keys[0]][keys[1]] + + def __contains__(self, key): + keys, depth = self._split_key(key) + if depth == 1: + if key in self.single_key_set: + return key in self._namespace_maps["default"] + return key in self._namespace_maps + elif depth == 2: + return any(key in mapping for mapping in self._namespace_maps) def __iter__(self): - """Yield sorted list of keys.""" + """Yield from sorted list of keys""" with _api.suppress_matplotlib_deprecation_warning(): - yield from sorted(dict.__iter__(self)) + yield from sorted(self.keys()) def __len__(self): - return dict.__len__(self) - - def find_all(self, pattern): - """ - Return the subset of this RcParams dictionary whose keys match, - using :func:`re.search`, the given ``pattern``. - - .. note:: - - Changes to the returned dictionary are *not* propagated to - the parent RcParams dictionary. + return sum(len(mapping) for mapping in self._namespace_maps) + def __repr__(self): + return repr(dict(self.items())) + + def keys(self): + keys = ( + ".".join((space, key)) + for space, mapping in self._namespace_maps.items() + for key in mapping.keys() + ) + return keys + + def values(self): + for key in self.keys(): + yield self[key] + + def items(self): + for key, value in zip(self.keys(), self.values()): + yield key, value + + def pop(self, key): + keys, depth = self._split_key(key) + if depth == 1: + self._mapping.pop() + elif depth == 2: + self._namespace_mapping[keys[0]].pop(keys[1]) + + def popitem(self): + return self._mapping.popitem() + + def clear(self): + self._mapping.clear() + + def setdefault(self, key, default=None): + self[key] = default + return default + + def get(self, key, default=None): + try: + return self[key] + except KeyError as e: + return default + + def update(self, other=(), /, **kwds): + """D.update([E, ]**F) -> None. Update D from mapping/iterable E and F. + If E present and has a .keys() method, does: for k in E: D[k] = E[k] + If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v + In either case, this is followed by: for k, v in F.items(): D[k] = v """ - pattern_re = re.compile(pattern) - return RcParams((key, value) - for key, value in self.items() - if pattern_re.search(key)) + if isinstance(other, Mapping): + for key in other: + self[key] = other[key] + elif hasattr(other, "keys"): + for key in other.keys(): + self[key] = other[key] + else: + for key, value in other: + self[key] = value + for key, value in kwds.items(): + self[key] = value 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 + return deepcopy(self) + +# MutableMapping.register(RcParams) def rc_params(fail_on_error=False): @@ -962,36 +1081,34 @@ 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) +rcParamsDefault.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())) -rcParamsOrig = rcParams.copy() +rcParamsDefault.setdefault("default.backend", rcsetup._auto_backend_sentinel) +params_dict = RcParams() +params_dict.update(rcParamsDefault.items()) +params_dict.update(_rc_params_in_file(matplotlib_fname())) +rcParamsOrig = params_dict.copy() with _api.suppress_matplotlib_deprecation_warning(): # This also checks that all rcParams are indeed listed in the template. # Assigning to rcsetup.defaultParams is left only for backcompat. defaultParams = rcsetup.defaultParams = { # We want to resolve deprecated rcParams, but not backend... - key: [(rcsetup._auto_backend_sentinel if key == "backend" else + key: [(rcsetup._auto_backend_sentinel if key == "default.backend" else rcParamsDefault[key]), validator] for key, validator in rcsetup._validators.items()} -if rcParams['axes.formatter.use_locale']: +if params_dict['axes.formatter.use_locale']: locale.setlocale(locale.LC_ALL, '') +rcParams = RcParams(params_dict) def rc(group, **kwargs): @@ -1133,7 +1250,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 +1299,24 @@ def rc_context(rc=None, fname=None): plt.plot(x, y) """ - orig = dict(rcParams.copy()) - del orig['backend'] try: + for space in rcParams._namespace_maps.keys(): + rcParams._namespace_maps[space] = rcParams._namespace_maps[ + space + ].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"] + for space in rcParams._namespace_maps.keys(): + rcParams._namespace_maps[space] = rcParams._namespace_maps[ + space + ].parents + rcParams["backend"] = backend def use(backend, *, force=True): @@ -1259,14 +1384,14 @@ def use(backend, *, force=True): # value which will be respected when the user finally imports # pyplot else: - rcParams['backend'] = backend + rcParams['default.backend'] = backend # if the user has asked for a given backend, do not helpfully # fallback - rcParams['backend_fallback'] = False + rcParams['default.backend_fallback'] = False if os.environ.get('MPLBACKEND'): - rcParams['backend'] = os.environ.get('MPLBACKEND') + rcParams['default.backend'] = os.environ.get('MPLBACKEND') def get_backend(): @@ -1277,14 +1402,14 @@ def get_backend(): -------- matplotlib.use """ - return rcParams['backend'] + return rcParams['default.backend'] def interactive(b): """ Set whether to redraw after every plotting command (e.g. `.pyplot.xlabel`). """ - rcParams['interactive'] = b + rcParams['default.interactive'] = b def is_interactive(): @@ -1296,7 +1421,7 @@ def is_interactive(): This function is only intended for use in backends. End users should use `.pyplot.isinteractive` instead. """ - return rcParams['interactive'] + return rcParams['default.interactive'] def _val_or_rc(val, rc_name): diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 9bd8a622092e..25e6aaa00d4d 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -82,7 +82,7 @@ ## PS PDF SVG Template ## You can also deploy your own backend outside of Matplotlib by referring to ## the module name (which must be in the PYTHONPATH) as 'module://my_backend'. -##backend: Agg +##default.backend: Agg ## The port to use for the web server in the WebAgg backend. #webagg.port: 8988 @@ -100,12 +100,12 @@ ## If you are running pyplot inside a GUI and your backend choice ## conflicts, we will automatically try to find a compatible one for ## you if backend_fallback is True -#backend_fallback: True +#default.backend_fallback: True -#interactive: False +#default.interactive: False #figure.hooks: # list of dotted.module.name:dotted.callable.name -#toolbar: toolbar2 # {None, toolbar2, toolmanager} -#timezone: UTC # a pytz timezone string, e.g., US/Central or Europe/Paris +#default.toolbar: toolbar2 # {None, toolbar2, toolmanager} +#default.timezone: UTC # a pytz timezone string, e.g., US/Central or Europe/Paris ## *************************************************************************** diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 7f4aa12c9ed6..e815ddb1b347 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -327,16 +327,16 @@ def switch_backend(newbackend: str) -> None: except ImportError: continue else: - rcParamsOrig['backend'] = candidate + rcParamsOrig['default.backend'] = candidate return else: # Switching to Agg should always succeed; if it doesn't, let the # exception propagate out. switch_backend("agg") - rcParamsOrig["backend"] = "agg" + rcParamsOrig["default.backend"] = "agg" return # have to escape the switch on access logic - old_backend = dict.__getitem__(rcParams, 'backend') + old_backend = rcParams['default.backend'] module = importlib.import_module(cbook._backend_module_name(newbackend)) canvas_class = module.FigureCanvas @@ -413,7 +413,7 @@ def draw_if_interactive() -> None: _log.debug("Loaded backend %s version %s.", newbackend, backend_mod.backend_version) - rcParams['backend'] = rcParamsDefault['backend'] = newbackend + rcParams['default.backend'] = rcParamsDefault['default.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 +745,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({ @@ -2472,11 +2472,11 @@ def polar(*args, **kwargs) -> list[Line2D]: # If rcParams['backend_fallback'] is true, and an interactive backend is # requested, ignore rcParams['backend'] and force selection of a backend that # is compatible with the current running interactive framework. -if (rcParams["backend_fallback"] +if (rcParams["default.backend_fallback"] and rcParams._get_backend_or_none() in ( # type: ignore set(rcsetup.interactive_bk) - {'WebAgg', 'nbAgg'}) and cbook._get_running_interactive_framework()): # type: ignore - rcParams._set("backend", rcsetup._auto_backend_sentinel) # type: ignore + rcParams._set("default.backend", rcsetup._auto_backend_sentinel) # type: ignore # fmt: on diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 38d4606024d3..bee0204a46b9 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -892,12 +892,12 @@ def _convert_validator_spec(key, conv): # The rcParams defaults are defined in lib/matplotlib/mpl-data/matplotlibrc, which # gets copied to matplotlib/mpl-data/matplotlibrc by the setup script. _validators = { - "backend": validate_backend, - "backend_fallback": validate_bool, + "default.backend": validate_backend, + "default.backend_fallback": validate_bool, "figure.hooks": validate_stringlist, - "toolbar": _validate_toolbar, - "interactive": validate_bool, - "timezone": validate_string, + "default.toolbar": _validate_toolbar, + "default.interactive": validate_bool, + "default.timezone": validate_string, "webagg.port": validate_int, "webagg.address": validate_string, diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 7e9008c56165..57ea5fa5df1d 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -39,9 +39,9 @@ STYLE_EXTENSION = 'mplstyle' # A list of rcParams that should not be applied from styles STYLE_BLACKLIST = { - 'interactive', 'backend', 'webagg.port', 'webagg.address', - 'webagg.port_retries', 'webagg.open_in_browser', 'backend_fallback', - 'toolbar', 'timezone', 'figure.max_open_warning', + 'default.interactive', 'default.backend', 'webagg.port', 'webagg.address', + 'webagg.port_retries', 'webagg.open_in_browser', 'default.backend_fallback', + 'default.toolbar', 'default.timezone', 'figure.max_open_warning', 'figure.raise_window', 'savefig.directory', 'tk.window_focus', 'docstring.hardcopy', 'date.epoch'} diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 65cd823f13a9..609e80bb4138 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -199,7 +199,7 @@ def test_axes_titlecolor_rcparams(): def test_Issue_1713(tmpdir): rcpath = Path(tmpdir) / 'test_rcparams.rc' - rcpath.write_text('timezone: UTC', encoding='utf-8') + rcpath.write_text('default.timezone: UTC', encoding='utf-8') with mock.patch('locale.getpreferredencoding', return_value='UTF-32-BE'): rc = mpl.rc_params_from_file(rcpath, True, False) assert rc.get('timezone') == 'UTC' diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index de17bb79bd4b..b96c5693f6a2 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1029,7 +1029,7 @@ def test_CheckButtons(ax): @pytest.mark.parametrize("toolbar", ["none", "toolbar2", "toolmanager"]) def test_TextBox(ax, toolbar): # Avoid "toolmanager is provisional" warning. - plt.rcParams._set("toolbar", toolbar) + plt.rcParams._set("default.toolbar", toolbar) submit_event = mock.Mock(spec=noop, return_value=None) text_change_event = mock.Mock(spec=noop, return_value=None) From 517750871872d7404f8e296d39dc2bb3cb625287 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Tue, 4 Apr 2023 16:52:51 -0500 Subject: [PATCH 02/22] Make namespaces variable compact --- lib/matplotlib/__init__.py | 53 +++++--------------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index a55deeb912de..22cc9a3cec8d 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -667,52 +667,13 @@ class RcParams(MutableMapping): :ref:`customizing-with-matplotlibrc-files` """ validate = rcsetup._validators - namespaces = ( - "backends", - "lines", - "patches", - "hatches", - "boxplot", - "font", - "text", - "latex", - "axes", - "date", - "xtick", - "ytick", - "grid", - "legend", - "figure", - "image", - "contour", - "errorbar", - "hist", - "scatter", - "agg", - "path", - "savefig", - "tk", - "ps", - "pdf", - "svg", - "pgf", - "docstring", - "keymap", - "animation", - "_internal", - "webagg", - "markers", - "pcolor", - "pcolormesh", - "patch", - "hatch", - "mathtext", - "polaraxes", - "axes3d", - "xaxis", - "yaxis", - "default" - ) + namespaces = ("backends", "lines", "patches", "hatches", "boxplot", "font", "text", + "latex", "axes", "date", "xtick", "ytick", "grid", "legend", + "figure", "image", "contour", "errorbar", "hist", "scatter", "agg", + "path", "savefig", "tk", "ps", "pdf", "svg", "pgf", "docstring", + "keymap", "animation", "_internal", "webagg", "markers", "pcolor", + "pcolormesh", "patch", "hatch", "mathtext", "polaraxes", "axes3d", + "xaxis", "yaxis", "default") single_key_set = {"backend", "toolbar", "interactive", "timezone", "backend_fallback"} From 45d4743b960c00bfc65b183ba30f2e70a0fcca71 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Thu, 13 Apr 2023 20:43:26 -0500 Subject: [PATCH 03/22] Raise KeyError when accessing namespace for now. --- lib/matplotlib/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 22cc9a3cec8d..e71e351bd4ce 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -740,7 +740,13 @@ def _get(self, key): if depth == 1: if key in self.single_key_set: return self._namespace_maps["default"].get(key) - return self._namespace_maps[key] + # Comment the following line and remove the raise statement + # to enable getting namespace parameters. + # return self._namespace_maps[key] + else: + raise KeyError( + f"{key} is not a valid rc parameter (see rcParams.keys() for " + f"a list of valid parameters)") elif depth == 2: return self._namespace_maps[keys[0]].get(keys[1]) @@ -770,7 +776,6 @@ def __setitem__(self, key, val): cval = self.validate[key](val) except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None - # breakpoint() self._set(key, cval) except KeyError as err: raise KeyError( From 133f2af557178644f8e83474872f69869ed2063b Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Thu, 13 Apr 2023 20:54:24 -0500 Subject: [PATCH 04/22] Make linter happy --- lib/matplotlib/pyplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index e815ddb1b347..a2b5190fff67 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -745,7 +745,7 @@ def xkcd( "xkcd mode is not compatible with text.usetex = True") stack = ExitStack() - stack.callback(rcParams.update, rcParams.copy()) # type: ignore + stack.callback(rcParams.update, rcParams.copy()) # type: ignore from matplotlib import patheffects rcParams.update({ From b7f4fb5cb8878fba9528808d4660c184d1529c7c Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Fri, 14 Apr 2023 11:46:47 -0500 Subject: [PATCH 05/22] Update repr, str and add find_all --- lib/matplotlib/__init__.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index e71e351bd4ce..c4f93d99f131 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -835,7 +835,16 @@ def __len__(self): return sum(len(mapping) for mapping in self._namespace_maps) def __repr__(self): - return repr(dict(self.items())) + class_name = self.__class__.__name__ + indent = len(class_name) + 1 + with _api.suppress_matplotlib_deprecation_warning(): + repr_split = pprint.pformat(dict(self.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()))) def keys(self): keys = ( @@ -897,7 +906,21 @@ def update(self, other=(), /, **kwds): def copy(self): return deepcopy(self) -# MutableMapping.register(RcParams) + def find_all(self, pattern): + """ + Return the subset of this RcParams dictionary whose keys match, + using :func:`re.search`, the given ``pattern``. + + .. note:: + + Changes to the returned dictionary are *not* propagated to + the parent RcParams dictionary. + + """ + pattern_re = re.compile(pattern) + return RcParams((key, value) + for key, value in self.items() + if pattern_re.search(key)) def rc_params(fail_on_error=False): From d135342b1c448bd7b82806c863662b4071f896c1 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Sun, 16 Apr 2023 22:08:07 -0500 Subject: [PATCH 06/22] Fix issue resolve testing backend --- lib/matplotlib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index c4f93d99f131..14ee36f05d7a 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -766,7 +766,7 @@ def __setitem__(self, key, val): _api.warn_deprecated( version, name=key, obj_type="rcparam", alternative=alt_key) return - elif key == 'backend': + elif key == 'backend' or key == "default.backend": if val is rcsetup._auto_backend_sentinel: if 'backend' in self: return @@ -1391,7 +1391,7 @@ def get_backend(): -------- matplotlib.use """ - return rcParams['default.backend'] + return rcParams['backend'] def interactive(b): From 47b48ee2282f5e66ac1f5990a9c69fa679d239a3 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Mon, 17 Apr 2023 16:51:32 -0500 Subject: [PATCH 07/22] Update a few functions in RcParams --- lib/matplotlib/__init__.py | 62 +++++++++++++++++++++------- lib/matplotlib/mpl-data/matplotlibrc | 10 ++--- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 14ee36f05d7a..4a4cbd6ca061 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -134,7 +134,7 @@ import atexit -from collections import namedtuple, ChainMap, defaultdict +from collections import namedtuple, ChainMap from collections.abc import MutableMapping, Mapping import contextlib import functools @@ -691,7 +691,7 @@ def _split_key(self, key, sep="."): keys = key.split(sep, maxsplit=1) return keys, len(keys) - def _set(self, key, value): + def _set(self, key, val): """ Directly write data bypassing deprecation and validation logic. @@ -712,10 +712,22 @@ def _set(self, key, value): keys, depth = self._split_key(key) if depth == 1: if key in self.single_key_set: - self._namespace_maps["default"][key] = value - self._namespace_maps[key] = value + self._namespace_maps["default"][key] = val + # Uncomment the following line and remove the raise statement + # to enable setting namespaces. + # else: + # if isinstance(val, dict): + # self._namespace_maps[key] = ChainMap({}, val) + # else: + # raise ValueError( + # f"{key} should be set using a dictionary but found " + # f"{type(val)}") + else: + raise KeyError( + f"{key} is not a valid rc parameter (see rcParams.keys() for " + f"a list of valid parameters)") elif depth == 2: - self._namespace_maps[keys[0]][keys[1]] = value + self._namespace_maps[keys[0]][keys[1]] = val def _get(self, key): """ @@ -740,7 +752,7 @@ def _get(self, key): if depth == 1: if key in self.single_key_set: return self._namespace_maps["default"].get(key) - # Comment the following line and remove the raise statement + # Uncomment the following line and remove the raise statement # to enable getting namespace parameters. # return self._namespace_maps[key] else: @@ -807,22 +819,31 @@ def __getitem__(self, key): def _get_backend_or_none(self): """Get the requested backend, if any, without triggering resolution.""" - backend = self._get("default.backend") + backend = self._get("backend") return None if backend is rcsetup._auto_backend_sentinel else backend def __delitem__(self, key): keys, depth = self._split_key(key) - if depth == 1: - del self._namespace_maps[key] - elif depth == 2: - del self._namespace_maps[keys[0]][keys[1]] + try: + if depth == 1: + if key in self.single_key_set: + del self._namespace_maps["default"][key] + else: + raise KeyError + elif depth == 2: + del self._namespace_maps[keys[0]][keys[1]] + except KeyError as err: + raise KeyError( + f"{key} is not a valid rc parameter (see rcParams.keys() for " + f"a list of valid parameters)") from err def __contains__(self, key): keys, depth = self._split_key(key) if depth == 1: if key in self.single_key_set: return key in self._namespace_maps["default"] - return key in self._namespace_maps + else: + return False elif depth == 2: return any(key in mapping for mapping in self._namespace_maps) @@ -865,15 +886,22 @@ def items(self): def pop(self, key): keys, depth = self._split_key(key) if depth == 1: - self._mapping.pop() + if key in self.single_key_set: + return self._namespace_mapping["default"][key] + else: + raise KeyError( + f"{key} is not a valid rc parameter (see rcParams.keys() for " + f"a list of valid parameters)") elif depth == 2: - self._namespace_mapping[keys[0]].pop(keys[1]) + return self._namespace_mapping[keys[0]].pop(keys[1]) def popitem(self): - return self._mapping.popitem() + raise NotImplementedError( + "popitem is not implemented for RcParams.") def clear(self): - self._mapping.clear() + for namespace in self._namespace_maps: + self._namespace_maps[namespace].clear() def setdefault(self, key, default=None): self[key] = default @@ -1005,6 +1033,8 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): config = RcParams() for key, (val, line, line_no) in rc_temp.items(): + if key in config.single_key_set: + key = f"default.{key}" if key in rcsetup._validators: if fail_on_error: config[key] = val # try to convert to proper type or raise diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 25e6aaa00d4d..9bd8a622092e 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -82,7 +82,7 @@ ## PS PDF SVG Template ## You can also deploy your own backend outside of Matplotlib by referring to ## the module name (which must be in the PYTHONPATH) as 'module://my_backend'. -##default.backend: Agg +##backend: Agg ## The port to use for the web server in the WebAgg backend. #webagg.port: 8988 @@ -100,12 +100,12 @@ ## If you are running pyplot inside a GUI and your backend choice ## conflicts, we will automatically try to find a compatible one for ## you if backend_fallback is True -#default.backend_fallback: True +#backend_fallback: True -#default.interactive: False +#interactive: False #figure.hooks: # list of dotted.module.name:dotted.callable.name -#default.toolbar: toolbar2 # {None, toolbar2, toolmanager} -#default.timezone: UTC # a pytz timezone string, e.g., US/Central or Europe/Paris +#toolbar: toolbar2 # {None, toolbar2, toolmanager} +#timezone: UTC # a pytz timezone string, e.g., US/Central or Europe/Paris ## *************************************************************************** From 9a0854487040b8827a353c1f0aa00ea72898078a Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Mon, 17 Apr 2023 17:26:17 -0500 Subject: [PATCH 08/22] Update type stubs --- lib/matplotlib/__init__.pyi | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 8ef23a3dc4c2..1bbef561b9e4 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -65,13 +65,21 @@ def get_cachedir() -> str: ... def get_data_path() -> str: ... def matplotlib_fname() -> str: ... -class RcParams(dict[str, Any]): +class RcParams(MutableMapping): validate: dict[str, Callable] + namespaces: tuple + single_key_set: set def __init__(self, *args, **kwargs) -> None: ... + def _split_key(self, 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 keys(self) -> Generator[str, None, None]: ... + def values(self) -> Generator[Any, None, None]: ... def find_all(self, pattern: str) -> RcParams: ... def copy(self) -> RcParams: ... From 9a0ad032f586539d1c4239c46deb5ea1a8756964 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Mon, 24 Apr 2023 21:41:45 -0500 Subject: [PATCH 09/22] Cache splitting keys. --- lib/matplotlib/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 4a4cbd6ca061..18fa9906b4f6 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -685,9 +685,10 @@ def __init__(self, *args, **kwargs): name: mapping.new_child() for name, mapping in self._namespace_maps.items() } - self._mapping = ChainMap(*self._namespace_maps.values()) - def _split_key(self, key, sep="."): + @staticmethod + @functools.lru_cache + def _split_key(key, sep="."): keys = key.split(sep, maxsplit=1) return keys, len(keys) From 7a11e81abe38615f220eaba74a05f2bcc297a996 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Tue, 25 Apr 2023 21:11:29 -0500 Subject: [PATCH 10/22] Return views for .keys(), .values(), .items() --- lib/matplotlib/__init__.py | 25 +++++++------------------ lib/matplotlib/__init__.pyi | 4 +--- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 18fa9906b4f6..2dbacc9d989e 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -135,7 +135,7 @@ import atexit from collections import namedtuple, ChainMap -from collections.abc import MutableMapping, Mapping +from collections.abc import MutableMapping, Mapping, KeysView, ValuesView, ItemsView import contextlib import functools import importlib @@ -850,8 +850,13 @@ def __contains__(self, key): def __iter__(self): """Yield from sorted list of keys""" + keys = ( + ".".join((space, key)) + for space, mapping in self._namespace_maps.items() + for key in mapping.keys() + ) with _api.suppress_matplotlib_deprecation_warning(): - yield from sorted(self.keys()) + yield from sorted(keys) def __len__(self): return sum(len(mapping) for mapping in self._namespace_maps) @@ -868,22 +873,6 @@ def __repr__(self): def __str__(self): return '\n'.join(map('{0[0]}: {0[1]}'.format, sorted(self.items()))) - def keys(self): - keys = ( - ".".join((space, key)) - for space, mapping in self._namespace_maps.items() - for key in mapping.keys() - ) - return keys - - def values(self): - for key in self.keys(): - yield self[key] - - def items(self): - for key, value in zip(self.keys(), self.values()): - yield key, value - def pop(self, key): keys, depth = self._split_key(key) if depth == 1: diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 1bbef561b9e4..fc87f31afc7f 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -70,7 +70,7 @@ class RcParams(MutableMapping): namespaces: tuple single_key_set: set def __init__(self, *args, **kwargs) -> None: ... - def _split_key(self, key: str, sep: str = ".") -> tuple[list, int]: ... + 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: ... @@ -78,8 +78,6 @@ class RcParams(MutableMapping): def __delitem__(self, key: str) -> None: ... def __iter__(self) -> Generator[str, None, None]: ... def __len__(self) -> int: ... - def keys(self) -> Generator[str, None, None]: ... - def values(self) -> Generator[Any, None, None]: ... def find_all(self, pattern: str) -> RcParams: ... def copy(self) -> RcParams: ... From 4f9d8b6f5f782d5f152bf6769b6fadcd5bac531c Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Tue, 25 Apr 2023 21:13:20 -0500 Subject: [PATCH 11/22] Add type spec for MutableMapping --- lib/matplotlib/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index fc87f31afc7f..b5966b3be344 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -65,7 +65,7 @@ def get_cachedir() -> str: ... def get_data_path() -> str: ... def matplotlib_fname() -> str: ... -class RcParams(MutableMapping): +class RcParams(MutableMapping[str, Any]): validate: dict[str, Callable] namespaces: tuple single_key_set: set From 54a1a113616ebe3b9d2b1a80bb7750270ef3c34d Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Wed, 26 Apr 2023 15:10:57 -0500 Subject: [PATCH 12/22] Remove `default.` usage outside the RcParams class This is to make the interface such that the code other than RcParams doesn't have to deal with the `deafult.*` namespace anywhere. This also changes the keys returned by `.keys()` to not have the `default.*` in the key name. --- lib/matplotlib/__init__.py | 35 ++++++++++++--------------- lib/matplotlib/pyplot.py | 12 ++++----- lib/matplotlib/rcsetup.py | 10 ++++---- lib/matplotlib/style/core.py | 6 ++--- lib/matplotlib/tests/test_rcparams.py | 2 +- lib/matplotlib/tests/test_widgets.py | 2 +- 6 files changed, 32 insertions(+), 35 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 2dbacc9d989e..3be422ab3a3c 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -781,12 +781,12 @@ def __setitem__(self, key, val): return elif key == 'backend' or key == "default.backend": if val is rcsetup._auto_backend_sentinel: - if 'backend' in self: + if 'backend' in self or 'default.backend' in self: return - if key in self.single_key_set: - key = f"default.{key}" try: cval = self.validate[key](val) + if key in self.single_key_set: + key = f"default.{key}" except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None self._set(key, cval) @@ -851,7 +851,7 @@ def __contains__(self, key): def __iter__(self): """Yield from sorted list of keys""" keys = ( - ".".join((space, key)) + ".".join((space, key)) if space != 'default' else key for space, mapping in self._namespace_maps.items() for key in mapping.keys() ) @@ -1023,8 +1023,6 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): config = RcParams() for key, (val, line, line_no) in rc_temp.items(): - if key in config.single_key_set: - key = f"default.{key}" if key in rcsetup._validators: if fail_on_error: config[key] = val # try to convert to proper type or raise @@ -1101,23 +1099,22 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): # 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. -rcParamsDefault.setdefault("default.backend", rcsetup._auto_backend_sentinel) -params_dict = RcParams() -params_dict.update(rcParamsDefault.items()) -params_dict.update(_rc_params_in_file(matplotlib_fname())) -rcParamsOrig = params_dict.copy() +rcParamsDefault.setdefault("backend", rcsetup._auto_backend_sentinel) +rcParams = RcParams() +rcParams.update(rcParamsDefault.items()) +rcParams.update(_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. # Assigning to rcsetup.defaultParams is left only for backcompat. defaultParams = rcsetup.defaultParams = { # We want to resolve deprecated rcParams, but not backend... - key: [(rcsetup._auto_backend_sentinel if key == "default.backend" else + key: [(rcsetup._auto_backend_sentinel if key == "backend" else rcParamsDefault[key]), validator] for key, validator in rcsetup._validators.items()} -if params_dict['axes.formatter.use_locale']: +if rcParams['axes.formatter.use_locale']: locale.setlocale(locale.LC_ALL, '') -rcParams = RcParams(params_dict) def rc(group, **kwargs): @@ -1393,14 +1390,14 @@ def use(backend, *, force=True): # value which will be respected when the user finally imports # pyplot else: - rcParams['default.backend'] = backend + rcParams['backend'] = backend # if the user has asked for a given backend, do not helpfully # fallback - rcParams['default.backend_fallback'] = False + rcParams['backend_fallback'] = False if os.environ.get('MPLBACKEND'): - rcParams['default.backend'] = os.environ.get('MPLBACKEND') + rcParams['backend'] = os.environ.get('MPLBACKEND') def get_backend(): @@ -1418,7 +1415,7 @@ def interactive(b): """ Set whether to redraw after every plotting command (e.g. `.pyplot.xlabel`). """ - rcParams['default.interactive'] = b + rcParams['interactive'] = b def is_interactive(): @@ -1430,7 +1427,7 @@ def is_interactive(): This function is only intended for use in backends. End users should use `.pyplot.isinteractive` instead. """ - return rcParams['default.interactive'] + return rcParams['interactive'] def _val_or_rc(val, rc_name): diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index a2b5190fff67..945f12cd7137 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -327,16 +327,16 @@ def switch_backend(newbackend: str) -> None: except ImportError: continue else: - rcParamsOrig['default.backend'] = candidate + rcParamsOrig['backend'] = candidate return else: # Switching to Agg should always succeed; if it doesn't, let the # exception propagate out. switch_backend("agg") - rcParamsOrig["default.backend"] = "agg" + rcParamsOrig["backend"] = "agg" return # have to escape the switch on access logic - old_backend = rcParams['default.backend'] + old_backend = rcParams._get('backend') module = importlib.import_module(cbook._backend_module_name(newbackend)) canvas_class = module.FigureCanvas @@ -413,7 +413,7 @@ def draw_if_interactive() -> None: _log.debug("Loaded backend %s version %s.", newbackend, backend_mod.backend_version) - rcParams['default.backend'] = rcParamsDefault['default.backend'] = newbackend + rcParams['backend'] = rcParamsDefault['backend'] = newbackend _backend_mod = backend_mod for func_name in ["new_figure_manager", "draw_if_interactive", "show"]: globals()[func_name].__signature__ = inspect.signature( @@ -2472,11 +2472,11 @@ def polar(*args, **kwargs) -> list[Line2D]: # If rcParams['backend_fallback'] is true, and an interactive backend is # requested, ignore rcParams['backend'] and force selection of a backend that # is compatible with the current running interactive framework. -if (rcParams["default.backend_fallback"] +if (rcParams["backend_fallback"] and rcParams._get_backend_or_none() in ( # type: ignore set(rcsetup.interactive_bk) - {'WebAgg', 'nbAgg'}) and cbook._get_running_interactive_framework()): # type: ignore - rcParams._set("default.backend", rcsetup._auto_backend_sentinel) # type: ignore + rcParams._set("backend", rcsetup._auto_backend_sentinel) # type: ignore # fmt: on diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index bee0204a46b9..38d4606024d3 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -892,12 +892,12 @@ def _convert_validator_spec(key, conv): # The rcParams defaults are defined in lib/matplotlib/mpl-data/matplotlibrc, which # gets copied to matplotlib/mpl-data/matplotlibrc by the setup script. _validators = { - "default.backend": validate_backend, - "default.backend_fallback": validate_bool, + "backend": validate_backend, + "backend_fallback": validate_bool, "figure.hooks": validate_stringlist, - "default.toolbar": _validate_toolbar, - "default.interactive": validate_bool, - "default.timezone": validate_string, + "toolbar": _validate_toolbar, + "interactive": validate_bool, + "timezone": validate_string, "webagg.port": validate_int, "webagg.address": validate_string, diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 57ea5fa5df1d..7e9008c56165 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -39,9 +39,9 @@ STYLE_EXTENSION = 'mplstyle' # A list of rcParams that should not be applied from styles STYLE_BLACKLIST = { - 'default.interactive', 'default.backend', 'webagg.port', 'webagg.address', - 'webagg.port_retries', 'webagg.open_in_browser', 'default.backend_fallback', - 'default.toolbar', 'default.timezone', 'figure.max_open_warning', + 'interactive', 'backend', 'webagg.port', 'webagg.address', + 'webagg.port_retries', 'webagg.open_in_browser', 'backend_fallback', + 'toolbar', 'timezone', 'figure.max_open_warning', 'figure.raise_window', 'savefig.directory', 'tk.window_focus', 'docstring.hardcopy', 'date.epoch'} diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 609e80bb4138..65cd823f13a9 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -199,7 +199,7 @@ def test_axes_titlecolor_rcparams(): def test_Issue_1713(tmpdir): rcpath = Path(tmpdir) / 'test_rcparams.rc' - rcpath.write_text('default.timezone: UTC', encoding='utf-8') + rcpath.write_text('timezone: UTC', encoding='utf-8') with mock.patch('locale.getpreferredencoding', return_value='UTF-32-BE'): rc = mpl.rc_params_from_file(rcpath, True, False) assert rc.get('timezone') == 'UTC' diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index b96c5693f6a2..de17bb79bd4b 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1029,7 +1029,7 @@ def test_CheckButtons(ax): @pytest.mark.parametrize("toolbar", ["none", "toolbar2", "toolmanager"]) def test_TextBox(ax, toolbar): # Avoid "toolmanager is provisional" warning. - plt.rcParams._set("default.toolbar", toolbar) + plt.rcParams._set("toolbar", toolbar) submit_event = mock.Mock(spec=noop, return_value=None) text_change_event = mock.Mock(spec=noop, return_value=None) From c101fec43ab1da11c0ff4c2992dbe5463d0b318f Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Wed, 26 Apr 2023 15:46:13 -0500 Subject: [PATCH 13/22] Update _split_key stub to staticmethod --- lib/matplotlib/__init__.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index b5966b3be344..8691ef420166 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -70,6 +70,7 @@ class RcParams(MutableMapping[str, Any]): 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: ... From 67735b09bc22949b6b831db23da461d1d443e31f Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Wed, 10 May 2023 21:57:43 -0500 Subject: [PATCH 14/22] Add way to access default params --- lib/matplotlib/__init__.py | 52 ++++++++++++++++++++++----- lib/matplotlib/tests/test_rcparams.py | 5 +++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 3be422ab3a3c..d67d34f968f0 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -681,10 +681,7 @@ class RcParams(MutableMapping): def __init__(self, *args, **kwargs): self._namespace_maps = {name: ChainMap({}) for name in self.namespaces} self.update(*args, **kwargs) - self._namespace_maps = { - name: mapping.new_child() - for name, mapping in self._namespace_maps.items() - } + self._new_child() @staticmethod @functools.lru_cache @@ -692,6 +689,14 @@ def _split_key(key, sep="."): keys = key.split(sep, maxsplit=1) return keys, len(keys) + def _new_child(self): + for space in self._namespace_maps.keys(): + self._namespace_maps[space] = self._namespace_maps[space].new_child() + + def _parents(self): + for space in self._namespace_maps.keys(): + self._namespace_maps[space] = self._namespace_maps[space].parents + def _set(self, key, val): """ Directly write data bypassing deprecation and validation logic. @@ -818,6 +823,36 @@ def __getitem__(self, key): return self._get(key) + def _get_default(self, key): + keys, depth = self._split_key(key) + if depth == 1: + if key in self.single_key_set: + return self._namespace_maps["default"].get(key) + # Uncomment the following line and remove the raise statement + # to enable getting namespace parameters. + # return self._namespace_maps[key] + else: + raise KeyError( + f"{key} is not a valid rc parameter (see rcParams.keys() for " + f"a list of valid parameters)") + elif depth == 2: + return self._namespace_maps[keys[0]].maps[-1].get(keys[1]) + + def getdefault(self, key): + 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._get_default(alt_key) if alt_key else None + + return self._get_default(key) + def _get_backend_or_none(self): """Get the requested backend, if any, without triggering resolution.""" backend = self._get("backend") @@ -1021,6 +1056,7 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): raise config = RcParams() + config._parents() for key, (val, line, line_no) in rc_temp.items(): if key in rcsetup._validators: @@ -1049,6 +1085,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)) + config._new_child() return config @@ -1101,6 +1138,7 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): # fill in _auto_backend_sentinel. rcParamsDefault.setdefault("backend", rcsetup._auto_backend_sentinel) rcParams = RcParams() +rcParams._parents() rcParams.update(rcParamsDefault.items()) rcParams.update(_rc_params_in_file(matplotlib_fname())) rcParamsOrig = rcParams.copy() @@ -1115,6 +1153,7 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): for key, validator in rcsetup._validators.items()} if rcParams['axes.formatter.use_locale']: locale.setlocale(locale.LC_ALL, '') +rcParams._new_child() def rc(group, **kwargs): @@ -1318,10 +1357,7 @@ def rc_context(rc=None, fname=None): finally: # Revert to the original rcs. backend = rcParams["backend"] - for space in rcParams._namespace_maps.keys(): - rcParams._namespace_maps[space] = rcParams._namespace_maps[ - space - ].parents + rcParams._parents() rcParams["backend"] = backend diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 65cd823f13a9..a120e08413f2 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -652,3 +652,8 @@ 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.getdefault("image.lut") == 256 From 7e4022ada95a1e68c6451bf4d666574f7e9c0a66 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Wed, 10 May 2023 23:53:51 -0500 Subject: [PATCH 15/22] Remove `rcParamsDefault` --- lib/matplotlib/__init__.py | 28 ++++++++++++++++---------- lib/matplotlib/pyplot.py | 5 +++-- lib/matplotlib/style/core.py | 4 ++-- lib/matplotlib/tests/test_offsetbox.py | 5 +++++ lib/matplotlib/tests/test_rcparams.py | 7 +++++++ 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index d67d34f968f0..69344a420376 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -853,6 +853,12 @@ def getdefault(self, key): return self._get_default(key) + def getdefaults(self): + """Return default values set during initialization.""" + defaults = self.copy() + defaults.clear() + return defaults + def _get_backend_or_none(self): """Get the requested backend, if any, without triggering resolution.""" backend = self._get("backend") @@ -1110,7 +1116,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.getdefaults(), **config_from_file}) if "".join(config['text.latex.preamble']): _log.info(""" @@ -1125,21 +1131,22 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): return config -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) -rcParamsDefault.update(rcsetup._hardcoded_defaults) +for key in rcsetup._hardcoded_defaults: + space, subkey = key.split(".") + if not rcParams._namespace_maps[space]: + rcParams._namespace_maps[space] = ChainMap({}) +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. -rcParamsDefault.setdefault("backend", rcsetup._auto_backend_sentinel) -rcParams = RcParams() -rcParams._parents() -rcParams.update(rcParamsDefault.items()) +rcParams.setdefault("backend", rcsetup._auto_backend_sentinel) rcParams.update(_rc_params_in_file(matplotlib_fname())) rcParamsOrig = rcParams.copy() with _api.suppress_matplotlib_deprecation_warning(): @@ -1148,12 +1155,11 @@ 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.getdefault(key)), validator] for key, validator in rcsetup._validators.items()} if rcParams['axes.formatter.use_locale']: locale.setlocale(locale.LC_ALL, '') -rcParams._new_child() def rc(group, **kwargs): @@ -1253,8 +1259,8 @@ def rcdefaults(): 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}) + # rcParams.update({k: v for k, v in rcParams.items() + # if k not in STYLE_BLACKLIST}) def rc_file_defaults(): diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 945f12cd7137..8c7b6a873882 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -65,7 +65,8 @@ 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.artist import Artist from matplotlib.axes import Axes, Subplot # type: ignore from matplotlib.projections import PolarAxes # type: ignore @@ -413,7 +414,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( diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 7e9008c56165..392360920f92 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.getdefault(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_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py index 49b55e4c9326..edf17542fb17 100644 --- a/lib/matplotlib/tests/test_offsetbox.py +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -257,9 +257,14 @@ def test_anchoredtext_horizontal_alignment(): ax.add_artist(text2) +<<<<<<< HEAD @pytest.mark.parametrize("extent_kind", ["window_extent", "tightbbox"]) def test_annotationbbox_extents(extent_kind): plt.rcParams.update(plt.rcParamsDefault) +======= +def test_annotationbbox_extents(): + plt.rcParams.clear() +>>>>>>> 57d742c95e (Remove `rcParamsDefault`) fig, ax = plt.subplots(figsize=(4, 3), dpi=100) ax.axis([0, 1, 0, 1]) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index a120e08413f2..50df368ec227 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -657,3 +657,10 @@ def test_rcparams_path_sketch_from_file(tmpdir, value): def test_rcparams_getdefault(): with mpl.rc_context({"image.lut": 128}): assert mpl.rcParams.getdefault("image.lut") == 256 + + +def test_rcparams_getdefaults(): + mpl.rc("image", lut=128) + defaults = mpl.rcParams.getdefaults() + mpl.rcParams.clear() + assert defaults == mpl.rcParams From c23cb2914beba5fb945c22b05b0b80daaaaab90c Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Thu, 11 May 2023 15:18:24 -0500 Subject: [PATCH 16/22] Update `rcdefaults` to use `style.core.use()` --- lib/matplotlib/__init__.py | 11 ++++------- lib/matplotlib/style/core.py | 1 + lib/matplotlib/tests/test_rcparams.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 69344a420376..8db49cd9fe4f 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1254,13 +1254,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 rcParams.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(): diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 392360920f92..6b2e6467d4a3 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -149,6 +149,7 @@ def use(style): else: filtered[k] = style[k] mpl.rcParams.update(filtered) + mpl.rcParams._new_child() @contextlib.contextmanager diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 50df368ec227..f1a53bfaef9e 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -664,3 +664,17 @@ def test_rcparams_getdefaults(): defaults = mpl.rcParams.getdefaults() mpl.rcParams.clear() assert defaults == mpl.rcParams + + +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.getdefault("lines.linewidth") + mpl.rcParams["lines.linewidth"] = lw + 1 + mpl.rcdefaults() + assert mpl.rcParams["webagg.port"] == 9000 + assert mpl.rcParams["lines.linewidth"] == lw From 0374f464a1997a46c73057d801785262b87d7ecd Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Thu, 11 May 2023 21:52:05 -0500 Subject: [PATCH 17/22] Make namespaces private and correct setdefault --- lib/matplotlib/__init__.py | 40 +++++++++++++++++++++--------------- lib/matplotlib/style/core.py | 1 - 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 8db49cd9fe4f..43716ffa00b0 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -667,19 +667,19 @@ class RcParams(MutableMapping): :ref:`customizing-with-matplotlibrc-files` """ validate = rcsetup._validators - namespaces = ("backends", "lines", "patches", "hatches", "boxplot", "font", "text", - "latex", "axes", "date", "xtick", "ytick", "grid", "legend", - "figure", "image", "contour", "errorbar", "hist", "scatter", "agg", - "path", "savefig", "tk", "ps", "pdf", "svg", "pgf", "docstring", - "keymap", "animation", "_internal", "webagg", "markers", "pcolor", - "pcolormesh", "patch", "hatch", "mathtext", "polaraxes", "axes3d", - "xaxis", "yaxis", "default") + _namespaces = ("backends", "lines", "patches", "hatches", "boxplot", "font", "text", + "latex", "axes", "date", "xtick", "ytick", "grid", "legend", + "figure", "image", "contour", "errorbar", "hist", "scatter", "agg", + "path", "savefig", "tk", "ps", "pdf", "svg", "pgf", "docstring", + "keymap", "animation", "_internal", "webagg", "markers", "pcolor", + "pcolormesh", "patch", "hatch", "mathtext", "polaraxes", "axes3d", + "xaxis", "yaxis", "default") - single_key_set = {"backend", "toolbar", "interactive", - "timezone", "backend_fallback"} + _single_key_set = {"backend", "toolbar", "interactive", + "timezone", "backend_fallback"} def __init__(self, *args, **kwargs): - self._namespace_maps = {name: ChainMap({}) for name in self.namespaces} + self._namespace_maps = {name: ChainMap({}) for name in self._namespaces} self.update(*args, **kwargs) self._new_child() @@ -717,7 +717,7 @@ def _set(self, key, val): """ keys, depth = self._split_key(key) if depth == 1: - if key in self.single_key_set: + if key in self._single_key_set: self._namespace_maps["default"][key] = val # Uncomment the following line and remove the raise statement # to enable setting namespaces. @@ -756,7 +756,7 @@ def _get(self, key): """ keys, depth = self._split_key(key) if depth == 1: - if key in self.single_key_set: + if key in self._single_key_set: return self._namespace_maps["default"].get(key) # Uncomment the following line and remove the raise statement # to enable getting namespace parameters. @@ -790,7 +790,7 @@ def __setitem__(self, key, val): return try: cval = self.validate[key](val) - if key in self.single_key_set: + if key in self._single_key_set: key = f"default.{key}" except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None @@ -826,7 +826,7 @@ def __getitem__(self, key): def _get_default(self, key): keys, depth = self._split_key(key) if depth == 1: - if key in self.single_key_set: + if key in self._single_key_set: return self._namespace_maps["default"].get(key) # Uncomment the following line and remove the raise statement # to enable getting namespace parameters. @@ -868,7 +868,7 @@ def __delitem__(self, key): keys, depth = self._split_key(key) try: if depth == 1: - if key in self.single_key_set: + if key in self._single_key_set: del self._namespace_maps["default"][key] else: raise KeyError @@ -882,7 +882,7 @@ def __delitem__(self, key): def __contains__(self, key): keys, depth = self._split_key(key) if depth == 1: - if key in self.single_key_set: + if key in self._single_key_set: return key in self._namespace_maps["default"] else: return False @@ -917,7 +917,7 @@ def __str__(self): def pop(self, key): keys, depth = self._split_key(key) if depth == 1: - if key in self.single_key_set: + if key in self._single_key_set: return self._namespace_mapping["default"][key] else: raise KeyError( @@ -935,6 +935,12 @@ def clear(self): self._namespace_maps[namespace].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 diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 6b2e6467d4a3..392360920f92 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -149,7 +149,6 @@ def use(style): else: filtered[k] = style[k] mpl.rcParams.update(filtered) - mpl.rcParams._new_child() @contextlib.contextmanager From dd99c1d5687fae6f6d5adb2d71956135bea29e96 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Sun, 28 May 2023 14:03:35 -0700 Subject: [PATCH 18/22] Remove namespaces and keep chainmap for settings. As per @timhoffm's suggestion, decreasing scope of this PR to first just remove the dict inheritance and add a ChainMap to maintain settings. This is a fairly straightforward change and doesn't change the interface. Furthermore, the keys are still dotted and don't support namespacing as defining namespaces might take a little more discussion. --- lib/matplotlib/__init__.py | 143 +++++--------------------- lib/matplotlib/style/core.py | 2 +- lib/matplotlib/tests/test_rcparams.py | 12 ++- 3 files changed, 36 insertions(+), 121 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 43716ffa00b0..b850a69d8d3a 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -667,35 +667,11 @@ class RcParams(MutableMapping): :ref:`customizing-with-matplotlibrc-files` """ validate = rcsetup._validators - _namespaces = ("backends", "lines", "patches", "hatches", "boxplot", "font", "text", - "latex", "axes", "date", "xtick", "ytick", "grid", "legend", - "figure", "image", "contour", "errorbar", "hist", "scatter", "agg", - "path", "savefig", "tk", "ps", "pdf", "svg", "pgf", "docstring", - "keymap", "animation", "_internal", "webagg", "markers", "pcolor", - "pcolormesh", "patch", "hatch", "mathtext", "polaraxes", "axes3d", - "xaxis", "yaxis", "default") - - _single_key_set = {"backend", "toolbar", "interactive", - "timezone", "backend_fallback"} def __init__(self, *args, **kwargs): - self._namespace_maps = {name: ChainMap({}) for name in self._namespaces} + self._rcvalues = ChainMap({}) self.update(*args, **kwargs) - self._new_child() - - @staticmethod - @functools.lru_cache - def _split_key(key, sep="."): - keys = key.split(sep, maxsplit=1) - return keys, len(keys) - - def _new_child(self): - for space in self._namespace_maps.keys(): - self._namespace_maps[space] = self._namespace_maps[space].new_child() - - def _parents(self): - for space in self._namespace_maps.keys(): - self._namespace_maps[space] = self._namespace_maps[space].parents + self._rcvalues.new_child() def _set(self, key, val): """ @@ -715,25 +691,7 @@ def _set(self, key, val): :meta public: """ - keys, depth = self._split_key(key) - if depth == 1: - if key in self._single_key_set: - self._namespace_maps["default"][key] = val - # Uncomment the following line and remove the raise statement - # to enable setting namespaces. - # else: - # if isinstance(val, dict): - # self._namespace_maps[key] = ChainMap({}, val) - # else: - # raise ValueError( - # f"{key} should be set using a dictionary but found " - # f"{type(val)}") - else: - raise KeyError( - f"{key} is not a valid rc parameter (see rcParams.keys() for " - f"a list of valid parameters)") - elif depth == 2: - self._namespace_maps[keys[0]][keys[1]] = val + self._rcvalues[key] = val def _get(self, key): """ @@ -754,19 +712,7 @@ def _get(self, key): :meta public: """ - keys, depth = self._split_key(key) - if depth == 1: - if key in self._single_key_set: - return self._namespace_maps["default"].get(key) - # Uncomment the following line and remove the raise statement - # to enable getting namespace parameters. - # return self._namespace_maps[key] - else: - raise KeyError( - f"{key} is not a valid rc parameter (see rcParams.keys() for " - f"a list of valid parameters)") - elif depth == 2: - return self._namespace_maps[keys[0]].get(keys[1]) + return self._rcvalues[key] def __setitem__(self, key, val): try: @@ -790,8 +736,8 @@ def __setitem__(self, key, val): return try: cval = self.validate[key](val) - if key in self._single_key_set: - key = f"default.{key}" + # if key in self._single_key_set: + # key = f"default.{key}" except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None self._set(key, cval) @@ -824,21 +770,9 @@ def __getitem__(self, key): return self._get(key) def _get_default(self, key): - keys, depth = self._split_key(key) - if depth == 1: - if key in self._single_key_set: - return self._namespace_maps["default"].get(key) - # Uncomment the following line and remove the raise statement - # to enable getting namespace parameters. - # return self._namespace_maps[key] - else: - raise KeyError( - f"{key} is not a valid rc parameter (see rcParams.keys() for " - f"a list of valid parameters)") - elif depth == 2: - return self._namespace_maps[keys[0]].maps[-1].get(keys[1]) + return self._rcvalues.maps[-1][key] - def getdefault(self, key): + def get_default(self, key): if key in _deprecated_map: version, alt_key, alt_val, inverse_alt = _deprecated_map[key] _api.warn_deprecated( @@ -853,7 +787,7 @@ def getdefault(self, key): return self._get_default(key) - def getdefaults(self): + def get_defaults(self): """Return default values set during initialization.""" defaults = self.copy() defaults.clear() @@ -865,54 +799,35 @@ def _get_backend_or_none(self): return None if backend is rcsetup._auto_backend_sentinel else backend def __delitem__(self, key): - keys, depth = self._split_key(key) try: - if depth == 1: - if key in self._single_key_set: - del self._namespace_maps["default"][key] - else: - raise KeyError - elif depth == 2: - del self._namespace_maps[keys[0]][keys[1]] + del self._rcvalues[key] except KeyError as err: raise KeyError( f"{key} is not a valid rc parameter (see rcParams.keys() for " f"a list of valid parameters)") from err def __contains__(self, key): - keys, depth = self._split_key(key) - if depth == 1: - if key in self._single_key_set: - return key in self._namespace_maps["default"] - else: - return False - elif depth == 2: - return any(key in mapping for mapping in self._namespace_maps) + return key in self._rcvalues def __iter__(self): """Yield from sorted list of keys""" - keys = ( - ".".join((space, key)) if space != 'default' else key - for space, mapping in self._namespace_maps.items() - for key in mapping.keys() - ) with _api.suppress_matplotlib_deprecation_warning(): - yield from sorted(keys) + yield from sorted(self._rcvalues.keys()) def __len__(self): - return sum(len(mapping) for mapping in self._namespace_maps) + 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.items()), 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 pop(self, key): keys, depth = self._split_key(key) @@ -931,8 +846,7 @@ def popitem(self): "popitem is not implemented for RcParams.") def clear(self): - for namespace in self._namespace_maps: - self._namespace_maps[namespace].clear() + self._rcvalues.clear() def setdefault(self, key, default=None): """Insert key with a value of default if key is not in the dictionary. @@ -1067,8 +981,7 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): fname) raise - config = RcParams() - config._parents() + config = dict() for key, (val, line, line_no) in rc_temp.items(): if key in rcsetup._validators: @@ -1097,8 +1010,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)) - config._new_child() - return config + return RcParams(config) def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): @@ -1122,7 +1034,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({**rcParams.getdefaults(), **config_from_file}) + config = RcParams({**rcParams.get_defaults(), **config_from_file}) if "".join(config['text.latex.preamble']): _log.info(""" @@ -1142,10 +1054,10 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): # Strip leading comment. transform=lambda line: line[1:] if line.startswith("#") else line, fail_on_error=True) -for key in rcsetup._hardcoded_defaults: - space, subkey = key.split(".") - if not rcParams._namespace_maps[space]: - rcParams._namespace_maps[space] = ChainMap({}) +# for key in rcsetup._hardcoded_defaults: +# space, subkey = key.split(".") +# if not rcParams._namespace_maps[space]: +# rcParams._namespace_maps[space] = ChainMap({}) 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 @@ -1161,7 +1073,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 - rcParams.getdefault(key)), + rcParams.get_default(key)), validator] for key, validator in rcsetup._validators.items()} if rcParams['axes.formatter.use_locale']: @@ -1354,10 +1266,7 @@ def rc_context(rc=None, fname=None): """ try: - for space in rcParams._namespace_maps.keys(): - rcParams._namespace_maps[space] = rcParams._namespace_maps[ - space - ].new_child() + rcParams._rcvalues = rcParams._rcvalues.new_child() if fname: rc_file(fname) if rc: @@ -1366,7 +1275,7 @@ def rc_context(rc=None, fname=None): finally: # Revert to the original rcs. backend = rcParams["backend"] - rcParams._parents() + rcParams._rcvalues = rcParams._rcvalues.parents rcParams["backend"] = backend diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 392360920f92..57b3a60a0a54 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -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: mpl.rcParams.getdefault(k) for k in mpl.rcParams + 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 f1a53bfaef9e..8defa1735edf 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -656,12 +656,12 @@ def test_rcparams_path_sketch_from_file(tmpdir, value): def test_rcparams_getdefault(): with mpl.rc_context({"image.lut": 128}): - assert mpl.rcParams.getdefault("image.lut") == 256 + assert mpl.rcParams.get_default("image.lut") == 256 def test_rcparams_getdefaults(): mpl.rc("image", lut=128) - defaults = mpl.rcParams.getdefaults() + defaults = mpl.rcParams.get_defaults() mpl.rcParams.clear() assert defaults == mpl.rcParams @@ -673,8 +673,14 @@ def test_rcdefaults(): # lines.linewidth is not a style blacklisted key and should be # reset to the default value. # breakpoint() - lw = mpl.rcParams.getdefault("lines.linewidth") + 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_clear(): + mpl.rcParams["image.lut"] = 128 + mpl.rcParams.clear() + assert mpl.rcParams["image.lut"] == 256 From aa282d0d3a0dcacc2fc7a59569891cc50930e908 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Wed, 7 Jun 2023 21:10:55 -0700 Subject: [PATCH 19/22] Deprecate clear and other small updates for review --- lib/matplotlib/__init__.py | 57 +++++++-------------------- lib/matplotlib/tests/test_rcparams.py | 12 ++++-- 2 files changed, 23 insertions(+), 46 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index b850a69d8d3a..95f6b4e8e66e 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -671,7 +671,8 @@ class RcParams(MutableMapping): def __init__(self, *args, **kwargs): self._rcvalues = ChainMap({}) self.update(*args, **kwargs) - self._rcvalues.new_child() + self._rcvalues = self._rcvalues.new_child() + self._defaults = self._rcvalues.maps[-1] def _set(self, key, val): """ @@ -730,14 +731,12 @@ def __setitem__(self, key, val): _api.warn_deprecated( version, name=key, obj_type="rcparam", alternative=alt_key) return - elif key == 'backend' or key == "default.backend": + elif key == 'backend': if val is rcsetup._auto_backend_sentinel: - if 'backend' in self or 'default.backend' in self: + if 'backend' in self: return try: cval = self.validate[key](val) - # if key in self._single_key_set: - # key = f"default.{key}" except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None self._set(key, cval) @@ -769,10 +768,8 @@ def __getitem__(self, key): return self._get(key) - def _get_default(self, key): - return self._rcvalues.maps[-1][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( @@ -783,15 +780,13 @@ def get_default(self, key): version, alt_key = _deprecated_ignore_map[key] _api.warn_deprecated( version, name=key, obj_type="rcparam", alternative=alt_key) - return self._get_default(alt_key) if alt_key else None + return self._defaults[alt_key] if alt_key else None - return self._get_default(key) + return self._defaults[key] def get_defaults(self): """Return default values set during initialization.""" - defaults = self.copy() - defaults.clear() - return defaults + return self._defaults.copy() def _get_backend_or_none(self): """Get the requested backend, if any, without triggering resolution.""" @@ -845,7 +840,11 @@ def popitem(self): raise NotImplementedError( "popitem is not implemented for RcParams.") + @_api.deprecated("3.8") def clear(self): + pass + + def reset(self): self._rcvalues.clear() def setdefault(self, key, default=None): @@ -858,30 +857,6 @@ def setdefault(self, key, default=None): self[key] = default return default - def get(self, key, default=None): - try: - return self[key] - except KeyError as e: - return default - - def update(self, other=(), /, **kwds): - """D.update([E, ]**F) -> None. Update D from mapping/iterable E and F. - If E present and has a .keys() method, does: for k in E: D[k] = E[k] - If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v - In either case, this is followed by: for k, v in F.items(): D[k] = v - """ - if isinstance(other, Mapping): - for key in other: - self[key] = other[key] - elif hasattr(other, "keys"): - for key in other.keys(): - self[key] = other[key] - else: - for key, value in other: - self[key] = value - for key, value in kwds.items(): - self[key] = value - def copy(self): return deepcopy(self) @@ -1054,18 +1029,16 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): # Strip leading comment. transform=lambda line: line[1:] if line.startswith("#") else line, fail_on_error=True) -# for key in rcsetup._hardcoded_defaults: -# space, subkey = key.split(".") -# if not rcParams._namespace_maps[space]: -# rcParams._namespace_maps[space] = ChainMap({}) +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. -rcParams.setdefault("backend", rcsetup._auto_backend_sentinel) rcParams.update(_rc_params_in_file(matplotlib_fname())) +rcParams._rcvalues = rcParams._rcvalues.new_child() +rcParams.setdefault("backend", rcsetup._auto_backend_sentinel) rcParamsOrig = rcParams.copy() with _api.suppress_matplotlib_deprecation_warning(): # This also checks that all rcParams are indeed listed in the template. diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 8defa1735edf..dc4ff1f8cff5 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -662,8 +662,7 @@ def test_rcparams_getdefault(): def test_rcparams_getdefaults(): mpl.rc("image", lut=128) defaults = mpl.rcParams.get_defaults() - mpl.rcParams.clear() - assert defaults == mpl.rcParams + assert defaults == mpl.rcParams._defaults def test_rcdefaults(): @@ -680,7 +679,12 @@ def test_rcdefaults(): assert mpl.rcParams["lines.linewidth"] == lw -def test_rcparams_clear(): +def test_rcparams_reset(): mpl.rcParams["image.lut"] = 128 - mpl.rcParams.clear() + mpl.rcParams.reset() assert mpl.rcParams["image.lut"] == 256 + + +def test_rcparams_clear(): + with pytest.raises(mpl.MatplotlibDeprecationWarning): + mpl.rcParams.clear() From b1754a2e97b71ccfed41243ee3bf1a2425fa51a4 Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Wed, 7 Jun 2023 22:06:31 -0700 Subject: [PATCH 20/22] Update delitem --- lib/matplotlib/__init__.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 95f6b4e8e66e..7b8be618af23 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -794,12 +794,16 @@ def _get_backend_or_none(self): 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"{key} is not a valid rc parameter (see rcParams.keys() for " - f"a list of valid parameters)") from err + f"No custom value set for {key}. Cannot delete default value." + ) from err def __contains__(self, key): return key in self._rcvalues @@ -824,22 +828,6 @@ def __repr__(self): def __str__(self): return '\n'.join(map('{0[0]}: {0[1]}'.format, sorted(self._rcvalues.items()))) - def pop(self, key): - keys, depth = self._split_key(key) - if depth == 1: - if key in self._single_key_set: - return self._namespace_mapping["default"][key] - else: - raise KeyError( - f"{key} is not a valid rc parameter (see rcParams.keys() for " - f"a list of valid parameters)") - elif depth == 2: - return self._namespace_mapping[keys[0]].pop(keys[1]) - - def popitem(self): - raise NotImplementedError( - "popitem is not implemented for RcParams.") - @_api.deprecated("3.8") def clear(self): pass @@ -1037,8 +1025,8 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): # (resulting in a normal `#backend: foo` line) in which case we should *not* # fill in _auto_backend_sentinel. rcParams.update(_rc_params_in_file(matplotlib_fname())) -rcParams._rcvalues = rcParams._rcvalues.new_child() 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. From faed9bacfd4c6018fa0d7349f272135852c7c3db Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Sat, 14 Oct 2023 10:19:26 -0400 Subject: [PATCH 21/22] Make flake8 happy --- lib/matplotlib/__init__.pyi | 8 ++++---- lib/matplotlib/pyplot.py | 7 ++++--- lib/matplotlib/tests/test_offsetbox.py | 5 ----- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 8691ef420166..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 @@ -79,8 +79,8 @@ class RcParams(MutableMapping[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 8c7b6a873882..5669ee82f2f3 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -67,6 +67,7 @@ from matplotlib.gridspec import GridSpec, SubplotSpec 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 @@ -302,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', @@ -2475,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/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py index edf17542fb17..49b55e4c9326 100644 --- a/lib/matplotlib/tests/test_offsetbox.py +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -257,14 +257,9 @@ def test_anchoredtext_horizontal_alignment(): ax.add_artist(text2) -<<<<<<< HEAD @pytest.mark.parametrize("extent_kind", ["window_extent", "tightbbox"]) def test_annotationbbox_extents(extent_kind): plt.rcParams.update(plt.rcParamsDefault) -======= -def test_annotationbbox_extents(): - plt.rcParams.clear() ->>>>>>> 57d742c95e (Remove `rcParamsDefault`) fig, ax = plt.subplots(figsize=(4, 3), dpi=100) ax.axis([0, 1, 0, 1]) From dfbb73f3f1a544eca2449a0884a308bf8d468c8e Mon Sep 17 00:00:00 2001 From: Chahak Mehta Date: Mon, 23 Oct 2023 21:16:55 -0400 Subject: [PATCH 22/22] Remove unnecessary deprecation warning context --- lib/matplotlib/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 7b8be618af23..623b22852e30 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -810,8 +810,7 @@ def __contains__(self, key): def __iter__(self): """Yield from sorted list of keys""" - with _api.suppress_matplotlib_deprecation_warning(): - yield from sorted(self._rcvalues.keys()) + yield from sorted(self._rcvalues.keys()) def __len__(self): return len(self._rcvalues)