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