/
core.py
245 lines (198 loc) · 8.41 KB
/
core.py
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
239
240
241
242
243
244
245
"""
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 logging
import os
from pathlib import Path
import sys
import warnings
if sys.version_info >= (3, 10):
import importlib.resources as importlib_resources
else:
# Even though Py3.9 has importlib.resources, it doesn't properly handle
# modules added in sys.path.
import importlib_resources
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()