Skip to content

RcParams should not inherit from dict #12577

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

Closed
wants to merge 1 commit into from
Closed
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
52 changes: 31 additions & 21 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
import atexit
from collections.abc import MutableMapping
import contextlib
import copy
import distutils.version
import functools
import importlib
Expand Down Expand Up @@ -759,10 +760,9 @@ def gen_candidates():
_all_deprecated = {*_deprecated_map, *_deprecated_ignore_map}


class RcParams(MutableMapping, dict):

class RcParams(MutableMapping):
"""
A dictionary object including validation
A dictionary object including validation.

validating functions are defined and associated with rc parameters in
:mod:`matplotlib.rcsetup`
Expand Down Expand Up @@ -803,6 +803,7 @@ def msg_backend_obsolete(self):

# validate values on the way in
def __init__(self, *args, **kwargs):
self._data = {}
self.update(*args, **kwargs)

def __setitem__(self, key, val):
Expand Down Expand Up @@ -840,7 +841,7 @@ def __setitem__(self, key, val):
cval = self.validate[key](val)
except ValueError as ve:
raise ValueError("Key %s: %s" % (key, str(ve)))
dict.__setitem__(self, key, cval)
self._data[key] = cval
except KeyError:
raise KeyError(
'%s is not a valid rc parameter. See rcParams.keys() for a '
Expand All @@ -851,41 +852,51 @@ def __getitem__(self, key):
version, alt_key, alt_val, inverse_alt = _deprecated_map[key]
cbook.warn_deprecated(
version, key, obj_type="rcparam", alternative=alt_key)
return inverse_alt(dict.__getitem__(self, alt_key))
return inverse_alt(self._data[alt_key])

elif key in _deprecated_ignore_map:
version, alt_key = _deprecated_ignore_map[key]
cbook.warn_deprecated(
version, key, obj_type="rcparam", alternative=alt_key)
return dict.__getitem__(self, alt_key) if alt_key else None
return self._data[alt_key] if alt_key else None

elif key == 'examples.directory':
cbook.warn_deprecated(
"3.0", "{} is deprecated; in the future, examples will be "
"found relative to the 'datapath' directory.".format(key))

elif key == "backend":
val = dict.__getitem__(self, key)
val = self._data[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._data[key]

def __repr__(self):
class_name = self.__class__.__name__
indent = len(class_name) + 1
repr_split = pprint.pformat(dict(self), indent=1,
repr_split = pprint.pformat(self._data, indent=1,
width=80 - indent).split('\n')
repr_indented = ('\n' + ' ' * indent).join(repr_split)
return '{}({})'.format(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._data.items())))

def __iter__(self):
"""Yield sorted list of keys."""
yield from sorted(dict.__iter__(self))
yield from sorted(self._data)

def __len__(self):
return len(self._data)

def __delitem__(self, key):
Copy link
Contributor

Choose a reason for hiding this comment

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

I actually don't think we should allow del'ing items from the rcParams as that has no sense. It may not have been worth it to prevent that with the old implementation, but now it's just a matter of replacing the implementation by a raise TypeError.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, you're right. The current code prevents it, because we did not reimplement the MutableMapping.__delitem__. Before we were just inheriting from dict, so del'ing should have been possible. Inheriting MutableMapping was introduced within a large commit #7549. I dare assuming that both behaviors were incidental and nobody really thought about it.

I'm extending the RcParams in my private code to be able to style and style-context some of my plot elements in specialized plots. While I'm currently not del'ing anything I could imagine that there's a use-case for that. Maybe we should prevent del'ing only for values that are in the defaults.

But as a first step it's ok to stay as restrictive as we currently are.

Copy link
Contributor

Choose a reason for hiding this comment

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

Talking about custom extensions to rcParams (none of which are really using the public API I guess), I realized that this PR would break https://github.com/anntzer/mplcairo#antialiasing where I use dict.__setitem__ to bypass the validation code on input (to set lines.antialiased to one of the many cairo antialiasing modes, whereas the validator just accepts True/False).
I guess that's related to your point in #12577 (comment)...

Copy link
Member Author

@timhoffm timhoffm Oct 25, 2018

Choose a reason for hiding this comment

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

I was hoping nobody would mess with the implementation details of RcParams in such a way except our own code. Ok, then we have to take it a step slower, define an official API for that and only later migrate to the MutableMapping.

By the way, What was your reason for introducing the multiple inheritance in the first place? IMO deriving from dict is not good but still far better than deriving from dict and MutableMapping.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was hoping nobody would mess with the implementation details of RcParams in such a way except our own code. Ok, then we have to take it a step slower, define an official API for that and only later migrate to the MutableMapping.

To be fully consistent with my own general approach wrt backcompat ("we should consider whether it'll actually matter to enough people that we reject a backcompat break"): I'm not going to throw a fit if the API changes here, and having a "more official" way to bypass the validator would be better anyways.

By the way, What was your reason for introducing the multiple inheritance in the first place? IMO deriving from dict is not good but still far better than deriving from dict and MutableMapping.

When I wrote it, it seemed to be a good way to provide update() cheaply.
In fact, looking at it again:

  • I guess you can still make it just inherit from dict, keeping most of the old code, and then do update = MutableMapping.update (thus easily stealing the implementation from MutableMapping without pulling in the whole multiple inheritance problem, and not breaking my approach in mplcairo :p);
  • I think the fact that len(rcParams) == 0, i.e. that calls MutableMapping.__len__, is a CPython bug: https://bugs.python.org/issue35063

Copy link
Member Author

Choose a reason for hiding this comment

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

LGTM

del self._data[key]

def copy(self):
return copy.deepcopy(self)

def find_all(self, pattern):
"""
Expand All @@ -900,7 +911,7 @@ def find_all(self, pattern):
"""
pattern_re = re.compile(pattern)
return RcParams((key, value)
for key, value in self.items()
for key, value in self._data.items()
if pattern_re.search(key))


Expand Down Expand Up @@ -1060,24 +1071,23 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
rcParams = rc_params()

# Don't trigger deprecation warning when just fetching.
if dict.__getitem__(rcParams, 'examples.directory'):
with warnings.catch_warnings():
warnings.simplefilter("ignore", MatplotlibDeprecationWarning)
examples_directory = rcParams['examples.directory']
Copy link
Contributor

Choose a reason for hiding this comment

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

underscore to not make this a publically accessible global

# paths that are intended to be relative to matplotlib_fname()
# are allowed for the examples.directory parameter.
# However, we will need to fully qualify the path because
# Sphinx requires absolute paths.
if not os.path.isabs(rcParams['examples.directory']):
if examples_directory and not os.path.isabs(examples_directory):
_basedir, _fname = os.path.split(matplotlib_fname())
# Sometimes matplotlib_fname() can return relative paths,
# Also, using realpath() guarantees that Sphinx will use
# the same path that matplotlib sees (in case of weird symlinks).
_basedir = os.path.realpath(_basedir)
_fullpath = os.path.join(_basedir, rcParams['examples.directory'])
_fullpath = os.path.join(_basedir, examples_directory)
rcParams['examples.directory'] = _fullpath


with warnings.catch_warnings():
warnings.simplefilter("ignore", MatplotlibDeprecationWarning)
rcParamsOrig = RcParams(rcParams.copy())
rcParamsOrig = RcParams(rcParams._data.copy())
rcParamsDefault = RcParams([(key, default) for key, (default, converter) in
defaultParams.items()
if key not in _all_deprecated])
Expand Down Expand Up @@ -1275,7 +1285,7 @@ def __init__(self, rc=None, fname=None):
def __fallback(self):
# If anything goes wrong, revert to the original rcs.
updated_backend = self._orig['backend']
dict.update(rcParams, self._orig)
rcParams._data.update(self._orig._data)
# except for the backend. If the context block triggered resolving
# the auto backend resolution keep that value around
if self._orig['backend'] is rcsetup._auto_backend_sentinel:
Expand Down Expand Up @@ -1328,7 +1338,7 @@ def use(arg, warn=True, force=False):
name = validate_backend(arg)

# if setting back to the same thing, do nothing
if (dict.__getitem__(rcParams, 'backend') == name):
if (rcParams._data['backend'] == name):
pass

# Check if we have already imported pyplot and triggered
Expand Down
20 changes: 10 additions & 10 deletions lib/matplotlib/backends/qt_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,31 @@
# First, check if anything is already imported.
if "PyQt5" in sys.modules:
QT_API = QT_API_PYQT5
dict.__setitem__(rcParams, "backend.qt5", QT_API)
rcParams._data["backend.qt5"] = QT_API
elif "PySide2" in sys.modules:
QT_API = QT_API_PYSIDE2
dict.__setitem__(rcParams, "backend.qt5", QT_API)
rcParams._data["backend.qt5"] = QT_API
elif "PyQt4" in sys.modules:
QT_API = QT_API_PYQTv2
dict.__setitem__(rcParams, "backend.qt4", QT_API)
rcParams._data["backend.qt4"] = QT_API
elif "PySide" in sys.modules:
QT_API = QT_API_PYSIDE
dict.__setitem__(rcParams, "backend.qt4", QT_API)
rcParams._data["backend.qt4"] = QT_API
# Otherwise, check the QT_API environment variable (from Enthought). This can
# only override the binding, not the backend (in other words, we check that the
# requested backend actually matches).
elif rcParams["backend"] in ["Qt5Agg", "Qt5Cairo"]:
if QT_API_ENV == "pyqt5":
dict.__setitem__(rcParams, "backend.qt5", QT_API_PYQT5)
rcParams._data["backend.qt5"] = QT_API_PYQT5
elif QT_API_ENV == "pyside2":
dict.__setitem__(rcParams, "backend.qt5", QT_API_PYSIDE2)
QT_API = dict.__getitem__(rcParams, "backend.qt5")
rcParams._data["backend.qt5"] = QT_API_PYSIDE2
QT_API = rcParams._data["backend.qt5"]
elif rcParams["backend"] in ["Qt4Agg", "Qt4Cairo"]:
if QT_API_ENV == "pyqt4":
dict.__setitem__(rcParams, "backend.qt4", QT_API_PYQTv2)
rcParams._data["backend.qt4"] = QT_API_PYQTv2
elif QT_API_ENV == "pyside":
dict.__setitem__(rcParams, "backend.qt4", QT_API_PYSIDE)
QT_API = dict.__getitem__(rcParams, "backend.qt4")
rcParams._data["backend.qt4"] = QT_API_PYSIDE
QT_API = rcParams._data["backend.qt4"]
# A non-Qt backend was selected but we still got there (possible, e.g., when
# fully manually embedding Matplotlib in a Qt app without using pyplot).
else:
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/cbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ def get_sample_data(fname, asfileobj=True):
If the filename ends in .gz, the file is implicitly ungzipped.
"""
# Don't trigger deprecation warning when just fetching.
if dict.__getitem__(matplotlib.rcParams, 'examples.directory'):
if dict._data['examples.directory']:
root = matplotlib.rcParams['examples.directory']
else:
root = os.path.join(matplotlib._get_data_path(), 'sample_data')
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2343,9 +2343,9 @@ def _autogen_docstring(base):
# requested, ignore rcParams['backend'] and force selection of a backend that
# is compatible with the current running interactive framework.
if (rcParams["backend_fallback"]
and dict.__getitem__(rcParams, "backend") in _interactive_bk
and rcParams._data["backend"] in _interactive_bk
and _get_running_interactive_framework()):
dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel)
rcParams._data["backend"] = rcsetup._auto_backend_sentinel
# Set up the backend.
switch_backend(rcParams["backend"])

Expand Down