Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
137 changes: 91 additions & 46 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -650,7 +651,7 @@ def gen_candidates():
@_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.

Expand All @@ -665,12 +666,13 @@ class RcParams(MutableMapping, dict):
--------
: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):
"""
Expand All @@ -690,7 +692,7 @@ def _set(self, key, val):

:meta public:
"""
dict.__setitem__(self, key, val)
self._rcvalues[key] = val

def _get(self, key):
"""
Expand All @@ -711,7 +713,7 @@ def _get(self, key):

:meta public:
"""
return dict.__getitem__(self, key)
return self._rcvalues[key]

def __setitem__(self, key, val):
try:
Expand Down Expand Up @@ -766,30 +768,84 @@ def __getitem__(self, key):

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
Copy link
Member

Choose a reason for hiding this comment

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

On one hand, clear is current a no-op because popitem raises KeyError which makes the default implementation from MutableMapping conclude that there is nothing to do and it is cleared despite doing nothing.

This is part of the public API of the MutableMapping API so I don't think we can (should) actually deprecate this.

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
Copy link
Member

Choose a reason for hiding this comment

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

Current setdefault behavior (side-effect of __setitem__ not accepting new keys): Return the value if key is valid, otherwise raise. We should keep this behavior for compatibility.

Note that this behavoir is identical to rcParams[key]. Optional: Deprecate setdefault and recommend rcParams[key] instead.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should just return the value (maybe catching the KeyError on misses to match the current error message exactly) and explicitly handle the one case we use this internally .


def copy(self):
return deepcopy(self)

def find_all(self, pattern):
"""
Expand All @@ -807,13 +863,6 @@ def find_all(self, pattern):
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."""
Expand Down Expand Up @@ -894,7 +943,7 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False):
fname)
raise

config = RcParams()
config = dict()

for key, (val, line, line_no) in rc_temp.items():
if key in rcsetup._validators:
Expand Down Expand Up @@ -923,7 +972,7 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False):
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):
Expand All @@ -947,7 +996,7 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
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("""
Expand All @@ -962,32 +1011,29 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
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']:
Expand Down Expand Up @@ -1086,13 +1132,10 @@ def rcdefaults():
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():
Expand Down Expand Up @@ -1133,7 +1176,7 @@ def rc_file(fname, *, use_default_template=True):
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})


Expand Down Expand Up @@ -1182,16 +1225,18 @@ def rc_context(rc=None, fname=None):
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):
Expand Down
17 changes: 12 additions & 5 deletions lib/matplotlib/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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. tuple[str, ...] (this is how you say homogeneous tuple of str, indeterminate length)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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: ...
Copy link
Member

Choose a reason for hiding this comment

The 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...

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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(
Expand Down
16 changes: 9 additions & 7 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@
FigureCanvasBase, FigureManagerBase, MouseButton)
from matplotlib.figure import Figure, FigureBase, figaspect
from matplotlib.gridspec import GridSpec, SubplotSpec
from matplotlib import rcsetup, rcParamsDefault, rcParamsOrig
from matplotlib import rcParams, get_backend, rcParamsOrig
from matplotlib.rcsetup import interactive_bk as _interactive_bk
from matplotlib.rcsetup import _auto_backend_sentinel
from matplotlib.artist import Artist
from matplotlib.axes import Axes, Subplot # type: ignore
from matplotlib.projections import PolarAxes # type: ignore
Expand Down Expand Up @@ -301,7 +303,7 @@ def switch_backend(newbackend: str) -> None:
# make sure the init is pulled up so we can assign to it later
import matplotlib.backends

if newbackend is rcsetup._auto_backend_sentinel:
if newbackend is _auto_backend_sentinel:
current_framework = cbook._get_running_interactive_framework()
mapping = {'qt': 'qtagg',
'gtk3': 'gtk3agg',
Expand Down Expand Up @@ -336,7 +338,7 @@ def switch_backend(newbackend: str) -> None:
rcParamsOrig["backend"] = "agg"
return
# have to escape the switch on access logic
old_backend = dict.__getitem__(rcParams, 'backend')
old_backend = rcParams._get('backend')

module = importlib.import_module(cbook._backend_module_name(newbackend))
canvas_class = module.FigureCanvas
Expand Down Expand Up @@ -413,7 +415,7 @@ def draw_if_interactive() -> None:
_log.debug("Loaded backend %s version %s.",
newbackend, backend_mod.backend_version)

rcParams['backend'] = rcParamsDefault['backend'] = newbackend
rcParams['backend'] = newbackend
_backend_mod = backend_mod
for func_name in ["new_figure_manager", "draw_if_interactive", "show"]:
globals()[func_name].__signature__ = inspect.signature(
Expand Down Expand Up @@ -745,7 +747,7 @@ def xkcd(
"xkcd mode is not compatible with text.usetex = True")

stack = ExitStack()
stack.callback(dict.update, rcParams, rcParams.copy()) # type: ignore
stack.callback(rcParams.update, rcParams.copy()) # type: ignore

from matplotlib import patheffects
rcParams.update({
Expand Down Expand Up @@ -2474,9 +2476,9 @@ def polar(*args, **kwargs) -> list[Line2D]:
# is compatible with the current running interactive framework.
if (rcParams["backend_fallback"]
and rcParams._get_backend_or_none() in ( # type: ignore
set(rcsetup.interactive_bk) - {'WebAgg', 'nbAgg'})
set(_interactive_bk) - {'WebAgg', 'nbAgg'})
and cbook._get_running_interactive_framework()): # type: ignore
rcParams._set("backend", rcsetup._auto_backend_sentinel) # type: ignore
rcParams._set("backend", _auto_backend_sentinel) # type: ignore

# fmt: on

Expand Down
Loading