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