Skip to content

Commit 31d92dc

Browse files
committed
Prepare to turn matplotlib.style into a plain module.
Having matplotlib.style be a package with the entire implementation in matplotlib.style.core is a bit overkill, and also makes it slightly awkward that USER_LIBRARY_PATHS is effectively a public API (clearly intended as so, per the comment, even though we may do it differently nowadays...) but only available in matplotlib.style.core, whereas everything else is re-exported by matplotlib.style. Prepare to flatten the implementation by deprecating matplotlib.style.core and reexporting USER_LIBRARY_PATHS in matplotlib.style. Once the deprecation elapses, we'll be able to move the implementation into a plain matplotlib/style.py module.
1 parent c44ae00 commit 31d92dc

File tree

7 files changed

+300
-228
lines changed

7 files changed

+300
-228
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
``matplotlib.style.core``
2+
~~~~~~~~~~~~~~~~~~~~~~~~~
3+
The ``matplotlib.style.core`` module is deprecated. All APIs intended for
4+
public use are now available in `matplotlib.style` directly (including
5+
``USER_LIBRARY_PATHS``, which was previously not reexported).
6+
7+
The following APIs of ``matplotlib.style.core`` have been deprecated with no
8+
replacement: ``BASE_LIBRARY_PATH``, ``STYLE_EXTENSION``, ``STYLE_BLACKLIST``,
9+
``update_user_library``, ``read_style_directory``, ``update_nested_dict``.

lib/matplotlib/style/__init__.py

Lines changed: 250 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,252 @@
1-
from .core import available, context, library, reload_library, use
1+
"""
2+
Core functions and attributes for the matplotlib style library:
23
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+
"""
313

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

lib/matplotlib/style/__init__.pyi

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from collections.abc import Generator
2+
import contextlib
3+
4+
from matplotlib import RcParams
5+
from matplotlib.typing import RcStyleType
6+
7+
USER_LIBRARY_PATHS: list[str] = ...
8+
9+
def use(style: RcStyleType) -> None: ...
10+
@contextlib.contextmanager
11+
def context(
12+
style: RcStyleType, after_reset: bool = ...
13+
) -> Generator[None, None, None]: ...
14+
15+
library: dict[str, RcParams]
16+
available: list[str]
17+
18+
def reload_library() -> None: ...
19+
20+
__all__ = ['use', 'context', 'available', 'library', 'reload_library']

0 commit comments

Comments
 (0)