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 re
19import warnings
20
21import matplotlib as mpl
22from matplotlib import _api, rc_params_from_file, rcParamsDefault
23
24_log = logging.getLogger(__name__)
25
26__all__ = ['use', 'context', 'available', 'library', 'reload_library']
27
28
29BASE_LIBRARY_PATH = os.path.join(mpl.get_data_path(), 'stylelib')
30# Users may want multiple library paths, so store a list of paths.
31USER_LIBRARY_PATHS = [os.path.join(mpl.get_configdir(), 'stylelib')]
32STYLE_EXTENSION = 'mplstyle'
33STYLE_FILE_PATTERN = re.compile(r'([\S]+).%s$' % STYLE_EXTENSION)
34
35
36# A list of rcParams that should not be applied from styles
37STYLE_BLACKLIST = {
38    'interactive', 'backend', 'backend.qt4', 'webagg.port', 'webagg.address',
39    'webagg.port_retries', 'webagg.open_in_browser', 'backend_fallback',
40    'toolbar', 'timezone', 'datapath', 'figure.max_open_warning',
41    'figure.raise_window', 'savefig.directory', 'tk.window_focus',
42    'docstring.hardcopy', 'date.epoch'}
43
44
45def _remove_blacklisted_style_params(d, warn=True):
46    o = {}
47    for key in d:  # prevent triggering RcParams.__getitem__('backend')
48        if key in STYLE_BLACKLIST:
49            if warn:
50                _api.warn_external(
51                    "Style includes a parameter, '{0}', that is not related "
52                    "to style.  Ignoring".format(key))
53        else:
54            o[key] = d[key]
55    return o
56
57
58def _apply_style(d, warn=True):
59    mpl.rcParams.update(_remove_blacklisted_style_params(d, warn=warn))
60
61
62def use(style):
63    """
64    Use Matplotlib style settings from a style specification.
65
66    The style name of 'default' is reserved for reverting back to
67    the default style settings.
68
69    .. note::
70
71       This updates the `.rcParams` with the settings from the style.
72       `.rcParams` not defined in the style are kept.
73
74    Parameters
75    ----------
76    style : str, dict, Path or list
77        A style specification. Valid options are:
78
79        +------+-------------------------------------------------------------+
80        | str  | The name of a style or a path/URL to a style file. For a    |
81        |      | list of available style names, see `style.available`.       |
82        +------+-------------------------------------------------------------+
83        | dict | Dictionary with valid key/value pairs for                   |
84        |      | `matplotlib.rcParams`.                                      |
85        +------+-------------------------------------------------------------+
86        | Path | A path-like object which is a path to a style file.         |
87        +------+-------------------------------------------------------------+
88        | list | A list of style specifiers (str, Path or dict) applied from |
89        |      | first to last in the list.                                  |
90        +------+-------------------------------------------------------------+
91
92    """
93    style_alias = {'mpl20': 'default',
94                   'mpl15': 'classic'}
95    if isinstance(style, (str, Path)) or hasattr(style, 'keys'):
96        # If name is a single str, Path or dict, make it a single element list.
97        styles = [style]
98    else:
99        styles = style
100
101    styles = (style_alias.get(s, s) if isinstance(s, str) else s
102              for s in styles)
103    for style in styles:
104        if not isinstance(style, (str, Path)):
105            _apply_style(style)
106        elif style == 'default':
107            # Deprecation warnings were already handled when creating
108            # rcParamsDefault, no need to reemit them here.
109            with _api.suppress_matplotlib_deprecation_warning():
110                _apply_style(rcParamsDefault, warn=False)
111        elif style in library:
112            _apply_style(library[style])
113        else:
114            try:
115                rc = rc_params_from_file(style, use_default_template=False)
116                _apply_style(rc)
117            except IOError as err:
118                raise IOError(
119                    "{!r} not found in the style library and input is not a "
120                    "valid URL or path; see `style.available` for list of "
121                    "available styles".format(style)) from err
122
123
124@contextlib.contextmanager
125def context(style, after_reset=False):
126    """
127    Context manager for using style settings temporarily.
128
129    Parameters
130    ----------
131    style : str, dict, Path or list
132        A style specification. Valid options are:
133
134        +------+-------------------------------------------------------------+
135        | str  | The name of a style or a path/URL to a style file. For a    |
136        |      | list of available style names, see `style.available`.       |
137        +------+-------------------------------------------------------------+
138        | dict | Dictionary with valid key/value pairs for                   |
139        |      | `matplotlib.rcParams`.                                      |
140        +------+-------------------------------------------------------------+
141        | Path | A path-like object which is a path to a style file.         |
142        +------+-------------------------------------------------------------+
143        | list | A list of style specifiers (str, Path or dict) applied from |
144        |      | first to last in the list.                                  |
145        +------+-------------------------------------------------------------+
146
147    after_reset : bool
148        If True, apply style after resetting settings to their defaults;
149        otherwise, apply style on top of the current settings.
150    """
151    with mpl.rc_context():
152        if after_reset:
153            mpl.rcdefaults()
154        use(style)
155        yield
156
157
158def load_base_library():
159    """Load style library defined in this package."""
160    library = read_style_directory(BASE_LIBRARY_PATH)
161    return library
162
163
164def iter_user_libraries():
165    for stylelib_path in USER_LIBRARY_PATHS:
166        stylelib_path = os.path.expanduser(stylelib_path)
167        if os.path.exists(stylelib_path) and os.path.isdir(stylelib_path):
168            yield stylelib_path
169
170
171def update_user_library(library):
172    """Update style library with user-defined rc files."""
173    for stylelib_path in iter_user_libraries():
174        styles = read_style_directory(stylelib_path)
175        update_nested_dict(library, styles)
176    return library
177
178
179def read_style_directory(style_dir):
180    """Return dictionary of styles defined in *style_dir*."""
181    styles = dict()
182    for path in Path(style_dir).glob(f"*.{STYLE_EXTENSION}"):
183        with warnings.catch_warnings(record=True) as warns:
184            styles[path.stem] = rc_params_from_file(
185                path, use_default_template=False)
186        for w in warns:
187            _log.warning('In %s: %s', path, w.message)
188    return styles
189
190
191def update_nested_dict(main_dict, new_dict):
192    """
193    Update nested dict (only level of nesting) with new values.
194
195    Unlike `dict.update`, this assumes that the values of the parent dict are
196    dicts (or dict-like), so you shouldn't replace the nested dict if it
197    already exists. Instead you should update the sub-dict.
198    """
199    # update named styles specified by user
200    for name, rc_dict in new_dict.items():
201        main_dict.setdefault(name, {}).update(rc_dict)
202    return main_dict
203
204
205# Load style library
206# ==================
207_base_library = load_base_library()
208
209library = None
210
211available = []
212
213
214def reload_library():
215    """Reload the style library."""
216    global library
217    library = update_user_library(_base_library)
218    available[:] = sorted(library.keys())
219
220
221reload_library()
222