core.py 8.2 KB

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