-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Proposal: Implement RcParams
using ChainMap and remove dict
inheritance
#25617
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
base: main
Are you sure you want to change the base?
Changes from all commits
ab1ea99
5177508
45d4743
133f2af
b7f4fb5
d135342
47b48ee
9a08544
9a0ad03
7a11e81
4f9d8b6
54a1a11
c101fec
67735b0
7e4022a
c23cb29
0374f46
dd99c1d
aa282d0
b1754a2
faed9ba
dfbb73f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -134,8 +134,8 @@ | |
|
||
|
||
import atexit | ||
from collections import namedtuple | ||
from collections.abc import MutableMapping | ||
from collections import namedtuple, ChainMap | ||
from collections.abc import MutableMapping, Mapping, KeysView, ValuesView, ItemsView | ||
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 @@ | |
@_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,12 +666,13 @@ | |
-------- | ||
:ref:`customizing-with-matplotlibrc-files` | ||
""" | ||
|
||
validate = rcsetup._validators | ||
|
||
# validate values on the way in | ||
def __init__(self, *args, **kwargs): | ||
self._rcvalues = ChainMap({}) | ||
self.update(*args, **kwargs) | ||
self._rcvalues = self._rcvalues.new_child() | ||
self._defaults = self._rcvalues.maps[-1] | ||
|
||
def _set(self, key, val): | ||
""" | ||
|
@@ -690,7 +692,7 @@ | |
|
||
:meta public: | ||
""" | ||
dict.__setitem__(self, key, val) | ||
self._rcvalues[key] = val | ||
|
||
def _get(self, key): | ||
""" | ||
|
@@ -711,7 +713,7 @@ | |
|
||
:meta public: | ||
""" | ||
return dict.__getitem__(self, key) | ||
return self._rcvalues[key] | ||
|
||
def __setitem__(self, key, val): | ||
try: | ||
|
@@ -766,30 +768,84 @@ | |
|
||
return self._get(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( | ||
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._defaults[alt_key] if alt_key else None | ||
|
||
return self._defaults[key] | ||
|
||
def get_defaults(self): | ||
"""Return default values set during initialization.""" | ||
return self._defaults.copy() | ||
|
||
def _get_backend_or_none(self): | ||
"""Get the requested backend, if any, without triggering resolution.""" | ||
backend = self._get("backend") | ||
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"No custom value set for {key}. Cannot delete default value." | ||
) from err | ||
|
||
def __contains__(self, key): | ||
return key in self._rcvalues | ||
|
||
def __iter__(self): | ||
"""Yield from sorted list of keys""" | ||
yield from sorted(self._rcvalues.keys()) | ||
|
||
def __len__(self): | ||
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), 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 __iter__(self): | ||
"""Yield sorted list of keys.""" | ||
with _api.suppress_matplotlib_deprecation_warning(): | ||
yield from sorted(dict.__iter__(self)) | ||
@_api.deprecated("3.8") | ||
def clear(self): | ||
pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On one hand, This is part of the public API of the We should either make sure the note says "we are going to make this error in the future" or just warn "this is not doing what you think it is doing". |
||
|
||
def __len__(self): | ||
return dict.__len__(self) | ||
def reset(self): | ||
self._rcvalues.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 | ||
Comment on lines
+837
to
+845
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Current Note that this behavoir is identical to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should just return the value (maybe catching the |
||
|
||
def copy(self): | ||
return deepcopy(self) | ||
|
||
def find_all(self, pattern): | ||
chahak13 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
|
@@ -807,13 +863,6 @@ | |
for key, value in self.items() | ||
if pattern_re.search(key)) | ||
|
||
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 | ||
|
||
|
||
def rc_params(fail_on_error=False): | ||
"""Construct a `RcParams` instance from the default Matplotlib rc file.""" | ||
|
@@ -894,7 +943,7 @@ | |
fname) | ||
raise | ||
|
||
config = RcParams() | ||
config = dict() | ||
|
||
for key, (val, line, line_no) in rc_temp.items(): | ||
if key in rcsetup._validators: | ||
|
@@ -923,7 +972,7 @@ | |
or from the matplotlib source distribution""", | ||
dict(key=key, fname=fname, line_no=line_no, | ||
line=line.rstrip('\n'), version=version)) | ||
return config | ||
return RcParams(config) | ||
|
||
|
||
def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): | ||
|
@@ -947,7 +996,7 @@ | |
return config_from_file | ||
|
||
with _api.suppress_matplotlib_deprecation_warning(): | ||
config = RcParams({**rcParamsDefault, **config_from_file}) | ||
config = RcParams({**rcParams.get_defaults(), **config_from_file}) | ||
|
||
if "".join(config['text.latex.preamble']): | ||
_log.info(""" | ||
|
@@ -962,32 +1011,29 @@ | |
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( | ||
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) | ||
dict.update(rcParamsDefault, rcsetup._hardcoded_defaults) | ||
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. | ||
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())) | ||
rcParams.update(_rc_params_in_file(matplotlib_fname())) | ||
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. | ||
# 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 | ||
rcParamsDefault[key]), | ||
rcParams.get_default(key)), | ||
validator] | ||
for key, validator in rcsetup._validators.items()} | ||
if rcParams['axes.formatter.use_locale']: | ||
|
@@ -1086,13 +1132,10 @@ | |
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 rcParamsDefault.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(): | ||
|
@@ -1133,7 +1176,7 @@ | |
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 +1225,18 @@ | |
plt.plot(x, y) | ||
|
||
""" | ||
orig = dict(rcParams.copy()) | ||
del orig['backend'] | ||
try: | ||
rcParams._rcvalues = rcParams._rcvalues.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"] | ||
rcParams._rcvalues = rcParams._rcvalues.parents | ||
rcParams["backend"] = backend | ||
|
||
|
||
def use(backend, *, force=True): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -65,15 +65,22 @@ def get_cachedir() -> str: ... | |
def get_data_path() -> str: ... | ||
def matplotlib_fname() -> str: ... | ||
|
||
class RcParams(dict[str, Any]): | ||
class RcParams(MutableMapping[str, Any]): | ||
validate: dict[str, Callable] | ||
namespaces: tuple | ||
single_key_set: set | ||
Comment on lines
+70
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can these be more specific about the type of the values? e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, thanks! I wasn't sure how to explain indeterminate length so kept it as just a tuple. I'll update it. |
||
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: ... | ||
def __setitem__(self, key: str, val: Any) -> None: ... | ||
def __getitem__(self, key: str) -> Any: ... | ||
def __delitem__(self, key: str) -> None: ... | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually inherited from MutableMapping, but doesn't hurt to put it here more explicitly... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added these mainly because we overwrite these functions. So, just to be consistent with having stubs for the functions implemented.. |
||
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( | ||
|
Uh oh!
There was an error while loading. Please reload this page.