""" 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()