core.py 8.0 KB

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