From ecf156c1a086def57e77b088b1ab76c7548ee152 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 14 Dec 2022 22:18:24 +0100 Subject: [PATCH] Data access API for rcParams This provides a defined API for accessing rcParams via `rcParams._data[key]` while circumventing any validation logic happening in `rcParams[key]`. Before, direct data access was realized through `dict.__getitem(rcParams, key)` / `dict.__setitem(rcParams, key, val)`, which depends on the implementation detail of `rcParams` being a dict subclass. The new data access API gets rid of this dependence and thus opens up a way to later move away from dict subclassing. We want to move away from dict subclassing and only guarantee the `MutableMapping` interface for `rcParams` in the future. This allows other future restructings like introducing a new configuration management and changing `rcParams` into a backward-compatible adapter. Co-authored-by: Oscar Gustafsson Co-authored-by: Jody Klymak Co-authored-by: Elliott Sales de Andrade Co-authored-by: story645 --- .../deprecations/24730-TH.rst | 24 ++++++++ lib/matplotlib/__init__.py | 57 ++++++++++++++++--- lib/matplotlib/pyplot.py | 10 ++-- .../tests/test_backends_interactive.py | 4 +- lib/matplotlib/tests/test_rcparams.py | 2 +- lib/matplotlib/tests/test_widgets.py | 2 +- 6 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/24730-TH.rst diff --git a/doc/api/next_api_changes/deprecations/24730-TH.rst b/doc/api/next_api_changes/deprecations/24730-TH.rst new file mode 100644 index 000000000000..0edb329e7bc6 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/24730-TH.rst @@ -0,0 +1,24 @@ +rcParams type +~~~~~~~~~~~~~ +Relying on ``rcParams`` being a ``dict`` subclass is deprecated. + +Nothing will change for regular users because ``rcParams`` will continue to +be dict-like (technically fulfill the ``MutableMapping`` interface). + +The `.RcParams` class does validation checking on calls to +``.RcParams.__getitem__`` and ``.RcParams.__setitem__``. However, there are rare +cases where we want to circumvent the validation logic and directly access the +underlying data values. Previously, this could be accomplished via a call to +the parent methods ``dict.__getitem__(rcParams, key)`` and +``dict.__setitem__(rcParams, key, val)``. + +Matplotlib 3.7 introduces ``rcParams._set(key, val)`` and +``rcParams._get(key)`` as a replacement to calling the parent methods. They are +intentionally marked private to discourage external use; However, if direct +`.RcParams` data access is needed, please switch from the dict functions to the +new ``_get()`` and ``_set()``. Even though marked private, we guarantee API +stability for these methods and they are subject to Matplotlib's API and +deprecation policy. + +Please notify the Matplotlib developers if you rely on ``rcParams`` being a +dict subclass in any other way, for which there is no migration path yet. diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index fd78e6f94f19..daa588f2fdcd 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -605,7 +605,7 @@ def gen_candidates(): ) class RcParams(MutableMapping, dict): """ - A dictionary object including validation. + A dict-like key-value store for config parameters, including validation. Validating functions are defined and associated with rc parameters in :mod:`matplotlib.rcsetup`. @@ -625,6 +625,47 @@ class RcParams(MutableMapping, dict): def __init__(self, *args, **kwargs): self.update(*args, **kwargs) + def _set(self, key, val): + """ + Directly write data bypassing deprecation and validation logic. + + Notes + ----- + As end user or downstream library you almost always should use + ``rcParams[key] = val`` and not ``_set()``. + + There are only very few special cases that need direct data access. + These cases previously used ``dict.__setitem__(rcParams, key, val)``, + which is now deprecated and replaced by ``rcParams._set(key, val)``. + + Even though private, we guarantee API stability for ``rcParams._set``, + i.e. it is subject to Matplotlib's API and deprecation policy. + + :meta public: + """ + dict.__setitem__(self, key, val) + + def _get(self, key): + """ + Directly read data bypassing deprecation, backend and validation + logic. + + Notes + ----- + As end user or downstream library you almost always should use + ``val = rcParams[key]`` and not ``_get()``. + + There are only very few special cases that need direct data access. + These cases previously used ``dict.__getitem__(rcParams, key, val)``, + which is now deprecated and replaced by ``rcParams._get(key)``. + + Even though private, we guarantee API stability for ``rcParams._get``, + i.e. it is subject to Matplotlib's API and deprecation policy. + + :meta public: + """ + return dict.__getitem__(self, key) + def __setitem__(self, key, val): try: if key in _deprecated_map: @@ -649,7 +690,7 @@ def __setitem__(self, key, val): cval = self.validate[key](val) except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None - dict.__setitem__(self, key, cval) + self._set(key, cval) except KeyError as err: raise KeyError( f"{key} is not a valid rc parameter (see rcParams.keys() for " @@ -660,27 +701,27 @@ def __getitem__(self, key): 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(dict.__getitem__(self, 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 dict.__getitem__(self, alt_key) if alt_key else None + return self._get(alt_key) if alt_key else None # In theory, this should only ever be used after the global rcParams # has been set up, but better be safe e.g. in presence of breakpoints. elif key == "backend" and self is globals().get("rcParams"): - val = dict.__getitem__(self, key) + val = self._get(key) if val is rcsetup._auto_backend_sentinel: from matplotlib import pyplot as plt plt.switch_backend(rcsetup._auto_backend_sentinel) - return dict.__getitem__(self, key) + return self._get(key) def _get_backend_or_none(self): """Get the requested backend, if any, without triggering resolution.""" - backend = dict.__getitem__(self, "backend") + backend = self._get("backend") return None if backend is rcsetup._auto_backend_sentinel else backend def __repr__(self): @@ -722,7 +763,7 @@ def find_all(self, pattern): def copy(self): rccopy = RcParams() for k in self: # Skip deprecations and revalidation. - dict.__setitem__(rccopy, k, dict.__getitem__(self, k)) + rccopy._set(k, self._get(k)) return rccopy diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index f11d66844a4c..54597c42852e 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -200,10 +200,10 @@ def _get_backend_mod(): This is currently private, but may be made public in the future. """ if _backend_mod is None: - # Use __getitem__ here to avoid going through the fallback logic (which - # will (re)import pyplot and then call switch_backend if we need to - # resolve the auto sentinel) - switch_backend(dict.__getitem__(rcParams, "backend")) + # Use rcParams._get("backend") to avoid going through the fallback + # logic (which will (re)import pyplot and then call switch_backend if + # we need to resolve the auto sentinel) + switch_backend(rcParams._get("backend")) return _backend_mod @@ -2197,7 +2197,7 @@ def polar(*args, **kwargs): and rcParams._get_backend_or_none() in ( set(_interactive_bk) - {'WebAgg', 'nbAgg'}) and cbook._get_running_interactive_framework()): - dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel) + rcParams._set("backend", rcsetup._auto_backend_sentinel) ################# REMAINING CONTENT GENERATED BY boilerplate.py ############## diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index e5e6b295e798..5cb007ba946e 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -247,13 +247,13 @@ def _impl_test_lazy_auto_backend_selection(): import matplotlib import matplotlib.pyplot as plt # just importing pyplot should not be enough to trigger resolution - bk = dict.__getitem__(matplotlib.rcParams, 'backend') + bk = matplotlib.rcParams._get('backend') assert not isinstance(bk, str) assert plt._backend_mod is None # but actually plotting should plt.plot(5) assert plt._backend_mod is not None - bk = dict.__getitem__(matplotlib.rcParams, 'backend') + bk = matplotlib.rcParams._get('backend') assert isinstance(bk, str) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index d5f316f54595..a9a4ccba21fa 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -543,7 +543,7 @@ def test_backend_fallback_headful(tmpdir): "sentinel = mpl.rcsetup._auto_backend_sentinel; " # Check that access on another instance does not resolve the sentinel. "assert mpl.RcParams({'backend': sentinel})['backend'] == sentinel; " - "assert dict.__getitem__(mpl.rcParams, 'backend') == sentinel; " + "assert mpl.rcParams._get('backend') == sentinel; " "import matplotlib.pyplot; " "print(matplotlib.get_backend())"], env=env, universal_newlines=True) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 446e272610ab..fda7a8a05bfb 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -965,7 +965,7 @@ def test_CheckButtons(ax): @pytest.mark.parametrize("toolbar", ["none", "toolbar2", "toolmanager"]) def test_TextBox(ax, toolbar): # Avoid "toolmanager is provisional" warning. - dict.__setitem__(plt.rcParams, "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) 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