Skip to content

Commit e8ff5bb

Browse files
committed
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.
1 parent 07af522 commit e8ff5bb

File tree

6 files changed

+74
-18
lines changed

6 files changed

+74
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
rcParams type
2+
~~~~~~~~~~~~~
3+
Relying on ``rcParams`` being a ``dict`` subclass is deprecated.
4+
5+
Nothing will change for regular users because ``rcParams`` will continue to
6+
be dict-like (technically fulfill the ``MutableMapping`` interface).
7+
8+
However, there are some rare known cases that use
9+
``dict.__getitem__(rcParams, key)`` and
10+
``dict.__setitem__(rcParams, key, val)`` for direct data access that
11+
circumvents any validation logic. While this is technically public, it
12+
depends on the implementation detail that ``rcParams`` is currently a ``dict``
13+
subclass.
14+
15+
Matplotlib 3.7 introduces ``rcParams.set(key, val)`` and ``rcParams.get(key)``
16+
as a replacements for these cases. They are intentionally marked private,
17+
because there is very little need for external users to use them. However, if
18+
you have a compelling reason for direct data access, please switch from the
19+
dict functions to the new ``_get()`` and `_set()``. Even though marked
20+
private, we guarantee API stability for these methods, i.e. they are subject
21+
to Matplotlib's API and deprecation policy.
22+
23+
Please notify the Matplotlib developers if you rely on ``rcParams`` being a
24+
dict subclass in any other way, for which there is no migration path yet.

lib/matplotlib/__init__.py

+41-9
Original file line numberDiff line numberDiff line change
@@ -600,12 +600,12 @@ def gen_candidates():
600600
_deprecated_remain_as_none = {}
601601

602602

603-
@_docstring.Substitution(
603+
_docstring.Substitution(
604604
"\n".join(map("- {}".format, sorted(rcsetup._validators, key=str.lower)))
605605
)
606606
class RcParams(MutableMapping, dict):
607607
"""
608-
A dictionary object including validation.
608+
A dict-like configuration parameter container including validation.
609609
610610
Validating functions are defined and associated with rc parameters in
611611
:mod:`matplotlib.rcsetup`.
@@ -625,6 +625,38 @@ class RcParams(MutableMapping, dict):
625625
def __init__(self, *args, **kwargs):
626626
self.update(*args, **kwargs)
627627

628+
def _set(self, key, val):
629+
"""
630+
Direct write data bypassing deprecation and validation logic.
631+
632+
As end user or downstream library you almost always should use
633+
``rcParams[key] = val`` and not ``_set()``.
634+
635+
There are only very few special cases that need direct data access.
636+
These cases previously used ``dict.__setitem__(rcParams, key, val)``,
637+
which is now deprecated and replaced by ``rcParams._set(key, val)``.
638+
639+
Even though private, we guarantee API stability for ``rcParams._set``,
640+
i.e. it is subject to Matplotlib's API and deprecation policy.
641+
"""
642+
dict.__setitem__(self, key, val)
643+
644+
def _get(self, key):
645+
"""
646+
Direct read data bypassing deprecation, backend and validation logic.
647+
648+
As end user or downstream library you almost always should use
649+
``val = rcParams[key]`` and not ``_get()``.
650+
651+
There are only very few special cases that need direct data access.
652+
These cases previously used ``dict.__getitem__(rcParams, key, val)``,
653+
which is now deprecated and replaced by ``rcParams._get(key)``.
654+
655+
Even though private, we guarantee API stability for ``rcParams._get``,
656+
i.e. it is subject to Matplotlib's API and deprecation policy.
657+
"""
658+
return dict.__getitem__(self, key)
659+
628660
def __setitem__(self, key, val):
629661
try:
630662
if key in _deprecated_map:
@@ -649,7 +681,7 @@ def __setitem__(self, key, val):
649681
cval = self.validate[key](val)
650682
except ValueError as ve:
651683
raise ValueError(f"Key {key}: {ve}") from None
652-
dict.__setitem__(self, key, cval)
684+
self._set(key, cval)
653685
except KeyError as err:
654686
raise KeyError(
655687
f"{key} is not a valid rc parameter (see rcParams.keys() for "
@@ -660,27 +692,27 @@ def __getitem__(self, key):
660692
version, alt_key, alt_val, inverse_alt = _deprecated_map[key]
661693
_api.warn_deprecated(
662694
version, name=key, obj_type="rcparam", alternative=alt_key)
663-
return inverse_alt(dict.__getitem__(self, alt_key))
695+
return inverse_alt(self._get(alt_key))
664696

665697
elif key in _deprecated_ignore_map:
666698
version, alt_key = _deprecated_ignore_map[key]
667699
_api.warn_deprecated(
668700
version, name=key, obj_type="rcparam", alternative=alt_key)
669-
return dict.__getitem__(self, alt_key) if alt_key else None
701+
return self._get(alt_key) if alt_key else None
670702

671703
# In theory, this should only ever be used after the global rcParams
672704
# has been set up, but better be safe e.g. in presence of breakpoints.
673705
elif key == "backend" and self is globals().get("rcParams"):
674-
val = dict.__getitem__(self, key)
706+
val = self._get(key)
675707
if val is rcsetup._auto_backend_sentinel:
676708
from matplotlib import pyplot as plt
677709
plt.switch_backend(rcsetup._auto_backend_sentinel)
678710

679-
return dict.__getitem__(self, key)
711+
return self._get(key)
680712

681713
def _get_backend_or_none(self):
682714
"""Get the requested backend, if any, without triggering resolution."""
683-
backend = dict.__getitem__(self, "backend")
715+
backend = self._get("backend")
684716
return None if backend is rcsetup._auto_backend_sentinel else backend
685717

686718
def __repr__(self):
@@ -722,7 +754,7 @@ def find_all(self, pattern):
722754
def copy(self):
723755
rccopy = RcParams()
724756
for k in self: # Skip deprecations and revalidation.
725-
dict.__setitem__(rccopy, k, dict.__getitem__(self, k))
757+
rccopy._set(k, self._get(k))
726758
return rccopy
727759

728760

lib/matplotlib/pyplot.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,10 @@ def _get_backend_mod():
200200
This is currently private, but may be made public in the future.
201201
"""
202202
if _backend_mod is None:
203-
# Use __getitem__ here to avoid going through the fallback logic (which
204-
# will (re)import pyplot and then call switch_backend if we need to
205-
# resolve the auto sentinel)
206-
switch_backend(dict.__getitem__(rcParams, "backend"))
203+
# Use rcParams.get("backend") to avoid going through the fallback
204+
# logic (which will (re)import pyplot and then call switch_backend if
205+
# we need to resolve the auto sentinel)
206+
switch_backend(rcParams._get("backend"))
207207
return _backend_mod
208208

209209

@@ -2197,7 +2197,7 @@ def polar(*args, **kwargs):
21972197
and rcParams._get_backend_or_none() in (
21982198
set(_interactive_bk) - {'WebAgg', 'nbAgg'})
21992199
and cbook._get_running_interactive_framework()):
2200-
dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel)
2200+
rcParams._set("backend", rcsetup._auto_backend_sentinel)
22012201

22022202

22032203
################# REMAINING CONTENT GENERATED BY boilerplate.py ##############

lib/matplotlib/tests/test_backends_interactive.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,13 @@ def _impl_test_lazy_auto_backend_selection():
247247
import matplotlib
248248
import matplotlib.pyplot as plt
249249
# just importing pyplot should not be enough to trigger resolution
250-
bk = dict.__getitem__(matplotlib.rcParams, 'backend')
250+
bk = matplotlib.rcParams._get('backend')
251251
assert not isinstance(bk, str)
252252
assert plt._backend_mod is None
253253
# but actually plotting should
254254
plt.plot(5)
255255
assert plt._backend_mod is not None
256-
bk = dict.__getitem__(matplotlib.rcParams, 'backend')
256+
bk = matplotlib.rcParams._get('backend')
257257
assert isinstance(bk, str)
258258

259259

lib/matplotlib/tests/test_rcparams.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ def test_backend_fallback_headful(tmpdir):
543543
"sentinel = mpl.rcsetup._auto_backend_sentinel; "
544544
# Check that access on another instance does not resolve the sentinel.
545545
"assert mpl.RcParams({'backend': sentinel})['backend'] == sentinel; "
546-
"assert dict.__getitem__(mpl.rcParams, 'backend') == sentinel; "
546+
"assert mpl.rcParams._get('backend') == sentinel; "
547547
"import matplotlib.pyplot; "
548548
"print(matplotlib.get_backend())"],
549549
env=env, universal_newlines=True)

lib/matplotlib/tests/test_widgets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,7 @@ def test_CheckButtons(ax):
965965
@pytest.mark.parametrize("toolbar", ["none", "toolbar2", "toolmanager"])
966966
def test_TextBox(ax, toolbar):
967967
# Avoid "toolmanager is provisional" warning.
968-
dict.__setitem__(plt.rcParams, "toolbar", toolbar)
968+
plt.rcParams._set("toolbar", toolbar)
969969

970970
submit_event = mock.Mock(spec=noop, return_value=None)
971971
text_change_event = mock.Mock(spec=noop, return_value=None)

0 commit comments

Comments
 (0)