Skip to content

Data access API for rcParams #24730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions doc/api/next_api_changes/deprecations/24730-TH.rst
Original file line number Diff line number Diff line change
@@ -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.
57 changes: 49 additions & 8 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ def gen_candidates():
)
class RcParams(MutableMapping, dict):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may not fully get the scope here, but I take it that one reason to do this is that we should not subclass from dict later on? Or is the whole idea to provide a stable API for this, but actually still subclass?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to get rid of dict subclassing eventually. Using a dict subclass dictates the internal data model. We likely don't want this in the future when we're remodelling the config data structure. Either the new structure will be a 100% API compatible drop in and the config object will be available via rcParams; or we have a new completely free to design config object and rcParams will loose all state and only be an adapter to that new config object. Either way, being bound to a dict subclass would be cumbersome.

"""
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`.
Expand All @@ -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:
Expand All @@ -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 "
Expand All @@ -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):
Expand Down Expand Up @@ -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


Expand Down
10 changes: 5 additions & 5 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 ##############
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_backends_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_rcparams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down