File size: 8,362 Bytes
63f8f42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | """
Core functions and attributes for the matplotlib style library:
``use``
Select style sheet to override the current matplotlib settings.
``context``
Context manager to use a style sheet temporarily.
``available``
List available style sheets.
``library``
A dictionary of style names and matplotlib settings.
"""
import contextlib
import importlib.resources
import logging
import os
from pathlib import Path
import warnings
import matplotlib as mpl
from matplotlib import _api, _docstring, _rc_params_in_file, rcParamsDefault
_log = logging.getLogger(__name__)
__all__ = ['use', 'context', 'available', 'library', 'reload_library']
BASE_LIBRARY_PATH = os.path.join(mpl.get_data_path(), 'stylelib')
# Users may want multiple library paths, so store a list of paths.
USER_LIBRARY_PATHS = [os.path.join(mpl.get_configdir(), 'stylelib')]
STYLE_EXTENSION = 'mplstyle'
# A list of rcParams that should not be applied from styles
STYLE_BLACKLIST = {
'interactive', 'backend', 'webagg.port', 'webagg.address',
'webagg.port_retries', 'webagg.open_in_browser', 'backend_fallback',
'toolbar', 'timezone', 'figure.max_open_warning',
'figure.raise_window', 'savefig.directory', 'tk.window_focus',
'docstring.hardcopy', 'date.epoch'}
@_docstring.Substitution(
"\n".join(map("- {}".format, sorted(STYLE_BLACKLIST, key=str.lower)))
)
def use(style):
"""
Use Matplotlib style settings from a style specification.
The style name of 'default' is reserved for reverting back to
the default style settings.
.. note::
This updates the `.rcParams` with the settings from the style.
`.rcParams` not defined in the style are kept.
Parameters
----------
style : str, dict, Path or list
A style specification. Valid options are:
str
- One of the style names in `.style.available` (a builtin style or
a style installed in the user library path).
- A dotted name of the form "package.style_name"; in that case,
"package" should be an importable Python package name, e.g. at
``/path/to/package/__init__.py``; the loaded style file is
``/path/to/package/style_name.mplstyle``. (Style files in
subpackages are likewise supported.)
- The path or URL to a style file, which gets loaded by
`.rc_params_from_file`.
dict
A mapping of key/value pairs for `matplotlib.rcParams`.
Path
The path to a style file, which gets loaded by
`.rc_params_from_file`.
list
A list of style specifiers (str, Path or dict), which are applied
from first to last in the list.
Notes
-----
The following `.rcParams` are not related to style and will be ignored if
found in a style specification:
%s
"""
if isinstance(style, (str, Path)) or hasattr(style, 'keys'):
# If name is a single str, Path or dict, make it a single element list.
styles = [style]
else:
styles = style
style_alias = {'mpl20': 'default', 'mpl15': 'classic'}
for style in styles:
if isinstance(style, str):
style = style_alias.get(style, style)
if style == "default":
# Deprecation warnings were already handled when creating
# rcParamsDefault, no need to reemit them here.
with _api.suppress_matplotlib_deprecation_warning():
# don't trigger RcParams.__getitem__('backend')
style = {k: rcParamsDefault[k] for k in rcParamsDefault
if k not in STYLE_BLACKLIST}
elif style in library:
style = library[style]
elif "." in style:
pkg, _, name = style.rpartition(".")
try:
path = importlib.resources.files(pkg) / f"{name}.{STYLE_EXTENSION}"
style = _rc_params_in_file(path)
except (ModuleNotFoundError, OSError, TypeError) as exc:
# There is an ambiguity whether a dotted name refers to a
# package.style_name or to a dotted file path. Currently,
# we silently try the first form and then the second one;
# in the future, we may consider forcing file paths to
# either use Path objects or be prepended with "./" and use
# the slash as marker for file paths.
pass
if isinstance(style, (str, Path)):
try:
style = _rc_params_in_file(style)
except OSError as err:
raise OSError(
f"{style!r} is not a valid package style, path of style "
f"file, URL of style file, or library style name (library "
f"styles are listed in `style.available`)") from err
filtered = {}
for k in style: # don't trigger RcParams.__getitem__('backend')
if k in STYLE_BLACKLIST:
_api.warn_external(
f"Style includes a parameter, {k!r}, that is not "
f"related to style. Ignoring this parameter.")
else:
filtered[k] = style[k]
mpl.rcParams.update(filtered)
@contextlib.contextmanager
def context(style, after_reset=False):
"""
Context manager for using style settings temporarily.
Parameters
----------
style : str, dict, Path or list
A style specification. Valid options are:
str
- One of the style names in `.style.available` (a builtin style or
a style installed in the user library path).
- A dotted name of the form "package.style_name"; in that case,
"package" should be an importable Python package name, e.g. at
``/path/to/package/__init__.py``; the loaded style file is
``/path/to/package/style_name.mplstyle``. (Style files in
subpackages are likewise supported.)
- The path or URL to a style file, which gets loaded by
`.rc_params_from_file`.
dict
A mapping of key/value pairs for `matplotlib.rcParams`.
Path
The path to a style file, which gets loaded by
`.rc_params_from_file`.
list
A list of style specifiers (str, Path or dict), which are applied
from first to last in the list.
after_reset : bool
If True, apply style after resetting settings to their defaults;
otherwise, apply style on top of the current settings.
"""
with mpl.rc_context():
if after_reset:
mpl.rcdefaults()
use(style)
yield
def update_user_library(library):
"""Update style library with user-defined rc files."""
for stylelib_path in map(os.path.expanduser, USER_LIBRARY_PATHS):
styles = read_style_directory(stylelib_path)
update_nested_dict(library, styles)
return library
def read_style_directory(style_dir):
"""Return dictionary of styles defined in *style_dir*."""
styles = dict()
for path in Path(style_dir).glob(f"*.{STYLE_EXTENSION}"):
with warnings.catch_warnings(record=True) as warns:
styles[path.stem] = _rc_params_in_file(path)
for w in warns:
_log.warning('In %s: %s', path, w.message)
return styles
def update_nested_dict(main_dict, new_dict):
"""
Update nested dict (only level of nesting) with new values.
Unlike `dict.update`, this assumes that the values of the parent dict are
dicts (or dict-like), so you shouldn't replace the nested dict if it
already exists. Instead you should update the sub-dict.
"""
# update named styles specified by user
for name, rc_dict in new_dict.items():
main_dict.setdefault(name, {}).update(rc_dict)
return main_dict
# Load style library
# ==================
_base_library = read_style_directory(BASE_LIBRARY_PATH)
library = {}
available = []
def reload_library():
"""Reload the style library."""
library.clear()
library.update(update_user_library(_base_library))
available[:] = sorted(library.keys())
reload_library()
|